区间 DP 重温
区间 DP
顾名思义,记录一个区间作为状态。
板子
记 f[L, R] 表示第 L 堆石子到第 R 堆石子合并成一堆石子的最小代价。
[L, R] 的石子必定会由 [L, R] 内相邻的两堆石子合并而成,那么我们可以枚举断点转移。
转移可以理解为,先加上分别合并为两部分的代价,再加上两部分合并起来的代价。
初始化就是刚开始只有一堆石子,所以不会产生合并,即 f[i, i]=0
需要注意一下,每次转移至少有两堆石子合并,所以端点不能与右端点重合。
还有注意进行转移的时候应先枚举区间长度,才满足 dp 转移为一个拓扑图。
#include <bits/stdc++.h>
using namespace std;
const int N=305;
int n, a[N], s[N], f[N][N];
int main()
{
scanf("%d", &n);
for (int i=1; i<=n; i++) scanf("%d", &a[i]);
for (int i=1; i<=n; i++) s[i]=s[i-1]+a[i];
memset(f, 63, sizeof f);
for (int i=1; i<=n; i++) f[i][i]=0;
for (int len=2; len<=n; len++)
for (int L=1; L+len-1<=n; L++)
{
int R=L+len-1;
for (int k=L; k<R; k++)
f[L][R]=min(f[L][R], f[L][k]+f[k+1][R]+s[R]-s[L-1]);
}
printf("%d", f[1][n]);
return 0;
}
例题
(破环为链)P1880 [NOI1995] 石子合并 - 洛谷
一共 n 个点,n-1 次合并就可以把 n 个点连成链。变成链后就很好处理了,直接用板子即可。
此时必然会有一个缺口。我们可以枚举缺口,然后不断做区间dp,O(n^4)。
这样有很多可能重复的信息没用上。
如果我们把数组复制一遍,长度为 n 的连续序列一定是原序列的一个带缺口的环,即链。
此时对 2*n 的数组做区间 dp,会变成 O(n^3),原因是有很多信息不需要重复计算。
然后最终答案就是 2*n 的数组中长度为 n 的连续序列。
注意一个细节,做区间 dp 的时候,len 的枚举范围还是 1~n,因为最终答案求的区间长度最大为 n。
#include <bits/stdc++.h>
using namespace std;
const int N=205;
int n, a[N], s[N], f[N][N], g[N][N], ansA=0, ansB=0;
int main()
{
scanf("%d", &n);
for (int i=1; i<=n; i++) scanf("%d", &a[i]), a[i+n]=a[i];
for (int i=1; i<=2*n; i++) s[i]=s[i-1]+a[i];
memset(f, 63, sizeof f);
for (int i=1; i<=2*n; i++) f[i][i]=g[i][i]=0;
for (int len=2; len<=n; len++)
for (int L=1; L+len-1<=2*n; L++)
{
int R=L+len-1;
for (int k=L; k<R; k++)
{
f[L][R]=min(f[L][R], f[L][k]+f[k+1][R]+s[R]-s[L-1]);
g[L][R]=max(g[L][R], g[L][k]+g[k+1][R]+s[R]-s[L-1]);
}
}
ansA=1e9;
for (int i=1; i<=n; i++) ansA=min(ansA, f[i][i+n-1]), ansB=max(ansB, g[i][i+n-1]);
printf("%d\n%d", ansA, ansB);
return 0;
}
基本和上题一样,不过多了点细节。因为是珠子首尾相连,所以答案的区间长度是 n+1
还有划分区间的时候,由于题目里珠子合并的特性,断点是可重复的,且不能和左右端点重合。
还有 len 从 3 开始,也是因为珠子合并的特性。
#include <bits/stdc++.h>
using namespace std;
const int N=205;
int n, a[N], f[N][N], ans=0;
int main()
{
scanf("%d", &n);
for (int i=1; i<=n; i++) scanf("%d", &a[i]), a[i+n]=a[i];
for (int len=3; len<=n+1; len++)
for (int L=1; L+len-1<=2*n; L++)
{
int R=L+len-1;
for (int k=L+1; k<R; k++)
{
f[L][R]=max(f[L][R], f[L][k]+f[k][R]+a[L]*a[k]*a[R]);
}
}
for (int i=1; i<=2*n; i++) ans=max(ans, f[i][i+n]);
printf("%d", ans);
return 0;
}

这其实就转化为了区间dp,不过定义状态需要转个弯。f[L, R] 表示 (L, L+1), (L+1, L+2), .... ,(R-1, R), (R, L) 所构成的多边形,划分成三角形后权值和最小。
那么转移就很容易分析了,主要是能够把此题抽象成区间 DP。
这也揭示了一个重要性质:区间 DP 转移时,是通过两个互相没有影响的区间转移到一个大区间上的!
注意这题数据范围较大,需要高精度。或者 __int128
#include <bits/stdc++.h>
using namespace std;
const int N=55;
__int128 read()
{
__int128 f=1, s=0;
char c=getchar();
while (c<'0' || c>'9')
{
if (c=='-') f=-1;
c=getchar();
}
while (c>='0' && c<='9') s=s*10+c-'0', c=getchar();
return f*s;
}
void write(__int128 x)
{
if (x<0) putchar('-'), x=-x;
if (x<10) putchar(x+'0');
else write(x/10), putchar(x%10+'0');
}
int n;
__int128 a[N], f[N][N], ans=0;
int main()
{
scanf("%d", &n);
for (int i=1; i<=n; i++) a[i]=read();
memset(f, 63, sizeof f);
for (int len=1; len<=2; len++)
for (int L=1; L+len-1<=n; L++) f[L][L+len-1]=0;
for (int len=3; len<=n; len++)
for (int L=1; L+len-1<=n; L++)
{
int R=L+len-1;
for (int k=L+1; k<R; k++)
f[L][R]=min(f[L][R], f[L][k]+f[k][R]+a[L]*a[R]*a[k]);
}
write(f[1][n]);
return 0;
}
求最长合法括号子序列 ,$1 \le n \le 100$
乍一看以为括号匹配,但实际上考虑的是子序列,所以只用栈的做法行不通。
观察到一个性质,合法的括号子序列一定是一个连续区间。对于状态能表示成连续一段的区间,就可以区间 DP 了。
这也是一般的区间 DP 转移方向:第一种是枚举区间的断点后拆成子区间转移,第二种区间左右两端扩展一下。
#include <bits/stdc++.h>
using namespace std;
const int N=105;
int n, f[N][N];
string s;
bool check(int L, int R)
{
if((s[L]=='(' && s[R]==')') || (s[L]=='[' && s[R]==']')) return 1;
return 0;
}
int main()
{
while (1)
{
cin>>s, s=" "+s, n=s.size()-1;
if (s==" end") break;
memset(f, 0, sizeof f);
for (int len=2; len<=n; len++)
for (int L=1, R=len; R<=n; L++, R++)
{
if (check(L, R)) f[L][R]=max(f[L][R], f[L+1][R-1]+2);
for (int k=L; k<R; k++) f[L][R]=max(f[L][R], f[L][k]+f[k+1][R]);
}
printf("%d\n", f[1][n]);
}
return 0;
}
P3146 [USACO16OPEN] 248 G(Code)
两个相邻且相等的数才能进行合成,所以一定是由某个区间的连续的一段数合成出某个数,所以可以考虑区间 DP。
然后每次都是两个区间合成一个大区间,也就是枚举断点转移就好。
注意一个细节,初始化 DP 数组要是负无穷代表不合法,不然转移时候两个区间的值都为 0 的话,实际代表都不能合成数,但在此时却合成了一个 1,就错了。
观察一下 s 和 t 交替组成字符串 p 会有什么性质。
因为是不断交替组成,所以 p 一定由 s 和 t 的某个连续字符串区间组成。也就是可以考虑成是个区间 DP,不过是两个区间罢了。
再考虑回文串,使用不断向左右两端扩展字符的方法计算长度,那就做完了。
细节挺多的,注意区间可以为空,以及要判越界的情况。
P4766 [CERC2014] Outer space invaders(Code)
很牛逼的一道题。
根据贪心,容易想到一个线性 DP。
显然只在恰好 $b_i$ 时刻发出射线是最优的。考虑先将外星人按 $b_i$ 升序排序,这样在后面的射线对是否消灭了前面的外星人没有影响。
设 $f_i$ 表示消灭前 $i$ 个外星人的最小代价,转移时最后一次发出射线的时刻就是 $b_j$,考虑最后一次射线把 $j\sim i$ 这一段的外星人全部消灭,则有:
$$f_i = f_{j - 1}+ \max_{k = j}^{i} d_k $$
答案即 $f_n$,总复杂度 $O(n)$ 级别。
然而假了。虽然排序后后面的射线不会影响前面的外星人,但前面的射线影响到后面的外星人。具有后效性,无法进行线性 DP。
发现每个外星人都是一段区间出现,考虑区间 DP。
设 $f_{i,j}$ 表示将满足 $i\le a_p < b_p\le j$ 的外星人 $p$ 全部消灭的代价。
仅考虑完全包含在 $[i, j]$ 范围内的区间。这样通过“完全包含”的状态划分子问题在转移的时候才会方便严谨,不然如果存在某些区间包含一半的话,会有后效性。
转移的就划分区间了,比较显然。
考虑在断点 $k(i\le k\le j)$ 进行的一次操作,在这次操作之前消灭了 $[a_p,b_p]\subseteq [i,k-1]$ 和 $[a_p, b_p]\subseteq [k+1,i]$ 中的所有外星人 $p$,这次操作消灭了满足 $i\le a_p \le k \le b_p\le j$ 的全部外星人,即有:
$$f_{i,j} = \min_{k=i}^{j}\left( f_{i, k - 1} + f_{k + 1, j} + d_{i,j,k}\right)$$
上式中 $d_{i,j,k}$ 表示满足:$i\le a_p \le k \le b_p\le j$ 的外星人 $p$ 中最远的距离,也是需要满足完全包含的限制,即:
$$d_{i,j,k} = \max_{i\le a_p \le k \le b_p\le j} d_p$$
发现 DP 的过程是 $O(n^3)$ 级别的,但预处理 $d$ 是 $O(n^4)$ 级别的,不预处理则 DP 变为 $O(n^4)$ 级别,复杂度无法接受。
下文称发射射线消灭外星人为操作。
发现 $[a_p,b_p]\subseteq [i,j]$ 的 $d_p$ 最大的外星人最终无论如何都是需要被消灭的,又发现操作的顺序并不影响正确性,我们不妨钦定这个外星人是在最后一次操作中被消灭的。
然后发现转移时仅考虑最后一次操作就好,因为这个操作无论如何都是需要进行的,不妨每次都进行最后的这一步操作,从而优化复杂度。
转移时先求得这个外星人的编号 $\operatorname{id}$,则枚举的分界点 $k$ 被限定在了 $[a_{\operatorname{id}}, b_{\operatorname{id}}]$ 中,有:
$$f_{i,j} = \min_{k=a_{\operatorname{id}}}^{b_{\operatorname{id}}}\left( f_{i, k - 1} + f_{k + 1, j} + d_{\operatorname{id}}\right)$$
仅需在枚举 $k$ 之前 $O(n)$ 地求得 $\operatorname{id}$ 即可。答案仍为 $f_{1,m}$。总复杂度 $O(n^3)$ 级别,复杂度就可以接受了。
小细节:需要离散化。
P5851 [USACO19DEC] Greedy Pie Eaters P(Code)
同样的,考虑到奶牛一定吃一个区间内的派,就可以上区间 DP 了。
定 $f_{L, R}$ 表示:$[L, R]$ 区间内的派被若干个奶牛被吃完(或者是被考虑完),这些奶牛的体重和最大值。
然后枚举断点转移,有:$f_{L, R}=max\{f_{L, k-1}+f_{k+1, R}+g_{L, R, k}\}$,其中 $g_{L, R, k}$ 表示完全包含在 $[L, R]$ 区间内,且经过点 $k$ 的奶牛的体重最大值。
根据贪心就知道,需要记录最大值。然后为了避免后效性,需要有完全包含的限制,不然会对区间外的派造成影响,然后就有后效性。
$g$ 可以预处理,也是同区间 DP 一样转移,不过这里不用枚举断点,左右扩展转移就好。
P3592 [POI 2015 R3] 洗车 Car washes(Code / Code2)
每个人都是选一段连续区间进行操作,可以考虑使用区间 DP。
首先可以对 $c$ 进行离散化,因为只需要知道相对大小关系就能判断人是否能洗车。
每个人是否洗车,关心的是区间内最小值,于是记录 $f_{L, R, c}$ 表示区间 $[L, R]$ 内的洗车店最便宜的钱数为 $c$ 元,人在这个区间内所花的最多钱数。
注意这里人的洗车店区间是要严格被包含在区间 $[L, R]$ 内的。
考虑如何转移,枚举区间内的最小值,以及最小值的位置就好。
$f_{L, R, c}=\max \{ f_{L, k-1, c1}+f_{k+1, R, c2}+c \times cnt(k) \} $
其中,满足 $c1 \ge c$,$c2 \ge c$,$cnt(k)$ 表示满足洗车区间严格在 $[L, R]$ 内同时包含点 $k$,且花的钱数大于等于 $c$ 的人的个数。
暴力转移复杂度是 $O(n^3m^3)$ 的,需要优化。
不难发现,$cnt(k)$ 可以单独预处理,前半部分的 $f$ 可以后缀和优化。
具体的,设 $s_{L, R, k} = \max \{f_{L, R, k2}\} $,其中 $k2 \ge k$。
原式变成:$f_{L, R, c}=\max \{ s_{L, k-1, c}+s_{k+1, R, c}+c \times cnt(k) \} $,复杂度降至 $O(n^3m)$,可以接受。
#include <bits/stdc++.h>
using namespace std;
const int N=55, M=4005;
struct node
{
int L, R, c;
}a[M];
int f[N][N][M], s[N][N][M];
int n, m, cnt[M], tot=0, res=0;
vector<int> vec;
void lsh()
{
for (int i=1; i<=m; i++) vec.push_back(a[i].c);
vec.push_back(-1);
sort(vec.begin(), vec.end());
vec.erase(unique(vec.begin(), vec.end()), vec.end()), tot=vec.size();
for (int i=1; i<=m; i++) a[i].c=lower_bound(vec.begin(), vec.end(), a[i].c)-vec.begin();
}
int main()
{
scanf("%d%d", &n, &m);
for (int i=1; i<=m; i++) scanf("%d%d%d", &a[i].L, &a[i].R, &a[i].c);
lsh();
for (int len=2; len<=n; len++)
for (int L=1, R=len; R<=n; L++, R++)
for (int k=L; k<=R; k++)
{
for (int c=1; c<=tot; c++) cnt[c]=0;
for (int i=1; i<=m; i++) if (a[i].L>=L && a[i].R<=R && a[i].L<=k && a[i].R>=k) cnt[a[i].c]++;
for (int c=tot; c>=1; c--) cnt[c]+=cnt[c+1];
for (int c=1; c<=tot; c++) f[L][R][c]=max(f[L][R][c], s[L][k-1][c]+s[k+1][R][c]+cnt[c]*vec[c]);
for (int c=tot; c>=1; c--) s[L][R][c]=max(s[L][R][c+1], f[L][R][c]);
}
for (int i=1; i<=tot; i++) if (f[1][n][i]>f[1][n][res]) res=i;
printf("%d\n", f[1][n][res]);
return 0;
}
当然,还需要输出方案。这题的难点我认为就在于输出方案。
在转移的时候,需要顺便记录决策点,这里的决策点有多个,应该全部记录上。
但是用了后缀和优化,我怎么知道 $c1$ 和 $c2$ 是什么值呢?在后缀和的时候也顺带记录就好。
然后递归打印方案就好。
需要注意:
1.如果当前区间没有最优解,也需要有决策点,不然决策点为 0,后续递归打印方案就会错,所以需要初始化决策点。而且这个初始化必须合理。
2.后缀和优化时,如果有最优解相同的情况,要更新决策点!!感性理解后缀和优化的本质就是要找第一个 >=c 的最优解。
P10600 BZOJ4350 括号序列再战猪猪侠(Code)
我认为的最难的题。
根据 $match_i$ 的定义,从左往右数第 $i$ 个左括号所对应的是第几个右括号。
可以发现一些很重要的事:限制仅针对于左括号。且限制对于左括号是有顺序要求的,相当于最终合法的括号序列中,某个区间内的左括号编号一定连续!
诶,左括号编号一定连续,由此引发我们用区间 DP 计数。具体的,定 $f_{L,R}$ 表示编号为 $L$ 到 $R$ 的左括号能构成的合法括号序列的方案数。
因为每次不断加入的左括号对于编号有顺序要求,不妨考虑成每次往括号序列的最左端插入一个左括号,然后考虑这个左括号对应的右括号的位置即可。其实就是相当于枚举断点了。
我们设当前插入左括号编号 $L$,``AA`` 为一个合法括号序列,其左括号序列对应 $f_{L+1, R}$。那么对 $L$ 对应的右括号位置进行讨论,就有如下情况:
- ()AA,此时需要满足对于每一个 $i$ 都有 $match_i > match_L$,其中 $L+1 \le i \le R$,才能转移。原因显然。
- (AA),此时需要满足对于每一个 $i$ 都有 $match_i < match_L$,其中 $L+1 \le i \le R$,才能转移。原因可以感性理解为需要满足就近匹配。
- (A)A,其实就是上面两种情况的结合。假设此时的断点为 $k$,需要满足对于每一个 $i$,$j$,$p$ 都有 $match_i < match_L$ 和 $match_j > match_p$,其中 $L+1 \le i \le k$,$k+1 \le j \le R$,$L \le p \le k$。
进一步观察下,可以发现把这些限制条件都可以描述成一个二元组 $(x, y)$,表示 $match_x < match_y$。如第一种情况,可以变化为:不存在任何一个 $i$,使得 $match_i < match_L$。第二种情况,可以变化为:不存在任何一个 $i$,使得 $match_L < match_i$。情况三同理。
统一限制条件,就好维护这些限制了。
对于一个二元组,挂到平面直角坐标系上就变成一个点,然后就可以转化为二维前缀和。
一点细节:注意判无解的情况,如果有 $a_i=b_i$ 肯定无解。
总结
只要发现,能用一段连续区间来描述状态,就能想一下区间 DP。

浙公网安备 33010602011771号