Longest Palindromic Subsequence After at Most K Operations
MedPrompt
You are given a string s and an integer k.
In one operation, you can replace the 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.
Example 1:
Input: s = "abced", k = 2
Output: 3
Explanation:
- Replace
s[1]with the next letter, andsbecomes"acced". - Replace
s[4]with the previous letter, andsbecomes"accec".
The subsequence "ccc" forms a palindrome of length 3, which is the maximum.
Example 2:
Input: s = "aaazzz", k = 4
Output: 6
Explanation:
- Replace
s[0]with the previous letter, andsbecomes"zaazzz". - Replace
s[4]with the next letter, andsbecomes"zaazaz". - Replace
s[3]with the next letter, andsbecomes"zaaaaz".
The entire string forms a palindrome of length 6.
Constraints:
1 <= s.length <= 2001 <= k <= 200sconsists of only lowercase English letters.
Approaches
2 approaches with complexity analysis and trade-offs.
This problem can be solved using dynamic programming. It's an extension of the classic Longest Palindromic Subsequence (LPS) problem. The standard LPS DP state dp[i][j] (length of LPS in s[i...j]) is insufficient because it doesn't track the cost of operations. We can incorporate the cost by adding a third dimension to our DP state: k. This leads to a 3D DP table, dp[i][j][k], which stores the maximum length of a palindromic subsequence for s[i...j] using at most k operations.
Algorithm
- Define a 3D DP table
dp[n][n][k+1], wheredp[i][j][rem_k]stores the maximum length of a palindromic subsequence for the substrings[i...j]using at mostrem_koperations. - Initialize the DP table with 0s.
- Iterate through the string
swith a decreasing outer loop for the start indexi(fromn-1to0) and an increasing inner loop for the end indexj(fromiton-1). - For each pair
(i, j), iterate through all possible remaining costsrem_kfrom0tok. - Base Case: If
i == j, the longest palindromic subsequence has length 1. So,dp[i][i][rem_k] = 1for allrem_k. - Recursive Step: For
i < j, calculatedp[i][j][rem_k]by considering three possibilities:- Skip character
s[i]: The length would bedp[i+1][j][rem_k]. - Skip character
s[j]: The length would bedp[i][j-1][rem_k]. - Match characters
s[i]ands[j]: Calculate the costcto make them identical. Ifrem_k >= c, the length is2 + dp[i+1][j-1][rem_k - c]. The inner partdp[i+1][j-1]corresponds to an empty substring (length 0) ifj = i+1.
- Skip character
dp[i][j][rem_k]is the maximum of the outcomes from these choices.- The final answer is
dp[0][n-1][k].
Walkthrough
We can define a function, say solve(i, j, k), that returns the length of the longest palindromic subsequence in s[i...j] with at most k operations. We can implement this using a bottom-up DP approach to avoid recursion overhead.
The state transition for dp[i][j][rem_k] is as follows:
- We consider the characters at the ends of our current substring,
s[i]ands[j]. - Option 1: Don't use
s[i]. We find the LPS in the smaller substrings[i+1...j]with the same budgetrem_k. The length isdp[i+1][j][rem_k]. - Option 2: Don't use
s[j]. We find the LPS ins[i...j-1]with budgetrem_k. The length isdp[i][j-1][rem_k]. - Option 3: Match
s[i]ands[j]. To make them part of the palindrome, they must be transformed into the same character. The minimum cost for this iscost(s[i], s[j]) = min(|s[i] - s[j]|, 26 - |s[i] - s[j]|). If we have enough budget (rem_k >= cost), we can form a palindrome of length2plus the length of the LPS from the inner substrings[i+1...j-1]with the remaining budgetrem_k - cost. This gives a length of2 + dp[i+1][j-1][rem_k - cost].
The value dp[i][j][rem_k] will be the maximum of these options. The final answer is the value for the entire string s[0...n-1] with the initial budget k, which is dp[0][n-1][k].
class Solution { private int cost(char c1, char c2) { int diff = Math.abs(c1 - c2); return Math.min(diff, 26 - diff); } public int longestPalindromeSubsequence(String s, int k) { int n = s.length(); int[][][] dp = new int[n][n][k + 1]; for (int i = n - 1; i >= 0; i--) { // Base case: len = 1, s[i...i] for (int rem_k = 0; rem_k <= k; rem_k++) { dp[i][i][rem_k] = 1; } for (int j = i + 1; j < n; j++) { for (int rem_k = 0; rem_k <= k; rem_k++) { // Option 1 & 2: Skip s[i] or s[j] int result = Math.max(dp[i + 1][j][rem_k], dp[i][j - 1][rem_k]); // Option 3: Match s[i] and s[j] int matchCost = cost(s.charAt(i), s.charAt(j)); if (rem_k >= matchCost) { int innerLength = (i + 1 > j - 1) ? 0 : dp[i + 1][j - 1][rem_k - matchCost]; result = Math.max(result, 2 + innerLength); } dp[i][j][rem_k] = result; } } } return dp[0][n - 1][k]; }}Complexity
Time
O(n^2 * k). We have three nested loops to fill the DP table: `i` from `n-1` to `0` (n iterations), `j` from `i+1` to `n-1` (at most n iterations), and `rem_k` from `0` to `k` (k+1 iterations).
Space
O(n^2 * k), where `n` is the length of the string and `k` is the maximum number of operations. This is for the 3D DP table of size `n x n x (k+1)`.
Trade-offs
Pros
It's a conceptually clear extension of the standard Longest Palindromic Subsequence algorithm.
The approach is guaranteed to find the optimal solution by systematically exploring all subproblems.
Cons
The space complexity of
O(n^2 * k)can be very high. For the given constraints (n=200, k=200), this would require approximately200 * 200 * 201 * 4bytes, which is about 32 MB of memory. This might be prohibitive in environments with stricter memory limits.
The 3D DP approach is correct but uses a large amount of memory. We can optimize the space complexity by observing the dependencies in the DP state transitions. The calculation for dp[i][...][...] (row i) only depends on values from dp[i+1][...][...] (the previous row) and values already computed in the current row i. This means we don't need to store the entire 3D table. We only need to keep track of the results for the current row and the previous row, reducing the space complexity significantly.
Algorithm
- Instead of a full 3D DP table, use two 2D tables:
dp[n][k+1]to store results for the current rowi, andprev_dp[n][k+1]to store results for the previous rowi+1. - Iterate
ifromn-1down to0. - At the beginning of each
iiteration,prev_dpholds the computed values for rowi+1. - Compute the values for the current row
iand store them indp. - The recurrence relation is modified to use the 2D tables:
dp[i+1][j]becomesprev_dp[j]dp[i][j-1]becomesdp[j-1]dp[i+1][j-1]becomesprev_dp[j-1]
- After iterating through
jfor a giveni, thedptable is complete for rowi. It then becomes theprev_dpfor the next iteration (i-1). - The final answer is
prev_dp[n-1][k]after the main loop finishes (as it will hold the results fori=0).
Walkthrough
We can implement the space optimization by maintaining two 2D arrays. Let's call them dp and prev_dp. prev_dp will store the DP values for the row i+1, while dp will be used to compute the values for the current row i.
We iterate i from n-1 down to 0. In each iteration:
dpis used to calculate the results for the current rowi.- The values needed from row
i+1are retrieved fromprev_dp. - After row
iis fully computed indp, we no longer need the results fromi+1. So,dpbecomes theprev_dpfor the next iteration (i-1). This can be done by swapping pointers or copying the array.
This reduces the space from O(n^2 * k) to O(n * k), making the solution much more memory-efficient and practical for the given constraints.
class Solution { private int cost(char c1, char c2) { int diff = Math.abs(c1 - c2); return Math.min(diff, 26 - diff); } public int longestPalindromeSubsequence(String s, int k) { int n = s.length(); int[][] dp = new int[n][k + 1]; for (int i = n - 1; i >= 0; i--) { int[][] current_dp = new int[n][k + 1]; // Base case: len = 1, s[i...i] for (int rem_k = 0; rem_k <= k; rem_k++) { current_dp[i][rem_k] = 1; } for (int j = i + 1; j < n; j++) { for (int rem_k = 0; rem_k <= k; rem_k++) { // dp is prev_dp (row i+1), current_dp is for row i // Option 1 & 2: Skip s[i] or s[j] int result = Math.max(dp[j][rem_k], current_dp[j - 1][rem_k]); // Option 3: Match s[i] and s[j] int matchCost = cost(s.charAt(i), s.charAt(j)); if (rem_k >= matchCost) { int innerLength = (i + 1 > j - 1) ? 0 : dp[j - 1][rem_k - matchCost]; result = Math.max(result, 2 + innerLength); } current_dp[j][rem_k] = result; } } dp = current_dp; } return dp[n - 1][k]; }}Complexity
Time
O(n^2 * k). The number of calculations remains the same as the unoptimized approach.
Space
O(n * k). We only need two 2D tables of size `n x (k+1)` to store the results for the current and previous rows of the main DP table.
Trade-offs
Pros
Significantly more memory-efficient than the unoptimized version.
Maintains the same time complexity while being more practical for the given constraints.
Cons
The code can be slightly more complex to reason about due to the reduced dimensions and pointer/array swapping.
Solutions
Solution
class Solution {private char[] s;private Integer[][][] f;public int longestPalindromicSubsequence(String s, int k) { this.s = s.toCharArray(); int n = s.length(); f = new Integer[n][n][k + 1]; return dfs(0, n - 1, k); }private int dfs(int i, int j, int k) { if (i > j) { return 0; } if (i == j) { return 1; } if (f[i][j][k] != null) { return f[i][j][k]; } int res = Math.max(dfs(i + 1, j, k), dfs(i, j - 1, k)); int d = Math.abs(s[i] - s[j]); int t = Math.min(d, 26 - d); if (t <= k) { res = Math.max(res, 2 + dfs(i + 1, j - 1, k - t)); } f[i][j][k] = res; return res; }}class Solution {public: int longestPalindromicSubsequence(string s, int k) { int n = s.size(); vector f(n, vector(n, vector<int>(k + 1, -1))); auto dfs = [&](this auto &&dfs, int i, int j, int k) -> int { if (i > j) { return 0; } if (i == j) { return 1; } if (f[i][j][k] != -1) { return f[i][j][k]; } int res = max(dfs(i + 1, j, k), dfs(i, j - 1, k)); int d = abs(s[i] - s[j]); int t = min(d, 26 - d); if (t <= k) { res = max(res, 2 + dfs(i + 1, j - 1, k - t)); } return f[i][j][k] = res; }; return dfs(0, n - 1, k); }};class Solution: def longestPalindromicSubsequence(self, s: str, k: int) -> int: @ cache def dfs(i: int, j: int, k: int) -> int: if i > j: return 0 if i == j: return 1 res = max(dfs(i + 1, j, k), dfs(i, j - 1, k)) d = abs(s[i] - s[j]) t = min(d, 26 - d) if t <= k: res = max(res, dfs(i + 1, j - 1, k - t) + 2) return res s = list(map(ord, s)) n = len(s) ans = dfs(0, n - 1, k) dfs . cache_clear() return ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.