区间dp

区间 dp 通常解决的问题顺序有着很重要的作用,也就是说“前 i 个”的状态并不能很好地刻画出完整的状态
状态一般设计为 f[l][r],表示 dp[l,r] 的区间的花费
后面经常补充状态,比如 [0/1][k] 来表示上一次取的是左/右端点,左右端点的状态,或者区间后面的一段情况,或者区间内数具有的性质


从中间转移

即每次扩展的是一个区间,用于解决合并类的问题

最典型的有石子合并
再补充一些题目:


P3146 [USACO16OPEN]248 G

发现同样是一个合并类问题,那么 f[i][j]=f[i][k]+1(f[i][k]==f[k][j])


CF149D Coloring Brackets

f[l][r][0/1/2][0/1/2] 表示左右端点颜色分别为 0/1/2 的方案数
可以看左右端点是否是匹配括号,是则 dp [l+1,r1],否则 dp [l,k][k+1,r]
这道题的一个启发点是对于这种括号匹配的问题,往往写成递归版的记忆化搜索会更好实现


凸多边形的划分

f[l][r]=f[l+1][k1]+f[k+1][r1]+a[l]a[k]a[r],表示 l,k,r 三个点形成了三角形并产生贡献


P7914 [CSP-S 2021] 括号序列

并不想讲这个题,来补充一个 道听途说未曾实现不保证正确 的小 trick
这种括号匹配经常有算重的问题,那么对于每一个 lr 从小到大枚举据说即可解决这个问题(理论上分析比较有道理)


Replace

CyberTree 教了一晚上

关键条件是转化关系是 1 对多的,那么设 f[l][r][c] 表示 T 串区间 [l,r] 转化为字母 c 的最优方案
那么对于多步转移,由于越来越少(11 需要特判),不会转移成环


Range Argmax

实际上就是枚举局部最大值的位置,可以发现这个是不能计数的,应该放在状态里
形式化地,为经过 i 位置的所有区间最大值均在 i
很显然有多个,那么钦定为第一个
那么转移从 [l,k)(k,r] 转移
其中左边一个区间可以发现需要迎合 k 是最大值这个限制
那么需要保证 kk 管辖区间无交,这个可以对每个 (l,r,k) 预处理出来
注意右边是没有这个限制的,因为定义了是最小的一个,而这个最大值并不唯一
发现左边的转移是一个区间,那么维护一个前缀和即可


从两侧转移

POJ 3280 Cheapest Palindrome

题意:给你长度为m的字符串,其中有n种字符,每种字符都有两个值,分别是插入这个字符的代价,删除这个字符的代价,让你求将原先给出的那串字符变成一个回文串的最小代价。

这道题的思想还是很妙的
分情况讨论:

  • 如果 l,r 相等,从 f[l+1,r1] 转移
  • 在前面插入 r 或后面删除 r,从 f[l,r1] 转移
  • 在后面插入 l 或前面删除 l,从 f[l+1,r] 转移

队列

很妙的题,当时不以为然,AT 又出来发现不会
分两个数组 f,g 分别维护小明和小华取完后的状态
那么 f[l,r]=max(f[l+1,r]+a[l],f[l,r1]+a[r])
g 的转移同理


P1220 关路灯

古老的题了,设 f[l][r][0/1] 表示区间 [l,r],最后一次老王在左/右端点的最优值


维护后缀信息

P2135 方块消除

f[l][r][s] 表示区间 [l,r],后面紧接着有 s 个与 r 相同的方块
这样的状态使得一种转移迎刃而解:即选取中间一个点 k,其中 kr 颜色相同,砍掉 [k+1,r1] 的部分,让 r 以及后面的 s 个与 k 拼上
那么转移有两个:f[l][r][s]=f[l][r1][0]+(s+a[r])2
f[l][r][s]=f[l][k][a[r]+s]+f[k+1][r1][0]

  • 这启示我们把转移不了的东西放进状态里
代码
#include<bits/stdc++.h>
using namespace std;
int read(){
	int x=0,f=1;
	char ch=getchar();
	while(!isdigit(ch)){
		if(ch=='-')f=-1;
		ch=getchar();
	}
	while(isdigit(ch)){
		x=x*10+ch-48;
		ch=getchar();
	}
	return x*f;
}
const int maxn=205;
const int inf=0x3f3f3f3f;
int n,col[maxn],a[maxn],f[maxn][maxn][maxn*2],sum[maxn];
int main(){
	n=read();
	for(int i=1;i<=n;i++)col[i]=read();
	for(int i=1;i<=n;i++)a[i]=read(),sum[i]=sum[i-1]+a[i];
//	for(int i=1;i<=n;i++)f[i][i][0]=a[i]*a[i];
	for(int len=0;len<=n-1;len++){
		for(int l=1;l<=n-len;l++){
			int r=l+len;
			for(int s=0;s<=sum[n]-sum[r-1];s++){
				f[l][r][s]=f[l][r-1][0]+(s+a[r])*(s+a[r]);
				for(int k=l;k<=r-1;k++){
					if(col[k]==col[r])
						f[l][r][s]=max(f[l][r][s],f[l][k][a[r]+s]+f[k+1][r-1][0]);
				}
			}
		}
	}
	cout<<f[1][n][0];
	return 0;
}

双倍经验


P5336 [THUSC2016]成绩单

发现既然一次分发只和最值有关,那么只记录区间最值即可
g[l][r][mn][mx] 表示 [l,r] 中最值分别为 mnmx 的最优情况
f[l][r] 表示区间 [l,r] 的最优解
那么 f[l][r]=ming[l][r][a][b]+B(ba)2+A
考虑枚举断点 k 来转移 g
g[l][r][a][b]=min(f[k+1][r]+g[l][k][a][b])
g[l][r][a][b]>g[l][r+1][min(a,wr+1)][max(b,wr+1)]
于是离散化后用 n5 的优秀复杂度完成了此题

代码
#include<bits/stdc++.h>
using namespace std;
int read(){
	int x=0,f=1;
	char ch=getchar();
	while(!isdigit(ch)){
		if(ch=='-')f=-1;
		ch=getchar();
	}
	while(isdigit(ch)){
		x=x*10+ch-48;
		ch=getchar();
	}
	return x*f;
}
const int maxn=55;
int n,m,aa,b,a[maxn],lsh[maxn],tot,f[maxn][maxn],g[maxn][maxn][maxn][maxn];
void placemin(int &a,int b){
	if(a>b)a=b;
	return ;
}
int main(){
	n=read(),aa=read(),b=read();
	for(int i=1;i<=n;i++)lsh[i]=a[i]=read();
	sort(lsh+1,lsh+n+1);
	tot=unique(lsh+1,lsh+n+1)-lsh-1;
	for(int i=1;i<=n;i++)a[i]=lower_bound(lsh+1,lsh+tot+1,a[i])-lsh;
	memset(g,0x3f,sizeof g),memset(f,0x3f,sizeof f);
	for(int i=1;i<=n;i++)g[i][i][a[i]][a[i]]=0;
	for(int len=0;len<=n-1;len++){
		for(int l=1;l<=n-len;l++){
			int r=l+len;
			for(int mn=1;mn<=tot;mn++){
				for(int mx=mn;mx<=tot;mx++){
					for(int k=l;k<r;k++){
						placemin(g[l][r][mn][mx],g[l][k][mn][mx]+f[k+1][r]);
					}
					placemin(g[l][r+1][min(mn,a[r+1])][max(mx,a[r+1])],g[l][r][mn][mx]);
					placemin(f[l][r],g[l][r][mn][mx]+aa+b*(lsh[mx]-lsh[mn])*(lsh[mx]-lsh[mn]));
				}
			}
		}
	}
	cout<<f[1][n];
	return 0;
}

P1436 棋盘分割

这是区间 dp 放到二维平面上的例子
f[l][r][i][j][k] 表示顶点为 (l,r)(i,j) 的矩形分割了 k 次的最小值
每次只扩展横纵坐标之一

代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=16;
int n,a[maxn][maxn],sum[maxn][maxn],f[maxn][maxn][maxn][maxn][maxn];
void tomin(int &a,int b){
	if(a>b)a=b;
	return ;
}
int main(){
	cin>>n;
	for(int i=1;i<=8;i++){
		for(int j=1;j<=8;j++){
			cin>>a[i][j];
			sum[i][j]=sum[i-1][j]+sum[i][j-1]+a[i][j]-sum[i-1][j-1];
		}
	}
	memset(f,0x3f,sizeof f);
	for(int i=1;i<=8;i++){
		for(int j=1;j<=8;j++){
			for(int l=i;l<=8;l++){
				for(int r=j;r<=8;r++){
					int w=sum[l][r]-sum[i-1][r]-sum[l][j-1]+sum[i-1][j-1];
					f[i][j][l][r][0]=w*w;
				}
			}
		}
	}
	for(int k=1;k<=n;k++){
		for(int i=1;i<=8;i++){
			for(int j=1;j<=8;j++){
				for(int l=1;l<=8;l++){
					for(int r=1;r<=8;r++){
						for(int a=i;a<l;a++){
							tomin(f[i][j][l][r][k],min(f[i][j][a][r][0]+f[a+1][j][l][r][k-1],f[i][j][a][r][k-1]+f[a+1][j][l][r][0]));
						}
						for(int b=j;b<r;b++){
							tomin(f[i][j][l][r][k],min(f[i][j][l][b][0]+f[i][b+1][l][r][k-1],f[i][j][l][b][k-1]+f[i][b+1][l][r][0]));
						}
					}
				}
			}
		}
	}
	cout<<f[1][1][8][8][n-1];
	return 0;
}

P1864 [NOI2009] 二叉查找树

根据题目的限制,不能更改数据值,也就是说这棵 BST 的中序遍历是一定了,要做的是通过改变权值来旋转
f[l][r][s] 表示中序 [l,r] 的节点,值 s 的最小值
那么枚举一个树根 k,转移分情况讨论:
如果 k 的值大于等于 s,那么 f[l][r][s]=f[l][k1][a[k].val]+f[k+1][j][a[k].val]+sum(i,j)
否则,f[l][r][s]=f[l][k1][s]+f[k+1][r][s]+sum(i,j)+K

代码
#include<bits/stdc++.h>
using namespace std;
int read(){
	int x=0,f=1;
	char ch=getchar();
	while(!isdigit(ch)){
		if(ch=='-')f=-1;
		ch=getchar();
	}
	while(isdigit(ch)){
		x=x*10+ch-48;
		ch=getchar();
	}
	return x*f;
}
const int maxn=75;
const int inf=1e9;
int n,m,f[maxn][maxn][maxn],sum[maxn],lsh[maxn];
struct Node{
	int val,val1,cost;
}a[maxn];
bool cmp(Node a,Node b){
	return a.val<b.val;
}
int main(){
	n=read();m=read();
	for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)for(int k=1;k<=n;k++)if(j!=i-1)f[i][j][k]=inf;
	for(int i=1;i<=n;i++)a[i].val=read();
	for(int i=1;i<=n;i++)lsh[i]=a[i].val1=read();
	for(int i=1;i<=n;i++)a[i].cost=read();
	sort(a+1,a+n+1,cmp);
	sort(lsh+1,lsh+n+1);
	for(int i=1;i<=n;i++)a[i].val1=lower_bound(lsh+1,lsh+n+1,a[i].val1)-lsh;
	for(int i=1;i<=n;i++)sum[i]=sum[i-1]+a[i].cost;
//	for(int i=1;i<=n;i++){
//		for(int j=a[i].val1;j<=n;j++)f[i][i][j]=a[i].cost;
//		for(int j=1;j<a[i].val1;j++)f[i][i][j]=a[i].cost+m;
//	}
	for(int len=0;len<=n-1;len++){
		for(int l=1;l<=n-len;l++){
			int r=l+len;
			for(int s=1;s<=n;s++){
				for(int k=l;k<=r;k++){
					if(a[k].val1>=s)f[l][r][s]=min(f[l][r][s],f[l][k-1][a[k].val1]+f[k+1][r][a[k].val1]+sum[r]-sum[l-1]);
					f[l][r][s]=min(f[l][r][s],f[l][k-1][s]+f[k+1][r][s]+sum[r]-sum[l-1]+m);
				}
			}
		}
	}
	cout<<f[1][n][1];
	return 0;
}

B. ZYB玩字符串

题意:每次可以删除串中的一个 S,最少剩下多长

其意义在于这种消除的模式并不能贪心,其具有区间合并性
dp 状态相当于也是维护了后缀信息
详见 这里

代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=205;
int t,n,lenans,nxt[maxn],cnt,sum[30],sum1[30];
bool f[maxn][maxn];
char a[maxn],b[maxn],c[maxn],ans[maxn];
void solve(int l,int r){
	int len=r-l+1;
	for(int i=l;i<=r;i++)b[i-l+1]=a[i];
	memset(f,0,sizeof f);
	for(int i=1;i<=n;i++){
		f[i][i-1]=true;
	}
	for(int len1=0;len1<=n-1;len1++){
		for(int l=1;l<=n-len1;l++){
			int r=l+len1;
			f[l][r]|=f[l][r-1]&(a[r]==b[len1%len+1]);
			for(int k=1;k*len<=len1;k++){
				f[l][r]|=f[l][r-k*len]&f[r-k*len+1][r];
				if(f[l][r])break;
			}
//			cout<<l<<" "<<r<<" "<<f[l][r]<<endl;
		}
	}
	if(f[1][n]){
		lenans=len;
		for(int i=1;i<=len;i++)ans[i]=b[i];
	}
	return ;
}
bool cmp(int len,int l,int r){
	if(!len)return true;
	for(int i=l;i<=r;i++){
		if(a[i]!=ans[i-l+1])return a[i]<ans[i-l+1];
	}
	return false;
}
bool check(int l,int r){
	memset(sum1,0,sizeof sum1);
	int base=n/(r-l+1);
	for(int i=l;i<=r;i++){
		sum1[a[i]-'a']++;
	}
//	cout<<"hhh"<<endl;
	for(int i=0;i<26;i++){
		if(sum1[i]*base!=sum[i])return false;
	}
	return true;
}
int main(){
	freopen("string.in","r",stdin);
	freopen("string.out","w",stdout);
	cin>>t;
	while(t--){
		memset(sum,0,sizeof sum);
		scanf("%s",a+1);
		n=strlen(a+1);lenans=0;
		for(int i=1;i<=n;i++){
			sum[a[i]-'a']++;
		}
		for(int len=0;len<=n-1;len++){
//			cout<<n<<" "<<len+1<<endl;
			if(n%(len+1))continue;
//			cout<"hhh";
			for(int l=1;l<=n-len;l++){
				int r=l+len;
				if(cmp(lenans,l,r)){
//					cout<<"hhh";
					if(check(l,r))solve(l,r);
				}
			}
			if(lenans)break;
		}
		for(int i=1;i<=lenans;i++){
			printf("%c",ans[i]);
		}
		puts("");
	}
	return 0;
}

  • 资料

日报

  • 咕咕咕

区间 dp 与四边形不等式连接紧密,继续往后咕

posted @   y_cx  阅读(64)  评论(0编辑  收藏  举报
编辑推荐:
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
历史上的今天:
2021-08-05 noip模拟30
点击右上角即可分享
微信分享提示