今天给大家简单介绍区间 dp,以及 c++ 代码。
区间 dp
文章目录
区间 dp 介绍
什么是区间 dp?
区间 dp 就是在区间上进行动态规划,求解一段区间上的最优解。
原理
通过枚举分割点得到并更新小区间的最优解,然后再通过合并小区间得到大区间的最优解。
什么时候要用区间 dp?
一般一下特征的问题适用于区间 dp:
- 问题范围是区间:例如字符串的子串、数组的子序列、数轴上的区间等。
- 决策影响整个区间:操作(如合并、分割、删除)会影响区间内的所有元素。
- 子问题可重复分割:大区间的最优解依赖小区间的最优解,且子问题需要多次计算。
常见的区间 dp 问题:
- 合并石子
- 分割字符串使子串满足条件
- 回文类问题
- ……
区间 dp 的模板
for(int len = 1; len <= n; len++)
{ //枚举长度
for(int j = 1; j+len <= n+1; j++)
{ //枚举起点,ends<=n
int ends = j + len - 1;
for(int i = j; i < ends; i++)
{ //枚举分割点,更新小区间最优解
dp[j][ends] = min(dp[j][ends], dp[j][i] + dp[i + 1][ends] + something);
}
}
}
例题 CF-245H
中文题面
回文串数量查询
- 每个测试的时间限制: 5 5 5 秒
- 每个测试的内存限制: 256 256 256 兆字节
题目描述
你有一个长度为 ∣ s ∣ |s| ∣s∣ 的字符串 s = s 1 , s 2 , … s ∣ s ∣ s=s1,s2, \dots s|s| s=s1,s2,…s∣s∣,由小写英文字母组成。同时有 q q q 个查询,每个查询由两个整数 l i , r i l_i,r_i li,ri 描述( 1 ≤ l i ≤ r i ≤ ∣ s ∣ 1\le l_i\le r_i≤|s| 1≤li≤ri≤∣s∣)。该查询的答案是字符串 s [ l i … r i ] s[l_i\dots r_i] s[li…ri] 的子串中,回文串的数量。
字符串 s [ l … r ] = s l , s l + 1 , ⋯ s r s[l\dots r]=s_l,s_l+1,⋯s_r s[l…r]=sl,sl+1,⋯sr( 1 ≤ l ≤ r ≤ ∣ s ∣ 1\le l\le r\le|s| 1≤l≤r≤∣s∣)是字符串 s = s 1 , s 2 , … , ⋯ s ∣ s ∣ s=s1,s2,\dots,⋯s|s| s=s1,s2,…,⋯s∣s∣ 的一个子串。
字符串 t t t 被称为回文串,当且仅当它从左到右和从右到左读起来是一样的。 形式上,如果 t = t 1 , t 2 , … t ∣ t ∣ = t ∣ t ∣ , t ∣ t ∣ − 1 , … t 1 t=t_1,t_2,\dots t_|t|=t|t|,t|t|−1,\dots t_1 t=t1,t2,…t∣t∣=t∣t∣,t∣t∣−1,…t1。
输入
第一行包含字符串 s s s( 1 ≤ s ≤ 5000 1\le s\le 5000 1≤s≤5000)。第二行包含一个整数 q q q( 1 ≤ q ≤ 106 1\le q\le 106 1≤q≤106),表示查询的数量。接下来的 q q q 行包含这些查询。其中第 i i i 行包含两个用空格分隔的整数 l i , r i l_i,r_i li,ri( 1 ≤ l i ≤ r i ≤ ∣ s ∣ 1\le l_i\le r_i\le|s| 1≤li≤ri≤∣s∣)表示第 i i i 个查询的描述。
保证给定的字符串仅由小写英文字母组成。
输出
输出 q q q 个整数,表示查询的答案。按照输入中查询的顺序打印答案。用空格分隔打印出的数字。
示例
- 输入示例
caaaba
5
1 1
1 4
2 3
4 6
4 5
- 输出示例
1
7
3
4
2
说明
考虑第一个测试用例中的第四个查询。字符串
s
[
4
…
6
]
s[4\dots6]
s[4…6] = aba。它的回文子串是:a,b,a,aba。
思路
首先这道题需要求出区间 [ l , r ] [l,r] [l,r] 内有多少个回文子串,所以可以想到区间 dp。
我们规定 dp 状态: d p i , j = k dp_{i,j}=k dpi,j=k 表示在区间 [ i , j ] [i,j] [i,j] 里回文子串有 k k k 个。
接着我们还需要一个数组 c n t i j cnt_{ij} cntij 来记录左端点 ≥ i \ge i ≥i,右端点等于 j j j 有多少个回文子串。
那么就可以得到转移方程: d p i , j = d p i , j − 1 + c n t i , j \color{red}dp_{i,j}=dp_{i,j-1}+cnt_{i,j} dpi,j=dpi,j−1+cnti,j。
于是我们的问题就变成了:如何用 Θ ( n 2 ) \Theta(n^2) Θ(n2) 得到 c n t cnt cnt 数组。
我们定义 f i , j f_{i,j} fi,j 表示 [ i , j ] [i,j] [i,j] 这个子串是不是回文串。
然后我们发现,如果这个 [ i + 1 , j − 1 ] [i+1,j-1] [i+1,j−1] 这个子串是回文串且第 i i i 位和 j j j 位相同(如下图所示),那么 [ i , j ] [i,j] [i,j] 就是一个回文串。

于是我们就得到了 f f f 数组:
for(int i = len; i >= 1; i--)
for(int j = i; j <= len; j++)
f[i][j] = (f[i+1][j-1] && s[i] == s[j]);
接下来我们来想如何得到 c n t cnt cnt。
也很简单,就是先枚举 j j j,因为 j j j 是固定的,然后按 j → 1 j\to 1 j→1 枚举 i i i。因此在计算 c n t i , j cnt_{i,j} cnti,j 是我们已经得到了 c n t i + 1 , j cnt_{i+1,j} cnti+1,j,而 c n t i , j cnt_{i,j} cnti,j 就是 c n t i + 1 , j cnt_{i+1,j} cnti+1,j 加上 [ i , j ] [i,j] [i,j] 是否为回文串,也就是 f i , j f_{i,j} fi,j,那么转移方程就是 c n t i , j = c n t i + 1 , j + f i , j \color{red}cnt_{i,j}=cnt_{i+1,j}+f_{i,j} cnti,j=cnti+1,j+fi,j。
于是预处理 c n t cnt cnt:
for(int j = 1; j <= len; j++)
for(int i = j; i >= 1; i--)
cnt[i][j] = cnt[i+1][j] + f[i][j];
最后 d p dp dp 就很简单了:
for(int i = 1; i <= len; i++)
for(int j = i; j <= len; j++)
dp[i][j] = dp[i][j-1] + cnt[i][j];
这就是这道题完整地思路。
4608

被折叠的 条评论
为什么被折叠?



