今天要分享的是一个非常有意思的算法问题——LeetCode 730题"统计不同回文子序列"。这个问题看似简单,但实际解决起来却需要一些巧妙的动态规划技巧。我最近在Qwen3.5-Plus模型上实现了这个算法,过程中积累了不少经验,特别是关于如何高效处理字符串中的回文子序列计数问题。
回文子序列计数是一个经典的动态规划问题,在字符串处理、生物信息学等领域都有广泛应用。这道题的特别之处在于它要求统计"不同"的回文子序列,而不是简单的所有可能。这意味着我们需要考虑去重的问题,使得解决方案更加复杂。
题目给定一个字符串s,要求计算其中不同的非空回文子序列的数量。回文子序列是指从字符串中删除0个或多个字符后,剩下的字符序列是回文的(正读反读相同)。例如,字符串"bccb"有以下不同的回文子序列:"b", "c", "bb", "cc", "bcb", "bccb"。
关键点在于:
这个问题看似简单,但暴力解法的时间复杂度极高。对于一个长度为n的字符串,可能的子序列数量是2^n(每个字符都有选或不选两种可能)。即使只考虑回文子序列,直接枚举所有可能性并检查是否为回文也是不可行的。
我们需要找到一个更聪明的方法,利用动态规划来避免重复计算,同时高效地统计不同的回文子序列。
动态规划是解决这类计数问题的利器。我们可以定义一个二维数组dp[i][j],表示字符串s从i到j的子串中不同的回文子序列的数量。我们的目标是计算dp[0][n-1],即整个字符串的不同回文子序列数。
关键观察:
定义dp[i][j]为区间[i,j]内不同的回文子序列数:
基本情况:
递推关系:
在实际实现中,我们需要特别注意边界条件的处理:
java复制public int countPalindromicSubsequences(String s) {
int n = s.length();
int MOD = 1000000007;
int[][] dp = new int[n][n];
for (int i = 0; i < n; i++) {
dp[i][i] = 1;
}
for (int len = 2; len <= n; len++) {
for (int i = 0; i <= n - len; i++) {
int j = i + len - 1;
if (s.charAt(i) == s.charAt(j)) {
int left = i + 1, right = j - 1;
while (left <= right && s.charAt(left) != s.charAt(i)) left++;
while (left <= right && s.charAt(right) != s.charAt(j)) right--;
if (left > right) {
dp[i][j] = dp[i+1][j-1] * 2 + 2;
} else if (left == right) {
dp[i][j] = dp[i+1][j-1] * 2 + 1;
} else {
dp[i][j] = dp[i+1][j-1] * 2 - dp[left+1][right-1];
}
} else {
dp[i][j] = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1];
}
dp[i][j] = dp[i][j] < 0 ? dp[i][j] + MOD : dp[i][j] % MOD;
}
}
return dp[0][n-1];
}
预处理字符位置:可以预先计算每个字符在字符串中的出现位置,这样在查找l和r时可以更快定位,减少内层循环的时间。
空间优化:由于dp[i][j]只依赖于当前行和上一行的数据,可以将空间复杂度从O(n^2)优化到O(n)。
模运算处理:由于结果可能很大,需要在每一步计算后进行模运算,但要小心处理负数情况。
基础实现的时间复杂度是O(n^3),因为:
通过预处理字符位置,可以将时间复杂度优化到O(n^2)。
基础实现的空间复杂度是O(n^2),用于存储dp数组。通过滚动数组技巧可以优化到O(n)。
这个算法不仅仅是一个理论练习,它在实际中有多种应用:
在我的测试中,对于长度为1000的字符串:
在实际实现这个算法时,我遇到了几个关键问题:
初始化问题:最初我忘记初始化dp[i][i] = 1,导致所有单字符回文未被正确计数。解决方法是在开始时就显式初始化所有长度为1的子串。
模运算问题:当dp[i][j]为负数时,直接取模会得到错误结果。我通过添加判断条件解决了这个问题:
java复制dp[i][j] = dp[i][j] < 0 ? dp[i][j] + MOD : dp[i][j] % MOD;
字符位置查找优化:最初的内层while循环在最坏情况下会使时间复杂度变为O(n^3)。通过预处理每个字符的位置索引,我将其优化为O(1)查找,显著提高了性能。
一个特别有用的调试技巧是:对于中等长度字符串(如"bccb"),手动绘制dp表格并与程序输出对比,这能快速定位逻辑错误。