区间dp小结

来源:互联网 发布:淘宝代办工商营业执照 编辑:程序博客网 时间:2024/06/07 07:28

序:这次的专题刷得很蛋疼有趣 各种大神代码和题解
区间dp,异于普通的线性dp,它的转移往往是建立于不同的规则上且诡异复杂的
区间dp的解题要点是找到一种转移使得我们能够得到最优解


HDU4283
因为要保持栈的合法,所以不能排序乱排瞎搞
取一段区间,可以保证区间的做端点能在该区间的任意点后出去
那么利用这一点,就可以来更新区间

for(int i=1;i<=n;i++){    scanf("%d",&A[i]);    Sum[i]=Sum[i-1]+A[i];    dp[i][i]=0;}for(int i=1;i<=n;i++)    for(int j=i+1;j<=n;j++)dp[i][j]=1e9;for(int l=1;l<n;l++){    for(int i=1;i<=n-l;i++){        int j=l+i;        for(int k=1;i+k-1<=j;k++){            dp[i][j]=Min(dp[i][j],dp[i+1][i+k-1]     +dp[i+k][j]+(Sum[j]-Sum[i+k-1])*k+A[i]*(k-1));        }    }}

HDU4293
这题不能算是一道实际意义上的区间dp
但我们要在这道题中发觉每个人对应的只有一个区间
那么我们应该选取几段不相交区间使得答案最大
记第i个点前最优解为dp[i]
可得状态转移方程为dp[i]=max(dp[i],dp[j]+sum[i][j])

for(int i=1;i<=n;i++){    int l,r;    scanf("%d%d",&l,&r);    if(l+r>n||Sum[l+1][n-r]==n-r-l)continue;    Sum[l+1][n-r]++;}for(int i=1;i<=n;i++)    for(int j=1;j<=i;j++)        Tmp[i]=Max(Tmp[i],Tmp[j-1]+Sum[j][i]);    //Tmp[i]表示i前最多人数    //Sum[i][j]表示区间[i,j]内最多站的人数 

HDU4412
据说这是整个专题中最难的题目具体的一些细节自己也没搞清楚
这题的主要解题思路为我们可以贪心地求出一段区间只去一个大本营的最小期望
然而这里是可以优化时间复杂度的
首先针对一个区间[l,r],他的上一个区间[l,r-1] 大本营的地点要么不变,要么就会往右移,所以找大本营的位置是O(1)[均摊]的
然后利用前缀和也可O(1)求出Cost[i][j]
最后的dp转移就十分轻松了O(n*n*m)
参照Komachi神犇代码

#include<bits/stdc++.h>using namespace std;#define M 1005map<int,double>mp;int Way[M];double P[M],Val[M],Sum[M],Cost[M][M],dp[55][M];int main(){    int n,m;    while(~scanf("%d%d",&n,&m)&&(n||m)){        mp.clear();        //read        for(int i=1;i<=n;i++){            int tmp;            scanf("%d",&tmp);            for(int k=1;k<=tmp;k++){                int a;double b;                scanf("%d%lf",&a,&b);                mp[a]+=b;            }        }        //init        int n=0;        for(map<int,double>::iterator it=mp.begin();it!=mp.end();++it){            Way[++n]=it->first,P[n]=it->second;        }        for(int i=1;i<=n;i++){            Sum[i]=Sum[i-1]+P[i]*Way[i];            Val[i]=Val[i-1]+P[i];        }        for(int i=1;i<=n;i++){            int p=i;            for(int j=i;j<=n;j++){                while(p<j&&Val[p]*2<(Val[i-1]+Val[j]))p++;                Cost[i][j]=Way[p]*(Val[p]-Val[i-1])-Way[p]*(Val[j]-Val[p])+                (Sum[j]-Sum[p])-(Sum[p]-Sum[i-1]);            }        }        //dp        for(int i=0;i<=m;i++)for(int j=1;j<=n;j++)dp[i][j]=1e15;        for(int i=0;i<=m;i++)dp[i][0]=0;        for(int i=1;i<=m;i++){            for(int j=1;j<=n;j++){                for(int k=0;k<j;k++){                    dp[i][j]=min(dp[i][j],dp[i-1][k]+Cost[k+1][j]);                }            }        }        printf("%.2lf\n",dp[m][n]);    }    return 0;}

求p的那部分实在看不懂完全可以用代替

Way[p]*(Val[p]-Val[i-1])-Way[p]*(Val[j]-Val[p])-2*Sum[p]>Way[p+1]*(Val[p+1]-Val[i-1])-Way[p+1]*(Val[j]-Val[p+1])-2*Sum[p+1]

HDU4597
网上题解全是记忆化搜索
献上一个递推的:

#include<bits/stdc++.h>using namespace std;int dp[25][25][25][25];int A[25],B[25],Sum[25],Tmp[25];int main(){    int T;    scanf("%d",&T);    while(T--){        int n;        scanf("%d",&n);        memset(dp,0,sizeof(dp));        for(int i=1;i<=n;i++) scanf("%d",&A[i]),Tmp[i]=Tmp[i-1]+A[i];        for(int i=1;i<=n;i++) scanf("%d",&B[i]),Sum[i]=Sum[i-1]+B[i];        for(int i=n+1;i>=1;i--){            for(int j=i-1;j<=n;j++){                for(int k=n+1;k>=1;k--){                    for(int h=k-1;h<=n;h++){                        int tot=Sum[h]-Sum[k-1]+Tmp[j]-Tmp[i-1];                        if(i<=j)dp[i][j][k][h]=max(dp[i][j][k][h],tot-dp[i+1][j][k][h]);                        if(k<=h)dp[i][j][k][h]=max(dp[i][j][k][h],tot-dp[i][j][k+1][h]);                        if(k<=h)dp[i][j][k][h]=max(dp[i][j][k][h],tot-dp[i][j][k][h-1]);                        if(i<=j)dp[i][j][k][h]=max(dp[i][j][k][h],tot-dp[i][j-1][k][h]);                    }                }             }        }        printf("%d\n",dp[1][n][1][n]);    }    return 0;} 

HDU4745
刚开始看到这道题的时候是懵逼
兔子可以转圈,那怎么搞,for循环都不好实现
后来发现其实把序列扩展成两倍就好了,因为兔子最多跳一圈
然后可以发现兔子的跳法就是最长回文子序列
其实感觉求答案的那部分还是很巧妙的
兔子可以挑任一点开始跳,那么区间的起点就可以任意取
即dp[i][i+n-1]或dp[i][i+n-2]+1【从同一点出发】

for(int i=1;i<=n;i++){    scanf("%d",&A[i]);    A[i+n]=A[i];    dp[i][i]=1;}int ans=0;for(int i=2*n;i>=1;i--){    for(int j=i+1;j<=2*n;j++){        dp[i][j]=max(dp[i][j-1],dp[i+1][j]);        if(A[i]==A[j]){            dp[i][j]=max(dp[i][j],dp[i+1][j-1]+2);        }    }}for(int i=1;i<=n;i++){    ans=max(ans,dp[i][i+n-1]);    ans=max(ans,dp[i][i+n-2]+1);  //两只兔子从同一个起点出发}

HDU5115
一道很经典的区间dp,特别有套路
枚举一个区间内,哪个点最先被打,然后更新最大值

for(int i=1;i<=n;i++)scanf("%d",&A[i]);for(int i=1;i<=n;i++)scanf("%d",&F[i]);for(int i=n;i>=1;i--){    for(int j=i;j<=n;j++){        if(i==j)dp[i][j]=A[i]+F[i-1]+F[i+1];        else{            dp[i][j]=1e9;            for(int k=i;k<=j;k++){                dp[i][j]=min(dp[i][j],dp[i][k-1]+dp[k+1][j]+F[i-1]+A[k]+F[j+1]);            }        }    }}

HDU5396
仔细领会网上的题解还是比较容易理解的
下面是我个人的见解

将dp[i][k]的方案数拆开,分别为(a1+a2+a3+a4+a5+…+a[(k-i)!])
同理dp[k+1][j]的方案数为(b1+b2+b3+…+b[(j-k-1)!])
若要把dp[i][k]与dp[k+1][j]相加,则要将dp[i][k]与dp[k+1][j]中所有元素相加
那么dp[i][k]中的每个元素要被加(j-k+1)!次 同理dp[k+1][j]中每个元素要被加(k-i)!次
所以为加号时res=dp[i][k](j-k+1)!+dp[k+1][j](k-i)! 为减号时同时满足这个规律 但为乘号时
可以发现res=(a1+a2+a3+…+a[(k-i)!])*(b1+b2+b3+…+b[(j-k+1)!])刚好是所有方案数两两相乘
合并完dp[i][k]和dp[k+1][j]后 我们可以保证dp[i][k]与dp[k+1][j]各自的运算符都是有序的
但他们之间运算符的顺序并没有确定 因为C[k]是最后的运算符,所以C[k]做为最后运算符得到的最终答案 为
res*C(j-i-1,k-i)

代码实现(注意阶乘预处理时fact[0]=1):

void init(){    c[0][0]=1;    for(int i=1;i<=100;i++){        c[i][0]=c[i][i]=1;        for(int j=1;j<=i;j++){            c[i][j]=c[i-1][j-1]+c[i-1][j];            c[i][j]%=P;        }    }     fact[0]=1;    fact[1]=1;    for(int i=2;i<=100;i++)fact[i]=fact[i-1]*i%P;}int main(){    int n;    init();    while(~scanf("%d",&n)){        memset(dp,0,sizeof(dp));        for(int i=1;i<=n;i++)scanf("%d",&A[i]),dp[i][i]=A[i];        scanf("%s",C+1);         for(int i=n;i>=1;i--){            for(int j=i+1;j<=n;j++){                for(int k=i;k<j;k++){                    long long res=0;                    if(C[k]=='*')res=dp[k+1][j]*dp[i][k]%P*c[j-i-1][k-i]%P;                    else if(C[k]=='+')res=(dp[i][k]*fact[j-k-1]%P+dp[k+1][j]*fact[k-i]%P)*c[j-i-1][k-i]%P;                    else res=(dp[i][k]*fact[j-k-1]%P-dp[k+1][j]*fact[k-i]%P)*c[j-i-1][k-i]%P;                    dp[i][j]=(dp[i][j]+res)%P;                }            }        }        printf("%lld\n",(dp[1][n]+P)%P);    }    return 0;}

HDU5900
有是一道写法不同于狼群但很有拓展性的题目
两个相邻的不互质的数,可以消除
消除之后一个区间两边的两个点可能就会并到一起
但这要在区间被削完的情况下
不然就可以把区间劈成两半更新最大值

for(int i=1;i<=n;i++)scanf("%d",&A[i]);for(int i=1;i<=n;i++)scanf("%d",&Val[i]),Sum[i]=Sum[i-1]+Val[i];for(int i=1;i<=n;i++)    for(int j=1;j<=n;j++)        if(gcd(A[i],A[j])!=1)mark[i][j]=1;        else mark[i][j]=0;for(int i=n;i>=1;i--){    for(int j=i;j<=n;j++){        if(Sum[j-1]-Sum[i]==dp[i+1][j-1]&&mark[i][j])            dp[i][j]=dp[i+1][j-1]+Val[i]+Val[j];        else for(int k=i;k<j;k++)chk_mx(dp[i][j],dp[i][k]+dp[k+1][j]);    }}

小结:
区间dp还是很有套路的
首先区间dp状态的划分是建立在区间的基础上的,即区间内的状态是可以不用考虑的
然后是更新,区间之间的联系需要结合具体题目,一般是枚举一段区间
然后在枚举这段区间内的一个点进行特殊处理(加上一些值…)
区间dp中的一种特殊类型就是消除,它会使原来无联系(不相邻)的两个区间合并成一个区间,这时一般采用向两边拓展的方式
首先要保证中间这段区间被削完,然后类似于dp[i][j]=dp[i+1][j-1]…

毅者自远!

原创粉丝点击