Medium

题目描述

给你一个字符串 s 和一个整数 k

在一次操作中,你可以将任意位置的字符替换为字母表中的下一个或前一个字母(循环包装,所以 ‘a’ 的下一个是 ‘z’)。例如,将 ‘a’ 替换为下一个字母得到 ‘b’,将 ‘a’ 替换为前一个字母得到 ‘z’。同样,将 ‘z’ 替换为下一个字母得到 ‘a’,将 ‘z’ 替换为前一个字母得到 ‘y’。

返回执行最多 k 次操作后,可以获得的 s 的最长回文子序列的长度。

示例 1:

输入:s = "abced", k = 2
输出:3
解释:
将 s[1] 替换为下一个字母,s 变成 "acced"。
将 s[4] 替换为前一个字母,s 变成 "accec"。
子序列 "ccc" 形成长度为 3 的回文,这是最大值。

示例 2:

输入:s = "aaazzz", k = 4
输出:6
解释:
将 s[0] 替换为前一个字母,s 变成 "zaazzz"。
将 s[4] 替换为下一个字母,s 变成 "zaazaz"。
将 s[3] 替换为下一个字母,s 变成 "zaaaaz"。
整个字符串形成长度为 6 的回文。

约束条件:

  • 1 <= s.length <= 200
  • 1 <= k <= 200
  • s 仅由小写英文字母组成

解题思路

这是一道经典的区间动态规划问题,需要考虑字符替换的成本。

核心思路: 定义 dp[i][j][cost] 表示在子串 s[i..j] 中,使用最多 cost 次操作能获得的最长回文子序列长度。

状态转移:

  1. 对于每个区间 [i, j],有三种选择:

    • 不选择 s[i]dp[i+1][j][cost]
    • 不选择 s[j]dp[i][j-1][cost]
    • 同时选择 s[i]s[j]:需要计算将它们变成相同字符的最小代价
  2. 关键计算 - 字符间距离: 对于两个字符 ab,在环形字母表中的最小距离为: min(abs(a-b), 26-abs(a-b))

  3. 优化技巧: 当选择 s[i]s[j] 时,我们枚举26个字母,找到使总代价最小且不超过 cost 的目标字符。

时间复杂度优化: 通过预计算字符间的最小距离,避免重复计算。状态转移时,对每个可能的目标字符进行尝试,选择代价最小的方案。

这种方法能够在 O(n³×k×26) 的时间复杂度内解决问题,对于给定的约束条件是可接受的。

代码实现

class Solution {
public:
    int longestPalindromicSubsequence(string s, int k) {
        int n = s.length();
        vector<vector<vector<int>>> dp(n, vector<vector<int>>(n, vector<int>(k + 1, 0)));
        
        // Base case: single characters
        for (int i = 0; i < n; i++) {
            for (int cost = 0; cost <= k; cost++) {
                dp[i][i][cost] = 1;
            }
        }
        
        // Fill dp table
        for (int len = 2; len <= n; len++) {
            for (int i = 0; i <= n - len; i++) {
                int j = i + len - 1;
                for (int cost = 0; cost <= k; cost++) {
                    // Option 1: don't include s[i]
                    dp[i][j][cost] = dp[i + 1][j][cost];
                    
                    // Option 2: don't include s[j]
                    dp[i][j][cost] = max(dp[i][j][cost], dp[i][j - 1][cost]);
                    
                    // Option 3: include both s[i] and s[j]
                    if (i + 1 <= j - 1) {
                        for (char target = 'a'; target <= 'z'; target++) {
                            int cost1 = min(abs(s[i] - target), 26 - abs(s[i] - target));
                            int cost2 = min(abs(s[j] - target), 26 - abs(s[j] - target));
                            int totalCost = cost1 + cost2;
                            
                            if (totalCost <= cost) {
                                dp[i][j][cost] = max(dp[i][j][cost], 
                                                   dp[i + 1][j - 1][cost - totalCost] + 2);
                            }
                        }
                    } else {
                        // Adjacent characters
                        for (char target = 'a'; target <= 'z'; target++) {
                            int cost1 = min(abs(s[i] - target), 26 - abs(s[i] - target));
                            int cost2 = min(abs(s[j] - target), 26 - abs(s[j] - target));
                            int totalCost = cost1 + cost2;
                            
                            if (totalCost <= cost) {
                                dp[i][j][cost] = max(dp[i][j][cost], 2);
                            }
                        }
                    }
                }
            }
        }
        
        return dp[0][n - 1][k];
    }
};
class Solution:
    def longestPalindromicSubsequence(self, s: str, k: int) -> int:
        n = len(s)
        dp = [[[0] * (k + 1) for _ in range(n)] for _ in range(n)]
        
        # Base case: single characters
        for i in range(n):
            for cost in range(k + 1):
                dp[i][i][cost] = 1
        
        # Fill dp table
        for length in range(2, n + 1):
            for i in range(n - length + 1):
                j = i + length - 1
                for cost in range(k + 1):
                    # Option 1: don't include s[i]
                    dp[i][j][cost] = dp[i + 1][j][cost]
                    
                    # Option 2: don't include s[j]
                    dp[i][j][cost] = max(dp[i][j][cost], dp[i][j - 1][cost])
                    
                    # Option 3: include both s[i] and s[j]
                    for target in range(26):
                        target_char = chr(ord('a') + target)
                        cost1 = min(abs(ord(s[i]) - ord(target_char)), 
                                   26 - abs(ord(s[i]) - ord(target_char)))
                        cost2 = min(abs(ord(s[j]) - ord(target_char)), 
                                   26 - abs(ord(s[j]) - ord(target_char)))
                        total_cost = cost1 + cost2
                        
                        if total_cost <= cost:
                            if i + 1 <= j - 1:
                                dp[i][j][cost] = max(dp[i][j][cost], 
                                                   dp[i + 1][j - 1][cost - total_cost] + 2)
                            else:
                                dp[i][j][cost] = max(dp[i][j][cost], 2)
        
        return dp[0][n - 1][k]
public class Solution {
    public int LongestPalindromicSubsequence(string s, int k) {
        int n = s.Length;
        int[,,] dp = new int[n, n, k + 1];
        
        // Base case: single characters
        for (int i = 0; i < n; i++) {
            for (int cost = 0; cost <= k; cost++) {
                dp[i, i, cost] = 1;
            }
        }
        
        // Fill dp table
        for (int len = 2; len <= n; len++) {
            for (int i = 0; i <= n - len; i++) {
                int j = i + len - 1;
                for (int cost = 0; cost <= k; cost++) {
                    // Option 1: don't include s[i]
                    dp[i, j, cost] = dp[i + 1, j, cost];
                    
                    // Option 2: don't include s[j]
                    dp[i, j, cost] = Math.Max(dp[i, j, cost], dp[i, j - 1, cost]);
                    
                    // Option 3: include both s[i] and s[j]
                    for (char target = 'a'; target <= 'z'; target++) {
                        int cost1 = Math.Min(Math.Abs(s[i] - target), 26 - Math.Abs(s[i] - target));
                        int cost2 = Math.Min(Math.Abs(s[j] - target), 26 - Math.Abs(s[j] - target));
                        int totalCost = cost1 + cost2;
                        
                        if (totalCost <= cost) {
                            if (i + 1 <= j - 1) {
                                dp[i, j, cost] = Math.Max(dp[i, j, cost], 
                                                        dp[i + 1, j - 1, cost - totalCost] + 2);
                            } else {
                                dp[i, j, cost] = Math.Max(dp[i, j, cost], 2);
                            }
                        }
                    }
                }
            }
        }
        
        return dp[0, n - 1, k];
    }
}
var longestPalindromicSubsequence = function(s, k) {
    const n = s.length;
    const dp = Array(n).fill(null).map(() => 
        Array(n).fill(null).map(() => Array(k + 1).fill(0))
    );
    
    // Base case: single characters
    for (let i = 0; i < n; i++) {
        for (let cost = 0; cost <= k; cost++) {
            dp[i][i][cost] = 1;
        }
    }
    
    // Fill dp table
    for (let len = 2; len <= n; len++) {
        for (let i = 0; i <= n - len; i++) {
            const j = i + len - 1;
            for (let cost = 0; cost <= k; cost++) {
                // Option 1: don't include s[i]
                dp[i][j][cost] = dp[i + 1][j][cost];
                
                // Option 2: don't include s[j]
                dp[i][j][cost] = Math.max(dp[i][j][cost], dp[i][j - 1][cost]);
                
                // Option 3: include both s[i] and s[j]
                for (let targetCode = 0; targetCode < 26; targetCode++) {
                    const target = String.fromCharCode(97 + targetCode); // 'a' + targetCode
                    const cost1 = Math.min(Math.abs(s.charCodeAt(i) - target.charCodeAt(0)), 
                                         26 - Math.abs(s.charCodeAt(i) - target.charCodeAt(0)));
                    const cost2 = Math.min(Math.abs(s.charCodeAt(j) - target.charCodeAt(0)), 
                                         26 - Math.abs(s.charCodeAt(j) - target.charCodeAt(0)));
                    const totalCost = cost1 + cost2;
                    
                    if (totalCost <= cost) {
                        if (i + 1 <= j - 1) {
                            dp[i][j][cost] = Math.max(dp[i][j][cost], 
                                                    dp[i + 1][j - 1][cost - totalCost] + 2);
                        } else {
                            dp[i][j][cost] = Math.max(dp[i][j][cost], 2);
                        }
                    }
                }
            }
        }
    }
    
    return dp[0][n - 1][k];
};

复杂度分析

复杂度类型复杂度说明
时间复杂度O(n³ × k × 26)n为字符串长度,需要遍历所有区间、所有成本值和所有可能的目标字符
空间复杂度O(n² × k)三维DP数组的空间开销