区间dp笔记

引:石子合并

设有 N(N300) 堆石子排成一排,其编号为 1,2,3,,N。每堆石子有一定的质量 Ai (Ai1000)。现在要将这 N 堆石子合并成为一堆。每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻。合并时由于选择的顺序不同,合并的总代价也不相同。试找出一种合理的方法,使总的代价最小,并输出最小代价。

若最初的第l堆石子和第r堆石子被合并成一堆,则说明lr之间的每堆石子也已经被合并,这样lr才有可能相邻。因此,在任意时刻,任意一堆石子均可以用一个闭区间[l,r]来描述,表示这堆石子是由最初的第lr堆石子合并而成的,其重量为i=lrAi。另外,一定存在一个整数k(lk<r),在这堆石子形成之前,先有第lk堆石子(闭区间[l,r])被合并成一堆,第k+1r堆石子(闭区间[k+1,r])被合并成一堆,然后这两堆石子才合并成[l,r]

对应到动态规划中,就意味着两个长度较小的区间上的信息向一个更长的区间发生了转移,划分点k就是转移的决策。自然地,应该把区间长度len作为DP的阶段。不过,区间长度可以由左端点和有端点表示出,即len=rl+1。本着动态规划“进行选择最小的能覆盖状态空间的维度集合”的思想,可以只用从左、右端点表示DP的状态。

F[l,r]表示把最初的第l堆到第r堆石子合并成一堆,需要的最少体力,根据上述分析,容易写出状态转移方程:

F[l,r]=minlk<rF[l,k]+F[k+1,r]+i=lrAi

初值:l[1,N],f[l,l]=0,其余为正无穷。

目标:F[1,N]

我们发现i=lrAi可使用前缀和计算。

memset(f,0x3f,sizeof(f));
for(int i=1;i<=n;i++){
    f[i][i]=0;
    sum[i]=sum[i-1]+a[i];
}
for(int len=2;len<=n;len++){
    for(int l=1,r=l+len-1;r<=n;l++,r++){
        for(int k=l;k<r;k++)
            f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]);
        f[l][r]+=sum[r]-sum[l-1];
    }
}

例1:能量项链(NOIP)

题目描述

在 Mars 星球上,每个 Mars 人都随身佩带着一串能量项链。在项链上有 N 颗能量珠。能量珠是一颗有头标记与尾标记的珠子,这些标记对应着某个正整数。并且,对于相邻的两颗珠子,前一颗珠子的尾标记一定等于后一颗珠子的头标记。因为只有这样,通过吸盘(吸盘是 Mars 人吸收能量的一种器官)的作用,这两颗珠子才能聚合成一颗珠子,同时释放出可以被吸盘吸收的能量。如果前一颗能量珠的头标记为 m,尾标记为 r,后一颗能量珠的头标记为 r,尾标记为 n,则聚合后释放的能量为 m×r×n(Mars 单位),新产生的珠子的头标记为 m,尾标记为 n

需要时,Mars 人就用吸盘夹住相邻的两颗珠子,通过聚合得到能量,直到项链上只剩下一颗珠子为止。显然,不同的聚合顺序得到的总能量是不同的,请你设计一个聚合顺序,使一串项链释放出的总能量最大。

例如:设 N=44 颗珠子的头标记与尾标记依次为 (2,3)(3,5)(5,10)(10,2)。我们用记号 表示两颗珠子的聚合操作,(jk) 表示第 j,k 两颗珠子聚合后所释放的能量。则第 41 两颗珠子聚合后释放的能量为:

(41)=10×2×3=60

这一串项链可以得到最优值的一个聚合顺序所释放的总能量为:

(((41)2)3)=10×2×3+10×3×5+10×5×10=710

思路

考虑区间DP,这道题的状态设计有一丝特别,设F[i][j]表示合并闭区间[i,i+j1]后得到的最大值,区间长度为2的单独处理。

很容易写出转移方程:

F[i][j]=maxlkrF[i,k]+F[i+k,jk]+a[i]a[i+j]a[i+k]

现在唯一有点棘手的问题是本题是求环上的最大值,最朴素的做法就是枚举每个断点进行DP,这样时间复杂度为O(n4)。结合以前处理环的方式可以想到再把序列复制一遍,区间[i,i+n1]即为以i为断点的最大值,时间复杂度O(n3)

代码

#include<bits/stdc++.h>
using namespace std;
const int N=410;
int n,a[N],f[N][N];
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    for(int i=n+1;i<=2*n;i++)a[i]=a[i-n];
    for(int i=1;i<=2*n;i++)f[i][2]=a[i]*a[i+1]*a[i+2];
    for(int j=3;j<=2*n;j++){
        for(int i=1;i<=2*n;i++){
            if(i+j-1>=2*n)break;
            for(int k=1;k<j;k++){
                f[i][j]=max(f[i][j],f[i][k]+f[i+k][j-k]+a[i]*a[i+j]*a[i+k]);
            }
        }
    }
    int ans=0;
    for(int i=1;i<=2*n;i++)ans=max(ans,f[i][n]);
    printf("%d\n",ans);
    return 0;
} 

例2:【USACO3.3.5】A Game游戏 IOI'96

题目描述

有如下一个双人游戏:N(2N100)个正整数的序列放在一个游戏平台上,游戏由玩家1开始,两人轮流从序列的任意一端取一个数,取数后该数字被去掉并累加到本玩家的得分中,当数取尽时,游戏结束。以最终得分多者为胜。

编一个执行最优策略的程序,最优策略就是使玩家在与最好的对手对弈时,能得到的在当前情况下最大的可能的总分的策略。你的程序要始终为第二位玩家执行最优策略。

思路

状态只有2种:从左边拿和从右边拿。

假设当前状态a1,a2,a3,a4,a5,如果第一个人选最左边的,则问题转化为四个数a2,a3,a4,a5,然后第二个人先选,由于题目说第二个人方案也最优,所以选的也是最优方案,即f[i+1][j];先选右边同理。

f[i][j]表示ij区间段第一个人选的最优方案。

所以dp转移方程为:

f[i][j]=maxsum[i+1][j]f[i+1][j]+ai,sum[i][j1]f[i][j1]+aj

sum[i][j]其实就等于sum[1][j]sum[1][i1],于是我们用一个s数组,s[i]表示前1i个数的和,就好了。But,你如果想偷懒直接O(n3)预处理每个区间的和也是可以的,毕竟n最大才100

所以dp转移方程也可写成f[i][j]=maxs[j]s[i1]f[i+1][j],s[j]s[i1]f[i][j1];

根据dp转移方程我们可以发现,要得到状态f[i][j],必须要得到状态f[i+1][j]f[i][j1]。然后我们就可以写出程序了。

代码

scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&a[i]),sum[i]=sum[i-1]+a[i],f[i][i]=a[i];
for(int len=1;len<n;len++){
    for(int i=1;i+len<=n;i++){
        int j=len+i;
        f[i][j]=max(sum[j]-sum[i-1]-f[i+1][j],sum[j]-sum[i-1]-f[i][j-1]);
    }
}
printf("%d %d\n",f[1][n],sum[n]-f[1][n]);

这道题妙在状态转移,本质上是类似于取反的关系,现在先手选,那么剩下的区间先手就变为后手,后手变为先手,以此类推。

例3:做错的作业

题目大意

给定一个长度为n括号序列,求最少添加多少个括号使得它为合法的括号序列?n300

思路分析

此题虽然很简单,但它分析的思路成为了以后这种括号序列的套路,如果凭借自己的思考在高强度的考场上大概率是想不出来的。

很明显,对于原括号序列的每个括号最后它要么是单独添加一个括号,要么是与之前的括号进行匹配。

设状态F[i][j]表示区间[i,j]成为合法括号序列的最少需要添加的括号数量。第一种状态转移很简单F[i,j]=F[i,j1]+1,可第二种转移怎么办呢?考虑这个区间被这对括号划分成了两个区间长度更小的部分,[i,k1][k+1,j1]两部分,而且我们发现这两个区间是不可能存在交集的,所以第二种转移就可以从F[i,k1]+F[k+1,j1]转移过来。

综上,转移方程为:

F[i][j]=max{F[i][j-1]+1——不与前面区间括号进行匹配,单独添加一个括号与之匹配F[i][k1]+F[k+1][j1] kij与前面区间括号进行匹配

代码

for(int i=1;i<=n;i++)for(int j=i+1;j<=n;j++)f[i][j]=9999999999;
for(int i=1;i<=n;i++)f[i][i]=1;
for(int len=1;len<n;len++){
    for(int i=1;i+len<=n;i++){
        int j=i+len;
        f[i][j]=min(f[i][j],f[i][j-1]+1);
        for(int k=i;k<=j-1;k++){
            if((s[k]=='('&&s[j]==')')||(s[k]=='['&&s[j]==']')||(s[k]=='<'&&s[j]=='>')||(s[k]=='{'&&s[j]=='}')){
                f[i][j]=min(f[i][j],f[i][k-1]+f[k+1][j-1]);
            }
        }
    }
}
printf("%d\n",f[1][n]);

例4:【CQOI2007】涂色

题目描述

假设你有一条长度为 5 的木板,初始时没有涂过任何颜色。你希望把它的 5 个单位长度分别涂上红、绿、蓝、绿、红色,用一个长度为 5 的字符串表示这个目标:RGBGR

每次你可以把一段连续的木板涂成一个给定的颜色,后涂的颜色覆盖先涂的颜色。例如第一次把木板涂成 RRRRR,第二次涂成 RGGGR,第三次涂成 RGBGR,达到目标。

用尽量少的涂色次数达到目标。

思路

典型的区间DP模板,设F[i,j]表示区间[i,j]的最少达成目标的次数。

很容易写出状态转移方程:

F[i][j]=min{F[i][k]+F[k+1][j]min F[i+1][j],F[i][j-1](ifa[i]==a[j])

代码

for(int i=1;i<=n;i++)f[i][i]=1;
for(int len=1;len<n;len++){
    for(int l=1;len+l<=n;l++){
        int r=len+l;
        if(a[l]==a[r])f[l][r]=min(f[l][r-1],f[l+1][r]);
        for(int k=l;k<r;k++){
            f[l][r]=min(f[l][k]+f[k+1][r],f[l][r]);
        }
    }
}
printf("%d\n",f[1][n]); 

例5:凸多边形的三角剖分

题目描述

给定一具有N个顶点(按顺时针方向1N编号)的凸多边形,每个顶点的权均已知。问如何把这个凸多边形划分成N2个互不相交的三角形,使得这些三角形顶点的权的乘积之和最小?N50

思路

考虑区间DP

Part 1 状态设计

设状态F[i,j]表示从(l,l+1),(l+1,l+2),....(r1,r)的划分的最小代价。

Part 2 状态转移

结合题目特性,我们发现只会用一种转移,就是合并两个更小的凸多边形,那么就写出状态转移方程。

F[i][j]=maxF[i][k]+F[k][j](kij1)

代码

注意开long long

#include<bits/stdc++.h>
using namespace std;
const int N=305;
int n,t;
struct node{
    int x,y;
}a[N];
int f[N][N],dis[N][N];
int dist(int i,int j){
    if(abs(j-i)==1)return 0;
    return abs(a[i].x+a[j].x)*abs(a[i].y+a[j].y)%t;
}
int main(){
    scanf("%d %d",&n,&t);
    for(int i=1;i<=n;i++)scanf("%d %d",&a[i].x,&a[i].y);
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++)f[i][j]=0x3f3f3f3f3f3f;
        if(i+1<=n)f[i][i+1]=0;
        else f[i][1]=0;
    }
    for(int len=2;len<n;len++){
        for(int i=1;i+len<=n;i++){
            int j=i+len;
            for(int k=i+1;k<j;k++){
                f[i][j]=min(f[i][j],f[i][k]+f[k][j]+dist(i,k)+dist(j,k));
            }
        }
    }
    printf("%d\n",f[1][n]);
    return 0;
}

想必到现在读者一定会对区间DP有更深的认识,下面来看一下更有深度的题目。

例6:字符串折叠

题目描述

折叠的定义如下:

  1. 一个字符串可以看成它自身的折叠。记作S =S

  2. X(S)是X(X>1)个S连接在一起的串的折叠。记作X(S) = SSSS…S(X个S)。

  3. 如果A = A’, B=B’,则AB = A’B’

例如,因为3(A) = AAA, 2(B) = BB,所以3(A)C2(B) = AAACBB,而2(3(A)C)2(B)=AAACAAACBB给一个字符串,求它的最短折叠。 例如AAAAAAAAAABABABCCD的最短折叠为:9(A)3(AB)CCD。

思路

这道题也是一样先设状态F[i,j]表示表示区间[i,j]需要的最少长度。

考虑转移,第一种转移即为不加括号和数字,区间[i,j]可以分成两个更小的区间进行转移即F[i][j]=minF[i][k]+F[k+1][j](kij1)

第二种转移为加括号和数字,比如一段小区间[i,k](kij1)它的区间长度为ki+1,很显然它必须得整除原区间长度ji+1,其次这段区间为原区间的循环节。只有在这个时候才可以发生转移。例如,此时若存在一个数k使得区间[i,k]为原区间[i,j]的循环节且区间长度整除原区间长度,那么F[i][j]=minF[i][j],F[i][k]+num[(ji+1)/(ki+1)]+2即可。

Code

#include<bits/stdc++.h>
using namespace std;
const int N=200;
string s;
int f[N][N],num[N];
int main(){
    cin>>s;
    int n=s.length();
    s=" "+s;
    memset(f,0x3f,sizeof(f));
    for(int i=1;i<=n;i++)f[i][i]=1;
    for(int i=1;i<=9;i++)num[i]=1;
    for(int i=10;i<=99;i++)num[i]=2;
    num[100]=3;
    for(int len=1;len<n;len++){
        for(int i=1;i+len<=n;i++){
            int j=i+len;
            for(int k=i;k<j;k++){
                f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]);
                if((len+1)%(k-i+1)==0){
                    bool flag=true;
                    int lenn=k-i+1;
                    for(int h=i;h<=j;h++)
                        if(s[h]!=s[(h-i)%lenn+i]){
                            flag=false;
                            break;
                        }
                    if(flag)f[i][j]=min(f[i][j],2+num[(len+1)/(k-i+1)]+f[i][k]);
                }
            }
        }
    }
    printf("%d\n",f[1][n]);
    return 0;
}

例7:加分二叉树(NOIP)

题目传送门

思路

和前面状态设计差不多,设F[l,r]表示[l,r]之间的最多的得分,则每次枚举一个小区间和它的根节点,那么状态转移为F[l,r]=F[l,k1]F[k+1,r]+a[k]

现在考虑怎么求具体方案,再开一个数组G[l][r]表示区间[l,r]的最优决策点,也就是根节点然后再分为两部分进行dfs

代码

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=35;
int f[N][N],n,a[N],root[N][N];
void print(int l,int r){
    if(l>r)return;
    printf("%lld ",root[l][r]);
    if(l==r)return;
    print(l,root[l][r]-1);print(root[l][r]+1,r);
}
signed main(){
    scanf("%lld",&n);
    for(int i=1;i<=n;i++)scanf("%lld",&a[i]),f[i][i]=a[i],root[i][i]=i;
    for(int len=1;len<n;len++){
        for(int i=1;i+len<=n;i++){
            int j=i+len;
            f[i][j]=f[i][i]+f[i+1][j];root[i][j]=i;
            for(int k=i+1;k<j;k++){
                if(f[i][j]<f[i][k-1]*f[k+1][j]+f[k][k]){
                    f[i][j]=f[i][k-1]*f[k+1][j]+f[k][k];
                    root[i][j]=k;
                }
            }
        }
    }
    printf("%lld\n",f[1][n]);
    print(1,n);
    return 0;
}

例8:[SCOI2007] 压缩

题目传送门

思路

此题最大的问题是在有无M的情况,那么就设F[i][j][0]表示区间[i,j]没有MF[i][j][1]表示区间[i,j]M

现在考虑转移:

mid=(i+j)/2

tip!

进行此种转移的前提必须是(i+j)能整除2

F[i][j][0]=min(f[i][mid][0]+1)

剩余是常规操作:F[i][j][0]=min(F[i][k][0]+jk)

F[i][j][1]=min(min(F[i][k][0]+F[i][k][1])+min(F[k+1][j][0],F[k+1][j][1])+1)

#include<bits/stdc++.h>
using namespace std;
const int N=55;
int f[N][N][2];
char a[N];
int n;
bool check(int l,int r){
    int len=(r-l+1)>>1;
    if((r-l+1)%2==1)return false;
    for(int i=l,j=l+len;i<l+len;i++,j++){
        if(a[i]!=a[j])return false;
    }
    return true;
}
int main(){
    scanf("%s",(a+1));
    n=strlen((a+1));
    for(int i=1;i<=n;i++){
        f[i][i][0]=1;
        f[i][i][1]=2;
    }
    for(int len=2;len<=n;len++){
        for(int i=1,j=i+len-1;j<=n;i++,j++){
            f[i][j][0]=f[i][j][1]=0x3f3f3f3f;
            if(check(i,j)){
                int mid=(i+j)>>1;
                f[i][j][0]=min(f[i][j][0],f[i][mid][0]+1);
            }
            for(int k=i;k<j;k++){
                f[i][j][0]=min(f[i][j][0],f[i][k][0]+j-k);
                f[i][j][1]=min(f[i][j][1],min(f[i][k][1],f[i][k][0])+min(f[k+1][j][0],f[k+1][j][1])+1);
            }
        }
    }
    printf("%d\n",min(f[1][n][0],f[1][n][1]));
    return 0;
}

例9:【JXOI2018】守卫

题目传送门

可以证明,在一个区间中,无法被右端点看到的每一个点构成的区间一定是无法被这个区间外除右端点的右边一个端点之外的点看到

因此状态可以直接用F[i][j]表示覆盖区间 [l,r] 所需的守卫,右端点必放,剩下的每个区间[i,j]f[i][j]f[i][j+1] 进行转移

posted @   CQWYB  阅读(14)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示