3472. Longest Palindromic Subsequence After at Most K Operations
Problem Description
You are given a string s
and an integer k
.
In one operation, you can replace a character at any position with the next or previous letter in the alphabet (wrapping around so that 'a'
is after 'z'
). For example, replacing 'a'
with the next letter results in 'b'
, and replacing 'a'
with the previous letter results in 'z'
. Similarly, replacing 'z'
with the next letter results in 'a'
, and replacing 'z'
with the previous letter results in 'y'
.
Return the length of the longest palindromic subsequence of s
that can be obtained after performing at most k
operations.
Intuition
The task requires us to find the longest palindromic subsequence in a string s
after applying at most k
operations, where each operation is a character replacement to its neighboring letter in the alphabet with wrapping around.
The solution uses a dynamic programming approach to achieve this. We define a function dfs(i, j, k)
that returns the length of the longest palindromic subsequence in the substring s[i..j]
with at most k
operations. The idea is to break down the problem for a given substring as follows:
-
Base Cases:
- If the start index
i
exceeds the end indexj
(i > j
), the longest palindromic subsequence is0
because there are no characters left. - If the substring length is
1
(i == j
), the longest palindromic subsequence is1
as a single character is always a palindrome.
- If the start index
-
Recursive Cases:
- We consider ignoring either the start (
s[i]
) or the end character (s[j]
). This is done by calculating recursively fordfs(i + 1, j, k)
ordfs(i, j - 1, k)
respectively. - Alternatively, we can attempt to match
s[i]
ands[j]
by performing necessary operations. We compute the differencet
between the ASCII values ofs[i]
ands[j]
, considering the wrap around thez-a
boundary. Ift
operations are feasible (t <= k
), we can try forming a palindrome by consideringdfs(i + 1, j - 1, k - t) + 2
, where this new palindrome includess[i]
ands[j]
.
- We consider ignoring either the start (
By considering these options, we compute the maximum between ignoring characters or forming a palindrome, and return the best value. Memoization is employed to store results of subproblems and avoid redundant calculations, thus optimizing the search process efficiently.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution to the problem employs a recursive approach enhanced by memoization to find the longest palindromic subsequence within a given string s
, allowing up to k
character modifications. Here’s a detailed breakdown of the implementation approach:
-
Recursive Function with Memoization:
- We define a helper function
dfs(i, j, k)
wherei
andj
are the indices defining the substrings[i..j]
andk
is the number of allowed operations left. This function calculates the longest palindromic subsequence within the given parameters. - The function is memoized using Python's
@cache
decorator, which helps store the results of previously computed states and improves efficiency by avoiding redundant calculations.
- We define a helper function
-
Base and Recursive Cases:
- Base Case 1: If
i > j
, return0
as there are no characters left to form a palindrome. - Base Case 2: If
i == j
, return1
because a single character is inherently a palindrome. - Recursive Case: For each substring:
- Compute the best result by either skipping a character from the start or the end (
dfs(i + 1, j, k)
ordfs(i, j - 1, k)
respectively). - Calculate the difference
d = abs(s[i] - s[j])
to determine the number of operations required to makes[i]
equal tos[j]
. Use wrap-around logic to account for the circular nature of the alphabet:t = min(d, 26 - d)
. - If
t <= k
, attempt to form a palindrome by matchings[i]
ands[j]
and calculatedfs(i + 1, j - 1, k - t) + 2
.
- Compute the best result by either skipping a character from the start or the end (
- Base Case 1: If
-
Implementation Details:
- Convert characters to their ASCII values using
list(map(ord, s))
to facilitate the calculation of differences and modification requirements. - Initiate the recursive process with
dfs(0, n - 1, k)
wheren
is the length ofs
. - After computing the required result, clear the cache using
dfs.cache_clear()
to manage memory usage effectively.
- Convert characters to their ASCII values using
This approach ensures that each potential subsequence is evaluated and the best palindrome is found under the constraint of k
operations.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's consider a simple example where the string s
is "abc"
and k
is 1
. We want to find the length of the longest palindromic subsequence after at most 1
operation.
-
Initial Setup:
- We will use a recursive function,
dfs(i, j, k)
, to calculate the longest palindromic subsequence between indicesi
andj
, allowing up tok
operations. - Initialize the process by calling
dfs(0, 2, 1)
, where0
and2
are the starting and ending indices of the string.
- We will use a recursive function,
-
Base Cases:
- For any call where
i > j
, return0
since there are no characters left to form a palindrome. - If
i == j
, return1
as a single character is always a palindromic subsequence.
- For any call where
-
Recursive Cases:
- Consider the substring
"abc"
:- Call
dfs(1, 2, 1)
(ignoring'a'
) anddfs(0, 1, 1)
(ignoring'c'
). - Compute the difference
d
between ASCII values of'a'
(97) and'c'
(99). The differenced = abs(97 - 99) = 2
(requires2
operations to equalize, using wrap-around logic:t = min(2, 26 - 2) = 2
). - Since
t > k
, we cannot match'a'
and'c'
with the current operation limitk = 1
.
- Call
- Consider the substring
-
Evaluation of Subproblems:
- Evaluate
dfs(1, 2, 1)
for the substring"bc"
:- Call
dfs(2, 2, 1)
anddfs(1, 1, 1)
to consider cases ignoring either side. - Compute
d = abs(98 - 99) = 1
(requires1
operation to match'b'
and'c'
,t = 1
). - Perform the operation as
t <= k
and calldfs(2, 1, 0) + 2
, resulting in a subsequence of length2
.
- Call
- Evaluate
-
Combine and Conclude:
- Backtrack to combine results according to maximum lengths obtained from all subproblems.
- Finally, the length of the longest palindromic subsequence for "abc" with
k=1
is2
(e.g., forming"aba"
,"cbc"
by replacing a single character).
By using a memoized recursive approach, the solution efficiently evaluates and returns the maximum possible length of a palindromic subsequence within the given constraints.
Solution Implementation
1from functools import cache
2
3class Solution:
4 def longestPalindromicSubsequence(self, s: str, k: int) -> int:
5 # Use memoization to cache results of dfs function
6 @cache
7 def dfs(i: int, j: int, remaining_k: int) -> int:
8 # Base case: if indices cross, no subsequence
9 if i > j:
10 return 0
11 # Base case: single character is a palindrome of length 1
12 if i == j:
13 return 1
14
15 # Calculate max length by skipping either i or j
16 res = max(dfs(i + 1, j, remaining_k), dfs(i, j - 1, remaining_k))
17
18 # Calculate character difference
19 diff = abs(s[i] - s[j])
20 char_change_cost = min(diff, 26 - diff)
21
22 # If the cost to make s[i] and s[j] equal is within remaining_k
23 if char_change_cost <= remaining_k:
24 # Include the pair s[i] and s[j] and deduct cost from remaining_k
25 res = max(res, dfs(i + 1, j - 1, remaining_k - char_change_cost) + 2)
26
27 return res
28
29 # Convert characters to their ordinal values
30 s = list(map(ord, s))
31 # Length of the string
32 n = len(s)
33 # Initiate recursion to compute the result
34 ans = dfs(0, n - 1, k)
35 # Clear cache after computation
36 dfs.cache_clear()
37 return ans
38
1class Solution {
2 private char[] s; // Array to store the characters of the input string
3 private Integer[][][] memo; // Memoization table to store intermediate results
4
5 public int longestPalindromicSubsequence(String s, int k) {
6 this.s = s.toCharArray(); // Convert the input string to a character array
7 int n = s.length(); // Length of the input string
8 memo = new Integer[n][n][k + 1]; // Initialize the memoization table
9 return dfs(0, n - 1, k); // Start the recursive depth-first search
10 }
11
12 private int dfs(int i, int j, int k) {
13 if (i > j) {
14 return 0; // If the left index exceeds the right, there is no subsequence
15 }
16 if (i == j) {
17 return 1; // If both indices are the same, it forms a single-character palindrome
18 }
19 if (memo[i][j][k] != null) {
20 return memo[i][j][k]; // Return the stored result if already computed
21 }
22
23 // Case 1: Either exclude the left or right character
24 int result = Math.max(dfs(i + 1, j, k), dfs(i, j - 1, k));
25
26 // Calculate the distance between characters s[i] and s[j] in the alphabet
27 int distance = Math.abs(s[i] - s[j]);
28 int cost = Math.min(distance, 26 - distance); // Minimum cost to make characters equal
29
30 // Case 2: Include both characters if the modification cost is within the allowed limit
31 if (cost <= k) {
32 result = Math.max(result, 2 + dfs(i + 1, j - 1, k - cost));
33 }
34
35 memo[i][j][k] = result; // Store the computed result
36 return result; // Return the longest palindromic subsequence length
37 }
38}
39
1#include <vector>
2#include <string>
3#include <cmath>
4#include <algorithm>
5
6class Solution {
7public:
8 int longestPalindromicSubsequence(std::string s, int k) {
9 int n = s.size();
10 // 3D vector to cache the results of subproblems: f[i][j][k] represents
11 // the longest palindromic subsequence in the substring s[i...j] allowing k modifications.
12 std::vector<std::vector<std::vector<int>>> cache(n, std::vector<std::vector<int>>(n, std::vector<int>(k + 1, -1)));
13
14 // Define a recursive function (dfs) to solve the problem.
15 std::function<int(int, int, int)> dfs = [&](int i, int j, int k) -> int {
16 // Base case: no characters left.
17 if (i > j) {
18 return 0;
19 }
20 // Base case: single character is a palindrome of length 1.
21 if (i == j) {
22 return 1;
23 }
24 // Return cached result if already computed.
25 if (cache[i][j][k] != -1) {
26 return cache[i][j][k];
27 }
28
29 // Take the maximum of ignoring either of the characters at the ends.
30 int result = std::max(dfs(i + 1, j, k), dfs(i, j - 1, k));
31
32 // Calculate the difference between the character codes of s[i] and s[j].
33 int difference = std::abs(s[i] - s[j]);
34 // Calculate the minimal cost to make s[i] equal to s[j].
35 int cost = std::min(difference, 26 - difference);
36
37 // If the cost is within the allowed modifications (k),
38 // consider making s[i] and s[j] equal and include both in the subsequence count.
39 if (cost <= k) {
40 result = std::max(result, 2 + dfs(i + 1, j - 1, k - cost));
41 }
42
43 // Cache the result for the current subproblem.
44 return cache[i][j][k] = result;
45 };
46
47 // Call the dfs from the full string scope with the given number of modifications allowed.
48 return dfs(0, n - 1, k);
49 }
50};
51
1function longestPalindromicSubsequence(s: string, k: number): number {
2 // Get the length of the string
3 const n = s.length;
4 // Convert each character to its Unicode code point value
5 const sCodes = s.split('').map(char => char.charCodeAt(0));
6
7 // Initialize the 3D memoization table with dimensions [n][n][k+1], defaulting to -1
8 const memo: number[][][] = Array.from({ length: n }, () =>
9 Array.from({ length: n }, () => Array(k + 1).fill(-1)),
10 );
11
12 // Depth-First Search with memoization
13 function dfs(i: number, j: number, remainingChanges: number): number {
14 // Base case: when pointers cross, the result is 0
15 if (i > j) {
16 return 0;
17 }
18 // Base case: when pointers are at the same position, single character is a palindrome
19 if (i === j) {
20 return 1;
21 }
22
23 // Return cached result if available
24 if (memo[i][j][remainingChanges] !== -1) {
25 return memo[i][j][remainingChanges];
26 }
27
28 // Option 1: Move the left pointer to the right
29 let result = Math.max(dfs(i + 1, j, remainingChanges), dfs(i, j - 1, remainingChanges));
30
31 // Calculate the distance between characters, consider the wrapped alphabet
32 const charDistance = Math.abs(sCodes[i] - sCodes[j]);
33 const wrappedDistance = Math.min(charDistance, 26 - charDistance);
34
35 // If distance can be covered within the limit of allowed changes
36 if (wrappedDistance <= remainingChanges) {
37 // Option 2: Consider the current pair as part of the palindromic subsequence
38 result = Math.max(result, 2 + dfs(i + 1, j - 1, remainingChanges - wrappedDistance));
39 }
40
41 // Store the result in the memoization table
42 return (memo[i][j][remainingChanges] = result);
43 }
44
45 // Initiate the depth-first search from the first to the last character with the initial available changes
46 return dfs(0, n - 1, k);
47}
48
Time and Space Complexity
The given code uses a dynamic programming approach with memoization (cache
) to solve the longest palindromic subsequence problem with an additional constraint.
Time Complexity
The function dfs(i, j, k)
recursively explores all possible subsequences within indices i
and j
with a constraint involving k
.
- There are
O(n^2)
pairs of(i, j)
since for eachi
,j
can vary fromi
ton-1
. - For each pair of indices
(i, j)
, the function evaluates different values ofk
, andk
can vary from0
to the initial givenk
.
Combining these factors, the time complexity is O(n^2 * k)
.
Space Complexity
-
The space required to store intermediate results of
dfs(i, j, k)
in the cache isO(n^2 * k)
because we store computations for each combination of(i, j, k)
. -
The recursive stack requires
O(n)
space in the worst case due to the depth of recursion.
Thus, the space complexity is O(n^2 * k)
.
Learn more about how to find time and space complexity quickly.
Which data structure is used to implement recursion?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https algomonster s3 us east 2 amazonaws com recursion jpg You first
Want a Structured Path to Master System Design Too? Don’t Miss This!