统计回文子串的个数问题(进阶版:包含通配符的回文计数)
字数 2401 2025-12-09 02:34:24

统计回文子串的个数问题(进阶版:包含通配符的回文计数)

这是一个区间动态规划的进阶问题,它要求统计一个字符串中所有回文子串的数量,但字符串中可能包含通配符?,它可以匹配任意单个字符。我们需要统计满足回文定义的非空子串的个数。

问题描述

给你一个字符串 s,其长度为 n,由小写字母和通配符 ? 组成。你需要计算并返回 s回文子串的个数。如果一个字符串正着读和反着读是一样的,那么我们称其为回文串。当字符串中包含 ? 时,? 可以视为任意一个小写字母,因此它可以帮助构成回文。例如:

  • 输入: s = "a?a"
  • 解释: 子串有 "a", "?", "a", "a?", "?a", "a?a"。
  • 其中是回文的有:"a" (位置0), "a" (位置2), "a?" (?视为'a'时, "aa"是回文), "?a" (?视为'a'时, "aa"是回文), "a?a" (?视为任意字符,两边都是'a',所以是回文)。
  • 输出: 5
    题目要求计算所有这样的子串数量。

解题过程

  1. 定义状态
    区间DP的核心是定义 dp[i][j] 表示子串 s[i..j](包含i和j)是否是回文串。它是一个布尔值(True/False)。
    但我们的目标是计数,而不是判断。一个直接的思路是:先计算出所有 dp[i][j],然后遍历所有 i<=j,统计 dp[i][j] 为 True 的个数。这就是基础的回文子串计数问题的动态规划解法。

  2. 状态转移方程
    如何知道 s[i..j] 是否是回文?

    • 基础情况:当子串长度为1(即 i == j)时,单个字符(包括?)自然是回文。dp[i][i] = True
    • 当子串长度为2(即 j == i+1)时,如果两个字符相等,或者其中至少有一个是?,那么它们可以匹配成回文。即,如果 s[i] == s[j]s[i] == '?'s[j] == '?',则 dp[i][j] = True
    • 当子串长度大于2(即 j > i+1)时,s[i..j] 是回文的充分必要条件是:
      1. 两端的字符能匹配:即 s[i] == s[j]s[i] == '?'s[j] == '?'
      2. 去掉两端后的内部子串 s[i+1..j-1] 也必须是回文,即 dp[i+1][j-1] 为 True。
        所以状态转移方程为:dp[i][j] = (s[i]和s[j]能匹配) and dp[i+1][j-1]
  3. 计算顺序
    由于 dp[i][j] 依赖于 dp[i+1][j-1](即更短的、更靠近中心的子串),我们不能简单地按i从0到n-1,j从0到n-1遍历。正确的顺序是按子串长度从小到大进行枚举。

    • 首先,初始化所有长度为1的子串(dp[i][i] = True)。
    • 然后,枚举长度 L 从 2 到 n:
      • 对于每个起始位置 i(从0到 n-L):
        • 计算结束位置 j = i + L - 1
        • 如果 L == 2,根据上述基础情况判断。
        • 如果 L > 2,根据状态转移方程判断。
  4. 统计结果
    在填充 dp 表的过程中,每当我们将一个 dp[i][j] 设置为 True,就将计数器 ans 加1。因为每个 dp[i][j] 对应一个唯一的子串 s[i..j]

  5. 示例推演
    s = "a?a" 为例 (n=3):

    • 初始化 dp 表 (3x3),ans=0
    • 长度 L=1: dp[0][0]=True (子串"a"),ans=1dp[1][1]=True (子串"?"),ans=2dp[2][2]=True (子串"a"),ans=3
    • 长度 L=2:
      • i=0, j=1: s[0]='a', s[1]='?',能匹配 -> dp[0][1]=True (子串"a?"),ans=4
      • i=1, j=2: s[1]='?', s[2]='a',能匹配 -> dp[1][2]=True (子串"?a"),ans=5
    • 长度 L=3:
      • i=0, j=2: 首先检查两端 s[0]='a' 和 s[2]='a' 能匹配。然后检查内部 dp[1][1] 是 True。所以 dp[0][2]=True (子串"a?a"),ans=6
        最终结果 ans=6。注意,我们示例解释中手动枚举得到5,那是因为在手动枚举时,我们把 "?" 这个子串本身单独考虑时,没有把它当作一个有效的回文(因为它单独一个?可以代表任意字符,所以本身也是回文)。按照题目对?的定义,"?" 是回文。所以DP得到的6是正确的("a", "?", "a", "a?", "?a", "a?a")。

算法复杂度

  • 时间复杂度:O(n²),因为我们需要填充一个 n x n 的DP表。
  • 空间复杂度:O(n²),用于存储DP表。可以通过滚动数组优化到O(n),但代码会更复杂,且本问题中n通常不会大到必须优化。

关键点总结

  1. 通配符的处理:在状态转移的条件判断中,? 被视为可以与任何字符(包括另一个?)匹配。这体现在 s[i]和s[j]能匹配 的条件是 s[i] == s[j]s[i] == '?'s[j] == '?'
  2. 递推顺序:必须按长度递增的顺序计算,确保在计算长区间时,它所依赖的更短区间已经计算完毕。
  3. 结果累加:在DP过程中直接累加计数,避免最后再遍历一次DP表。

这个解法清晰地将基础回文子串计数问题扩展到了包含通配符的情况,是区间DP处理带通配符匹配问题的典型应用。

统计回文子串的个数问题(进阶版:包含通配符的回文计数) 这是一个区间动态规划的进阶问题,它要求统计一个字符串中所有回文子串的数量,但字符串中可能包含通配符 ? ,它可以匹配任意单个字符。我们需要统计满足回文定义的非空子串的个数。 问题描述 给你一个字符串 s ,其长度为 n ,由小写字母和通配符 ? 组成。你需要计算并返回 s 中 回文子串 的个数。如果一个字符串正着读和反着读是一样的,那么我们称其为回文串。当字符串中包含 ? 时, ? 可以视为任意一个小写字母,因此它可以帮助构成回文。例如: 输入: s = "a?a" 解释: 子串有 "a", "?", "a", "a?", "?a", "a?a"。 其中是回文的有:"a" (位置0), "a" (位置2), "a?" (?视为'a'时, "aa"是回文), "?a" (?视为'a'时, "aa"是回文), "a?a" (?视为任意字符,两边都是'a',所以是回文)。 输出: 5 题目要求计算所有这样的子串数量。 解题过程 定义状态 区间DP的核心是定义 dp[i][j] 表示子串 s[i..j] (包含i和j)是否是回文串。它是一个布尔值(True/False)。 但我们的目标是计数,而不是判断。一个直接的思路是:先计算出所有 dp[i][j] ,然后遍历所有 i<=j ,统计 dp[i][j] 为 True 的个数。这就是基础的回文子串计数问题的动态规划解法。 状态转移方程 如何知道 s[i..j] 是否是回文? 基础情况 :当子串长度为1(即 i == j )时,单个字符(包括 ? )自然是回文。 dp[i][i] = True 。 当子串长度为2(即 j == i+1 )时,如果两个字符相等,或者其中至少有一个是 ? ,那么它们可以匹配成回文。即,如果 s[i] == s[j] 或 s[i] == '?' 或 s[j] == '?' ,则 dp[i][j] = True 。 当子串长度大于2(即 j > i+1 )时, s[i..j] 是回文的 充分必要条件 是: 两端的字符能匹配:即 s[i] == s[j] 或 s[i] == '?' 或 s[j] == '?' 。 去掉两端后的内部子串 s[i+1..j-1] 也必须是回文,即 dp[i+1][j-1] 为 True。 所以状态转移方程为: dp[i][j] = (s[i]和s[j]能匹配) and dp[i+1][j-1] 。 计算顺序 由于 dp[i][j] 依赖于 dp[i+1][j-1] (即更短的、更靠近中心的子串),我们不能简单地按i从0到n-1,j从0到n-1遍历。正确的顺序是 按子串长度从小到大 进行枚举。 首先,初始化所有长度为1的子串( dp[i][i] = True )。 然后,枚举长度 L 从 2 到 n: 对于每个起始位置 i (从0到 n-L): 计算结束位置 j = i + L - 1 。 如果 L == 2 ,根据上述基础情况判断。 如果 L > 2 ,根据状态转移方程判断。 统计结果 在填充 dp 表的过程中,每当我们将一个 dp[i][j] 设置为 True,就将计数器 ans 加1。因为每个 dp[i][j] 对应一个唯一的子串 s[i..j] 。 示例推演 以 s = "a?a" 为例 (n=3): 初始化 dp 表 (3x3), ans=0 。 长度 L=1: dp[0][0]=True (子串"a"), ans=1 ; dp[1][1]=True (子串"?"), ans=2 ; dp[2][2]=True (子串"a"), ans=3 。 长度 L=2: i=0, j=1: s[ 0]='a', s[ 1]='?',能匹配 -> dp[0][1]=True (子串"a?"), ans=4 。 i=1, j=2: s[ 1]='?', s[ 2]='a',能匹配 -> dp[1][2]=True (子串"?a"), ans=5 。 长度 L=3: i=0, j=2: 首先检查两端 s[ 0]='a' 和 s[ 2]='a' 能匹配。然后检查内部 dp[1][1] 是 True。所以 dp[0][2]=True (子串"a?a"), ans=6 。 最终结果 ans=6 。注意,我们示例解释中手动枚举得到5,那是因为在手动枚举时,我们把 "?" 这个子串本身单独考虑时,没有把它当作一个有效的回文(因为它单独一个 ? 可以代表任意字符,所以本身也是回文)。按照题目对 ? 的定义, "?" 是回文。所以DP得到的6是正确的("a", "?", "a", "a?", "?a", "a?a")。 算法复杂度 时间复杂度:O(n²),因为我们需要填充一个 n x n 的DP表。 空间复杂度:O(n²),用于存储DP表。可以通过滚动数组优化到O(n),但代码会更复杂,且本问题中n通常不会大到必须优化。 关键点总结 通配符的处理 :在状态转移的条件判断中, ? 被视为可以与任何字符(包括另一个 ? )匹配。这体现在 s[i]和s[j]能匹配 的条件是 s[i] == s[j] 或 s[i] == '?' 或 s[j] == '?' 。 递推顺序 :必须按长度递增的顺序计算,确保在计算长区间时,它所依赖的更短区间已经计算完毕。 结果累加 :在DP过程中直接累加计数,避免最后再遍历一次DP表。 这个解法清晰地将基础回文子串计数问题扩展到了包含通配符的情况,是区间DP处理带通配符匹配问题的典型应用。