区间动态规划例题:统计一个字符串中“回文子串”的总数
字数 2324 2025-12-11 12:38:23

好的,我们来看一个你列表中尚未出现的区间动态规划问题。

区间动态规划例题:统计一个字符串中“回文子串”的总数


题目描述

给定一个字符串 s,请你统计并返回这个字符串中 回文子串 的总数。

回文子串 是正着读和倒着读都一样的连续子字符串。

例如:

  • 输入:s = "abc"
    输出:3
    解释:三个回文子串:"a", "b", "c"
  • 输入:s = "aaa"
    输出:6
    解释:六个回文子串:"a", "a", "a", "aa", "aa", "aaa"

题目要求我们高效地计算总数。字符串长度 n 最大可达 1000,因此我们需要一个 O(n^2) 的解法。


循序渐进解题过程

这个问题虽然可以用中心扩展法在 O(n^2) 时间、O(1) 空间内解决,但为了深入理解 区间动态规划 的思想,我们将使用 DP 表 来系统化地判断所有可能的子串是否为回文。

第一步:定义 DP 状态

区间 DP 的核心思想是:dp[i][j] 表示字符串 s 中从索引 i 到索引 j 的子串(闭区间 [i, j])是否为回文串

那么:

  • dp[i][j] = true 表示 s[i...j] 是回文串。
  • dp[i][j] = false 表示 s[i...j] 不是回文串。
  • 最终,我们只需统计所有 dp[i][j] = true(i, j) 对的数量即可。

第二步:寻找状态转移方程

一个子串 s[i...j] 是回文串,需要满足两个条件:

  1. 两端的字符相等s[i] == s[j]
  2. 去掉两端字符后的子串也是回文串(或者这个子串本身长度很小):即 s[i+1...j-1] 是回文串。

我们可以根据子串长度 len = j - i + 1 来分类讨论状态转移:

  • 情况 1:长度为 1 (i == j)
    任何单个字符都是回文串。
    dp[i][i] = true

  • 情况 2:长度为 2 (j == i + 1)
    只有两个字符,只需判断它们是否相等。
    dp[i][j] = (s[i] == s[j])

  • 情况 3:长度大于等于 3 (j > i + 1)
    此时,s[i...j] 是回文串,当且仅当:

    1. s[i] == s[j]
    2. 中间的部分 s[i+1...j-1] 是回文串,即 dp[i+1][j-1] == true

    状态转移方程为:
    dp[i][j] = (s[i] == s[j]) && dp[i+1][j-1]

第三步:确定计算顺序和初始化

  • 初始化:所有长度为 1 的子串(i == j)可以直接初始化为 true。其他位置先初始化为 false

  • 计算顺序:观察状态转移方程 dp[i][j] 依赖于 dp[i+1][j-1]。这意味着,要计算 dp[i][j],我们需要先知道 更短长度、且起点索引更大的子串 的状态。

    • 因此,我们不能简单地按 i 从 0 到 n-1,j 从 i 到 n-1 的顺序计算,因为 dp[i+1][j-1] 可能还没算。
    • 正确的顺序是:按照子串的长度 len 从小到大进行枚举
      1. 先处理所有 len = 1 的子串。
      2. 再处理所有 len = 2 的子串。
      3. 然后依次处理 len = 3, 4, ..., n 的子串。

    这样,当我们计算 dp[i][j] (长度为 len) 时,它所依赖的 dp[i+1][j-1] (长度为 len-2) 一定已经在之前的循环中被计算出来了。

第四步:实现与统计

在计算过程中,每当我们将一个 dp[i][j] 设置为 true,我们就将最终答案 count 加 1。

伪代码:

n = s.length
初始化一个 n x n 的二维布尔数组 dp,所有元素为 false
count = 0

// 1. 处理所有长度为 1 的子串
for i from 0 to n-1:
    dp[i][i] = true
    count++

// 2. 处理所有长度为 2 的子串
for i from 0 to n-2:
    if s[i] == s[i+1]:
        dp[i][i+1] = true
        count++

// 3. 处理长度 >= 3 的子串
for len from 3 to n:
    for i from 0 to n-len: // 子串起始索引 i
        j = i + len - 1    // 子串结束索引 j
        if (s[i] == s[j]) and (dp[i+1][j-1] == true):
            dp[i][j] = true
            count++

返回 count

第五步:复杂度分析

  • 时间复杂度:我们需要填充一个 n x n 的 DP 表,每个状态的计算是 O(1) 的。因此总时间复杂度为 O(n²)
  • 空间复杂度:我们使用了一个 n x n 的二维数组,因此空间复杂度为 O(n²)

一个具体例子(s = "aaa"

让我们手动推导一遍,以加深理解。n = 3

  1. len = 1:

    • (0,0), (1,1), (2,2) 都是回文。count = 3
  2. len = 2:

    • (0,1): s[0]=='a', s[1]=='a',相等,是回文。count = 4
    • (1,2): s[1]=='a', s[2]=='a',相等,是回文。count = 5
  3. len = 3:

    • (0,2): s[0]=='a', s[2]=='a',相等。检查中间 dp[1][1],已知为 true。所以是回文。count = 6

最终结果 count = 6,与题目示例一致。


总结

这道题展示了区间动态规划的一个经典应用:枚举所有区间(子串),并利用更小区间的结果来判断当前区间。关键在于:

  1. 精确定义 dp[i][j]布尔值,表示子串 [i, j] 是否为回文。
  2. 根据子串首尾字符和中间部分的状态,建立清晰的状态转移方程
  3. 确定正确的计算顺序(按长度递增),确保依赖的子问题已解决。
  4. 在填充 DP 表的同时进行累加统计,得到最终答案。

通过这个解法,你可以系统地处理所有子串,避免了重复判断,是解决此类“子串计数”问题的有力工具。

好的,我们来看一个你列表中尚未出现的区间动态规划问题。 区间动态规划例题:统计一个字符串中“回文子串”的总数 题目描述 给定一个字符串 s ,请你统计并返回这个字符串中 回文子串 的总数。 回文子串 是正着读和倒着读都一样的连续子字符串。 例如: 输入: s = "abc" 输出: 3 解释:三个回文子串: "a" , "b" , "c" 。 输入: s = "aaa" 输出: 6 解释:六个回文子串: "a" , "a" , "a" , "aa" , "aa" , "aaa" 。 题目要求我们高效地计算总数。字符串长度 n 最大可达 1000,因此我们需要一个 O(n^2) 的解法。 循序渐进解题过程 这个问题虽然可以用中心扩展法在 O(n^2) 时间、 O(1) 空间内解决,但为了深入理解 区间动态规划 的思想,我们将使用 DP 表 来系统化地判断所有可能的子串是否为回文。 第一步:定义 DP 状态 区间 DP 的核心思想是: 用 dp[i][j] 表示字符串 s 中从索引 i 到索引 j 的子串(闭区间 [i, j] )是否为回文串 。 那么: dp[i][j] = true 表示 s[i...j] 是回文串。 dp[i][j] = false 表示 s[i...j] 不是回文串。 最终,我们只需统计所有 dp[i][j] = true 的 (i, j) 对的数量即可。 第二步:寻找状态转移方程 一个子串 s[i...j] 是回文串,需要满足两个条件: 两端的字符相等 : s[i] == s[j] 。 去掉两端字符后的子串也是回文串(或者这个子串本身长度很小) :即 s[i+1...j-1] 是回文串。 我们可以根据子串长度 len = j - i + 1 来分类讨论状态转移: 情况 1:长度为 1 ( i == j ) 任何单个字符都是回文串。 dp[i][i] = true 。 情况 2:长度为 2 ( j == i + 1 ) 只有两个字符,只需判断它们是否相等。 dp[i][j] = (s[i] == s[j]) 。 情况 3:长度大于等于 3 ( j > i + 1 ) 此时, s[i...j] 是回文串,当且仅当: s[i] == s[j] 。 中间的部分 s[i+1...j-1] 是回文串,即 dp[i+1][j-1] == true 。 状态转移方程为: dp[i][j] = (s[i] == s[j]) && dp[i+1][j-1] 。 第三步:确定计算顺序和初始化 初始化 :所有长度为 1 的子串( i == j )可以直接初始化为 true 。其他位置先初始化为 false 。 计算顺序 :观察状态转移方程 dp[i][j] 依赖于 dp[i+1][j-1] 。这意味着,要计算 dp[i][j] ,我们需要先知道 更短长度、且起点索引更大的子串 的状态。 因此, 我们不能简单地按 i 从 0 到 n-1, j 从 i 到 n-1 的顺序计算 ,因为 dp[i+1][j-1] 可能还没算。 正确的顺序是: 按照子串的长度 len 从小到大进行枚举 。 先处理所有 len = 1 的子串。 再处理所有 len = 2 的子串。 然后依次处理 len = 3, 4, ..., n 的子串。 这样,当我们计算 dp[i][j] (长度为 len ) 时,它所依赖的 dp[i+1][j-1] (长度为 len-2 ) 一定已经在之前的循环中被计算出来了。 第四步:实现与统计 在计算过程中,每当我们将一个 dp[i][j] 设置为 true ,我们就将最终答案 count 加 1。 伪代码: 第五步:复杂度分析 时间复杂度 :我们需要填充一个 n x n 的 DP 表,每个状态的计算是 O(1) 的。因此总时间复杂度为 O(n²) 。 空间复杂度 :我们使用了一个 n x n 的二维数组,因此空间复杂度为 O(n²) 。 一个具体例子( s = "aaa" ) 让我们手动推导一遍,以加深理解。 n = 3 。 len = 1 : (0,0) , (1,1) , (2,2) 都是回文。 count = 3 。 len = 2 : (0,1) : s[0]=='a' , s[1]=='a' ,相等,是回文。 count = 4 。 (1,2) : s[1]=='a' , s[2]=='a' ,相等,是回文。 count = 5 。 len = 3 : (0,2) : s[0]=='a' , s[2]=='a' ,相等。检查中间 dp[1][1] ,已知为 true 。所以是回文。 count = 6 。 最终结果 count = 6 ,与题目示例一致。 总结 这道题展示了区间动态规划的一个经典应用: 枚举所有区间(子串),并利用更小区间的结果来判断当前区间 。关键在于: 精确定义 dp[i][j] 为 布尔值 ,表示子串 [i, j] 是否为回文。 根据子串首尾字符和中间部分的状态,建立清晰的 状态转移方程 。 确定正确的 计算顺序 (按长度递增),确保依赖的子问题已解决。 在填充 DP 表的同时进行 累加统计 ,得到最终答案。 通过这个解法,你可以系统地处理所有子串,避免了重复判断,是解决此类“子串计数”问题的有力工具。