最长回文子序列的变种:统计所有回文子序列的个数(进阶版:考虑重复子序列的统计)
字数 2625 2025-11-16 22:24:28

最长回文子序列的变种:统计所有回文子序列的个数(进阶版:考虑重复子序列的统计)

题目描述
给定一个字符串 s,统计其中所有不同的非空回文子序列的个数。注意,即使两个回文子序列的内容相同,但如果它们在原字符串中的位置不同(即起始和结束下标不同),也被视为不同的子序列。结果可能很大,需要对 10^9 + 7 取模。

例如,对于字符串 "bccb",不同的回文子序列包括:

  • "b"(位置0)、"b"(位置3)、"c"(位置1)、"c"(位置2)、"cc""bcb""bccb"
    总共有7个不同的回文子序列。

解题思路
这是一个线性动态规划问题,我们需要统计所有不同的回文子序列。为了避免重复计数,我们使用动态规划来记录以每个字符开头和结尾的回文子序列数量。关键在于处理相同字符的情况,确保不重复计算相同的子序列。

步骤详解

  1. 定义状态
    定义 dp[i][j] 为字符串 s 从索引 ij 的子串中,所有不同的回文子序列的个数。注意,这里统计的是不同的子序列,即使内容相同但位置不同也算不同。

  2. 状态转移方程
    考虑子串 s[i:j]

    • 如果 s[i] == s[j],那么我们可以将 s[i]s[j] 作为新的回文子序列的边界。此时,所有在 i+1j-1 之间的回文子序列都可以被扩展成新的回文子序列,同时还要加上 s[i]s[j] 自身形成的子序列。
    • 如果 s[i] != s[j],那么我们需要减去重复计算的部分,即 dp[i+1][j-1] 中的子序列。

    具体来说:

    • s[i] == s[j] 时,dp[i][j] = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1] + (dp[i+1][j-1] + 1)
      简化后:dp[i][j] = dp[i+1][j] + dp[i][j-1] + 1
    • s[i] != s[j] 时,dp[i][j] = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1]

    这里加1是因为当 s[i] == s[j] 时,s[i]s[j] 可以形成一个新的回文子序列。

  3. 初始化
    对于所有单个字符,即 i == j 的情况,dp[i][i] = 1,因为单个字符本身就是一个回文子序列。

  4. 计算顺序
    由于 dp[i][j] 依赖于 dp[i+1][j]dp[i][j-1]dp[i+1][j-1],我们需要从子串长度从小到大的顺序进行计算。即先计算所有长度为1的子串,然后是长度为2,依此类推,直到整个字符串。

  5. 结果提取
    最终结果是 dp[0][n-1],其中 n 是字符串 s 的长度。

示例演算
以字符串 "bccb" 为例:

  • 初始化:所有单个字符的 dp[i][i] = 1
  • 计算长度为2的子串:
    • "bc": s[0] != s[1],所以 dp[0][1] = dp[1][1] + dp[0][0] - dp[1][0] = 1 + 1 - 0 = 2(子序列:"b", "c"
    • "cc": s[1] == s[2],所以 dp[1][2] = dp[2][2] + dp[1][1] + 1 = 1 + 1 + 1 = 3(子序列:"c", "c", "cc"
    • "cb": s[2] != s[3],所以 dp[2][3] = dp[3][3] + dp[2][2] - dp[3][2] = 1 + 1 - 0 = 2(子序列:"c", "b"
  • 计算长度为3的子串:
    • "bcc": s[0] != s[2],所以 dp[0][2] = dp[1][2] + dp[0][1] - dp[1][1] = 3 + 2 - 1 = 4(子序列:"b", "c", "c", "cc", "bc", "cc"? 这里需要仔细去重)
    • "ccb": s[1] != s[3],所以 dp[1][3] = dp[2][3] + dp[1][2] - dp[2][2] = 2 + 3 - 1 = 4(子序列:"c", "c", "b", "cc", "cb", "ccb"?)
  • 计算整个字符串 "bccb":
    • s[0] == s[3],所以 dp[0][3] = dp[1][3] + dp[0][2] + 1 = 4 + 4 + 1 = 9,但实际只有7个不同的回文子序列。

通过这个例子可以看出,直接使用上述状态转移方程可能会重复计算一些子序列。因此,我们需要更精细的去重处理。

优化去重
s[i] == s[j] 时,我们需要确保不重复计算中间相同的子序列。具体来说,我们可以在 s[i+1]s[j-1] 之间找到第一个和最后一个与 s[i] 相同的字符,然后调整状态转移方程以避免重复。

修正后的状态转移方程:

  • s[i] == s[j] 时:
    • lefti+1j-1 之间第一个等于 s[i] 的索引,right 为最后一个等于 s[i] 的索引。
    • 如果 left > right,说明中间没有相同的字符,那么 dp[i][j] = 2 * dp[i+1][j-1] + 2
    • 如果 left == right,说明中间有一个相同的字符,那么 dp[i][j] = 2 * dp[i+1][j-1] + 1
    • 如果 left < right,说明中间有多个相同的字符,那么 dp[i][j] = 2 * dp[i+1][j-1] - dp[left+1][right-1]
  • s[i] != s[j] 时,dp[i][j] = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1]

这样,我们可以准确统计所有不同的回文子序列,而不会重复计算。

最终答案
通过上述动态规划过程,我们可以得到字符串 s 中所有不同的回文子序列的个数,并对 10^9 + 7 取模。

最长回文子序列的变种:统计所有回文子序列的个数(进阶版:考虑重复子序列的统计) 题目描述 给定一个字符串 s ,统计其中所有不同的非空回文子序列的个数。注意,即使两个回文子序列的内容相同,但如果它们在原字符串中的位置不同(即起始和结束下标不同),也被视为不同的子序列。结果可能很大,需要对 10^9 + 7 取模。 例如,对于字符串 "bccb" ,不同的回文子序列包括: "b" (位置0)、 "b" (位置3)、 "c" (位置1)、 "c" (位置2)、 "cc" 、 "bcb" 、 "bccb" 总共有7个不同的回文子序列。 解题思路 这是一个线性动态规划问题,我们需要统计所有不同的回文子序列。为了避免重复计数,我们使用动态规划来记录以每个字符开头和结尾的回文子序列数量。关键在于处理相同字符的情况,确保不重复计算相同的子序列。 步骤详解 定义状态 定义 dp[i][j] 为字符串 s 从索引 i 到 j 的子串中,所有不同的回文子序列的个数。注意,这里统计的是不同的子序列,即使内容相同但位置不同也算不同。 状态转移方程 考虑子串 s[i:j] : 如果 s[i] == s[j] ,那么我们可以将 s[i] 和 s[j] 作为新的回文子序列的边界。此时,所有在 i+1 到 j-1 之间的回文子序列都可以被扩展成新的回文子序列,同时还要加上 s[i] 和 s[j] 自身形成的子序列。 如果 s[i] != s[j] ,那么我们需要减去重复计算的部分,即 dp[i+1][j-1] 中的子序列。 具体来说: 当 s[i] == s[j] 时, dp[i][j] = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1] + (dp[i+1][j-1] + 1) 简化后: dp[i][j] = dp[i+1][j] + dp[i][j-1] + 1 当 s[i] != s[j] 时, dp[i][j] = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1] 这里加1是因为当 s[i] == s[j] 时, s[i] 和 s[j] 可以形成一个新的回文子序列。 初始化 对于所有单个字符,即 i == j 的情况, dp[i][i] = 1 ,因为单个字符本身就是一个回文子序列。 计算顺序 由于 dp[i][j] 依赖于 dp[i+1][j] 、 dp[i][j-1] 和 dp[i+1][j-1] ,我们需要从子串长度从小到大的顺序进行计算。即先计算所有长度为1的子串,然后是长度为2,依此类推,直到整个字符串。 结果提取 最终结果是 dp[0][n-1] ,其中 n 是字符串 s 的长度。 示例演算 以字符串 "bccb" 为例: 初始化:所有单个字符的 dp[i][i] = 1 。 计算长度为2的子串: "bc" : s[0] != s[1] ,所以 dp[0][1] = dp[1][1] + dp[0][0] - dp[1][0] = 1 + 1 - 0 = 2 (子序列: "b" , "c" ) "cc" : s[1] == s[2] ,所以 dp[1][2] = dp[2][2] + dp[1][1] + 1 = 1 + 1 + 1 = 3 (子序列: "c" , "c" , "cc" ) "cb" : s[2] != s[3] ,所以 dp[2][3] = dp[3][3] + dp[2][2] - dp[3][2] = 1 + 1 - 0 = 2 (子序列: "c" , "b" ) 计算长度为3的子串: "bcc" : s[0] != s[2] ,所以 dp[0][2] = dp[1][2] + dp[0][1] - dp[1][1] = 3 + 2 - 1 = 4 (子序列: "b" , "c" , "c" , "cc" , "bc" , "cc" ? 这里需要仔细去重) "ccb" : s[1] != s[3] ,所以 dp[1][3] = dp[2][3] + dp[1][2] - dp[2][2] = 2 + 3 - 1 = 4 (子序列: "c" , "c" , "b" , "cc" , "cb" , "ccb" ?) 计算整个字符串 "bccb" : s[0] == s[3] ,所以 dp[0][3] = dp[1][3] + dp[0][2] + 1 = 4 + 4 + 1 = 9 ,但实际只有7个不同的回文子序列。 通过这个例子可以看出,直接使用上述状态转移方程可能会重复计算一些子序列。因此,我们需要更精细的去重处理。 优化去重 当 s[i] == s[j] 时,我们需要确保不重复计算中间相同的子序列。具体来说,我们可以在 s[i+1] 到 s[j-1] 之间找到第一个和最后一个与 s[i] 相同的字符,然后调整状态转移方程以避免重复。 修正后的状态转移方程: 当 s[i] == s[j] 时: 设 left 为 i+1 到 j-1 之间第一个等于 s[i] 的索引, right 为最后一个等于 s[i] 的索引。 如果 left > right ,说明中间没有相同的字符,那么 dp[i][j] = 2 * dp[i+1][j-1] + 2 如果 left == right ,说明中间有一个相同的字符,那么 dp[i][j] = 2 * dp[i+1][j-1] + 1 如果 left < right ,说明中间有多个相同的字符,那么 dp[i][j] = 2 * dp[i+1][j-1] - dp[left+1][right-1] 当 s[i] != s[j] 时, dp[i][j] = dp[i+1][j] + dp[i][j-1] - dp[i+1][j-1] 这样,我们可以准确统计所有不同的回文子序列,而不会重复计算。 最终答案 通过上述动态规划过程,我们可以得到字符串 s 中所有不同的回文子序列的个数,并对 10^9 + 7 取模。