区间DP
区间DP
两个难点:
1、枚举所有可能的区间
2、状态转移方程
经典问题1:石子合并
1274:【例9.18】合并石子 时间限制: 1000 ms 内存限制: 65536 KB 提交数: 5121 通过数: 3243 【题目描述】 在一个操场上一排地摆放着N堆石子。现要将石子有次序地合并成一堆。规定每次只能选相邻的2堆石子合并成新的一堆,并将新的一堆石子数记为该次合并的得分。 计算出将N堆石子合并成一堆的最小得分。 【输入】 第一行为一个正整数N (2≤N≤100); 以下N行,每行一个正整数,小于10000,分别表示第i堆石子的个数(1≤i≤N)。 【输出】 一个正整数,即最小得分。 【输入样例】 7 13 7 8 16 21 4 18 【输出样例】 239
状态:f[i][j]表示区间i到j之间的最小花费,那么f[1][n]就是最后的答案
状态转移方程:f[i][j]=f[i][k]+f[k][j]+sum[i][j-i+1];
所以在这里需要求一个前缀和sum[i][j-i+1] 就相当于sum[j]-sum[i-1]
#include <bits/stdc++.h> using namespace std; const int N=130,inf=0x3f3f3f;; int sum[N],c[N]; int f[N][N],n,x,t; inline int read() { int x=0;char y='*',z=getchar(); while(z<'0'||z>'9') y=z,z=getchar(); while(z>='0'&&z<='9') x=(x<<3)+(x<<1)+(z-'0'),z=getchar(); return y=='-'?-x:x; } int main() { n=read(); for(int i=1;i<=n;i++){ x=read(); sum[i]=sum[i-1]+x;//前缀和 } memset(f,127/3,sizeof(f)); for(int i=1;i<=n;i++) f[i][i]=0; for(int i=n-1;i>=1;i--) for(int j=i+1;j<=n;j++) for(int k=i;k<=j-1;k++) f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+sum[j]-sum[i-1]); cout<<f[1][n]<<endl; return 0; }
上述代码有三重循环,所以复杂度为O(n3),所以这种问题只能处理n<250的问题;
该问题在动态规划中也有写过,咳咳还有一个环形的情况
优化:
平行四边形优化原理:是区间DP常用的优化方法;主要思想就是用s[i][j]来存储最优分割点
只需要更改几个地方:(红色)!!
#include <bits/stdc++.h> using namespace std; const int N=130,inf=0x3f3f3f;; int sum[N],c[N],s[N][N]; int f[N][N],n,x,t; inline int read() { int x=0;char y='*',z=getchar(); while(z<'0'||z>'9') y=z,z=getchar(); while(z>='0'&&z<='9') x=(x<<3)+(x<<1)+(z-'0'),z=getchar(); return y=='-'?-x:x; } int main() { n=read(); for(int i=1;i<=n;i++){ x=read(); sum[i]=sum[i-1]+x;//前缀和 } memset(f,127/3,sizeof(f)); for(int i=1;i<=n;i++) { f[i][i]=0;s[i][i]=i; } for(int i=n-1;i>=1;i--) for(int j=i+1;j<=n;j++) for(int k=s[i][j-1];k<=s[i+1][j];k++) if(f[i][j]>f[i][k]+f[k+1][j]+sum[j]-sum[i-1]) { f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+sum[j]-sum[i-1]); s[i][j]=k; } cout<<f[1][n]<<endl; return 0; }
这样优化以后,复杂度更加接近于O(n2),科技解决接近n<3000的问题了~
经典问题2:回文串
回文串是正读反读都是一样的字符串,最经典的问题就是:给定一个字符串,通过增加或者删除得到一个回文串。
poj 3280 给出一个由m中字母组成的长度为n的串,给出m种字母添加和删除花费的代价,求让给出的串变成回文串的代价。
我们定义dp [ i ] [ j ] 为区间 i 到 j 变成回文的最小代价。
那么有三种情况:
首先:对于一个串如果s【i】==s【j】,那么dp【i】【j】=dp【i+1】【j-1】
其次:如果dp【i+1】【j】是回文串,那么dp【i】【j】=dp【i+1】【j】+min(add【i】,del【i】);
最后,如果dp【i】【j-1】是回文串,那么dp【i】【j】=dp【i】【j-1】 + min(add【j】,del【j】);
参考代码:
int main() { int n,m,t,i,j; while(~scanf("%d%d",&n,&m)) { scanf("%s",s); for(i=0; i<n; i++) { scanf("%s",c); scanf("%d%d",&cost[c[0]-'a'],&t); if(t<cost[c[0]-'a']) cost[c[0]-'a']=t; } memset(dp,0,sizeof(dp)); for(i=m-1; i>=0; i--) { for(j=i+1; j<m; j++) { dp[i][j]=min(dp[i+1][j]+cost[s[i]-'a'],dp[i][j-1]+cost[s[j]-'a']); if(s[i]==s[j]) dp[i][j]=min(dp[i][j],dp[i+1][j-1]); } } printf("%d\n",dp[0][m-1]); } return 0; }
例题:回文词
Description 回文词是一种对称的字符串——也就是说,一个回文词,从左到右读和从右到左读得到的结果是一样的。任意给定一个字符串,通过插入若干字符,都可以变成一个回文词。你的任务是写一个程序,求出将给定字符串变成回文词所需插入的最少字符数。比如字符串“Ab3bd”,在插入两个字符后可以变成一个回文词(“dAb3bAd”“Adb3bdA”)。然而,插入两个以下的字符无法使它变成一个回文词。 Input 第一行包含一个整数N,表示给定字符串的长度(3≤N≤50 00)。 第二行是一个长度为N的字符串。字符串仅由大写字母“A”到“Z”,小写字母“a”到“z”和数字“0”到“9”构成。大写字母和小写字母将被认为是不同的。 Output 只有一行,包含一个整数,表示需要插入的最少字符数。 Sample Input 5 Ab3bd Sample Output 2
用字符串长度来减去(原字符串与反串)的最长公共子序列长度只能得到70分
还是得用这种记忆化的dp才能AC:
#include <bits/stdc++.h> using namespace std; const int N=5005,inf=0x3f3f3f;; int f[N][N],n,x; char s[N],rs[N]; int dfs(int i,int j) { if (f[i][j]!=0xfffffff) return f[i][j]; if (s[i-1]==s[j-1]) return f[i][j]=dfs(i+1,j-1); else { f[i][j]=min(dfs(i+1,j),dfs(i,j-1))+1; } return f[i][j]; } int main() { scanf("%d",&n); cin>>s; for (int i=1;i<=n;i++) for (int j=i+1;j<=n;j++) { f[i][j]=0xfffffff;f[i][i]=0; } f[1][n]=dfs(1,n); printf("%d",f[1][n]); return 0; }
区间动态规划例题:(做到就来补一补)
添加括号:
Description 【题目背景】 给定一个正整数序列a1,a2,...,an,(1<=n<=20) 不改变序列中每个元素在序列中的位置,把它们相加,并用括号记每次加法所得的和,称为中间和。 例如: 给出序列是4,1,2,3。 第一种添括号方法: ((4+1)+(2+3))=((5)+(5))=(10) 有三个中间和是5,5,10,它们之和为:5+5+10=20 第二种添括号方法 (4+((1+2)+3))=(4+((3)+3))=(4+(6))=(10) 中间和是3,6,10,它们之和为19。 【问题描述】 现在要添上n-1对括号,加法运算依括号顺序进行,得到n-1个中间和,求出使中间和之和最小的添括号方法。 Input 共两行。 第一行,为整数n。(1<=n<=20) 第二行,为a1,a2,...,an这n个正整数,每个数字不超过100。 Output 输出3行。 第一行,为添加括号的方法。 第二行,为最终的中间和之和。 第三行,为n-1个中间和,按照从里到外,从左到右的顺序输出。 Sample Input 4 4 1 2 3 Sample Output (4+((1+2)+3)) 19 3 6 10
主要是输出!!!其实计算最小值就跟合并石子差不多
#include <bits/stdc++.h> using namespace std; const int N=110,inf=0x3f3f3f; int n,f[N][N],a[N],sum[N],loc[N][N]; void print_equa(int l,int r) { if(l==r){ cout<<a[l];return; } cout<<"("; int k=loc[l][r]; print_equa(l,k); cout<<"+"; print_equa(k+1,r); cout<<")"; } void print_sum(int l,int r) { if(!loc[l][r]) return; int k=loc[l][r]; print_sum(l,k); print_sum(k+1,r); cout<<sum[r]-sum[l-1]<<" "; } int main() { cin>>n; for(int i=1;i<=n;i++){ cin>>a[i]; sum[i]=sum[i-1]+a[i]; } memset(f,0x3f,sizeof(f)); for(int i=1;i<=n;i++) f[i][i]=0; for(int t=2;t<=n;t++) for(int i=1;i<=n-t+1;i++) { int j=i+t-1; for(int k=i;k<j;k++) { if(f[i][j]>f[i][k]+f[k+1][j]+sum[j]-sum[i-1]) { f[i][j]=f[i][k]+f[k+1][j]+sum[j]-sum[i-1]; loc[i][j]=k;//记录两点之间的最佳位置 } } } print_equa(1,n); cout<<endl<<f[1][n]<<endl; print_sum(1,n); }
能量项链:
【题目描述】 原题来自:NOIP 2006 在 Mars 星球上,每个 Mars 人都随身佩带着一串能量项链。在项链上有 NN 颗能量珠。能量珠是一颗有头标记和尾标记的珠子,这些标记对应着某个正整数。并且,对于相邻的两颗珠子,前一颗珠子的尾标记必定等于后一颗珠子的头标记。因为只有这样,通过吸盘——Mars 人吸收能量的器官的作用,这两颗珠子才能聚合成一颗珠子,同时释放出可被吸盘吸收的能量。如果一颗能量珠头标记为 mm,尾标记为 rr,后一颗能量珠头标记为 rr,尾标记为 nn,则聚合后释放出 m×r×nm×r×n Mars单位的能量,新珠子头标记为 mm,尾标记为 nn。 当需要时,Mars 人就用吸盘夹住相邻的两颗珠子,通过聚合得到能量,直到项链上只剩下一颗珠子为止。显然,不同的聚合顺序得到的总能量是不一样的。请设计一个聚合顺序使得一串珠子聚合后释放出的总能量最大。 例如,设 N=4N=4,四颗珠子头标记与尾标记分别为 (2,3),(3,5),(5,10),(10,2)(2,3),(3,5),(5,10),(10,2)。我们用记号 ⨂⨂ 表示两颗珠子的聚合操作,(j⨂kj⨂k) 表示 j,kj,k 两颗珠子聚合后释放出的能量,则4,14,1两颗珠子聚合后所释放的能量为(4⨂1)=10×2×3=60(4⨂1)=10×2×3=60,这一串项链可以得到最优值的一个聚合顺序所释放出的总能量为(((4⨂1)⨂2)⨂3)=10×2×3+10×3×5+10×5×10=710(((4⨂1)⨂2)⨂3)=10×2×3+10×3×5+10×5×10=710 现在给你一串项链,项链上有 nn 颗珠子,相邻两颗珠子可以合并成一个,合并同时会放出一定的能量,不同珠子合并放出能量不相同,请问按怎样的次序合并才能使得释放的能量最多? 【输入】 第一行一个正整数 nn 第二行 nn 个不超过 10001000 的正整数,第 i(1≤i≤n)i(1≤i≤n) 个数为第 ii 颗珠子的头标记,当 i≠ni≠n 时第 ii 颗珠子的尾标记等于第 i+1i+1 颗珠子的头标记,当 i=ni=n 时第 ii 颗珠子的尾标记等于第 11 颗珠子的头标记。 至于珠子的顺序,你可以这样确定:将项链放在桌面上,不要出现交叉,随机指定一颗珠子为第一颗珠子,按顺时针确定其它珠子的顺序。 【输出】 输出只有一行,一个不超过 2.1×1092.1×109 的正整数,表示最优聚合顺序所释放的能量。 【输入样例】 4 2 3 5 10 【输出样例】 710 【提示】 数据范围与提示: 对于 100% 的数据,4≤n≤1004≤n≤100。
和环形的石子合并题目十分相似
#include <bits/stdc++.h> using namespace std; const int N=410,inf=0x3f3f3f;; int tail[N],head[N]; int f[N][N],n,x,maxx; int main() { cin>>n; for(int i=1;i<=n;i++){ cin>>head[i]; head[i+n]=head[i];//环 } for(int i=1;i<=2*n-1;i++) tail[i]=head[i+1]; tail[2*n]=head[1]; for(int i=1;i<=2*n;i++) f[i][i]=0;//初始化 for(int t=1;t<=n-1;t++) for(int i=1;i<=2*n-t;i++) { int j=i+t; for(int k=i;k<=j-1;k++) { f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]+head[i]*tail[k]*tail[j]); } } for(int i=1;i<=n;i++) maxx=max(maxx,f[i][i+n-1]); cout<<maxx<<endl; return 0; }
凸多边形的划分:
难!的地方就是在所有的都是高精度计算
【题目描述】 给定一个具有 NN 个顶点的凸多边形,将顶点从 11 至 NN 标号,每个顶点的权值都是一个正整数。将这个凸多边形划分成 N−2N−2 个互不相交的三角形,试求这些三角形顶点的权值乘积和至少为多少。 【输入】 输入第一行为顶点数 NN 第二行依次为顶点 11 至顶点 NN 的权值。 【输出】 输出仅一行,为这些三角形顶点的权值乘积和的最小值。 【输入样例】 5 121 122 123 245 231 【输出样例】 12214884 【提示】 数据范围与提示: 对于 100% 的数据,有 N≤50N≤50,每个点权值小于 109109 。
#include <bits/stdc++.h> using namespace std; const int N=110,inf=0x3f3f3f;; /*思路跟前面一样,但是这个题需要高精度计算*/ typedef long long ll; ll f[N][N][N],a[N],s1[N],s2[N],s3[N]; int n; void Mark(ll c[]) { for(int i=1;i<=c[0];i++) { c[i+1]+=c[i]/10000; c[i]%=10000; } while(c[c[0]+1]) { c[0]++; c[c[0]+1]+=c[c[0]]/10000; c[c[0]]%=10000; } } void Add(ll a[],ll b[],ll c[]) { if(a[0]>b[0]) c[0]=a[0]; else c[0]=b[0]; for(int i=1;i<=c[0];i++) c[i]=a[i]+b[i]; Mark(c); } int Compare(ll a[],ll b[]) { if(a[0]<b[0]) return 0; if(a[0]>b[0]) return 1; for(int i=a[0];i>=1;i--) if(a[i]<b[i]) return 0; else if(a[i]>b[i]) return 1; return 0; } void print() { cout<<f[1][n][f[1][n][0]]; for(int i=f[1][n][0]-1;i>=1;i--) { cout<<f[1][n][i]/1000; cout<<f[1][n][i]/100%10; cout<<f[1][n][i]/10%10; cout<<f[1][n][i]%10; } cout<<endl; } void Mul(ll a1,ll a2,ll a3,ll c[]) { c[0]=c[1]=1; for(int i=1;i<=c[0];i++) c[i]*=a1; Mark(c); for(int i=1;i<=c[0];i++) c[i]*=a2; Mark(c); for(int i=1;i<=c[0];i++) c[i]*=a3; Mark(c); } int main() { cin>>n; for(int i=1;i<=n;i++) cin>>a[i]; for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) f[i][j][0]=0;//初始化 for(int t=2;t<=n-1;t++) for(int i=1;i<=n-t;i++) { int j=i+t; f[i][j][0]=60;//这一步不懂? for(int k=i+1;k<=j-1;k++){//枚举中间的位置 memset(s1,0,sizeof(s1)); memset(s2,0,sizeof(s2)); memset(s3,0,sizeof(s3)); Mul(a[i],a[k],a[j],s1);//把三个数相乘放进s1中 Add(f[i][k],f[k][j],s2);//把这两个相加放进s2中 Add(s1,s2,s3);//把S1与s2加起来放s3中 if(Compare(f[i][j],s3)){ memcpy(f[i][j],s3,sizeof(s3)); } //就是f[i][j]=min(f[i][j],f[i][k]+f[k][j]+a[i]*a[j]*a[k]); } } print(); return 0; } //其实最难的还是高精度的计算,大体的方向还是知道,但是一些小的细节的地方还是不是很清楚
括号匹配:
比较重要的思想就是一个算式:如果s[i]和s[j]相等的话,就有f[i][j]=f[i+1][j-1]+2;
【题目描述】 Hecy 又接了个新任务:BEBE 处理。BEBE 中有一类被称为 GBEGBE。 以下是 GBEGBE 的定义: 空表达式是 GBEGBE 如果表达式 AA 是 GBEGBE,则 [A][A] 与 (A)(A) 都是 GBEGBE 如果 AA 与 BB 都是 GBEGBE,那么 ABAB 是 GBEGBE。 【输入】 输入仅一行,为字符串 BEBE。 【输出】 输出仅一个整数,表示增加的最少字符数 【输入样例】 []) 【输出样例】 1 【提示】 数据范围与提示: 对于 100% 的数据,输入的字符串长度小于 100100。
#include <bits/stdc++.h> using namespace std; const int N=110,inf=0x3f3f3f; int f[N][N]; string s; int main() { cin>>s; for(int t=1;t<=s.size();t++) for(int i=0;i<=s.size()-t;i++) { int j=i+t; if((s[i]=='('&&s[j]==')')||(s[i]=='['&&s[j]==']')) f[i][j]=f[i+1][j-1]+2; for(int k=i;k<j;k++) f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]); } cout<<s.size()-f[0][s.size()-1]<<endl; }
分离与合体:
这个题的其实也是典型的区间DP,唯一可能有区别的就是输出,是有次数的,哎,反正开始读题不是很理解这个输出。这是我自己最好理解的输出办法了
【题目描述】 经过在机房里数日的切磋,LYD 从杜神牛那里学会了分离与合体,出关前,杜神牛给了他一个测试…… 杜神牛造了 nn 个区域,他们紧邻着排成一行,编号 1..n1..n。在每个区域里都放着一把 OI 界的金钥匙,每一把都有一定的价值,LYD 当然想得到他们了。然而杜神牛规定 LYD 不能一下子把他们全部拿走,而是每次只可以拿一把。为了尽快得到所有金钥匙,LYD 自然就用上了刚学的分离与合体特技。 一开始 LYD 可以选择 1..n−11..n−1 中的任何一个区域进入,我们不妨把这个区域记为 kk。进入后 LYD 会在 kk 区域发生分离,从而分离成两个小 LYD。分离完成的同时会有一面墙在 kk 区域和 k+1k+1 区域间升起,从而把 1..k1..k 和 k+1..nk+1..n 阻断成两个独立的区间,并在各自区间内任选除区间末尾之外(即从 1..k−11..k−1 和 k+1..n−1k+1..n−1中选取)的任意一个区域再次发生分离,这样就有了四个小小 LYD……重复以上所叙述的分离,直到每个小 LYD 发现自己所在的区间只剩下了一个区域,那么他们就可以抱起自己梦寐以求的 OI 金钥匙。 但是 LYD 不能就分成这么多个个体存在于世界上,这些小 LYD 还会再合体,合体的小 LYD 所在区间中间的墙会消失。合体会获得 ((合并后所在区间左右端区域里金钥匙价值之和)×(之前分离的时候所在区域的金钥匙价值))。 例如,LYD 曾在 1..31..3 区间中的 22 号区域分离成为 1..21..2 和 3..33..3 两个区间,合并时获得的价值就是 (( 11 号金钥匙价值 +3+3 号金钥匙价值)×( 22 号金钥匙价值))。 LYD 请你编程求出最终可以获得的最大总价值,并按照分离阶段从前到后,区域从左到右的顺序,输出发生分离区域编号。若有多种方案,选择分离区域尽量靠左的方案(也可以理解为输出字典序最小的)。 例如先打印一分为二的区域,然后从左到右打印二分为四的分离区域,然后是四分为八的…… 【输入】 第一行一个正整数 nn 第二行 nn 个用空格分开的正整数 aiai ,表示 1..n1..n 区域里每把金钥匙的价值。 【输出】 第一行一个数,表示获得的最大价值 第二行按照分离阶段从前到后,区域从左到右的顺序,输出发生分离区域编号。若有多种方案,选择分离区域尽量靠左的方案(也可以理解为输出字典序最小的)。 【输入样例】 7 1 2 3 4 5 6 7 【输出样例】 238 1 2 3 4 5 6 【提示】 数据范围与提示: 对于 20% 的数据,n≤10n≤10; 对于 40% 的数据,n≤50n≤50; 对于 100% 的数据,n,ai≤300n,ai≤300,保证运算过程和结果不超过 3232 位正整数范围。
//yrnddup c++ code #include <bits/stdc++.h> using namespace std; const int N=305,inf=0x3f3f3f; int n; long long f[N][N],a[N],loc[N][N],h[N][N],o[N]; void print(int l,int r,int count) { if(l==r) return; h[count][++o[count]]=loc[l][r]; print(l,loc[l][r],count+1); print(loc[l][r]+1,r,count+1); } int main() { cin>>n; for(int i=1;i<=n;i++) cin>>a[i]; for(int t=1;t<n;t++) for(int i=1;i<=n-t;i++) { int j=i+t; for(int k=i;k<j;k++) if(f[i][j]<f[i][k]+f[k+1][j]+(a[i]+a[j])*a[k]) { f[i][j]=f[i][k]+f[k+1][j]+(a[i]+a[j])*a[k]; loc[i][j]=k; } } cout<<f[1][n]<<endl; print(1,n,1); int i=1; while(o[i]) { for(int j=1;j<=o[i];j++) cout<<h[i][j]<<" "; i++; } return 0; }