一本通提高篇 区间类动态规划

10/27 10:42
阅读数 36

2020.6.21 2020.6.21 2020.6.21考完月考的第一天还没出成绩 赶紧来加一篇 d p dp dp
2020.10.13 2020.10.13 2020.10.13没报上 C S P CSP CSP要考联赛得用老师的推荐名额,所以赶紧回来好好学两个月拿个一等奖
现在就啥对联赛有用就学啥,先弄 d p dp dp,再弄图论,最后整点数奆结垢和数学
先可简单的整吧


基本概念

大家应该都知道线性 d p dp dp,即在线性结构上进行状态转移,而区间类动态规划是线性动态规划的拓展,它在分阶段划分问题时,与阶段中元素出现的顺序和由前一阶段哪些元素合并而来有很大的关系。如状态 f [ i ] [ j ] f[i][j] f[i][j],它表示以 已合并的次数为阶段, 以 区卷左端点 i i i为状态,它的值取决于第 i i i个元素和第 j j j个元素断开的位置 k k k,即 f [ i ] [ k ] + f [ k + 1 ] [ j ] f[i][k]+f[k+1][j] f[i][k]+f[k+1][j]的值。这一类型的动态规划,阶段特征得长明显,求最优值时需要预先设置阶段内的区间统计值,还要以动态规划的起始位置来判断。

操作

区间 d p dp dp一般只有两步操作
1. 1. 1.合并:即将两个或多个部分进行整合,当然也可以反过来,也就是对一个问题分解成两个或多个部分
2. 2. 2.求解:对整个问题设最优值,枚举合并点,将问题分解成左右两个部分,最后合并左右两个部分得最优值得到原问题的最优值。类似分治算法的思想。

所以一个问题能不能用区间 d p dp dp,就是要看这个问题是否能被分解成为两两合并的形式。

这块也没啥难的 直接上题

石子合并

题面
S o l u t i o n : Solution: Solution:这题做的时候注意一下石头是按圆形操场摆放的 不是排一排
那怎么转化为排成一排的呢 只需要把序列再复制一遍就可以了
转移方程根本不用推。。
最大时 d p [ i ] [ j ] = m i n k = i j − 1 { d p [ i ] [ k ] + d p [ k + 1 ] [ j ] + ∑ l = i j a [ l ] } dp[i][j]=min_{k=i}^{j-1}\{dp[i][k]+dp[k+1][j]+\sum_{l=i}^{j}a[l]\} dp[i][j]=mink=ij1{ dp[i][k]+dp[k+1][j]+l=ija[l]}
最小时 d p [ i ] [ j ] = m a k = i j − 1 { d p [ i ] [ k ] + d p [ k + 1 ] [ j ] + ∑ l = i j a [ l ] } dp[i][j]=ma_{k=i}^{j-1}\{dp[i][k]+dp[k+1][j]+\sum_{l=i}^{j}a[l]\} dp[i][j]=mak=ij1{ dp[i][k]+dp[k+1][j]+l=ija[l]}
上代码:





#include<bits/stdc++.h>
using namespace std;
#define N 404
#define maxx 999999999
int n,ans,a[N],s[N],dp[N][N];
inline void read(int &x){
   
   
	int s=0,w=1;char ch=getchar();
	while(ch<'0'||ch>'9'){
   
   if(ch=='-')w=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){
   
   s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
	x=s*w;
}
int main(){
   
   
	read(n);
	for(int i=1;i<=n;i++)read(a[i]),a[i+n]=a[i];
	for(int i=1;i<=n<<1;i++)s[i]=s[i-1]+a[i];
	for(int l=1;l<n;l++){
   
   
		for(int i=1,j=l+1;j<=n<<1;i++,j++){
   
   
			dp[i][j]=maxx;
			for(int k=i;k<j;k++)dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+s[j]-s[i-1]);
		}
	}
	ans=maxx;
	for(int i=1;i<=n;i++)ans=min(dp[i][i+n-1],ans);
	printf("%d\n",ans);
	for(int l=1;l<n;l++){
   
   
		for(int i=1,j=l+1;j<=n<<1;i++,j++){
   
   
			dp[i][j]=0;
			for(int k=i;k<j;k++)dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]+s[j]-s[i-1]);
		}
	}
	ans=0;
	for(int i=1;i<=n;i++)ans=max(dp[i][i+n-1],ans);
	printf("%d\n",ans);
}

能量项链

题面
S o l u t i o n : Solution: Solution:果题 需要注意的是 i − k i-k ik区间和 k − j k-j kj区间合并时 加上的是 a [ i ] × a [ k + 1 ] × a [ j + 1 ] a[i]×a[k+1]×a[j+1] a[i]×a[k+1]×a[j+1] 别乘错了!
状态转移方程: d p [ i ] [ j ] = m a x k = i j − 1 { d p [ i ] [ k ] + d p [ k ] [ j ] + a [ i ] × a [ k + 1 ] × a [ j + 1 ] } dp[i][j]=max_{k=i}^{j-1}\{dp[i][k]+dp[k][j]+a[i]×a[k+1]×a[j+1]\} dp[i][j]=maxk=ij1{ dp[i][k]+dp[k][j]+a[i]×a[k+1]×a[j+1]}
代码↓


#include<bits/stdc++.h>
using namespace std;
#define N 220
int n,ans,a[N],dp[N][N];
int main(){
   
   
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
   
   
		scanf("%d",&a[i]);
		a[i+n]=a[i];
	}
	for(int l=1;l<n;l++){
   
   
		for(int i=1,j=l+1;j<=n<<1;i++,j++){
   
   
			for(int k=i;k<j;k++)dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]+a[i]*a[k+1]*a[j+1]);
		}
	}
	for(int i=1;i<=n;i++)if(dp[i][i+n-1]>ans)ans=dp[i][i+n-1];
	printf("%d\n",ans);
}

凸多边形的划分

题目描述
给定一个具有N(N≤50)个顶点(从1到N编号)的凸多边形,每个顶点的权均是一个正整教,
问:如何把这个凸多边形划分成N-2个互不相交的三角形,使得这些三角形顶点的权的乘积之和最小?

输入
输入文件的第一一行为顶点数N;第二行为N个顶点(从1到N)的权值。
输出
输出一行为这些三角形顶点的权的乘积之和的最小值。
样例输入
5
121 122 123 245 231
样例输出
12214884
提示
S o l u t i o n : Solution: Solution:
先将节点顺时针依次编号
d p [ i ] [ j ] dp[i][j] dp[i][j]表示从 i i i节点到 j j j节点划分得到的乘积之和最小值
如图在这里插入图片描述
那么如何状态转移呢
k ∈ ( i , j ) k∈(i,j) k(i,j),将 i − j i-j ij分成 i − k , k − j i-k,k-j ik,kj来转移
在这里插入图片描述
如图,在 k k k点分割之后出现了三个三角形
绿色部分分别为 d p [ i ] [ k ] dp[i][k] dp[i][k] d p [ k ] [ j ] dp[k][j] dp[k][j]
黄色部分为 a [ i ] × a [ j ] × a [ k ] a[i]×a[j]×a[k] a[i]×a[j]×a[k]
那么 d p [ i ] [ j ] = m i n k = i + 1 j − 1 { d p [ i ] [ k ] + d p [ k ] [ j ] + a [ i ] × a [ j ] × a [ k ] } dp[i][j]=min_{k=i+1}^{j-1}\{dp[i][k]+dp[k][j]+a[i]×a[j]×a[k]\} dp[i][j]=mink=i+1j1{ dp[i][k]+dp[k][j]+a[i]×a[j]×a[k]}
初始状态 d p [ i ] [ j ] = m a x dp[i][j]=max dp[i][j]=max d p [ i ] [ i + 1 ] = 0 dp[i][i+1]=0 dp[i][i+1]=0
需要注意的是不开高精只有50分 q a q qaq qaq
就当打高精模板了
上代码























#include<bits/stdc++.h>
using namespace std;
#define N 55
#define ll long long
ll n,a[N];
struct bigint{
   
   
	int len,s[303];
	bigint(){
   
   
		memset(s,0,sizeof(s));
		len=1;
	}
	bool operator <(const bigint &A)const{
   
   
		if(len!=A.len)return len<A.len;
		for(int i=len-1;~i;i--)
			if(s[i]!=A.s[i])return s[i]<A.s[i];
		return false;
	}
	bigint operator +(const bigint &A)const{
   
   
		bigint B;
		B.len=max(len,A.len);
		for(int i=0;i<B.len;i++){
   
   
			B.s[i]+=s[i]+A.s[i];
			B.s[i+1]+=B.s[i]/10,B.s[i]%=10;
		}
		if(B.s[B.len])B.len++;
		return B;
	}
	bigint operator *(const bigint &A)const{
   
   
		bigint B;
		B.len=len+A.len-1;
		for(int i=0;i<A.len;i++){
   
   
			for(int j=0;j<len;j++){
   
   
				B.s[i+j]+=A.s[i]*s[j];
				B.s[i+j+1]+=B.s[i+j]/10,B.s[i+j]%=10;
			}
		}
		if(B.s[B.len])B.len++;
		return B;
	}
	void print(){
   
   
		int now=len-1;
		while(!s[now]&&now)now--;
		for(int i=now;~i;i--)printf("%d",s[i]);
	}
}dp[N][N];
bigint change(ll x){
   
   
	bigint t;int cnt=0;
	while(x){
   
   
		t.s[cnt++]=x%10;
		x/=10;
	}
	t.len=cnt;
	return t;
}
int main(){
   
   
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int l=2;l<n;l++){
   
   
		for(int i=1,j=l+1;j<=n;i++,j++){
   
   
			dp[i][j].len=300,dp[i][j].s[299]=1;
			for(int k=i+1;k<j;k++){
   
   
				bigint t=change(a[i])*change(a[j])*change(a[k]);
				bigint newdp=dp[i][k]+dp[k][j]+t;
				if(newdp<dp[i][j])dp[i][j]=newdp;
			}
		}
	}
	dp[1][n].print();
}

括号配对

题面
S o l u t i o n : Solution: Solution:判断需要最少加几个括号就是找最少有多少个括号没匹配
逆向思维直接找能匹配的最多的括号
可以直接用 S T L STL STL栈过 还是练下区间 d p dp dp
d p [ i ] [ j ] dp[i][j] dp[i][j]表示 i − j i-j ij区间内可以匹配到的最大括号数量
在序列 i − j i-j ij中 如果 s [ i ] s[i] s[i] s [ j ] s[j] s[j]可以匹配,则 d p [ i ] [ j ] = d p [ i + 1 ] [ j − 1 ] + 2 dp[i][j]=dp[i+1][j-1]+2 dp[i][j]=dp[i+1][j1]+2
之后再直接判断 d p [ i ] [ j ] = m a x k = i j − 1 { d p [ i ] [ k ] + d p [ k ] [ j ] } dp[i][j]=max_{k=i}^{j-1}\{dp[i][k]+dp[k][j]\} dp[i][j]=maxk=ij1{ dp[i][k]+dp[k][j]}
上代码






#include<bits/stdc++.h>
using namespace std;
int n,dp[110][110];
char s[110];
int main(){
   
   
	gets(s);
	n=strlen(s);
	for(int l=1;l<n;l++){
   
   
		for(int i=0,j=l;j<n;i++,j++){
   
   
			if((s[i]=='('&&s[j]==')')||(s[i]=='['&&s[j]==']'))dp[i][j]=dp[i+1][j-1]+2;
			for(int k=i;k<j;k++)dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]);
		}
	}
	cout<<n-dp[0][n-1]<<endl;
}

分离与合体

题面
S o l u t i o n : Solution: Solution:这题题面不太好懂,先看第一问,大概意思就是说一个序列,每两个数可以合并成一个数,问最后合并的数最大是多少
很显然的区间 d p dp dp,转移方程一眼就能看出来
d p [ i ] [ j ] = m a x k = i j − 1 { d p [ i ] [ k ] + d p [ k − 1 ] [ j ] + ( a [ i ] + a [ j ] ) × a [ k ] } dp[i][j]=max_{k=i}^{j-1}\{dp[i][k]+dp[k-1][j]+(a[i]+a[j])×a[k]\} dp[i][j]=maxk=ij1{ dp[i][k]+dp[k1][j]+(a[i]+a[j])×a[k]}
但是问题是第二问,如何将每次分裂记下来
p [ i ] [ j ] p[i][j] p[i][j] i i i j j j区间分裂的位置,只需在每次更新 d p [ i ] [ j ] dp[i][j] dp[i][j]时顺便维护 p p p数组
即当 d p [ i ] [ k ] + d p [ k − 1 ] [ j ] + ( a [ i ] + a [ j ] ) × a [ k ] > d p [ i ] [ j ] dp[i][k]+dp[k-1][j]+(a[i]+a[j])×a[k]>dp[i][j] dp[i][k]+dp[k1][j]+(a[i]+a[j])×a[k]>dp[i][j]时 让 p [ i ] [ j ] = k p[i][j]=k p[i][j]=k即可
输出时用搜索来找每一层分裂的位置,因为除第一层外有很多个分裂点,所以用 v e c t o r vector vector来存储每一层分裂的位置,最后再排序输出
需要注意的是 最后一个数不要输出空格 不然会格式错误…
上代码








#include<bits/stdc++.h>
using namespace std;
#define N 330
int n,dep,a[N],dp[N][N],p[N][N];
vector<int> ans[N];
void dfs(int l, int r, int depth){
   
   
	if(l==r)return ;
	if(depth>dep)dep=depth;
	ans[depth].push_back(p[l][r]);
	dfs(l,p[l][r],depth+1);
	dfs(p[l][r]+1,r,depth+1);
}
int main(){
   
   
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int l=1;l<n;l++){
   
   
		for(int i=1,j=l+1;j<=n;i++,j++){
   
   
			for(int k=i;k<j;k++){
   
   
				int now=dp[i][k]+dp[k+1][j]+(a[i]+a[j])*a[k];
				if(now>dp[i][j])dp[i][j]=now,p[i][j]=k;
			}
		}
	}
	cout<<dp[1][n]<<endl;
	dfs(1,n,1);
	for(int i=1;i<=dep;i++)sort(ans[i].begin(),ans[i].end());
	for(int i=1;i<=dep;i++){
   
   
		int siz=ans[i].size();
		for(int j=0;j<siz;j++){
   
   
			if(j==siz-1&&i==dep)printf("%d",ans[i][j]);
			else printf("%d ",ans[i][j]);
		}
	}
	cout<<endl;
}

矩阵取数游戏

题面
S o l u t i o n : Solution: Solution:这题还是个 N O I P NOIP NOIP题哈哈
每行取的数只与本行之前的方案有关,所以我们只需要让每一行都取最优
d p [ i ] [ j ] dp[i][j] dp[i][j]表示每行处理 i − j i-j ij时,当前的最优方案
d p [ i ] [ j ] = m a x { d p [ i − 1 ] [ j ] + 2 m − i − j + 1 , d p [ i ] [ j + 1 ] + 2 m − i − j + 1 } dp[i][j]=max\{dp[i-1][j]+2^{m-i-j+1},dp[i][j+1]+2^{m-i-j+1}\} dp[i][j]=max{ dp[i1][j]+2mij+1,dp[i][j+1]+2mij+1}
答案需要在区间为 1 1 1 d p [ i ] [ i ] dp[i][i] dp[i][i]里找
a n s = ∑ k = 1 n m a x i = 1 m { d p [ i ] [ i ] + 2 i × a [ i ] } ans=\sum\limits_{k=1}^{n}max_{i=1}^{m}\{dp[i][i]+2^i×a[i]\} ans=k=1nmaxi=1m{ dp[i][i]+2i×a[i]}
预处理出 2 n 2^n 2n,再打个高精 就可以了
代码在机房 不想再打一次高精了 ( ( (哭哭 周二更代码
周一中午跑来交代码了哈哈 我 D a w n Dawn Dawn绝不鸽!
上代码↓









#include<bits/stdc++.h>
using namespace std;
#define reg register
#define N 82
struct bigint{
   
   
	int s[301],len;
	bigint(){
   
   
		memset(s,0,sizeof(s));
		len=1;
	}
	bool operator <(const bigint &A)const{
   
   
		if(len!=A.len)return len<A.len;
		for(reg int i=len-1;~i;i--)
			if(s[i]!=A.s[i])return s[i]<A.s[i];
		return false;
	}
	bigint operator +(const bigint &A)const{
   
   
		bigint B;
		B.len=max(len,A.len);
		for(int i=0;i<B.len;i++)
			B.s[i]+=s[i]+A.s[i],B.s[i+1]+=B.s[i]/10,B.s[i]%=10;
		if(B.s[B.len])B.len++;
		return B;
	}
	bigint operator *(const bigint &A)const{
   
   
		bigint B;
		B.len=len+A.len-1;
		for(int i=0;i<A.len;i++)
			for(int j=0;j<len;j++)
				B.s[i+j]+=A.s[i]*s[j],B.s[i+j+1]+=B.s[i+j]/10,B.s[i+j]%=10;
		if(B.s[B.len])B.len++;
		return B;
	}
	void print(){
   
   
		int now=len-1;
		while(!s[now]&&now)now--;
		for(reg int i=now;~i;i--)putchar(s[i]+'0');
		putchar(10);
	}
}dp[N][N],bas[N],cnt;
void work(){
   
   
	bas[0].len=1,bas[0].s[0]=1;
	bigint t;t.len=1,t.s[0]=2;
	for(reg int i=1;i<=80;i++)bas[i]=bas[i-1]*t;
}
bigint change(int x){
   
   
	int sum=0;
	bigint num;
	while(x){
   
   
		num.s[sum++]=x%10;
		x/=10;
	}
	num.len=sum;
	return num;
}
int n,m,a[N];
int main(){
   
   
	cin>>n>>m;
	work();
	for(reg int f=1;f<=n;f++){
   
   
		for(int i=1;i<=m;i++){
   
   
			for(int j=1;j<=m;j++)
				dp[i][j].len=1,dp[i][j].s[0]=0;
		}
		for(reg int i=1;i<=m;i++)scanf("%d",&a[i]);
		for(reg int i=1;i<=m;i++)
			for(reg int j=m;j>=i;j--){
   
   
				bigint p=change(a[i-1]),q=change(a[j+1]);
				bigint s1=dp[i-1][j]+p*bas[m+i-j-1],s2=dp[i][j+1]+q*bas[m-j+i-1];
				if(dp[i][j]<s1)dp[i][j]=s1;
				if(dp[i][j]<s2)dp[i][j]=s2;
		}
		bigint ans;
		for(reg int i=1;i<=m;i++){
   
   
			bigint p=change(a[i]);
			if(ans<p*bas[m]+dp[i][i])ans=p*bas[m]+dp[i][i];
		}
		cnt=cnt+ans;
	}
	cnt.print();putchar(10);
}

总结

区间 d p dp dp的复杂度一般都是 O ( n 3 ) O(n^3) O(n3),故数据范围在 100 100 100左右时可以考虑区间 d p dp dp,区间 d p dp dp的状态转移方程好想,总体来说是个不错的解题方法。

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
OSCHINA
登录后可查看更多优质内容
返回顶部
顶部