zoukankan      html  css  js  c++  java
  • 【暖*墟】 #DP专题# 区间DP的总结

    区间DP

    { 1. 概念引入 }

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

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

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

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

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

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

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

    区间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 <bits/stdc++.h>
    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('
    ');
        }
        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 <bits/stdc++.h>
    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<n;++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;k<j;++k) 
                    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 <bits/stdc++.h>
    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<r;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<<maxn<<endl;
    
        for(int i=1;i<=n;i++){ //第一步删除的策略
            int l=i,r=l+n-1;
            if(f[l][r][0]==maxn) cout<<i<<" ";
        }
    
        return 0;
    }
     

    【例题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】凸多边形的划分

    #include <bits/stdc++.h>
    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<k<j<=n)  
    ->可以看成多边形剖分过程,分成多个多边形一步一步进展,每一步累加即可。*/
    
    //初态(由边来描述):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
    ",f[1][n]);
        return 0;
    }

    【例题7】括号配对

    #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; 
    即:从小区间开始,不断向外扩展。*/
    
    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
    ",f[1][n]);
        }
        return 0;
    }

    【例题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;
    }
    View Code

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

  • 相关阅读:
    Linux下安装MySQL5.6
    Linux软件安装管理:rpm与yum
    Linux进程管理
    Linux网络配置
    Linux磁盘分区、挂载
    Django基础之模型层(models.py)、ORM查询之单表查询、多表查询和跨表查询
    Django基础之视图层(views.py)和模板层
    数据的编辑和删除、Django基础之路由层(urls.py)
    小白必会三板斧、静态文件配置、request对象及方法、pycharm和django连接数据库、字段的增删改查和数据的增查
    初识web框架和Django框架
  • 原文地址:https://www.cnblogs.com/FloraLOVERyuuji/p/9558267.html
Copyright © 2011-2022 走看看