好的,我们来看一个你列表中尚未出现的区间动态规划问题。
区间动态规划例题:统计一个字符串中“回文子串”的总数
题目描述
给定一个字符串 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 = 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。
-
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 表的同时进行累加统计,得到最终答案。
通过这个解法,你可以系统地处理所有子串,避免了重复判断,是解决此类“子串计数”问题的有力工具。