最长回文子序列的变种:统计回文子序列的个数(进阶版:考虑字符集限制,并统计长度至少为L的不同回文子序列个数)
字数 8454 2025-12-08 01:59:49

最长回文子序列的变种:统计回文子序列的个数(进阶版:考虑字符集限制,并统计长度至少为L的不同回文子序列个数)

这是一个关于最长回文子序列的进阶问题,我们需要统计给定字符串中所有不同的回文子序列的个数,但这里有一些额外条件:字符串由小写英文字母组成(字符集限制),并且我们只关心长度至少为L不同回文子序列。我们将详细、循序渐进地讲解解题思路和步骤。


第一步:问题理解与定义

题目描述
给定一个字符串 s(只包含小写字母,长度设为 n),以及一个正整数 L。要求统计 s 中所有不同的回文子序列的数量,其中每个回文子序列的长度必须至少为 L

注意

  1. 子序列:从原字符串中删除一些字符(也可以不删除)后,剩余的字符相对顺序保持不变形成的序列。
  2. 回文子序列:该子序列正读和反读相同。
  3. 不同:即使子序列在 s 中出现多次,只要内容相同,只算一种。
  4. 字符集限制:只有小写字母(26种字符),这会影响我们设计状态。
  5. 长度至少 L:最终统计时,只考虑长度 >= L 的子序列。

举例
s = "abca", L = 2
回文子序列有:

  • 长度为1: "a", "b", "c"(忽略,因为长度<2)
  • 长度为2: "aa", "bb", "cc"(其中"aa"出现2次,但只算1个),"ab"不是回文,"ac"不是,"bc"不是,"ba"不是... 实际上符合的:"aa", "bb", "cc" 都不是子序列?等一下,检查:"aa" 是子序列吗?取下标0和3的字符'a','a',是的。
  • 长度为3: "aba", "aca", "aaa"("aaa" 能组成吗?s中只有两个'a',所以不行)。"aba":取0,1,3(a,b,a)是回文,长度为3。
  • 长度为4: 整个"abca"不是回文。

但我们需要不同的回文子序列,长度>=2。
列出所有回文子序列(不同):
长度2: "aa"(下标0,3)、"bb"(下标1,1?不行,单个字符不算长度2,需要不同下标),"bb"不存在(只有一个b),"cc"不存在(只有一个c),所以只有"aa"。
长度3: "aba"(0,1,3),"aca"(0,2,3),"aaa"不存在。
长度4: 无。
所以不同回文子序列:{"aa", "aba", "aca"},共3个。

目标:编写算法高效计算这个数目。


第二步:基础思路

如果不考虑“不同”,只统计所有回文子序列的个数(包括重复位置但相同内容),那是一个经典DP问题:设 dp[i][j] 表示子串 s[i..j] 中回文子序列的个数(包含空序列吗?通常不包含,但我们只统计非空回文子序列)。转移方程是:

如果 s[i] == s[j]

  • 新增的回文子序列包括:s[i] + 子序列 + s[j],其中子序列来自 dp[i+1][j-1] 中的每一个回文子序列(包括空序列,对应长度为2的回文 s[i]s[j]),所以新增数量 = dp[i+1][j-1] + 1(这个+1是 s[i]s[j] 和可能只取两端的组合?实际上经典公式是:dp[i][j] = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1] + (s[i]==s[j] ? dp[i+1][j-1] + 1 : 0))。

但这样会重复计算相同内容但不同下标的子序列。比如 s = "aaa",子序列 "aa" 可以由多种下标组合得到,但内容相同。我们的目标是不同内容,所以需要去重。


第三步:去重与字符集利用

由于字符集只有26个小写字母,我们可以利用字符的最后一次出现位置来去重。
一个常用技巧是:
定义 dp[i][j] 表示在子串 s[i..j] 中,以字符 s[i] 开头(或任意字符) 的回文子序列个数?不,这样不便于去重。

更好方法是:定义 f[i][j] 为子串 s[i..j]不同的回文子序列个数(长度任意,包括空序列吗?我们最后会减去长度<L的部分)。
为了去重,我们考虑:当 s[i] == s[j] 时,如果中间有同样的字符,要避免重复计算。
标准解法(LeetCode 730 统计不同回文子序列)的DP定义是:

dp[i][j] 表示子串 s[i..j] 中不同的回文子序列个数(包括空序列吗?通常包括,但最后减去空序列和长度为1的)。
转移时,如果 s[i] != s[j],则 dp[i][j] = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1](容斥原理)。
如果 s[i] == s[j],假设字符是 ch = s[i],我们找到子串 s[i+1..j-1] 中左侧第一个 ch 的位置 left 和右侧第一个 ch 的位置 right(相对于 i+1 和 j-1 的索引)。

  • 如果 left > right,说明中间没有字符 ch,则新增的回文子序列是:chchXch 形式,其中 X 是 dp[i+1][j-1] 中的任意回文子序列,再加上 chchch(长度为2)。实际上公式是:dp[i][j] = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1] + dp[i+1][j-1] + 1 吗?需要细化。

更精确的(LeetCode 730 解法):

  1. 如果 s[i] != s[j]dp[i][j] = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1]
  2. 如果 s[i] == s[j],令 ch = s[i],找到 i < left <= right < j 使得 s[left] = chleft 最小,s[right] = chright 最大。
    • 如果 left 不存在(即中间没有相同字符),则 dp[i][j] = dp[i+1][j-1] * 2 + 2。这里的 +2 是新增 chchch 两个回文序列。
    • 如果 left == right(中间恰好一个相同字符),则 dp[i][j] = dp[i+1][j-1] * 2 + 1
    • 如果 left < right(中间至少两个相同字符),则 dp[i][j] = dp[i+1][j-1] * 2 - dp[left+1][right-1]

这个公式的目的是去重,确保相同的回文序列不重复计数。
但我们现在还需要考虑长度至少 L 的限制。


第四步:加入长度限制

我们可以修改 DP 状态,让它同时记录长度信息。但那样复杂度会变高(n^2 * L)。
另一种思路:先计算所有不同回文子序列(长度任意),但最后只累加长度 >= L 的。问题是我们 DP 的 dp[i][j] 是总数,没有按长度分类。

所以我们需要 DP 状态增加一维:长度。
定义 dp[i][j][k] 为子串 s[i..j] 中,长度为 k 的不同回文子序列的个数。
但 k 最大为 n,空间 O(n^3) 太大(n 可能 1000+),不可行。

观察到字符集很小(26),我们可以用另一种状态表示:
定义 f[len][c] 表示长度为 len,以字符 c 开头(和结尾)的不同回文子序列的个数。但中间部分不确定,难以转移。

更好的方法是:在原来的去重 DP 基础上,我们额外记录长度信息
原来 dp[i][j] 是一个整数(个数),现在我们让 dp[i][j] 变成一个长度为 n+1 的数组(或列表),dp[i][j][l] 表示子串 s[i..j] 中,长度为 l 的不同回文子序列个数。
但这样空间 O(n^3) 仍然大,但 n 较小时可行(比如 n <= 200)。如果 n 更大,需要优化。


第五步:优化思路(字符集小)

因为字符集只有 26 种,我们可以用另一种 DP:
定义 f[l][c1][c2] 表示长度为 l,开头字符为 c1,结尾字符为 c2 的不同回文子序列个数。但中间部分也需要匹配回文,这需要更复杂状态。

一个可行方法是:

  • 先预处理出每个字符在字符串中的出现位置列表。
  • 用区间 DP,状态为 (i, j, len),但我们可以用记忆化搜索,只计算必要的状态。
  • 由于要求不同,我们强制规定:对于一个回文子序列,我们取它在原字符串中最早出现的那组下标作为代表,避免重复。如何判断最早?可以定义:对于子序列,我们取它在 s 中所有可能下标组合中,字典序最小的一组下标。但这在 DP 中不容易直接体现。

实际上,LeetCode 730 的 DP 已经统计了所有不同回文子序列(任意长度),我们可以用类似的 DP,但把长度信息嵌入。


第六步:最终 DP 设计

我们定义 dp[i][j] 为一个长度为 (j-i+2) 的数组(或列表),dp[i][j][l] 表示在子串 s[i..j] 中,长度为 l不同回文子序列的个数。

转移方程(基于 LeetCode 730 的思路扩展):

  1. 如果 s[i] != s[j]
    dp[i][j][l] = dp[i+1][j][l] + dp[i][j-1][l] - dp[i+1][j-1][l](对于所有 l)。
    注意:长度为 0 的序列(空序列)我们不考虑,所以 l 从 1 开始。

  2. 如果 s[i] == s[j],设 ch = s[i],找到 left(i, j) 中大于 i 的最小索引使 s[left] = chright 为小于 j 的最大索引使 s[right] = ch

    • 如果 left 不存在(即中间没有相同字符):
      新增的回文序列是:
      a) 单个字符 ch(长度 1):dp[i][j][1] += 1(如果之前没有计数过,但这里要小心去重,单个字符在更小区间可能被计过,所以要用新计算)。
      更系统的做法是:
      我们先把 dp[i+1][j-1] 的所有长度序列搬过来,然后对每个长度 len,新增 dp[i+1][j-1][len] 个序列(两边加 ch,长度+2)。
      另外新增两个序列:ch(长1)和 chch(长2)。
      所以:
      dp[i][j][1] = 1(如果之前没有,实际上要合并 dp[i+1][j] 等,我们得用总公式)。
      实际上,经典总个数公式是:dp[i][j] = 2*dp[i+1][j-1] + 2
      对应到长度:新增长度 = 原长度+2,所以 dp[i][j][l] += dp[i+1][j-1][l-2](对 l>=2),再加上 l=1 和 l=2 各加1(如果没被前面区间算过)。
      但注意,dp[i+1][j-1] 中可能已经有长度为 1 的 ch,但那是子串 i+1..j-1 里的,我们现在是 i..j 区间,新加的 ch 是 s[i] 这个位置的字符,可能和之前的 ch 重复吗?不会,因为中间没有相同字符,所以这个 ch 是新的不同子序列(单个字符)。同样 chch 也是新的。
      所以公式:
      dp[i][j][l] = dp[i+1][j][l] + dp[i][j-1][l] - dp[i+1][j-1][l](先继承),
      然后对 l>=2:dp[i][j][l] += dp[i+1][j-1][l-2]
      然后 dp[i][j][1] += 1dp[i][j][2] += 1
      但这样会重复计算,因为 dp[i+1][j] 中可能已经包含单个 ch(如果 s[i+1]=ch),我们要去重。这就是为什么需要找 left, right 的原因。

    为了去重,我们分情况(基于 left, right 位置):

    • 如果 left 不存在:
      中间没有相同字符,则新增的序列是:chchch,以及所有 dp[i+1][j-1] 中的序列两边加 ch。
      所以:dp[i][j][l] = (dp[i+1][j][l] + dp[i][j-1][l] - dp[i+1][j-1][l]) + (l>=2 ? dp[i+1][j-1][l-2] : 0) + (l==1 ? 1 : 0) + (l==2 ? 1 : 0)
      但要注意,dp[i+1][j] 中可能已经有 ch 了(如果 s[j]=ch),但 s[i] 和 s[j] 相同,我们新增的 ch 和已有的可能重复,但因为我们定义“不同”是指内容,所以同一个字符 'a' 无论在哪个位置,单个 'a' 是同一个子序列。所以如果 dp[i+1][j] 中已经包含 'a',我们不能再加。
      所以更好的做法是:不去加 l=1 的 1,而是看当前区间是否第一次出现 'a' 这个字符作为单个字符子序列。实际上,单个字符子序列的个数就是区间内不同字符的个数。所以我们不能简单加1。

      为了统一,我们采用 LeetCode 730 的总数公式,然后拆分成长度分布。
      total[i][j] 为不同回文子序列总个数(长度任意)。
      公式:total[i][j] = total[i+1][j] + total[i][j-1] - total[i+1][j-1](如果 s[i]!=s[j])。
      如果 s[i]==s[j]:
      找 left, right:

      1. 如果 left 不存在:total[i][j] = total[i+1][j-1] * 2 + 2
      2. 如果 left == right:total[i][j] = total[i+1][j-1] * 2 + 1
      3. 如果 left < right:total[i][j] = total[i+1][j-1] * 2 - total[left+1][right-1]

      这个公式已经去重。我们需要把 total 拆分成按长度的计数。
      注意到:新增的部分主要是由 total[i+1][j-1] 中的每个回文子序列两边加 ch 得到的,所以长度+2。
      另外额外新增的 1 或 2 个回文,分别是长度为 1 的 ch 和长度为 2 的 chch。
      所以我们可以用类似 DP 推长度分布。


第七步:递推长度分布公式

lenDP[i][j][l] 表示区间 [i,j] 中长度为 l 的不同回文子序列个数。
我们也可以用递归/记忆化搜索实现。

递归思路
函数 dfs(i, j) 返回一个数组 cnt[1..n],表示区间 [i,j] 中不同回文子序列按长度的计数。
递归过程:

  1. 如果 i > j: 返回全 0 数组(没有子序列)。
  2. 如果 i == j: 只有 1 个长度为 1 的子序列(s[i]),所以 cnt[1] = 1,其他为 0。
  3. 否则,先计算 left = dfs(i+1, j), right = dfs(i, j-1), mid = dfs(i+1, j-1) 的计数数组。
  4. 如果 s[i] != s[j],则 cnt[l] = left[l] + right[l] - mid[l](注意去重,但这里 left 和 right 可能有重叠,重叠就是 mid 中的序列)。
  5. 如果 s[i] == s[j],找到 leftPos, rightPos:
    • 如果 leftPos 不存在:则新增的序列是:mid 中每个长度 l 的序列变成 l+2 的序列,再加一个长度为 1 的 ch 和一个长度为 2 的 chch。
      但要注意 mid 中可能已经有长度为 1 的 ch 吗?如果有,说明在更小区间已经有 ch 这个子序列,那么当前区间新增的长度 1 的 ch 和之前的是同一个,不能重复加。
      但我们的 mid 是区间 [i+1, j-1],如果中间有 ch,则 leftPos 存在,所以这种情况不会发生。
      所以:
      cnt[l] = left[l] + right[l] - mid[l](先合并)
      然后对 l >= 3: cnt[l] += mid[l-2](因为 l=2 时,mid[0] 不存在,我们单独处理 l=2)
      对于 l=2: cnt[2] += 1(新增 chch)
      对于 l=1: cnt[1] += 1(新增 ch)

    • 如果 leftPos == rightPos:
      中间只有一个 ch,那么新增的序列是:mid 中每个序列两边加 ch(长度+2),以及新增一个长度为 2 的 chch(但长度为 1 的 ch 已经存在,因为中间有一个 ch,所以 mid 中已有长度为 1 的 ch,不能再加)。
      所以:
      cnt[l] = left[l] + right[l] - mid[l]
      对 l>=3: cnt[l] += mid[l-2]
      对 l=2: cnt[2] += 1(新增 chch)

    • 如果 leftPos < rightPos:
      中间有至少两个 ch,那么新增的序列是:mid 中每个序列两边加 ch(长度+2),但要减去重复部分,即 leftPos+1 .. rightPos-1 区间内形成的回文子序列两边加 ch 得到的序列(因为这部分在更内层已经算过)。
      设 inner = dfs(leftPos+1, rightPos-1) 的计数数组。
      则 cnt[l] = left[l] + right[l] - mid[l]
      对 l>=3: cnt[l] += mid[l-2] - inner[l-2](注意 inner[l-2] 可能为0)
      对 l=2: cnt[2] += 1(新增 chch)
      对 l=1: 不新增,因为 ch 已经存在。

这样我们就得到了每个区间的长度分布。


第八步:计算答案

我们只需计算 dfs(0, n-1) 得到的数组,对 l >= L 的部分求和即可。


第九步:复杂度与优化

空间:O(n^2 * n) 在 n=200 时是 200^3=8e6,可能勉强,但我们可以用 short 类型存储计数,因为结果可能很大,需要取模。
时间:O(n^3) 在 n=200 时 8e6 可接受,但 n 再大就不行。
优化:由于字符集只有 26,我们可以预处理每个字符的下一个和上一个出现位置,这样找 leftPos, rightPos 可以 O(1)。但 DP 仍需 O(n^3)。

如果 n 较大(如 1000),我们可以用另一种思路:
定义 dp[l][i] 表示以 i 开头、长度为 l 的回文子序列的个数,然后用字符位置转移,但去重困难。

在竞赛中,通常 n<=200,O(n^3) 可接受。


第十步:举例验证

s = "abca", L=2

区间 [0,3]:
s[0]=a, s[3]=a 相等,找 leftPos: 下一个 a 在位置 3(但 leftPos 要在 (0,3) 内),没有,所以 leftPos 不存在。
mid = dfs(1,2) = 区间 "bc" 的回文子序列:b, c 两个长度为1的。
所以 mid[1]=2, mid[2]=0, ...
cnt[l] 先由 left=[1,2], right=[0,2] 合并计算(略)。
然后新增:
l>=3: cnt[l] += mid[l-2] → 没有。
l=2: cnt[2] += 1(新增 "aa")
l=1: cnt[1] += 1(新增 "a")

但注意区间 [0,2] 和 [1,3] 等会贡献其他回文。
最终我们得到 cnt[2]=1 ("aa"), cnt[3]=2 ("aba","aca"),长度>=2 的总数=1+2=3,与手工计算一致。


总结

我们通过扩展经典统计不同回文子序列的 DP,加入长度维度,按照字符匹配情况分三类递推长度分布,最终求和长度 >= L 的部分,得到答案。这个算法在字符集小、n 不太大时可行,时间复杂度 O(n^3),空间 O(n^3) 可优化到 O(n^2) 如果只存必要区间长度数组。

最长回文子序列的变种:统计回文子序列的个数(进阶版:考虑字符集限制,并统计长度至少为L的不同回文子序列个数) 这是一个关于最长回文子序列的进阶问题,我们需要统计给定字符串中所有不同的回文子序列的个数,但这里有一些额外条件:字符串由小写英文字母组成(字符集限制),并且我们只关心 长度至少为L 的 不同 回文子序列。我们将详细、循序渐进地讲解解题思路和步骤。 第一步:问题理解与定义 题目描述 给定一个字符串 s (只包含小写字母,长度设为 n ),以及一个正整数 L 。要求统计 s 中所有 不同 的回文子序列的数量,其中每个回文子序列的长度必须 至少为 L 。 注意 : 子序列:从原字符串中删除一些字符(也可以不删除)后,剩余的字符相对顺序保持不变形成的序列。 回文子序列:该子序列正读和反读相同。 不同:即使子序列在 s 中出现多次,只要内容相同,只算一种。 字符集限制:只有小写字母(26种字符),这会影响我们设计状态。 长度至少 L:最终统计时,只考虑长度 >= L 的子序列。 举例 : s = "abca" , L = 2 回文子序列有: 长度为1: "a", "b", "c"(忽略,因为长度 <2) 长度为2: "aa", "bb", "cc"(其中"aa"出现2次,但只算1个),"ab"不是回文,"ac"不是,"bc"不是,"ba"不是... 实际上符合的:"aa", "bb", "cc" 都不是子序列?等一下,检查:"aa" 是子序列吗?取下标0和3的字符'a','a',是的。 长度为3: "aba", "aca", "aaa"("aaa" 能组成吗?s中只有两个'a',所以不行)。"aba":取0,1,3(a,b,a)是回文,长度为3。 长度为4: 整个"abca"不是回文。 但我们需要 不同 的回文子序列,长度>=2。 列出所有回文子序列(不同): 长度2: "aa"(下标0,3)、"bb"(下标1,1?不行,单个字符不算长度2,需要不同下标),"bb"不存在(只有一个b),"cc"不存在(只有一个c),所以只有"aa"。 长度3: "aba"(0,1,3),"aca"(0,2,3),"aaa"不存在。 长度4: 无。 所以不同回文子序列:{"aa", "aba", "aca"},共3个。 目标:编写算法高效计算这个数目。 第二步:基础思路 如果不考虑“不同”,只统计所有回文子序列的个数(包括重复位置但相同内容),那是一个经典DP问题:设 dp[i][j] 表示子串 s[i..j] 中回文子序列的个数(包含空序列吗?通常不包含,但我们只统计非空回文子序列)。转移方程是: 如果 s[i] == s[j] : 新增的回文子序列包括: s[i] + 子序列 + s[j] ,其中子序列来自 dp[i+1][j-1] 中的每一个回文子序列(包括空序列,对应长度为2的回文 s[i]s[j] ),所以新增数量 = dp[i+1][j-1] + 1 (这个+1是 s[i]s[j] 和可能只取两端的组合?实际上经典公式是: dp[i][j] = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1] + (s[i]==s[j] ? dp[i+1][j-1] + 1 : 0) )。 但这样会重复计算相同内容但不同下标的子序列。比如 s = "aaa" ,子序列 "aa" 可以由多种下标组合得到,但内容相同。我们的目标是 不同 内容,所以需要去重。 第三步:去重与字符集利用 由于字符集只有26个小写字母,我们可以利用字符的最后一次出现位置来去重。 一个常用技巧是: 定义 dp[i][j] 表示在子串 s[i..j] 中, 以字符 s[i] 开头(或任意字符) 的回文子序列个数?不,这样不便于去重。 更好方法是:定义 f[i][j] 为子串 s[i..j] 中 不同的 回文子序列个数(长度任意,包括空序列吗?我们最后会减去长度 <L的部分)。 为了去重,我们考虑:当 s[i] == s[j] 时,如果中间有同样的字符,要避免重复计算。 标准解法(LeetCode 730 统计不同回文子序列)的DP定义是: dp[i][j] 表示子串 s[i..j] 中不同的回文子序列个数(包括空序列吗?通常包括,但最后减去空序列和长度为1的)。 转移时,如果 s[i] != s[j] ,则 dp[i][j] = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1] (容斥原理)。 如果 s[i] == s[j] ,假设字符是 ch = s[i] ,我们找到子串 s[i+1..j-1] 中左侧第一个 ch 的位置 left 和右侧第一个 ch 的位置 right (相对于 i+1 和 j-1 的索引)。 如果 left > right ,说明中间没有字符 ch ,则新增的回文子序列是: ch 和 chXch 形式,其中 X 是 dp[i+1][j-1] 中的任意回文子序列,再加上 ch 和 chch (长度为2)。实际上公式是: dp[i][j] = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1] + dp[i+1][j-1] + 1 吗?需要细化。 更精确的(LeetCode 730 解法): 如果 s[i] != s[j] , dp[i][j] = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1] 。 如果 s[i] == s[j] ,令 ch = s[i] ,找到 i < left <= right < j 使得 s[left] = ch 且 left 最小, s[right] = ch 且 right 最大。 如果 left 不存在(即中间没有相同字符),则 dp[i][j] = dp[i+1][j-1] * 2 + 2 。这里的 +2 是新增 ch 和 chch 两个回文序列。 如果 left == right (中间恰好一个相同字符),则 dp[i][j] = dp[i+1][j-1] * 2 + 1 。 如果 left < right (中间至少两个相同字符),则 dp[i][j] = dp[i+1][j-1] * 2 - dp[left+1][right-1] 。 这个公式的目的是去重,确保相同的回文序列不重复计数。 但我们现在还需要考虑 长度至少 L 的限制。 第四步:加入长度限制 我们可以修改 DP 状态,让它同时记录长度信息。但那样复杂度会变高(n^2 * L)。 另一种思路:先计算所有不同回文子序列(长度任意),但最后只累加长度 >= L 的。问题是我们 DP 的 dp[i][j] 是总数,没有按长度分类。 所以我们需要 DP 状态增加一维:长度。 定义 dp[i][j][k] 为子串 s[i..j] 中,长度为 k 的不同回文子序列的个数。 但 k 最大为 n,空间 O(n^3) 太大(n 可能 1000+),不可行。 观察到字符集很小(26),我们可以用另一种状态表示: 定义 f[len][c] 表示长度为 len ,以字符 c 开头(和结尾)的不同回文子序列的个数。但中间部分不确定,难以转移。 更好的方法是:在原来的去重 DP 基础上,我们额外记录 长度信息 。 原来 dp[i][j] 是一个整数(个数),现在我们让 dp[i][j] 变成一个长度为 n+1 的数组(或列表), dp[i][j][l] 表示子串 s[i..j] 中,长度为 l 的不同回文子序列个数。 但这样空间 O(n^3) 仍然大,但 n 较小时可行(比如 n <= 200)。如果 n 更大,需要优化。 第五步:优化思路(字符集小) 因为字符集只有 26 种,我们可以用另一种 DP: 定义 f[l][c1][c2] 表示长度为 l ,开头字符为 c1 ,结尾字符为 c2 的不同回文子序列个数。但中间部分也需要匹配回文,这需要更复杂状态。 一个可行方法是: 先预处理出每个字符在字符串中的出现位置列表。 用区间 DP,状态为 (i, j, len) ,但我们可以用记忆化搜索,只计算必要的状态。 由于要求不同,我们强制规定:对于一个回文子序列,我们取它在原字符串中 最早出现的那组下标 作为代表,避免重复。如何判断最早?可以定义:对于子序列,我们取它在 s 中所有可能下标组合中,字典序最小的一组下标。但这在 DP 中不容易直接体现。 实际上,LeetCode 730 的 DP 已经统计了所有不同回文子序列(任意长度),我们可以用类似的 DP,但把长度信息嵌入。 第六步:最终 DP 设计 我们定义 dp[i][j] 为一个长度为 (j-i+2) 的数组(或列表), dp[i][j][l] 表示在子串 s[i..j] 中,长度为 l 的 不同 回文子序列的个数。 转移方程(基于 LeetCode 730 的思路扩展): 如果 s[i] != s[j] : dp[i][j][l] = dp[i+1][j][l] + dp[i][j-1][l] - dp[i+1][j-1][l] (对于所有 l)。 注意:长度为 0 的序列(空序列)我们不考虑,所以 l 从 1 开始。 如果 s[i] == s[j] ,设 ch = s[i] ,找到 left 为 (i, j) 中大于 i 的最小索引使 s[left] = ch , right 为小于 j 的最大索引使 s[right] = ch 。 如果 left 不存在(即中间没有相同字符): 新增的回文序列是: a) 单个字符 ch (长度 1): dp[i][j][1] += 1 (如果之前没有计数过,但这里要小心去重,单个字符在更小区间可能被计过,所以要用新计算)。 更系统的做法是: 我们先把 dp[i+1][j-1] 的所有长度序列搬过来,然后对每个长度 len ,新增 dp[i+1][j-1][len] 个序列(两边加 ch,长度+2)。 另外新增两个序列: ch (长1)和 chch (长2)。 所以: dp[i][j][1] = 1 (如果之前没有,实际上要合并 dp[i+1][j] 等,我们得用总公式)。 实际上,经典总个数公式是: dp[i][j] = 2*dp[i+1][j-1] + 2 。 对应到长度:新增长度 = 原长度+2,所以 dp[i][j][l] += dp[i+1][j-1][l-2] (对 l>=2),再加上 l=1 和 l=2 各加1(如果没被前面区间算过)。 但注意, dp[i+1][j-1] 中可能已经有长度为 1 的 ch ,但那是子串 i+1..j-1 里的,我们现在是 i..j 区间,新加的 ch 是 s[ i] 这个位置的字符,可能和之前的 ch 重复吗?不会,因为中间没有相同字符,所以这个 ch 是新的不同子序列(单个字符)。同样 chch 也是新的。 所以公式: dp[i][j][l] = dp[i+1][j][l] + dp[i][j-1][l] - dp[i+1][j-1][l] (先继承), 然后对 l>=2: dp[i][j][l] += dp[i+1][j-1][l-2] , 然后 dp[i][j][1] += 1 , dp[i][j][2] += 1 。 但这样会重复计算,因为 dp[i+1][j] 中可能已经包含单个 ch (如果 s[ i+1 ]=ch),我们要去重。这就是为什么需要找 left, right 的原因。 为了去重,我们分情况(基于 left, right 位置): 如果 left 不存在: 中间没有相同字符,则新增的序列是: ch 和 chch ,以及所有 dp[i+1][j-1] 中的序列两边加 ch。 所以: dp[i][j][l] = (dp[i+1][j][l] + dp[i][j-1][l] - dp[i+1][j-1][l]) + (l>=2 ? dp[i+1][j-1][l-2] : 0) + (l==1 ? 1 : 0) + (l==2 ? 1 : 0) 。 但要注意, dp[i+1][j] 中可能已经有 ch 了(如果 s[ j]=ch),但 s[ i] 和 s[ j] 相同,我们新增的 ch 和已有的可能重复,但因为我们定义“不同”是指内容,所以同一个字符 'a' 无论在哪个位置,单个 'a' 是同一个子序列。所以如果 dp[i+1][j] 中已经包含 'a',我们不能再加。 所以更好的做法是:不去加 l=1 的 1,而是看当前区间是否第一次出现 'a' 这个字符作为单个字符子序列。实际上,单个字符子序列的个数就是区间内不同字符的个数。所以我们不能简单加1。 为了统一,我们采用 LeetCode 730 的总数公式,然后拆分成长度分布。 设 total[i][j] 为不同回文子序列总个数(长度任意)。 公式: total[i][j] = total[i+1][j] + total[i][j-1] - total[i+1][j-1] (如果 s[ i]!=s[ j ])。 如果 s[ i]==s[ j ]: 找 left, right: 如果 left 不存在: total[i][j] = total[i+1][j-1] * 2 + 2 如果 left == right: total[i][j] = total[i+1][j-1] * 2 + 1 如果 left < right: total[i][j] = total[i+1][j-1] * 2 - total[left+1][right-1] 这个公式已经去重。我们需要把 total 拆分成按长度的计数。 注意到:新增的部分主要是由 total[i+1][j-1] 中的每个回文子序列两边加 ch 得到的,所以长度+2。 另外额外新增的 1 或 2 个回文,分别是长度为 1 的 ch 和长度为 2 的 chch。 所以我们可以用类似 DP 推长度分布。 第七步:递推长度分布公式 设 lenDP[i][j][l] 表示区间 [ i,j ] 中长度为 l 的不同回文子序列个数。 我们也可以用递归/记忆化搜索实现。 递归思路 : 函数 dfs(i, j) 返回一个数组 cnt[1..n] ,表示区间 [ i,j ] 中不同回文子序列按长度的计数。 递归过程: 如果 i > j: 返回全 0 数组(没有子序列)。 如果 i == j: 只有 1 个长度为 1 的子序列(s[ i]),所以 cnt[ 1 ] = 1,其他为 0。 否则,先计算 left = dfs(i+1, j) , right = dfs(i, j-1) , mid = dfs(i+1, j-1) 的计数数组。 如果 s[ i] != s[ j],则 cnt[ l] = left[ l] + right[ l] - mid[ l ](注意去重,但这里 left 和 right 可能有重叠,重叠就是 mid 中的序列)。 如果 s[ i] == s[ j ],找到 leftPos, rightPos: 如果 leftPos 不存在:则新增的序列是:mid 中每个长度 l 的序列变成 l+2 的序列,再加一个长度为 1 的 ch 和一个长度为 2 的 chch。 但要注意 mid 中可能已经有长度为 1 的 ch 吗?如果有,说明在更小区间已经有 ch 这个子序列,那么当前区间新增的长度 1 的 ch 和之前的是同一个,不能重复加。 但我们的 mid 是区间 [ i+1, j-1 ],如果中间有 ch,则 leftPos 存在,所以这种情况不会发生。 所以: cnt[ l] = left[ l] + right[ l] - mid[ l ](先合并) 然后对 l >= 3: cnt[ l] += mid[ l-2](因为 l=2 时,mid[ 0 ] 不存在,我们单独处理 l=2) 对于 l=2: cnt[ 2 ] += 1(新增 chch) 对于 l=1: cnt[ 1 ] += 1(新增 ch) 如果 leftPos == rightPos: 中间只有一个 ch,那么新增的序列是:mid 中每个序列两边加 ch(长度+2),以及新增一个长度为 2 的 chch(但长度为 1 的 ch 已经存在,因为中间有一个 ch,所以 mid 中已有长度为 1 的 ch,不能再加)。 所以: cnt[ l] = left[ l] + right[ l] - mid[ l ] 对 l>=3: cnt[ l] += mid[ l-2 ] 对 l=2: cnt[ 2 ] += 1(新增 chch) 如果 leftPos < rightPos: 中间有至少两个 ch,那么新增的序列是:mid 中每个序列两边加 ch(长度+2),但要减去重复部分,即 leftPos+1 .. rightPos-1 区间内形成的回文子序列两边加 ch 得到的序列(因为这部分在更内层已经算过)。 设 inner = dfs(leftPos+1, rightPos-1) 的计数数组。 则 cnt[ l] = left[ l] + right[ l] - mid[ l ] 对 l>=3: cnt[ l] += mid[ l-2] - inner[ l-2](注意 inner[ l-2 ] 可能为0) 对 l=2: cnt[ 2 ] += 1(新增 chch) 对 l=1: 不新增,因为 ch 已经存在。 这样我们就得到了每个区间的长度分布。 第八步:计算答案 我们只需计算 dfs(0, n-1) 得到的数组,对 l >= L 的部分求和即可。 第九步:复杂度与优化 空间:O(n^2 * n) 在 n=200 时是 200^3=8e6,可能勉强,但我们可以用 short 类型存储计数,因为结果可能很大,需要取模。 时间:O(n^3) 在 n=200 时 8e6 可接受,但 n 再大就不行。 优化:由于字符集只有 26,我们可以预处理每个字符的下一个和上一个出现位置,这样找 leftPos, rightPos 可以 O(1)。但 DP 仍需 O(n^3)。 如果 n 较大(如 1000),我们可以用另一种思路: 定义 dp[l][i] 表示以 i 开头、长度为 l 的回文子序列的个数,然后用字符位置转移,但去重困难。 在竞赛中,通常 n <=200,O(n^3) 可接受。 第十步:举例验证 s = "abca" , L=2 。 区间 [ 0,3 ]: s[ 0]=a, s[ 3 ]=a 相等,找 leftPos: 下一个 a 在位置 3(但 leftPos 要在 (0,3) 内),没有,所以 leftPos 不存在。 mid = dfs(1,2) = 区间 "bc" 的回文子序列:b, c 两个长度为1的。 所以 mid[ 1]=2, mid[ 2 ]=0, ... cnt[ l] 先由 left=[ 1,2], right=[ 0,2 ] 合并计算(略)。 然后新增: l>=3: cnt[ l] += mid[ l-2 ] → 没有。 l=2: cnt[ 2 ] += 1(新增 "aa") l=1: cnt[ 1 ] += 1(新增 "a") 但注意区间 [ 0,2] 和 [ 1,3 ] 等会贡献其他回文。 最终我们得到 cnt[ 2]=1 ("aa"), cnt[ 3 ]=2 ("aba","aca"),长度>=2 的总数=1+2=3,与手工计算一致。 总结 我们通过扩展经典统计不同回文子序列的 DP,加入长度维度,按照字符匹配情况分三类递推长度分布,最终求和长度 >= L 的部分,得到答案。这个算法在字符集小、n 不太大时可行,时间复杂度 O(n^3),空间 O(n^3) 可优化到 O(n^2) 如果只存必要区间长度数组。