区间DP
{ 1. 概念引入 }
以 “ 区间长度 ” 作为DP的 “ 阶段 ”,用 “ 区间左右端点 ” 描述 “ 维度 ” 。
一个状态、由若干个比它更小、且包含于它的区间、所代表的状态转移而来。
区间DP的初态一般由长度为1的 “ 元区间 ” 构成(dp[i][i] 初始化为自身的值)。
特征:能将问题分解为两两合并的形式。也可以将多个问题整合分析。
典型应用:石子合并,能量项链,凸多边形的划分等问题。
区间DP的模式可以概括为:向下划分,再向上递推。
区间DP的状态转移方法:
- 记忆化搜索。
- 从小到大枚举区间长度,枚举对应长度的区间。
{ 2. 例题详解 }
【例题1】洛谷 p1430 序列取数
- 给定一个长为n的整数序列(n<=1000),由A和B轮流取数(A先取)。
- 每个人可从序列的左端或右端取若干个数(至少一个),但不能两端都取。
- 所有数都被取走后,两人分别统计所取数的和作为各自的得分。
- 假设A和B都足够聪明,都使自己得分尽量高,求A的最终得分。
题目分析:
最大化A的得分=最大化(A-B)。
因为每次只能从左边取或右边取,所以剩下的一定是中间的区间。
用 d[ l ][ r ] 表示目前剩下区间为l、r时,先手可能达到的max得分。
状态转移时,我们要枚举(对该区间而言)从左还是右取,以及取多少个, 即对于断点k,剩下一个(k,j)或是(i,k)的子序列(i<=k<=j)。 再用sum[i][j]表示i~j的和,则有: d[i][j]=sum[i][j]-min(d[i+1][j],d[i+2][j],...,d[j][j],d[i][j-2],d[i][j-1],d[i][i],0); 其中 0 表示全取完。最终答案为d[1][n]。 优化:定义 f[i][j]=min(d[i][j],d[i+1][j],d[i+2][j],...,d[j][j]); g[i][j]=min(d[i][j],d[i][j-1],d[i][j-2],...,d[i][i]); 那么转移方程变为:d[i][j]=sum(i,j)-min(f[i+1][j],g[i][j-1],0); f[i][j]=min(d[i][j],f[i+1][j]); g[i][j]=min(d[i][j],g[i][j-1]);
代码实现:
【例题2】洛谷 p4170 涂色
- 假设你有一条长度为n的木版,初始时没有涂过任何颜色。
- n=5时,想要把它的5个单位长度分别涂上红、绿、蓝、绿、红色,
- 用一个长度为5的字符串表示这个目标:RGBGR。
- 每次 [ 把一段连续的木版涂成一个给定的颜色 ] ,颜色可以覆盖。
- 用尽量少的涂色次数达到目标。
代码实现:
【例题3】洛谷 p4342 Polygon
- n个顶点的多边形。第一步,删除其中一条边。
- 随后n-1步:选择一条边连接的两个顶点V1和V2,
- 用边运算符计算V1和V2,得到的结果[作为新顶点替换这两个顶点]。
- 游戏结束时,只有一个顶点,点的值即为得分。
- 编写一个程序,给定一个多边形,计算最高可能的分数。
任意选择删除边,[破环成链],然后把剩下的链复制一倍接在末尾。
(以被删除的边逆时针方向的第一个节点为开头,接上这个链)。
这样,我们只需要对前N个阶段进行DP,每个阶段不会超过2N个状态。
最后的答案为:max { f[ i ][ i+N-1 ][ 1 ] }。
代码实现:
【例题4】洛谷 p1880 石子合并
#include <bits/stdc++.h> using namespace std; typedef long long ll; /*【洛谷p1880】石子合并【区间DP】 在一个圆形操场的四周摆放N堆石子,现要将石子有次序地合并成一堆。 规定每次只能选相邻的2堆合并,并将新的一堆的石子数,记为该次合并的得分。 试设计出1个算法,计算出将N堆石子合并成1堆的最小得分和最大得分。*/ /*【分析】用sum[i]维护序列前缀和。 f_max[l,r]表示合并l~r堆内的所有石子后最大得分。 f_min[l,r]表示合并l~r堆内的所有石子后最小得分。 初始条件:f_max[i][j]=0; f_min[i][i]=0; f_min[i][j]=INF; f_max[i][j]=max{f_max[i][k]+f_max[k+1][j]+sum[j]-sum[i-1]}; f_min[i][j]=min{f_min[i][k]+f_min[k+1][j]+sum[j]-sum[i-1]};*/ /*【优化】环的处理——[破环成链] 选取一处破环成链,再把链复制一倍接在末尾。 枚举f[1][N],f[2][N+1],...,f[N][2*N-1]取最优。 最后的答案为:max(或min){f[i][i+N-1]}。 */ //注意:定义变量的时候不能用fmax和fmin。 const int maxn=227,INF=0x7fffffff/2; int f_max[maxn][maxn],f_min[maxn][maxn],s[maxn][maxn]={0}; int a[maxn],sum[maxn]={0},n,ans_max=0,ans_min=INF; int main(){ int n; cin>>n; for(int i=1;i<=n;i++){ cin>>a[i]; a[i+n]=a[i]; } //↑↑↑破环为链,并将链复制一遍 for(int i=1;i<=2*n;i++){ sum[i]=sum[i-1]+a[i]; f_max[i][i]=0; f_min[i][i]=0; } for(int L=2;L<=n;L++) //枚举区间长 for(int i=1;i<=2*n-L+1;i++){ //合并的起始位置 int j=i+L-1; //推算出合并的终止位置 f_max[i][j]=0; f_min[i][j]=INF; for(int k=i;k<j;k++){ f_max[i][j]=max(f_max[i][j],f_max[i][k]+f_max[k+1][j]); f_min[i][j]=min(f_min[i][j],f_min[i][k]+f_min[k+1][j]); } f_max[i][j]+=sum[j]-sum[i-1]; f_min[i][j]+=sum[j]-sum[i-1]; } for(int i=1;i<=n;i++) ans_max=max(ans_max,f_max[i][i+n-1]); for(int i=1;i<=n;i++) ans_min=min(ans_min,f_min[i][i+n-1]); cout<<ans_min<<endl<<ans_max<<endl; return 0; }
【例题5】洛谷 p1063 能量项链
#include <bits/stdc++.h> using namespace std; typedef long long ll; /*【洛谷p1063】能量项链【区间DP】 能量球组成的项链。相邻两球可以合并产生新球。 合并规则:如果前一颗能量珠的头标记为m,尾标记为r, 后一颗能量珠的头标记为r,尾标记为n,则聚合后释放的能量为m*r*n。 问:一条项链怎样合并才能得到最大能量?求最大能量值。 */ /*【优化】环的处理——[破环成链] 选取一处破环成链,再把链复制一倍接在末尾。 枚举f[1][N],f[2][N+1],...,f[N][2*N-1]取最优。 最后的答案为:max(或min){f[i][i+N-1]}。 */ int a[309],nextt[309],f[309][309]; //记得开两倍以上 int main(){ int n,ans=0; cin>>n; for(int i=1;i<=n;i++){ cin>>a[i]; a[i+n]=a[i]; } //↑↑↑珠子由环拆分为链,重复存储一遍 for(int i=1;i<=2*n-1;i++){ nextt[i]=a[i+1]; f[i][i]=0; } nextt[2*n]=a[1]; //nextt[i]为i~nextt的项链,尾部的对应值 for(int L=2;L<=n;L++) //区间长度 for(int i=1;i<=2*n-L+1;i++){ int j=i+L-1; for(int k=i;k<j;k++) f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]+a[i]*nextt[k]*nextt[j]); } for(int i=1;i<=n;i++) ans=max(ans,f[i][i+n-1]); cout<<ans<<endl; return 0; }
【例题6】凸多边形的划分
【例题7】括号配对
【例题8】括号配对(升级版)
#include <bits/stdc++.h> using namespace std; typedef long long ll; /*【括号配对】--输出此时的序列 //思路版 定义如下规则序列:1.空序列是规则序列; 2.如果S是规则序列,那么(S)和[S]也是规则序列; 3.如果A和B都是规则序列,那么AB也是规则序列。 由‘(’,‘)’,‘[’,‘]’构成的序列,请添加尽量少的括号,得到一个规则序列。 */ /*【分析】枚举区间i,j,dp[i][j]表示添加的最少括号数, 如果i和j处的括号能够匹配,则dp[i][j]=dp[i+1][j-1]+1; 即:从小区间开始,不断向外扩展。*/ //p.s.这是一个莫名其妙wa了的代码 string s; //注意:输入串可能是空串,不能用scanf int f[500][500]; void print(int i,int j){ //递归法输出 if(i>j) return; if(i==j){ if(s[i]=='('||s[i]==')') printf("()"); else printf("[]"); return; } int ans=f[i][j]; //区间需要新加入的括号数 if(((s[i]=='('&&s[j]==')') ||(s[i]=='['&&s[j]==']'))&&ans==f[i+1][j-1]){ printf("%c",s[i]); print(i+1,j-1); printf("%c",s[j]); return; } for(int k=i;k<j;k++) if(ans==f[i][k]+f[k+1][j]){ print(i,k); //分成两半递归 print(k+1,j); return; } } int main() { int T; scanf("%d",&T); while(T--){ cin>>s; memset(f,0,sizeof(f)); int n=s.size(); //计算序列长度 for(int i=0;i<n;i++) f[i+1][i]=0, f[i][i]=1; //初始化 for(int i=n-2;i>=0;i--) //逆序 for(int j=i+1;j<n;j++){ f[i][j]=n; if((s[i]=='('&&s[j]==')')||(s[i]=='['&&s[j]==']')) f[i][j]=min(f[i][j],f[i+1][j-1]); //匹配成功,状态向外扩展 for(int k=i;k<=j-1;k++) //枚举断点,将区间分成两个子问题 f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]); } print(0,n-1); puts(""); if(T) puts(""); } return 0; }
洛谷ac版:
#include <cmath> #include <iostream> #include <cstdio> #include <string> #include <cstring> #include <vector> #include <algorithm> #include <stack> #include <queue> #include <set> using namespace std; typedef long long ll; /*【括号配对】--输出此时的序列 定义如下规则序列:1.空序列是规则序列; 2.如果S是规则序列,那么(S)和[S]也是规则序列; 3.如果A和B都是规则序列,那么AB也是规则序列。 由‘(’,‘)’,‘[’,‘]’构成的序列,请添加尽量少的括号,得到一个规则序列。 */ int stacks[101],top; //手写栈 char s[101],ss[101]; int main(){ int n; scanf("%s",s); n=strlen(s); for(int i=0;i<n;i++){ if(s[i]=='('){ stacks[++top]=i; ss[i]=')'; } if(s[i]=='['){ stacks[++top]=i; ss[i]=']'; } if(s[i]==')'||s[i]==']'){ if(!top||ss[stacks[top]]!=s[i]) if(s[i]==')') ss[i]='('; else ss[i]='['; else ss[stacks[top--]]=' '; } } for(int i=0;i<n;i++){ if(ss[i]=='('||ss[i]=='[') printf("%c",ss[i]); printf("%c",s[i]); if(ss[i]==')'||ss[i]==']') printf("%c",ss[i]); } return 0; }
——时间划过风的轨迹,那个少年,还在等你。