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

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

凸多边形的划分

题面
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),故数据范围在 300 300 300左右时可以考虑区间 d p dp dp,区间 d p dp dp的状态转移方程好想,总体来说是个不错的解题方法。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
《信息学奥赛一本提高》是一本针对信息学奥林匹克竞赛的学习资料,在提高信息学竞赛能力方面非常有帮助。这本书主要分为四个部分,包括基础知识、经典题型、解题技巧和实战训练。 首先,基础知识部分系统地介绍了信息学竞赛中常见的数据结构、算法和编程技巧。过深入理解这些基础知识,我们可以建立起解题的思维框架,提高解题的效率和准确度。 其次,经典题型部分列举了一些常见的信息学竞赛题型,并给出了详细的解题分析。过研究这些经典题目的解题思路和方法,我们可以学习到一些解题的常用套路和技巧,提高解题的能力。 接下来,解题技巧部分介绍了一些常用的解题技巧,如递推法、贪心算法动态规划等。过学习这些解题技巧,我们可以更加灵活地应用不同的算法思想,解决更加复杂的竞赛题目。 最后,实战训练部分提供了大量的信息学竞赛题目和练习,帮助我们过实践巩固所学的知识和技巧。这些题目有不同的难度和型,可以提供充分的练习机会,并提供了详细的解题思路和方法供参考。 总结来说,《信息学奥赛一本提高》是一本非常实用的学习资料,能够帮助学生提高信息学竞赛能力。过深入学习基础知识、研究经典题型、掌握解题技巧和进行实战训练,我们可以有效地提升在信息学竞赛中的表现,并取得更好的成绩。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值