区间DP

摘抄于_简书浅谈区间动态规划

围绕几道题说起。。石子归并、涂色、括号序列

啥是区间动态规划呢,我觉得似乎是指在一段区间上的dp,通过枚举左右子区间来求出解。

那么问题来了,如何去枚举左右子区间呢?

一般来说都是循环一个变量len,表示区间长度,然后循环左区间从开始到结尾,一般来说是1~n。

对于区间dp的话,我大致理解就是先求出小区间(部分)最优解,然后一个又一个小区间合并成稍微大点的大区间,最后合成答案——即总区间。

所以代码就这玩意:

for(int len=1;len<=n;len++)
{
    for(int l=1,r;(r=l+len)<=n;l++) { //do something for(int k=l;k<r;k++) { //update dp array,such as'min(dp[l][r],dp[l][i]+dp[i+1][r])' } } } 

所以就是这样咯,循环一个长度,然后枚举区间。

区间的话一定要注意是l<=k<r,不然的话[i+1][r]直接6了。。。

好像。。。区间dp就这些内容了吧,哦对了还有平行四边形优化!

普通的区间dp的时间复杂度大约是O(n^3),从三个嵌套的for就能看出来。

不过有一些区间dp可以优化成O(n^2),要用一个叫做“四边形不等式”的东西进行优化。

大致思想就是保存枚举的k中最优子区间,每次可以将枚举k时的时间复杂度去掉,所以就只剩下了长度与左区间的枚举。

四边形不等式优化

下面就是三道入门题。。。


//石子归并
#include <iostream> #include <algorithm> #include <cstdio> #include <cstring> #include <cstdlib> using namespace std; char in[1000]; int dp[1000][1000],n,w[1000],sum[1000]; int main() { scanf("%d",&n); for(int i=1;i<=n;i++) { scanf("%d",&w[i]); sum[i]=sum[i-1]+w[i];//由于合并的是一个区间,所以记录一下前缀和 } for(int len=1;len<=n;len++)//循环长度 { for(int l=1,r;(r=l+len)<=n;l++) { dp[l][r]=0x3fffffff;//初始化为INF for(int i=l;i<r;i++)//枚举中点 dp[l][r]=min(dp[l][r],dp[l][i]+dp[i+1][r]+sum[r]-sum[l-1]);//判断区间l,r分割成两个小区间后是否更优 } } printf("%d\n",dp[1][n]);//答案 } 

//bzoj1260涂色
#include <iostream> #include <algorithm> #include <cstdio> #include <cstring> #include <cstdlib> using namespace std; char in[1000]; int dp[1000][1000]; int main() { scanf("%s",in+1);//方便下面dp int n=strlen(in+1); for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)dp[i][j]=i==j?1:0x3fffffff;//对于一个点的涂色次数,只能是1次 for(int len=1;len<=n;len++)//循环长度 { for(int l=1,r;(r=l+len)<=n;l++)//生成左右区间 { if(in[l]==in[r])//如果相等那么就可以如下这么转移咯 { if(len==1)dp[l][r]=1;//如果区间长度为1,也就是说l,r是相邻的两个格子,所以只能一笔涂上 else dp[l][r]=min(dp[l+1][r-1]+1,min(dp[l][r-1],dp[l+1][r])); /* 否则的话说明可以从区间l,r中进行转移。 判断dp[l][r]所包含的三个子区间 然后就是dp[l+1][r]跟dp[l][r-1]了。 先把最右端/最左端为起点一笔涂到另一头,应该是这意思吧? */ } else//否则的话只能将区间l,r分割成两个小区间然后min咯 for(int i=l;i<r;i++) dp[l][r]=min(dp[l][r],dp[l][i]+dp[i+1][r]); } } printf("%d\n",dp[1][n]);//答案 } 

//tyvj P1193 括号序列
/*
    至于为什么这个dp是对的,小谈一下我的个人理解。
    由于区间长度len是从小到的枚举的,所以每一轮都是从很小的单位区间开始更新,似乎可以看作bfs(雾)?
    因为len是由小到大进行枚举,所以每一次min的时候都是从将已经判断好的子区间进行min。
    或许用滚雪球形容会好点?从一个小区间慢慢滚成了一个大区间?
    以上就是我对于初级区间dp的理解。。。
*/
#include <cstring> #include <iostream> #include <algorithm> #include <cstdio> using namespace std; char in[1000]; int dp[1000][1000],n; bool test(char a,char b)//判断a,b是否匹配 { if(a=='(')return b==')'; if(a=='[')return b==']'; if(a=='<')return b=='>'; if(a=='{')return b=='}'; return 0; } int main() { /* dp[i][i]=1 若是单个括号的话只能是添加所对应的括号完成匹配,所以为1 dp[l][r]:区间l,r的最小添加括号数量 dp[l][r]=min{n,dp[l+1][r-1](match(s[l],s[r]),dp[l][k]+dp[k+1][r]|l<=k<r} */ gets(in+1);//为了能够顺手的写代码,所以下标从1开始 n=strlen(in+1); while(in[n]=='\n'||in[n]=='\r')n--; //不知道为啥gets会莫名其妙的读入一个换行/回车,所以第一次就WA了。。。所以加上这个while判一下是否有换行,不过似乎最多就循环一次? for(int i=1;i<=n;i++)dp[i][i]=1; for(int len=1;len<=n;len++) { for(int l=1,r;(r=l+len)<=n;l++) { dp[l][r]=r-l+1;//边界,对于区间l,r一共有r-l+1个括号,所以最坏情况下需要添加r-l+1个括号,不过似乎写成n也能A掉?好吧写成dp[l][r]=n会快点。。。毕竟避免了加减法运算 if(test(in[l],in[r]))//如果左右两端匹配,那么就可以进行一次转移 dp[l][r]=min(dp[l][r],dp[l+1][r-1]);//如果当前区间左右都是匹配的括号的话,状态转移为dp[l+1][r-1],也就是说转移到去掉括号后的序列 for(int i=l;i<r;i++)//枚举区间l,r中的所有区间 dp[l][r]=min(dp[l][r],dp[l][i]+dp[i+1][r]); } } printf("%d\n",dp[1][n]);//最后的结果就是dp[1][n] }


作者:KingSann
链接:https://www.jianshu.com/p/24feb3ccaf2e
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

转载于:https://www.cnblogs.com/Swust-lyon/p/8590984.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值