区间 DP 重温

区间 DP


顾名思义,记录一个区间作为状态。

 

板子


P1775 石子合并(弱化版) - 洛谷

记 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;
}

  

 
 
题目说三角形互不相交,也就是对一个多边形,中间划分一个三角形,左右必定会有2个互不影响的多边形。

这其实就转化为了区间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 了。

有两种转移方式,假设 A,B 是合法的括号序列,那么 AB,(A)这两种情况也是合法的。

这也是一般的区间 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。

 

posted @ 2025-03-01 16:48  cn是大帅哥886  阅读(8)  评论(0)    收藏  举报