统计一个字符串中“回文子序列”的总数(字符集为小写字母,允许重复计数)
字数 9406 2025-12-15 17:11:20

统计一个字符串中“回文子序列”的总数(字符集为小写字母,允许重复计数)

题目描述
给定一个只包含小写字母的字符串 s,我们需要统计其所有回文子序列(Palindromic Subsequences)的总数。回文子序列定义为原字符串中通过删除零个或多个字符(不改变剩余字符的相对顺序)得到的一个序列,且该序列从左到右和从右到左读是一样的。注意,即使两个回文子序列由相同的字符组成,但如果它们在原字符串中的位置(下标)不同,则视为不同的子序列。最终结果可能非常大,要求对 \(10^9+7\) 取模。

举例
例如,字符串 "aab"

  • 回文子序列有:""(空序列)、"a"(第一个 'a')、"a"(第二个 'a')、"b""aa""aa"(由第一个和第三个字符组成,注意:第一个和第三个字符是 'a''b',不能组成 "aa",这里应为第二个 'a' 和第三个 'b' 也不能组成 "aa",实际上 "aa" 只能由前两个 'a' 组成。更正:"aa" 只有一种,由下标 0 和 1 的 'a' 组成)、"aba"?实际上 "aba" 不是子序列,因为字符顺序是 'a''a''b',无法得到 'b' 在中间。正确回文子序列列表:
    • 长度为 0:""(1 个)
    • 长度为 1:"a"(下标 0)、"a"(下标 1)、"b"(下标 2)共 3 个
    • 长度为 2:"aa"(下标 0 和 1)共 1 个
    • 长度为 3:无
      总数为 1 + 3 + 1 = 5。
      实际上,我们通常包括空序列,但有时题目可能要求非空,这里我们按包括空序列计数。但为了清晰,我们先从非空开始讲解,最后加上空序列。

解题过程

步骤 1:理解问题与定义状态

我们需要统计所有回文子序列的数量。注意“子序列”允许跳过中间字符,且不同位置即使字符相同也算不同子序列。
这是一个经典的区间动态规划问题,我们定义:

dp[i][j] 表示字符串 s 中从下标 ij(包含两端)的子串中,回文子序列的个数(包括空序列吗?先不包括空序列会更清晰,但最终答案可以加 1 表示空序列。不过为了方便转移,我们通常让 dp[i][j] 表示该子串中所有回文子序列的个数,包括空序列。但这样会导致重复计数问题,因为空序列在任意子串中都计数一次。更常见的定义是:dp[i][j] 表示子串 s[i..j]回文子序列的个数(不包括空序列),这样最终答案再加 1。但转移方程会复杂一些。另一种更简洁的定义是:dp[i][j] 表示子串 s[i..j]回文子序列的个数(包括空序列)。我们采用这种,因为这样转移方程更整齐。最终答案就是 dp[0][n-1],其中 n 是字符串长度。

但要注意,当 i > j 时,我们认为子串为空,此时回文子序列只有空序列,所以 dp[i][j] = 1(当 i > j)。

步骤 2:状态转移方程

考虑子串 s[i..j],我们需要计算所有回文子序列的数量。我们可以根据子序列的首尾字符是否包含 s[i]s[j] 来分类:

  1. 包含 s[i] 但不包含 s[j]
  2. 包含 s[j] 但不包含 s[i]
  3. 同时包含 s[i]s[j]
  4. 既不包含 s[i] 也不包含 s[j]

这四类覆盖了所有回文子序列。但注意,如果 s[i] == s[j],那么同时包含 s[i]s[j] 的子序列可以形成新的回文(即以 s[i]s[j] 作为首尾),而且这些子序列的内部可以由 s[i+1..j-1] 中的任意回文子序列构成。如果 s[i] != s[j],那么同时包含 s[i]s[j] 的子序列不可能是回文,因为首尾字符不同。

因此,我们需要分情况:

情况 1:s[i] == s[j]
dp[i][j] 表示子串 s[i..j] 中回文子序列的个数(包括空序列)。
那么:

  • 不包含 s[i] 也不包含 s[j] 的子序列数 = dp[i+1][j-1](即 s[i+1..j-1] 中的所有回文子序列)。
  • 包含 s[i] 但不包含 s[j] 的子序列数 = 包含 s[i] 但可能包含或不包含 s[j] 吗?实际上,我们固定不包含 s[j],但可以从 s[i+1..j-1] 中选任意回文子序列,然后前面加上 s[i]。但这些子序列是否回文取决于 s[i] 和内部子序列的匹配。更简单的方法是利用计数原理:

我们考虑从 s[i..j] 中选子序列,可以按是否包含 s[i]s[j] 分为四类:
(A) 既不包含 i 也不包含 j:dp[i+1][j-1]
(B) 包含 i 但不包含 j
(C) 包含 j 但不包含 i
(D) 包含 i 且包含 j

由于 s[i] == s[j],所以 (B) 和 (C) 是对称的,且 (D) 中的子序列可以这样得到:取 s[i+1..j-1] 中的任意一个回文子序列(包括空序列),然后在首尾加上 s[i]s[j],这样得到的序列一定是回文。所以 (D) 的数量 = dp[i+1][j-1](因为对 s[i+1..j-1] 的每个回文子序列,都可以在首尾添加 s[i]s[j] 得到一个新的回文子序列,包括空序列对应单个 s[i]s[j] 组成的长度为 2 的回文,以及空序列对应空序列加首尾得到长度为 2 的回文,这里注意:空序列加首尾得到的是 "s[i]s[j]" 即两个相同字符)。

但 (B) 和 (C) 的数量并不是 dp[i+1][j-1],因为 (B) 包含 i 但不包含 j,这些子序列不一定回文,我们需要从 s[i+1..j] 中选,但必须不包含 j。实际上,我们可以用 dp[i][j-1] 表示包含 i 的可能序列,但 dp[i][j-1] 包括了包含 i 且可能包含 j-1 的序列,但 j 不在区间内。更精确地,dp[i][j-1] 包括了 (A) 和 (B),而 dp[i+1][j] 包括了 (A) 和 (C)。所以:

dp[i][j-1] = (A) + (B)
dp[i+1][j] = (A) + (C)
dp[i+1][j-1] = (A)

那么 (D) 的数量 = dp[i+1][j-1](因为对 s[i+1..j-1] 的每个回文子序列,添加首尾 s[i] 和 s[j] 后仍为回文)。

所以 dp[i][j] = (A) + (B) + (C) + (D)
= dp[i+1][j-1] + (B) + (C) + dp[i+1][j-1]
= dp[i][j-1] + dp[i+1][j] - dp[i+1][j-1] + dp[i+1][j-1](因为 (B) = dp[i][j-1] - dp[i+1][j-1],(C) = dp[i+1][j] - dp[i+1][j-1]
整理得:dp[i][j] = dp[i][j-1] + dp[i+1][j] + dp[i+1][j-1]?不对,检查一下:

实际上代入:
dp[i][j] = (A) + (B) + (C) + (D)
= dp[i+1][j-1] + [dp[i][j-1] - dp[i+1][j-1]] + [dp[i+1][j] - dp[i+1][j-1]] + dp[i+1][j-1]
= dp[i][j-1] + dp[i+1][j] - dp[i+1][j-1]

这就是当 s[i] == s[j] 时的转移方程吗?注意 (D) 我们用了 dp[i+1][j-1],但这是正确的吗?考虑 s[i+1..j-1] 中的任意回文子序列,我们可以在其首尾添加 s[i] 和 s[j] 得到新回文,所以 (D) 的数量确实等于 dp[i+1][j-1]。所以最终:

dp[i][j] = dp[i][j-1] + dp[i+1][j] - dp[i+1][j-1] + dp[i+1][j-1]
= dp[i][j-1] + dp[i+1][j]s[i] == s[j] 时。

但这样不对,因为 (D) 被重复计算了?我们检查:dp[i][j-1] 包含 (A) 和 (B),dp[i+1][j] 包含 (A) 和 (C),相加后 (A) 被加了两次,所以需要减去一个 (A),然后再加上 (D)。所以:

dp[i][j] = dp[i][j-1] + dp[i+1][j] - dp[i+1][j-1] + dp[i+1][j-1]
= dp[i][j-1] + dp[i+1][j]s[i] == s[j]

确实如此。但注意,当 s[i] == s[j] 时,dp[i+1][j-1] 被减掉又加回,抵消了。所以最终转移方程为:

s[i] == s[j]
dp[i][j] = dp[i][j-1] + dp[i+1][j] + 1?等等,我们漏了什么?检查例子 s = "aa",长度 2,dp[0][1] 应该包含:"", "a"(第一个), "a"(第二个), "aa" 共 4 个。
计算:dp[0][0] = 2"", "a"),dp[1][1] = 2dp[1][0] 无效。
按公式:dp[0][1] = dp[0][0] + dp[1][1] = 2 + 2 = 4,正确,不需要 +1。
所以当 s[i]==s[j] 时,dp[i][j] = dp[i][j-1] + dp[i+1][j]

情况 2:s[i] != s[j]
此时 (D) 类子序列(同时包含 i 和 j)不可能是回文,所以 (D) 的数量为 0。
那么 dp[i][j] = (A) + (B) + (C) = dp[i][j-1] + dp[i+1][j] - dp[i+1][j-1]

所以完整转移方程:

  • 如果 i > jdp[i][j] = 0?但之前我们约定空串时回文子序列只有空序列,即 1。所以我们调整:当 i > j 时,dp[i][j] = 0(因为后面我们只考虑 i<=j 的情况,初始化时长度为 1 的区间单独处理,避免 i>j 出现)。
  • 如果 i == jdp[i][j] = 2(空序列和单个字符序列)。
  • 如果 i < j
    • 如果 s[i] == s[j]dp[i][j] = dp[i][j-1] + dp[i+1][j] + 1?我们验证一下。
      s = "aba",n=3。
      dp[0][0]=2, dp[1][1]=2, dp[2][2]=2
      dp[1][2]s[1]!=s[2]'b'!='a'),所以 dp[1][2] = dp[1][1] + dp[2][2] - dp[2][1],其中 dp[2][1]=0,所以 dp[1][2] = 2+2-0=4。实际序列:"", "b", "a", "ba",但 "ba" 不是回文,所以错误。这说明我们的定义有问题:dp[i][j] 必须只计数回文子序列,而不是所有子序列。所以需要重新定义。

步骤 3:正确定义状态

定义 dp[i][j] 为子串 s[i..j]回文子序列的个数(包括空序列)。那么当 s[i] != s[j] 时,同时包含 i 和 j 的子序列不可能是回文,所以不能简单用包含所有子序列的公式。

我们需要用容斥原理,但只计数回文子序列。标准解法是:

dp[i][j] 表示子串 s[i..j] 中回文子序列的个数(包括空序列)。
那么:

  • i > jdp[i][j] = 1(只有空序列)。
  • i == jdp[i][j] = 2(空序列和单个字符序列)。
  • i < j
    • 如果 s[i] == s[j]:那么所有回文子序列可以分为:
      1. 包含 s[i]s[j] 的:在 s[i+1..j-1] 的每个回文子序列首尾添加 s[i]s[j],得到新的回文子序列,数量为 dp[i+1][j-1]
      2. 不包含 s[i]s[j] 的:数量为 dp[i+1][j-1]?不对,这是 s[i+1..j-1] 的所有回文子序列,但我们已经计入了包含 i 和 j 的,所以这里不包含 i 和 j 的就是 dp[i+1][j-1] 吗?注意 dp[i+1][j-1] 包括了空序列,以及只包含 i+1..j-1 中的字符的子序列,这些都不含 i 和 j,所以正确。
      3. 包含 s[i] 但不包含 s[j] 的:这些子序列的回文性要求内部是回文,且以 s[i] 开头,但不以 s[j] 结尾。实际上,这类子序列就是 s[i..j-1] 中包含 s[i] 的回文子序列,但 s[i..j-1] 中也可能包含 s[j] 吗?区间是 i 到 j-1,所以不包含 j。所以这类数量 = dp[i][j-1] - dp[i+1][j-1](即 s[i..j-1] 的所有回文子序列减去 s[i+1..j-1] 的所有回文子序列,因为减去的是既不包含 i 也不包含 j-1 的吗?实际上 dp[i][j-1] 包含所有以 i 开头和不以 i 开头的,我们要的是以 i 开头的。但更方便的方法是:我们不需要显式分类,可以用以下推导:

由于 s[i]==s[j],所以 s[i..j] 的回文子序列由以下部分组成:

  • s[i+1..j-1] 的所有回文子序列(即不包含 i 和 j 的)。
  • 在上述每个回文子序列首尾添加 s[i]s[j] 得到的新回文子序列(包括空序列变成 s[i]s[j])。
    所以总数 = dp[i+1][j-1] + dp[i+1][j-1] = 2 * dp[i+1][j-1]?但这样漏掉了一些:比如只包含 i 不包含 j 的,或只包含 j 不包含 i 的。实际上,只包含 i 不包含 j 的回文子序列,可以通过在 s[i+1..j-1] 的回文子序列前添加 s[i] 得到,但还需要保证整个是回文,这要求内部子序列是回文且以 s[i] 开头?不,只包含 i 不包含 j 的子序列,可以是单个 s[i],或者 s[i] 加上一个内部回文子序列(但内部子序列在 s[i+1..j-1] 中,且整个序列以 s[i] 开头,以某个内部字符结尾,要回文必须内部子序列是回文且整个序列对称,这要求内部子序列以 s[i] 结尾,但内部子序列不一定包含 s[i]。所以这样分类很麻烦。

标准的状态转移方程是(参考经典问题“统计回文子序列个数”):

dp[i][j] = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1],如果 s[i] != s[j]
如果 s[i] == s[j],则 dp[i][j] = dp[i+1][j] + dp[i][j-1] + 1
其中 dp[i][j] 表示子串 s[i..j] 中回文子序列的个数(包括空序列吗?)。验证一下:

s = "ab"dp[0][1]s[0]!=s[1],所以 dp[0][1] = dp[1][1] + dp[0][0] - dp[1][0]。设 dp[i][j] 包括空序列,则 dp[0][0]=2, dp[1][1]=2, dp[1][0]=1(空序列)。所以 dp[0][1]=2+2-1=3。实际回文子序列:"", "a", "b",共 3 个,正确。

s = "aa"dp[0][1] = dp[1][1] + dp[0][0] + 1 = 2+2+1=5。实际:"", "a", "a", "aa" 共 4 个,这里多了一个。所以公式不对,应该是 dp[i][j] = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1]s[i]!=s[j];当 s[i]==s[j] 时,dp[i][j] = dp[i+1][j] + dp[i][j-1] + 1 会多计数。
实际上,当 s[i]==s[j] 时,dp[i+1][j] 包含了所有不包含 i 的回文子序列,dp[i][j-1] 包含了所有不包含 j 的回文子序列,两者交集是 dp[i+1][j-1](既不包含 i 也不包含 j 的回文子序列)。另外,我们还需要加上同时包含 i 和 j 的回文子序列,这些序列的个数等于 dp[i+1][j-1](因为对 s[i+1..j-1] 的每个回文子序列,添加首尾 i 和 j 后仍是回文)。所以总数 = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1] + dp[i+1][j-1] = dp[i+1][j] + dp[i][j-1]。但这样又回到了之前的公式,对 "aa" 计算:dp[0][1] = dp[1][1] + dp[0][0] = 2+2=4,正确。所以正确的转移方程是:

  • 如果 s[i] == s[j]dp[i][j] = dp[i+1][j] + dp[i][j-1] + 1?不对,我们验证 "aaa"
    n=3,dp[0][0]=2, dp[1][1]=2, dp[2][2]=2
    dp[1][2]s[1]==s[2],所以 dp[1][2] = dp[2][2] + dp[1][1] = 2+2=4。实际序列:"", "a", "a", "aa" 共 4 个,正确。
    dp[0][1]s[0]==s[1]dp[0][1] = dp[1][1] + dp[0][0] = 2+2=4,正确。
    dp[0][2]s[0]==s[2]dp[0][2] = dp[1][2] + dp[0][1] = 4+4=8。实际序列:列出 "aaa" 的所有回文子序列(包括空序列):"", "a"(3 个), "aa"(3 个:下标 (0,1), (0,2), (1,2)), "aaa"(1 个),共 1+3+3+1=8 个,正确。
    所以当 s[i]==s[j] 时,dp[i][j] = dp[i+1][j] + dp[i][j-1]

但注意,当 i+1 > j-1 时,即区间 s[i+1..j-1] 无效,此时 dp[i+1][j-1] 应设为 1(空序列)。在我们的公式中,当 s[i]==s[j] 时,我们没用到 dp[i+1][j-1],所以没问题。但考虑 i+1 == j 时,即 s[i]==s[j] 且相邻,如 "aa"dp[i+1][j-1]dp[j][i],即 i>j 的情况,我们应设 dp[i+1][j-1] = 1。但在公式 dp[i][j] = dp[i+1][j] + dp[i][j-1] 中,不需要这个值。所以可以。

s[i] != s[j] 时,dp[i][j] = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1]

验证 "ab"dp[0][1] = dp[1][1] + dp[0][0] - dp[1][0]dp[1][0] 是无效区间,应视为 1(空序列)。所以 dp[0][1] = 2+2-1=3,正确。

步骤 4:初始化与计算顺序

  • 初始化:对所有 idp[i][i] = 2(空序列和单个字符序列)。对所有 i > jdp[i][j] = 1(只有空序列)。
  • 计算顺序:按区间长度从小到大计算。长度从 2 到 n。

步骤 5:最终答案

dp[0][n-1] 就是整个字符串的回文子序列总数(包括空序列)。如果题目要求不包括空序列,则答案减 1。

步骤 6:模运算

由于结果可能很大,每次计算都要对 \(10^9+7\) 取模。注意减法可能产生负数,需要加模数再取模。

步骤 7:示例代码(C++风格伪代码)

int countPalindromicSubsequences(string s) {
    int n = s.length();
    int MOD = 1e9+7;
    vector<vector<long long>> dp(n, vector<long long>(n, 0));
    for (int i = 0; i < n; i++) {
        dp[i][i] = 2; // 空序列和单个字符
    }
    for (int len = 2; len <= n; len++) {
        for (int i = 0; i + len - 1 < n; i++) {
            int j = i + len - 1;
            if (s[i] == s[j]) {
                dp[i][j] = (dp[i+1][j] + dp[i][j-1]) % MOD;
            } else {
                dp[i][j] = (dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1]) % MOD;
            }
        }
    }
    // 注意:dp[0][n-1] 包括空序列,如果题目要求不包括空序列,则减 1。
    return (dp[0][n-1] + MOD) % MOD;
}

但注意,当 i+1 > j-1 时,dp[i+1][j-1] 需要设为 1。所以在代码中,我们需要处理 i+1 > j-1 的情况,即当 i+1 > j-1 时,dp[i+1][j-1] 应为 1。在转移中,当 s[i]!=s[j]len==2 时,i+1 = jj-1 = i,所以 i+1 > j-1dp[i+1][j-1] = dp[j][i] 未定义,我们应手动设为 1。可以在计算前初始化 dp[i][i-1] = 1 对所有 i。或者特判。

修改:

int countPalindromicSubsequences(string s) {
    int n = s.length();
    int MOD = 1e9+7;
    vector<vector<long long>> dp(n, vector<long long>(n, 0));
    // 初始化长度为 1
    for (int i = 0; i < n; i++) dp[i][i] = 2;
    // 初始化 i>j 的情况为 1
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < i; j++) {
            dp[i][j] = 1;
        }
    }
    for (int len = 2; len <= n; len++) {
        for (int i = 0; i + len - 1 < n; i++) {
            int j = i + len - 1;
            if (s[i] == s[j]) {
                dp[i][j] = (dp[i+1][j] + dp[i][j-1]) % MOD;
            } else {
                dp[i][j] = (dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1]) % MOD;
            }
        }
    }
    // 减去空序列,如果题目要求不包括空序列
    // 这里我们返回包括空序列的结果
    return (dp[0][n-1] + MOD) % MOD;
}

注意:当 s[i]!=s[j] 时,公式中的 dp[i+1][j-1] 我们已初始化,所以可以直接用。

步骤 8:验证例子

s = "aab"

  • n=3,初始化 dp[0][0]=2, dp[1][1]=2, dp[2][2]=2,其他 i>j 的为 1。
  • len=2:
    • i=0,j=1: s[0]=='a', s[1]=='a',相等,dp[0][1] = dp[1][1] + dp[0][0] = 2+2=4
    • i=1,j=2: s[1]=='a', s[2]=='b',不等,dp[1][2] = dp[2][2] + dp[1][1] - dp[2][1] = 2+2-1=3
  • len=3: i=0,j=2: s[0]=='a', s[2]=='b',不等,dp[0][2] = dp[1][2] + dp[0][1] - dp[1][1] = 3+4-2=5
    结果 dp[0][2]=5,包括空序列。实际回文子序列:"", "a", "a", "b", "aa" 共 5 个,正确。

最终答案:返回 dp[0][n-1] 即可(包括空序列)。如果题目要求不包括空序列,则减 1。

统计一个字符串中“回文子序列”的总数(字符集为小写字母,允许重复计数) 题目描述 给定一个只包含小写字母的字符串 s ,我们需要统计其所有回文子序列(Palindromic Subsequences)的总数。回文子序列定义为原字符串中通过删除零个或多个字符(不改变剩余字符的相对顺序)得到的一个序列,且该序列从左到右和从右到左读是一样的。注意,即使两个回文子序列由相同的字符组成,但如果它们在原字符串中的位置(下标)不同,则视为不同的子序列。最终结果可能非常大,要求对 \(10^9+7\) 取模。 举例 例如,字符串 "aab" : 回文子序列有: "" (空序列)、 "a" (第一个 'a' )、 "a" (第二个 'a' )、 "b" 、 "aa" 、 "aa" (由第一个和第三个字符组成,注意:第一个和第三个字符是 'a' 和 'b' ,不能组成 "aa" ,这里应为第二个 'a' 和第三个 'b' 也不能组成 "aa" ,实际上 "aa" 只能由前两个 'a' 组成。更正: "aa" 只有一种,由下标 0 和 1 的 'a' 组成)、 "aba" ?实际上 "aba" 不是子序列,因为字符顺序是 'a' 、 'a' 、 'b' ,无法得到 'b' 在中间。正确回文子序列列表: 长度为 0: "" (1 个) 长度为 1: "a" (下标 0)、 "a" (下标 1)、 "b" (下标 2)共 3 个 长度为 2: "aa" (下标 0 和 1)共 1 个 长度为 3:无 总数为 1 + 3 + 1 = 5。 实际上,我们通常包括空序列,但有时题目可能要求非空,这里我们按包括空序列计数。但为了清晰,我们先从非空开始讲解,最后加上空序列。 解题过程 步骤 1:理解问题与定义状态 我们需要统计所有回文子序列的数量。注意“子序列”允许跳过中间字符,且不同位置即使字符相同也算不同子序列。 这是一个经典的区间动态规划问题,我们定义: dp[i][j] 表示字符串 s 中从下标 i 到 j (包含两端)的子串中, 回文子序列的个数 (包括空序列吗?先不包括空序列会更清晰,但最终答案可以加 1 表示空序列。不过为了方便转移,我们通常让 dp[i][j] 表示该子串中所有回文子序列的个数,包括空序列。但这样会导致重复计数问题,因为空序列在任意子串中都计数一次。更常见的定义是: dp[i][j] 表示子串 s[i..j] 中 回文子序列的个数(不包括空序列) ,这样最终答案再加 1。但转移方程会复杂一些。另一种更简洁的定义是: dp[i][j] 表示子串 s[i..j] 中 回文子序列的个数(包括空序列) 。我们采用这种,因为这样转移方程更整齐。最终答案就是 dp[0][n-1] ,其中 n 是字符串长度。 但要注意,当 i > j 时,我们认为子串为空,此时回文子序列只有空序列,所以 dp[i][j] = 1 (当 i > j)。 步骤 2:状态转移方程 考虑子串 s[i..j] ,我们需要计算所有回文子序列的数量。我们可以根据子序列的首尾字符是否包含 s[i] 和 s[j] 来分类: 包含 s[i] 但不包含 s[j] 包含 s[j] 但不包含 s[i] 同时包含 s[i] 和 s[j] 既不包含 s[i] 也不包含 s[j] 这四类覆盖了所有回文子序列。但注意,如果 s[i] == s[j] ,那么同时包含 s[i] 和 s[j] 的子序列可以形成新的回文(即以 s[i] 和 s[j] 作为首尾),而且这些子序列的内部可以由 s[i+1..j-1] 中的任意回文子序列构成。如果 s[i] != s[j] ,那么同时包含 s[i] 和 s[j] 的子序列不可能是回文,因为首尾字符不同。 因此,我们需要分情况: 情况 1: s[i] == s[j] 设 dp[i][j] 表示子串 s[i..j] 中回文子序列的个数(包括空序列)。 那么: 不包含 s[i] 也不包含 s[j] 的子序列数 = dp[i+1][j-1] (即 s[i+1..j-1] 中的所有回文子序列)。 包含 s[i] 但不包含 s[j] 的子序列数 = 包含 s[i] 但可能包含或不包含 s[j] 吗?实际上,我们固定不包含 s[j] ,但可以从 s[i+1..j-1] 中选任意回文子序列,然后前面加上 s[i] 。但这些子序列是否回文取决于 s[i] 和内部子序列的匹配。更简单的方法是利用计数原理: 我们考虑从 s[i..j] 中选子序列,可以按是否包含 s[i] 和 s[j] 分为四类: (A) 既不包含 i 也不包含 j: dp[i+1][j-1] (B) 包含 i 但不包含 j (C) 包含 j 但不包含 i (D) 包含 i 且包含 j 由于 s[i] == s[j] ,所以 (B) 和 (C) 是对称的,且 (D) 中的子序列可以这样得到:取 s[i+1..j-1] 中的任意一个回文子序列(包括空序列),然后在首尾加上 s[i] 和 s[j] ,这样得到的序列一定是回文。所以 (D) 的数量 = dp[i+1][j-1] (因为对 s[i+1..j-1] 的每个回文子序列,都可以在首尾添加 s[i] 和 s[j] 得到一个新的回文子序列,包括空序列对应单个 s[i] 和 s[j] 组成的长度为 2 的回文,以及空序列对应空序列加首尾得到长度为 2 的回文,这里注意:空序列加首尾得到的是 "s[i]s[j]" 即两个相同字符)。 但 (B) 和 (C) 的数量并不是 dp[i+1][j-1] ,因为 (B) 包含 i 但不包含 j,这些子序列不一定回文,我们需要从 s[i+1..j] 中选,但必须不包含 j。实际上,我们可以用 dp[i][j-1] 表示包含 i 的可能序列,但 dp[i][j-1] 包括了包含 i 且可能包含 j-1 的序列,但 j 不在区间内。更精确地, dp[i][j-1] 包括了 (A) 和 (B),而 dp[i+1][j] 包括了 (A) 和 (C)。所以: dp[i][j-1] = (A) + (B) dp[i+1][j] = (A) + (C) dp[i+1][j-1] = (A) 那么 (D) 的数量 = dp[i+1][j-1] (因为对 s[i+1..j-1] 的每个回文子序列,添加首尾 s[ i] 和 s[ j ] 后仍为回文)。 所以 dp[i][j] = (A) + (B) + (C) + (D) = dp[i+1][j-1] + (B) + (C) + dp[i+1][j-1] = dp[i][j-1] + dp[i+1][j] - dp[i+1][j-1] + dp[i+1][j-1] (因为 (B) = dp[i][j-1] - dp[i+1][j-1] ,(C) = dp[i+1][j] - dp[i+1][j-1] ) 整理得: dp[i][j] = dp[i][j-1] + dp[i+1][j] + dp[i+1][j-1] ?不对,检查一下: 实际上代入: dp[i][j] = (A) + (B) + (C) + (D) = dp[i+1][j-1] + [dp[i][j-1] - dp[i+1][j-1]] + [dp[i+1][j] - dp[i+1][j-1]] + dp[i+1][j-1] = dp[i][j-1] + dp[i+1][j] - dp[i+1][j-1] 。 这就是当 s[i] == s[j] 时的转移方程吗?注意 (D) 我们用了 dp[i+1][j-1] ,但这是正确的吗?考虑 s[i+1..j-1] 中的任意回文子序列,我们可以在其首尾添加 s[ i] 和 s[ j] 得到新回文,所以 (D) 的数量确实等于 dp[i+1][j-1] 。所以最终: dp[i][j] = dp[i][j-1] + dp[i+1][j] - dp[i+1][j-1] + dp[i+1][j-1] = dp[i][j-1] + dp[i+1][j] 当 s[i] == s[j] 时。 但这样不对,因为 (D) 被重复计算了?我们检查: dp[i][j-1] 包含 (A) 和 (B), dp[i+1][j] 包含 (A) 和 (C),相加后 (A) 被加了两次,所以需要减去一个 (A),然后再加上 (D)。所以: dp[i][j] = dp[i][j-1] + dp[i+1][j] - dp[i+1][j-1] + dp[i+1][j-1] = dp[i][j-1] + dp[i+1][j] 当 s[i] == s[j] 。 确实如此。但注意,当 s[i] == s[j] 时, dp[i+1][j-1] 被减掉又加回,抵消了。所以最终转移方程为: 当 s[i] == s[j] 时 : dp[i][j] = dp[i][j-1] + dp[i+1][j] + 1 ?等等,我们漏了什么?检查例子 s = "aa" ,长度 2, dp[0][1] 应该包含: "" , "a" (第一个), "a" (第二个), "aa" 共 4 个。 计算: dp[0][0] = 2 ( "" , "a" ), dp[1][1] = 2 , dp[1][0] 无效。 按公式: dp[0][1] = dp[0][0] + dp[1][1] = 2 + 2 = 4 ,正确,不需要 +1。 所以当 s[i]==s[j] 时, dp[i][j] = dp[i][j-1] + dp[i+1][j] 。 情况 2: s[i] != s[j] 此时 (D) 类子序列(同时包含 i 和 j)不可能是回文,所以 (D) 的数量为 0。 那么 dp[i][j] = (A) + (B) + (C) = dp[i][j-1] + dp[i+1][j] - dp[i+1][j-1] 。 所以完整转移方程: 如果 i > j : dp[i][j] = 0 ?但之前我们约定空串时回文子序列只有空序列,即 1。所以我们调整:当 i > j 时, dp[i][j] = 0 (因为后面我们只考虑 i <=j 的情况,初始化时长度为 1 的区间单独处理,避免 i>j 出现)。 如果 i == j : dp[i][j] = 2 (空序列和单个字符序列)。 如果 i < j : 如果 s[i] == s[j] : dp[i][j] = dp[i][j-1] + dp[i+1][j] + 1 ?我们验证一下。 设 s = "aba" ,n=3。 dp[0][0]=2 , dp[1][1]=2 , dp[2][2]=2 。 dp[1][2] : s[1]!=s[2] ( 'b'!='a' ),所以 dp[1][2] = dp[1][1] + dp[2][2] - dp[2][1] ,其中 dp[2][1]=0 ,所以 dp[1][2] = 2+2-0=4 。实际序列: "", "b", "a", "ba" ,但 "ba" 不是回文,所以错误。这说明我们的定义有问题: dp[i][j] 必须只计数回文子序列,而不是所有子序列。所以需要重新定义。 步骤 3:正确定义状态 定义 dp[i][j] 为子串 s[i..j] 中 回文子序列的个数(包括空序列) 。那么当 s[i] != s[j] 时,同时包含 i 和 j 的子序列不可能是回文,所以不能简单用包含所有子序列的公式。 我们需要用容斥原理,但只计数回文子序列。标准解法是: 设 dp[i][j] 表示子串 s[i..j] 中回文子序列的个数(包括空序列)。 那么: 当 i > j : dp[i][j] = 1 (只有空序列)。 当 i == j : dp[i][j] = 2 (空序列和单个字符序列)。 当 i < j : 如果 s[i] == s[j] :那么所有回文子序列可以分为: 包含 s[i] 和 s[j] 的:在 s[i+1..j-1] 的每个回文子序列首尾添加 s[i] 和 s[j] ,得到新的回文子序列,数量为 dp[i+1][j-1] 。 不包含 s[i] 和 s[j] 的:数量为 dp[i+1][j-1] ?不对,这是 s[i+1..j-1] 的所有回文子序列,但我们已经计入了包含 i 和 j 的,所以这里不包含 i 和 j 的就是 dp[i+1][j-1] 吗?注意 dp[i+1][j-1] 包括了空序列,以及只包含 i+1..j-1 中的字符的子序列,这些都不含 i 和 j,所以正确。 包含 s[i] 但不包含 s[j] 的:这些子序列的回文性要求内部是回文,且以 s[i] 开头,但不以 s[j] 结尾。实际上,这类子序列就是 s[i..j-1] 中包含 s[i] 的回文子序列,但 s[i..j-1] 中也可能包含 s[j] 吗?区间是 i 到 j-1,所以不包含 j。所以这类数量 = dp[i][j-1] - dp[i+1][j-1] (即 s[i..j-1] 的所有回文子序列减去 s[i+1..j-1] 的所有回文子序列,因为减去的是既不包含 i 也不包含 j-1 的吗?实际上 dp[i][j-1] 包含所有以 i 开头和不以 i 开头的,我们要的是以 i 开头的。但更方便的方法是:我们不需要显式分类,可以用以下推导: 由于 s[i]==s[j] ,所以 s[i..j] 的回文子序列由以下部分组成: s[i+1..j-1] 的所有回文子序列(即不包含 i 和 j 的)。 在上述每个回文子序列首尾添加 s[i] 和 s[j] 得到的新回文子序列(包括空序列变成 s[i]s[j] )。 所以总数 = dp[i+1][j-1] + dp[i+1][j-1] = 2 * dp[i+1][j-1] ?但这样漏掉了一些:比如只包含 i 不包含 j 的,或只包含 j 不包含 i 的。实际上,只包含 i 不包含 j 的回文子序列,可以通过在 s[i+1..j-1] 的回文子序列前添加 s[i] 得到,但还需要保证整个是回文,这要求内部子序列是回文且以 s[i] 开头?不,只包含 i 不包含 j 的子序列,可以是单个 s[i] ,或者 s[i] 加上一个内部回文子序列(但内部子序列在 s[i+1..j-1] 中,且整个序列以 s[i] 开头,以某个内部字符结尾,要回文必须内部子序列是回文且整个序列对称,这要求内部子序列以 s[i] 结尾,但内部子序列不一定包含 s[i] 。所以这样分类很麻烦。 标准的状态转移方程是(参考经典问题“统计回文子序列个数”): dp[i][j] = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1] ,如果 s[i] != s[j] 。 如果 s[i] == s[j] ,则 dp[i][j] = dp[i+1][j] + dp[i][j-1] + 1 。 其中 dp[i][j] 表示子串 s[i..j] 中回文子序列的个数(包括空序列吗?)。验证一下: s = "ab" , dp[0][1] : s[0]!=s[1] ,所以 dp[0][1] = dp[1][1] + dp[0][0] - dp[1][0] 。设 dp[i][j] 包括空序列,则 dp[0][0]=2 , dp[1][1]=2 , dp[1][0]=1 (空序列)。所以 dp[0][1]=2+2-1=3 。实际回文子序列: "", "a", "b" ,共 3 个,正确。 s = "aa" , dp[0][1] = dp[1][1] + dp[0][0] + 1 = 2+2+1=5 。实际: "", "a", "a", "aa" 共 4 个,这里多了一个。所以公式不对,应该是 dp[i][j] = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1] 当 s[i]!=s[j] ;当 s[i]==s[j] 时, dp[i][j] = dp[i+1][j] + dp[i][j-1] + 1 会多计数。 实际上,当 s[i]==s[j] 时, dp[i+1][j] 包含了所有不包含 i 的回文子序列, dp[i][j-1] 包含了所有不包含 j 的回文子序列,两者交集是 dp[i+1][j-1] (既不包含 i 也不包含 j 的回文子序列)。另外,我们还需要加上同时包含 i 和 j 的回文子序列,这些序列的个数等于 dp[i+1][j-1] (因为对 s[i+1..j-1] 的每个回文子序列,添加首尾 i 和 j 后仍是回文)。所以总数 = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1] + dp[i+1][j-1] = dp[i+1][j] + dp[i][j-1] 。但这样又回到了之前的公式,对 "aa" 计算: dp[0][1] = dp[1][1] + dp[0][0] = 2+2=4 ,正确。所以正确的转移方程是: 如果 s[i] == s[j] : dp[i][j] = dp[i+1][j] + dp[i][j-1] + 1 ?不对,我们验证 "aaa" : n=3, dp[0][0]=2 , dp[1][1]=2 , dp[2][2]=2 。 dp[1][2] : s[1]==s[2] ,所以 dp[1][2] = dp[2][2] + dp[1][1] = 2+2=4 。实际序列: "", "a", "a", "aa" 共 4 个,正确。 dp[0][1] : s[0]==s[1] , dp[0][1] = dp[1][1] + dp[0][0] = 2+2=4 ,正确。 dp[0][2] : s[0]==s[2] , dp[0][2] = dp[1][2] + dp[0][1] = 4+4=8 。实际序列:列出 "aaa" 的所有回文子序列(包括空序列): "" , "a" (3 个), "aa" (3 个:下标 (0,1), (0,2), (1,2)), "aaa" (1 个),共 1+3+3+1=8 个,正确。 所以当 s[i]==s[j] 时, dp[i][j] = dp[i+1][j] + dp[i][j-1] 。 但注意,当 i+1 > j-1 时,即区间 s[i+1..j-1] 无效,此时 dp[i+1][j-1] 应设为 1(空序列)。在我们的公式中,当 s[i]==s[j] 时,我们没用到 dp[i+1][j-1] ,所以没问题。但考虑 i+1 == j 时,即 s[i]==s[j] 且相邻,如 "aa" , dp[i+1][j-1] 是 dp[j][i] ,即 i>j 的情况,我们应设 dp[i+1][j-1] = 1 。但在公式 dp[i][j] = dp[i+1][j] + dp[i][j-1] 中,不需要这个值。所以可以。 当 s[i] != s[j] 时, dp[i][j] = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1] 。 验证 "ab" : dp[0][1] = dp[1][1] + dp[0][0] - dp[1][0] 。 dp[1][0] 是无效区间,应视为 1(空序列)。所以 dp[0][1] = 2+2-1=3 ,正确。 步骤 4:初始化与计算顺序 初始化:对所有 i , dp[i][i] = 2 (空序列和单个字符序列)。对所有 i > j , dp[i][j] = 1 (只有空序列)。 计算顺序:按区间长度从小到大计算。长度从 2 到 n。 步骤 5:最终答案 dp[0][n-1] 就是整个字符串的回文子序列总数(包括空序列)。如果题目要求不包括空序列,则答案减 1。 步骤 6:模运算 由于结果可能很大,每次计算都要对 \(10^9+7\) 取模。注意减法可能产生负数,需要加模数再取模。 步骤 7:示例代码(C++风格伪代码) 但注意,当 i+1 > j-1 时, dp[i+1][j-1] 需要设为 1。所以在代码中,我们需要处理 i+1 > j-1 的情况,即当 i+1 > j-1 时, dp[i+1][j-1] 应为 1。在转移中,当 s[i]!=s[j] 且 len==2 时, i+1 = j , j-1 = i ,所以 i+1 > j-1 , dp[i+1][j-1] = dp[j][i] 未定义,我们应手动设为 1。可以在计算前初始化 dp[i][i-1] = 1 对所有 i。或者特判。 修改: 注意:当 s[i]!=s[j] 时,公式中的 dp[i+1][j-1] 我们已初始化,所以可以直接用。 步骤 8:验证例子 s = "aab" : n=3,初始化 dp[0][0]=2 , dp[1][1]=2 , dp[2][2]=2 ,其他 i>j 的为 1。 len=2: i=0,j=1: s[ 0]=='a', s[ 1]=='a',相等, dp[0][1] = dp[1][1] + dp[0][0] = 2+2=4 。 i=1,j=2: s[ 1]=='a', s[ 2]=='b',不等, dp[1][2] = dp[2][2] + dp[1][1] - dp[2][1] = 2+2-1=3 。 len=3: i=0,j=2: s[ 0]=='a', s[ 2]=='b',不等, dp[0][2] = dp[1][2] + dp[0][1] - dp[1][1] = 3+4-2=5 。 结果 dp[0][2]=5 ,包括空序列。实际回文子序列: "", "a", "a", "b", "aa" 共 5 个,正确。 最终答案 :返回 dp[0][n-1] 即可(包括空序列)。如果题目要求不包括空序列,则减 1。