【暖*墟】 #DP专题# 区间DP的总结

区间DP

 

{ 1. 概念引入 }

以 “ 区间长度 ” 作为DP的 “ 阶段 ”,用 “ 区间左右端点 ” 描述 “ 维度 ” 。

一个状态、由若干个比它更小、且包含于它的区间、所代表的状态转移而来。

区间DP的初态一般由长度为1的 “ 元区间 ” 构成(dp[i][i] 初始化为自身的值)。

特征:能将问题分解为两两合并的形式。也可以将多个问题整合分析。

典型应用:石子合并,能量项链,凸多边形的划分等问题。

区间DP的模式可以概括为:向下划分,再向上递推。

决策:dp[i][j]=min{ dp[i][k]+dp[k+1][j] | i<=k

 

区间DP的状态转移方法:

  1. 记忆化搜索。
  2. 从小到大枚举区间长度,枚举对应长度的区间。
for(int len=1;len<=N;++len) //区间长度

    for(int l=1,r=len;r<=N;++l,++r)

        { 考虑F[l][r]的转移方式 } 

 

{ 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]);

代码实现:

#include 
using namespace std;

const int N=1005,inf=1e9;

int a[N],sum[N],d[N][N],f[N][N],g[N][N]; //用f、g数组来优化DP数组d

int read(){ //读入优化
    int x=0,f=1;char ch=getchar();
    while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x*=10;x+=(ch-'0');ch=getchar();}
    return x*f;
}

void print(int x){ //输出优化
    if(x<0) putchar('-'),x=-x;
    if(x>9) print(x/10); 
    putchar(x%10+'0');
}

int main(){
    int T,n; T=read();
    while(T--) {
        n=read();
        for(int i=1;i<=n;i++){
            a[i]=read(); sum[i]=sum[i-1]+a[i]; //前缀和sum数组
        }
        for(int i=1;i<=n;i++) f[i][i]=g[i][i]=d[i][i]=a[i]; //初始化边界
        for(int L=1;L<=n;L++){ //枚举长度
            for(int i=1;i<=n-L;i++){ //区间向后滚动
                int j=i+L,cnt=0; //递推部分
                cnt=min(cnt,min(f[i+1][j],g[i][j-1]));
                d[i][j]=sum[j]-sum[i-1]-cnt; 
                //↑↑↑ d[i][j]:目前剩下区间为i、j时,先手可能达到的max得分
                f[i][j]=min(d[i][j],f[i+1][j]);
                g[i][j]=min(d[i][j],g[i][j-1]);
            }
        }
        print(d[1][n]); putchar('\n');
    }
    return 0;
}

 

【例题2】洛谷 p4170 涂色

  • 假设你有一条长度为n的木版,初始时没有涂过任何颜色。
  • n=5时,想要把它的5个单位长度分别涂上红、绿、蓝、绿、红色,
  • 用一个长度为5的字符串表示这个目标:RGBGR。
  • 每次 [ 把一段连续的木版涂成一个给定的颜色 ] ,颜色可以覆盖。
  • 用尽量少的涂色次数达到目标。

【分析】

f[l][r]表示把区间[l,r]全部染成正确颜色的最小次数。

1.枚举分界点m:f[l][m]+f[m+1][r]+价值。

2.当col[l]=col[r]时:把[l,r]全部染成col[l]的颜色,
对于剩下的区间[_l,_r],再进行转移f[l][r]=f[_l][_r]+1。

3.求出_l,_r 
  1)剩余区间刚好同色,得到f[l][r]=1;
  2)枚举_l,直到第一个col[_l]!=col[l]的位置;
    枚举_r,直到第一个col[_r]!=col[r]的位置。

【求f[i][j]具体步骤】

当i==j时,子串明显只需要涂色一次,于是f[i][j]=1。

当i!=j且s[i]==s[j]时,可以想到只需要在首次涂色时多涂一格即可,
可以直接继承之前的状态,于是f[i][j]=min(f[i][j-1],f[i+1][j])。

当i!=j且s[i]!=s[j]时,我们需要考虑将子串断成两部分来涂色,
于是需要枚举子串的断点,设断点为k,那么f[i][j]=min(f[i][j],f[i][k]+f[k+1][j])。

 


 

代码实现:

#include 
using namespace std;

char s[52];
int f[52][52];

int main() {
    int n; scanf("%s",s+1);
    n=strlen(s+1);
    memset(f,63,sizeof(f));  //初始化为较大数
    for(int i=1;i<=n;++i) f[i][i]=1; 
    for(int l=1;l//区间长度
        for(int i=1,j=1+l;j<=n;++i,++j){ //按照长度滚动
            if(s[i]==s[j]) f[i][j]=min(f[i+1][j],f[i][j-1]);
            else for(int k=i;kk) 
                f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]);
        }
    printf("%d",f[1][n]);
    return 0;
}

 

 

【例题3】洛谷 p4342 Polygon

  • n个顶点的多边形。第一步,删除其中一条边。
  • 随后n-1步:选择一条边连接的两个顶点V1和V2,
  • 用边运算符计算V1和V2,得到的结果[作为新顶点替换这两个顶点]。
  • 游戏结束时,只有一个顶点,点的值即为得分。
  • 编写一个程序,给定一个多边形,计算最高可能的分数。

 

【分析】
f[l,r]表示合成l,r区间内的所有顶点后得出的最值。
可此时需要记录最大值和最小值。

因为max的来源只可能是两个max相加、相乘,或两个min相乘(负负得正); 
同时,min的来源只可能是两个min相加、相乘,或一个最大值和一个最小值相乘(正负得负)。

用f[l][r][0]记录max; 用f[l][r][1]记录min。
f[l][r][0]=max{max{f[l][k][0] ‘*’or‘+’ f[k+1][r][0],f[l][k][1] * f[k+1][r][1]}}
f[l][r][1]=min{min{f[l][k][1] ‘*’or‘+’ f[k+1][r][1],
                    f[l][k][1] * f[k+1][r][0],f[l][k][0] * f[k+1][r][1]}}

初值:f[i][i][0]=f[i][i][1]=a[i],其余的设为INF。目标:f[1][N][0]。

【优化】还可以进一步优化:优化枚举第一步删除边的耗时。

任意选择删除边,[破环成链],然后把剩下的链复制一倍接在末尾。

(以被删除的边逆时针方向的第一个节点为开头,接上这个链)。

这样,我们只需要对前N个阶段进行DP,每个阶段不会超过2N个状态。

最后的答案为:max { f[ i ][ i+N-1 ][ 1 ] }。


 

代码实现:

#include 
using namespace std;

const int SIZE=55;
int a[SIZE<<1]; //点的数值
char op[SIZE<<1]; //边上的符号
int f[SIZE<<1][SIZE<<1][2];

int main(){
    int n; cin>>n;

    for(int i=1;i<=n;i++){ //空间开2倍,把环复制成两倍的链
        cin>>op[i]>>a[i];
        op[i+n]=op[i],a[i+n]=a[i];
    }
    for(int i=1;i<=2*n;i++){ //初始化
        for(int j=1;j<=2*n;j++){
            if(i==j) f[i][i][0]=f[i][i][1]=a[i];
            else f[i][j][1]=32768,f[i][j][0]=-32769;
        } //↑↑↑题中给出:对于任何操作,顶点数字都在[-32768,32767]的范围内
    }
    for(int i=1;i<=2*n;i++) f[i][i][0]=f[i][i][1]=a[i];

    for(int L=1;L<=n;L++) //区间长度
      for(int i=1;i<=2*n-L+1;i++){ //区间起点
        int l=i,r=i+L-1;
        for(int k=l;k//枚举区间断点k
          switch(op[k+1]){ //字符的匹配函数
            case 'x': //乘法运算
              f[l][r][0]=max(f[l][r][0],max(f[l][k][0]*f[k+1][r][0],f[l][k][1]*f[k+1][r][1]));
              f[l][r][1]=min(f[l][r][1],min(f[l][k][1]*f[k+1][r][0],f[l][k][0]*f[k+1][r][1]));
              break;
            case 't': //加法运算
              f[l][r][0]=max(f[l][r][0],f[l][k][0]+f[k+1][r][0]);
              f[l][r][1]=min(f[l][r][1],f[l][k][1]+f[k+1][r][0]);
              break;
          }
      }

    int maxn=-32769;
    for(int i=1;i<=n+1;i++){ //最高得分
        int l=i,r=l+n-1;
        maxn=max(maxn,f[l][r][0]);
    }
    cout<endl;

    for(int i=1;i<=n;i++){ //第一步删除的策略
        int l=i,r=l+n-1;
        if(f[l][r][0]==maxn) cout<" ";
    }

    return 0;
}
 

【例题4】洛谷 p1880 石子合并

#include 
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){
                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<endl;
    return 0;
}

 

【例题5】洛谷 p1063 能量项链

#include 
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)
        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<endl;
  return 0;
}

 

【例题6】凸多边形的划分

#include 
using namespace std;
typedef long long ll;

/*【凸多边形的划分】
具有 N 个顶点(从 1 到 N 编号)的凸多边形,每个顶点的权均已知。
问:如何把这个凸多边形划分成 N−2 个互不相交的三角形,
使得这些三角形顶点的权值乘积之和最小? */

/*【分析】将顶点按顺时针编号,可以用两个顶点描述一个凸多边形。
设 f(i,j) 表示 i j 这一段连续顶点的多边形划分后最小乘积。
枚举点 k。i、j 和 k 相连成三角形,并把原多边形划分成两个子多边形。
则有:f(i,j)=min{f(i,k)+f(k,j)+a[i]∗a[j]∗a[k]}; (1<=i可以看成多边形剖分过程,分成多个多边形一步一步进展,每一步累加即可。*/

//初态(由边来描述):f[i][i+1]=0; 目标状态:f[1][n]。

//注意:此题原意是用高精度来实现,此处只用longlong代替

ll w[100],f[60][60];

int main(){
    ll n; scanf("%lld",&n); 
    memset(f,0x3f,sizeof(f));
    for(ll i=1;i<=n;i++) scanf("%lld",&w[i]); //点权
    for(ll i=n;i>=1;i--)
        for(ll j=i+1;j<=n;j++){
            if(j-i==1) f[i][j]=0; //初态,描述边
            else if(j-i==2) f[i][j]=w[i]*w[i+1]*w[i+2]; //最小的三角形
            else for(LL k=i+1;k<=j-1;k++) //寻找中间k值,合并为大多边形
                f[i][j]=min(f[i][j],f[i][k]+f[k][j]+w[i]*w[j]*w[k]);
        }
    printf("%lld\n",f[1][n]);
    return 0;
}

 

【例题7】括号配对

#include 
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; 
即:从小区间开始,不断向外扩展。*/

const int INF=2147483647;
int f[500][500];

int main() {
    string s; //输入序列s
    while(cin>>s){
        memset(f,0,sizeof(f));
    int n=s.size(); //计算序列长度
    for(int w=1;w<=n;w++) f[w][w]=1; //初始化
    for(int l=2;l<=n;l++) //区间长
        for(int i=1;i<=n-l+1;++i){ //区间起点
            int j=l+i-1; f[i][j]=INF;
            if((s[i-1]=='('&&s[j-1]==')')||(s[i-1]=='['&&s[j-1]==']'))
                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]);         
        }     
    printf("%d\n",f[1][n]);
    }
    return 0;
}

 

【例题8】括号配对(升级版)

#include 
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)
        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;i1][i]=0, f[i][i]=1; //初始化
        for(int i=n-2;i>=0;i--) //逆序
            for(int j=i+1;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 
#include 
#include 
#include <string>
#include 
#include 
#include 
#include 
#include 
#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){
        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){
        if(ss[i]=='('||ss[i]=='[') printf("%c",ss[i]);
        printf("%c",s[i]);
        if(ss[i]==')'||ss[i]==']') printf("%c",ss[i]);
    }
    return 0;
}
View Code

 

 

 

                                               ——时间划过风的轨迹,那个少年,还在等你。

 

转载于:https://www.cnblogs.com/FloraLOVERyuuji/p/9558267.html

你可能感兴趣的:(【暖*墟】 #DP专题# 区间DP的总结)