区间dp

本文深入探讨区间动态规划(区间DP)的概念与应用,通过解析经典案例,如石子合并问题,详细介绍如何确定状态、初始化DP数组及进行状态转移。此外,文章还介绍了四边形不等式优化技巧,用于提高算法效率,并通过多个实战题目,如回文子序列计数、最长回文子序列求解、序列操作次数最小化等,进一步巩固区间DP的理解与运用。

Part1:总概

区间dp

顾名思义,就是解决一些区间内最优值的问题,通常的时间复杂度为n2n^2n2 或者 n3n^3n3

大致思路:

  • 首先确定状态

  • 初始化长度为1(or 2,3…具体因题而异)的dp数组的值

  • 然后枚举区间长度,枚举区间的起始点,(有的题目还需要枚举断点) 由小区间转移到大区间。

  • 最后dp[1][n]dp[1][n]dp[1][n]往往就是答案。

Part2:例题精讲

NOI1995 石子合并

思路分析:

贪心~很明显是不行的

好的,那我们还是开始想dp吧。区间dp常用的一个状态就是dp[i][j]表示i~j这个区间的最优值是多少?

我们可以看出题目中这个合并过程,很像是区间dp的一种合并,也就是说dp[i][j]dp[i][j]dp[i][j]可以由dp[i][k]dp[i][k]dp[i][k]dp[k+1][j]dp[k+1][j]dp[k+1][j]转移过
那么我们自然而然的想到要枚举上一次合并是在哪个位置,也就是断点k,然后进行转
不过,由于这个题目的特殊限制,我们需要记录g[i][j]表示i到j这个区间有多少石子,来进行辅助转移

f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+g[i][k]+g[k+1][j])(i<=k<=j)f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+g[i][k]+g[k+1][j]) ( i<=k<=j)f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+g[i][k]+g[k+1][j])(i<=k<=j)

对于环,我们只需要把整个数组复制一遍,然后在统计答案的时候,把1-n开头的长度为n的区间的答案求一个min 就可以啦

Code

#include<bits/stdc++.h>
using namespace std;   
int n,minl,maxl,f1[300][300],f2[300][300],num[300];  
int s[300];  
int d(int i,int j)
{
    return s[j]-s[i-1];
}  
int main()
{
    cin>>n;
    for(int i=1;i<=n+n;i++)  
    {  
        cin>>num[i]; 
        num[i+n]=num[i];  
        s[i]=s[i-1]+num[i];  
    }  
    for(int p=1;p<n;p++)  
    {  
        for(int i=1,j=i+p;(j<n+n)&&(i<n+n);i++,j=i+p)  
        {  
            f2[i][j]=999999999;  
            for(int k=i;k<j;k++)  
            {  
                f1[i][j]=max(f1[i][j],f1[i][k]+f1[k+1][j]+d(i,j));   
                f2[i][j]=min(f2[i][j],f2[i][k]+f2[k+1][j]+d(i,j));  
            }  
        }  
    }  
    minl=999999999;  
    for(int i=1;i<=n;i++)  
    {  
        maxl=max(maxl,f1[i][i+n-1]);
        minl=min(minl,f2[i][i+n-1]);  
    }
    cout<<minl<<endl<<maxl;
    return 0;  
}

不过,这道题在hdu上(hdu3506)n^3的时间复杂度过不了

那么我们就需要考虑,四边形不等式优化!!

如果有f[a][c]+f[b][d]<=f[b][c]+f[a][d]f[a][c]+f[b][d]<=f[b][c]+f[a][d]f[a][c]+f[b][d]<=f[b][c]+f[a][d]
可以理解为一句话,交叉小于包含,即交叉的两个区间,a到c和b到d的值满足小于等于包含的两个区间[bc包含于ad])

则说这个东西满足四边形不等式,当然这个东西可能是dp数组,也可以是其他数组

这里有两个定理

1、如果上述的w函数同时满足区间包含单调性和四边形不等式性质,那么函数dp也满足四边形不等式性质

我们再定义s(i,j)s(i,j)s(i,j)表示 dp(i,j)dp(i,j)dp(i,j) 取得最优值时对应的下标(即 i≤k≤ji≤k≤jikj 时,k 处的 dp 值最大,则 s(i,j)=ks(i,j)=ks(i,j)=k此时有如下定理

2、假如dp(i,j)dp(i,j)dp(i,j)满足四边形不等式,那么s(i,j)s(i,j)s(i,j)单调,即 s(i,j)≤s(i,j+1)≤s(i+1,j+1)s(i,j)≤s(i,j+1)≤s(i+1,j+1)s(i,j)s(i,j+1)s(i+1,j+1)

那么我们就可以开一个s数组 ,s[i][j]s[i][j]s[i][j]这个数组记录i ji~ji j这个区间的转移的最优点是哪个点

那么枚举断点是时候 直接可以从s[i][j−1] s[i+1[j]s[i][j-1] ~ s[i+1[j]s[i][j1] s[i+1[j]枚举转移

(1)当函数w[i,j]满足w[i,j]+w[i′,j′]<=w[i′,j]+w[i,j′],i<=i′<=j<=j′w[i,j]+w[i',j']<=w[i',j]+w[i,j'],i<=i'<=j<=j'w[i,j]+w[i,j]<=w[i,j]+w[i,j],i<=i<=j<=j时,称w满足四边形不等式。

(2)当函数w[i,j]满足w[i’,j]≤w[i,j’],i<=i′<=j<=j′w[i’,j]≤w[i,j’],i<=i'<=j<=j'w[i,j]w[i,j],i<=i<=j<=j时称w关于区间包含关系单调。

很明显的,一开始定义的w[I,j]满足区间包含关系和四边形不等式。

现在我们要证明f[I,j]=minf[I,k]+f[k+1,j]+w[I,j]i<=k<jf[I,j]=min{f[I,k]+f[k+1,j]+w[I,j]}{i<=k<j}f[I,j]=minf[I,k]+f[k+1,j]+w[I,j]i<=k<j也满足四边形不等式

我们利用数学归纳法对四边形不等式中的区间长度进行归纳。

证明如下

当i=i’或j=j’时,不等式显然成立。由此可知,当l≤1时,函数f满足四边形不等式。

下面分两种情形进行归纳证明:

  • 情形1:i<i’=j<j’i<i’=j<j’i<i=j<j
    在这种情形下,四边形不等式简化为如下的不等式:f[i,j]+f[j,j’]≤f[i,j’]+0f[i,j]+f[j,j’] ≤f[i,j’]+0f[i,j]+f[j,j]f[i,j]+0,设k=maxp∣f[i,j’]=f[i,p−1]+f[p,j’]+w[i,j’]k=max{p| f[i,j’]=f[i,p-1]+f[p,j’]+w[i,j’] }k=maxpf[i,j]=f[i,p1]+f[p,j]+w[i,j],再分两种情形k≤jk≤jkjk>jk>jk>j。下面只讨论k≤jk≤jkjk>jk>jk>j 的情况是类似的。

  • 情形1.1:k≤jk≤jkj,此时:

  • 情形1.2:k>jk>jk>j,略去

  • 情形2:i<i’<j<j’i<i’<j<j’i<i<j<j
    y=maxp∣f[i’,j]=f[i’,p−1]+f[p,j]+w[i’,j]y=max{p | f[i’,j]=f[i’,p-1]+f[p,j]+w[i’,j] }y=maxpf[i,j]=f[i,p1]+f[p,j]+w[i,j]f[p,j’]+w[i,j’]{f[p,j’]+w[i,j’] }f[p,j]+w[i,j]
    仍需再分两种情形讨论,即z≤yz≤yzyz>yz>yz>y。下面只讨论z≤yz≤yzyz>yz>yz>y的情况是类似的。
    情形2.1,i<z≤y≤ji<z≤y≤ji<zyj
    显然的,我们有:
    f[i,j]+f[i′,j′]<=w[i,j]+f[i,z−1]+f[z,j]+w[i′,j′]+f[i′,y−1]+f[y,j′]f[i,j]+f[i',j']<=w[i,j]+f[i,z-1]+f[z,j]+w[i',j']+f[i',y-1]+f[y,j']f[i,j]+f[i,j]<=w[i,j]+f[i,z1]+f[z,j]+w[i,j]+f[i,y1]+f[y,j]
    因为w满足四边形不等式,所以:
    f[i,j]+f[i′,j′]<=w[i,j′]+w[i′,j]+f[i′,y−1]+f[i,z−1]+f[z,j]+f[y,j′]f[i,j]+f[i',j']<=w[i,j']+w[i',j]+f[i',y-1]+f[i,z-1]+f[z,j]+f[y,j']f[i,j]+f[i,j]<=w[i,j]+w[i,j]+f[i,y1]+f[i,z1]+f[z,j]+f[y,j]
    因为f[i,j′]+f[i′,j]=w[i,j′]+w[i′,j]+f[i′,y−1]+f[i,z−1]+f[y,j]+f[z,j′]f[i,j']+f[i',j]=w[i,j']+w[i',j]+f[i',y-1]+f[i,z-1]+f[y,j]+f[z,j']f[i,j]+f[i,j]=w[i,j]+w[i,j]+f[i,y1]+f[i,z1]+f[y,j]+f[z,j]
    所以,要证f[i,j′]+f[i′j]>=f[i,j]+f[i′,j′]f[i,j']+f[i'j]>=f[i,j]+f[i',j']f[i,j]+f[ij]>=f[i,j]+f[i,j],就要证f[z,j]+f[y,j′]<=f[y,j]+f[z,j′]f[z,j]+f[y,j']<=f[y,j]+f[z,j']f[z,j]+f[y,j]<=f[y,j]+f[z,j]
    因为i<z≤y≤ji<z≤y≤ji<zyj,所以区间的长度会在不断地迭代中越变越小,最后归于前面的几种情况。

  • 情形2.1,i<y≤z≤ji<y≤z≤ji<yzj。证明过程略。
    综上所述,f[i,j]f[i,j]f[i,j]满足四边形不等式。
    事实上,对于任意f[I,j]=maxf[I,k]+f[k+1,j]+w[I,j]i<=k<jf[I,j]=max{f[I,k]+f[k+1,j]+w[I,j]}{i<=k<j}f[I,j]=maxf[I,k]+f[k+1,j]+w[I,j]i<=k<j,如果w满足四边形不等式,那么f也满足四边形不等式。
    前面千辛万苦证明了f满足四边形不等式,那么它有什么用呢?
    之所以要证明四边形不等式,就是为了证明f满足决策单调性,即:
    s[i,j−1]≤s[i,j]≤s[i+1,j],i≤js[i,j-1]≤s[i,j]≤s[i+1,j], i≤js[i,j1]s[i,j]s[i+1,j],ij
    其中,s[I,j]s[I,j]s[I,j]为区间[I,j][I,j][I,j]上的最优决策。
    i=ji=ji=j时,单调性显然成立。因此下面只讨论i<ji<ji<j的情形。由于对称性,只要证明s[i,j]≤s[i,j+1]s[i,j]≤s[i,j+1]s[i,j]s[i,j+1]
    fk[i,j]=f[i,k−1]+f[k,j]+w[i,j]fk[i,j]=f[i,k-1]+f[k,j]+w[i,j]fk[i,j]=f[i,k1]+f[k,j]+w[i,j]。要证明s[i,j]≤s[i,j+1]s[i,j]≤s[i,j+1]s[i,j]s[i,j+1],只要证明对于所有i<k≤k’≤ji<k≤k’≤ji<kkjfk’[i,j]≤fk[i,j]fk’[i,j]≤fk[i,j]fk[i,j]fk[i,j],有:fk’[i,j+1]≤fk[i,j+1]fk’[i,j+1]≤fk[i,j+1]fk[i,j+1]fk[i,j+1]
    事实上,我们可以证明一个更强的不等式

fk[i,j]−fk’[i,j]≤fk[i,j+1]−fk’[i,j+1]fk[i,j]-fk’[i,j]≤fk[i,j+1]-fk’[i,j+1]fk[i,j]fk[i,j]fk[i,j+1]fk[i,j+1]

也就是:fk[i,j]+fk’[i,j+1]≤fk[i,j+1]+fk’[i,j]fk[i,j]+fk’[i,j+1]≤fk[i,j+1]+fk’[i,j]fk[i,j]+fk[i,j+1]fk[i,j+1]+fk[i,j]

利用递推定义式将其展开整理可得:f[k,j]+f[k’,j+1]≤f[k’,j]+f[k,j+1]f[k,j]+f[k’,j+1]≤f[k’,j]+f[k,j+1]f[k,j]+f[k,j+1]f[k,j]+f[k,j+1],这正是k≤k’≤j<j+1k≤k’≤j<j+1kkj<j+1时的四边形不等式

综上所述,当www满足四边形不等式时,函数s[i,j]s[i,j]s[i,j]具有单调性。

因此,我们可以得到一个更优化的转移方程

上述方法利用四边形不等式推出最优决策的单调性,从而减少每个状态转移的状态数,降低算法的时间复杂度。

这样这道题就解决了。

一般的,对于状态转移方程形如:f[i,j]=optf[i,k]+f[k+1,j]+w[i,j],i<jf[i , j] = opt{f[i , k] + f[k+1 , j]} + w[i , j], i < jf[i,j]=optf[i,k]+f[k+1,j]+w[i,j],i<j的动规,若w满足四边形不等式,则f满足四边形不等式,则f有决策单调性。

Code

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
 
using std::max;
using std::min;
 
const int maxn=300;
 
int base[maxn];
int dp[maxn][maxn],s[maxn][maxn];
int MAX[maxn][maxn];
 
int main()
{
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&base[i]);
        base[i+n]=base[i];
    }
    for(int i=1;i<=n*2;i++)
        base[i]+=base[i-1];
    for(int i=1;i<=n*2;i++)
        s[i][i]=i;
    for(int l=2;l<=n;l++)
        for(int i=1;i<=(n*2)-l+1;i++)
        {
            int j=i+l-1;
            int x=s[i][j-1],y=s[i+1][j];
            dp[i][j]=0x7fffffff;
            MAX[i][j]=max(MAX[i][j-1],MAX[i+1][j])+base[j]-base[i-1];//最大值不满足四边形不等式,但最有决策一定出现在端点处
            for(int k=x;k<=y;k++)
                if(dp[i][k]+dp[k+1][j]+base[j]-base[i-1]<dp[i][j])
                {
                    dp[i][j]=dp[i][k]+dp[k+1][j]+base[j]-base[i-1];
                    s[i][j]=k;
                }
        }
    int ansMin=0x7fffffff,ansMax=0;
    for(int i=1;i<n;i++)
        ansMin=min(ansMin,dp[i][i+n-1]),
        ansMax=max(ansMax,MAX[i][i+n-1]);
    printf("%d\n%d",ansMin,ansMax);
    return 0;
}

hdu4632 **

思路:

大致题意是给定一个字符串,求回文子序列个数,最后的答案要%10007,要求一个n^2的做法

首先定义f数组f[l][r]f[l][r]f[l][r]表示l rl~rl r区间的回文子序列个数,f[i][i]=1f[i][i]=1f[i][i]=1;

显然 根据容斥原理 :f[l][r]=f[l][r−1]+f[l+1][r]−f[l+1][r−1]f[l][r]=f[l][r-1]+f[l+1][r]-f[l+1][r-1]f[l][r]=f[l][r1]+f[l+1][r]f[l+1][r1] (因为中间的个数会算两遍);

然后,我们考虑s[l]==s[r]s[l]==s[r]s[l]==s[r]的情况,如果这两个位置相等,那么l+1 r−1l+1 ~ r-1l+1 r1这个区间的所有子序列,都可以加入l和r这两个元素,构成一个新的回文子序列,除此之外 l和r这两个元素也可以构成一个回文子序列

那么 if(s[l]==s[r])f[l][r]+=f[l+1][r−1]+1if (s[l]==s[r]) f[l][r]+=f[l+1][r-1]+1if(s[l]==s[r])f[l][r]+=f[l+1][r1]+1;
Attention
做减法的时候,可能会有负数,为了使%不出现问题,我们需要先加mod再%mo

Code:

#inclue<bits/stdc++.h>
using namespace std;
#define inf 0x3f3f3f3f
#define mod 10007
int dp[1005][1005];
char s[1005]
int main()
{
	int t,cas=1;
    cin>>t;
    while(t--)
    {
        cin>>s;
        int len=strlen(s);
        memset(dp,0,sizeof dp);
        for(int i=0;i<len;i++)
            dp[i][i]=1;
        for(int j=0;j<len;j++)
        {
            for(int i=j-1;i>=0;i--)
            {
                dp[i][j]=(dp[i][j-1]+dp[i+1][j]-dp[i+1][j-1]+mod)%mod;
                if(s[i]==s[j])
                    dp[i][j]=(dp[i][j]+dp[i+1][j-1]+1)%mod;
            }
        }
        printf("Case %d: %d\n",cas++,dp[0][len-1]);
    }
    return 0;
}

hdu4745*********

题目大意:

给定一个数列,求一个最长的回文子序列

思路:

我们定义f[l][r]f[l][r]f[l][r]表示l到r区间的最长的回文子序列长度

如果s[l]==s[r]s[l]==s[r]s[l]==s[r]f[l][r]=f[l+1][r−1]+2f[l][r]=f[l+1][r-1]+2f[l][r]=f[l+1][r1]+2;

不然 f[l][r]=max(f[l+1][r],f[l][r−1])f[l][r]=max(f[l+1][r],f[l][r-1])f[l][r]=max(f[l+1][r],f[l][r1]);

然后统计答案的时候

因为是环

由于考虑到可以从

同一个点出发,长度为n-1,+1

或者从不同的点出发,长度为n

Code:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <iostream>
#include <algorithm>
using namespace std;
int s[1005],dp[2005][2005];
int main(){                                     //一个顺时针一个逆时针,并且元素
    int n,i,j,l,ans;                            //相同,因此相当于在环上求最长回
    while(scanf("%d",&n)!=EOF&&n){              //文子序列
        memset(dp,0,sizeof(dp));
        for(i=1;i<=n;i++){
            scanf("%d",&s[i]);
            s[i+n]=s[i];
            dp[i][i]=dp[i+n][i+n]=1;
        }
        for(l=2;l<=2*n;l++){
            for(i=1;i<=2*n-l+1;i++){
                j=i+l-1;
                dp[i][j]=max(dp[i+1][j],dp[i][j-1]);
                if(s[i]==s[j])
                dp[i][j]=max(dp[i][j],dp[i+1][j-1]+2);
            }
        }                                       //当s[i]==s[j]时长度加2
        ans=0;
        for(i=1;i<=n;i++){                      //因为是环,所以起点可能在一个点,
            ans=max(ans,dp[i][i+n-1]);          //这样只需求长度是n-1的再加1即可
            ans=max(ans,dp[i][i+n-2]+1);
        }
        printf("%d\n",ans);
    }
    return 0;
}

hdu2476******

题目大意:

给定一个A串,一个B串,每次修改只能将一段连续区间内的字母,改成同一个字母,询问将A变成B 的最少操作次数

思路:

因为考虑到有相等的和不相等的部分,所以不能直接做

先假设A和B所有的对应位置都不相等,然后令f[l][r]f[l][r]f[l][r]表示l~r这个区间最小操作的次数

然后对一个区间l r 我们可以考虑,首先将f[l][r]=f[l+1][r]+1f[l][r]=f[l+1][r]+1f[l][r]=f[l+1][r]+1;表示需要在上一个区间的基础上,额外操作一次。然后枚举一个kkk

l+1l+1l+1rrr,如果b[k]==b[l]b[k]==b[l]b[k]==b[l] 那么 l这个位置 就可以在刷k的时候 刷上,而且不影响之前的区间。

f[l][r]=min(f[l][r],f[l][k]+f[k+1][r])f[l][r]=min(f[l][r],f[l][k]+f[k+1][r])f[l][r]=min(f[l][r],f[l][k]+f[k+1][r])

然后 我们令ans[i]ans[i]ans[i]表示A的第1到第iii 刷成和BBB相同的 需要的 最小的操作次数

a[i]==b[i]a[i]==b[i]a[i]==b[i]ans[i]=ans[i−1]ans[i]=ans[i-1]ans[i]=ans[i1] (因为不需要刷)

else 枚举一个j 表示这个ans[i]ans[i]ans[i]从哪个答案转移过来

ans[i]=min(ans[i],ans[j]+f[j+1][i])ans[i]=min(ans[i],ans[j]+f[j+1][i])ans[i]=min(ans[i],ans[j]+f[j+1][i]);

hdu4283 ********

题目大意:

现在给定一个序列,表示每个人的不开心值D,假如他是第x个上场,那他就有(x-1)*D的不开心值,因为他等了k-1个人,你有一个类似于栈的东西,可以使用它来改变整个序列的顺序,求最少的 不开心值的和

思路:

由于修改顺序的时候,是要用类似栈的方式QWQ所以没法贪心…

这个题有一个性质,若第i个人是通过栈调整之后,第k个表演的 那么他能造成的影响 是可以比较容易计算的。

我们令f[l][r]f[l][r]f[l][r]表示 l rl~rl r这些人的最小等待时间

首先预处理一下所有人的等待时间的前缀和sum[i]sum[i]sum[i],便于计算

对于一个区间[l,r][l,r][l,r] 我们枚举第l个人是第几个出去的

kkkl rl~rl r

假设他是第k个出去的

f[l][r]=min(f[l][r],f[l+1][k]+f[k+1][r]+(k−l)∗a[i]+(k−l+1)∗(sum[r]−sum[k]))f[l][r]=min(f[l][r],f[l+1][k]+f[k+1][r]+(k-l)*a[i]+(k-l+1)*(sum[r]-sum[k]))f[l][r]=min(f[l][r],f[l+1][k]+f[k+1][r]+(kl)a[i]+(kl+1)(sum[r]sum[k]))

这里有几个要注意的地方:

  • f[l+1][k]f[l+1][k]f[l+1][k] 而不是 f[l][k]f[l][k]f[l][k] 因为这里枚举的就是第lll个人的出栈顺序

  • 2>(k−l)2>(k-l)2>(kl)表示在这个人之前有几个人出去 (k−1+1)(k-1+1)k1+1表示算上这个人 出去了几个人

poj2955

题目大意

给定一个只含小括号和中括号的序列,求一个最长的合法序列。

其中() [] ()[] (()) 等 是合法的

)( (] 等 是不合法的

思路

由于两个本身合法的序列拼在一起也是合法的序列,所以我们自然而然的就想到

f[l][r]f[l][r]f[l][r]表示l rl~rl r这个区间的最长合法序列的程度

根据a[l]a[r]f[l+1][r−1]a[l] a[r] f[l+1][r-1]a[l]a[r]f[l+1][r1]进行初始化

然后枚举断点,进行转移

这里有一个小tip就是

可以把左右括号分别变成正负的数字,便于判断

poj1651

题目大意

给定你一个序列

其中取走一个数的代价是它乘上它相邻的两个数

两头的数不可以取~

求最小代价

思路

f[l][r]f[l][r]f[l][r]表示把l+1 r−1l+1 ~ r-1l+1 r1的数都取走的最小代价

首先初始化f[i][i+2]f[i][i+2]f[i][i+2]这个区间

然后之后枚举长度,起始点,断点,进行转移即可

f[l][r]=min(f[l][r],f[l][k]+f[k][r]+a[l]∗a[k]∗a[r]f[l][r]=min(f[l][r],f[l][k]+f[k][r]+a[l]*a[k]*a[r]f[l][r]=min(f[l][r],f[l][k]+f[k][r]+a[l]a[k]a[r];

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值