动态规划:常见的基础问题整理

写在前面:动态规划与分治有一定的类似之处,都是将原问题分解成子问题解决。但是,动态分解得到的子问题往往不是独立的,子问题之间可能共享相同的子问题;而分治的子问题相互独立互不影响。动态规划常用于求最优解的问题。

解决动态规划问题的关键点在于确定状态量和状态转移方程,并选择合适的复杂度范围。状态量要能完全表示出状态的特征,状态间的转移完全依赖于各个状态本身。


 

递推问题,利用了DP的思想。对于一个问题,如果它的解和它的子问题相关,则先处理子问题,再由子问题的解递推出当前问题的解。一个典型问题就是错排,有n对物品两两对应,但是现在要求全部不对应的排列方式有几种,F[n]=(n-1)*F[n-1]+(n-1)*F[n-2]


LIS问题,最长递增子序列。以F[i]来表示以第i个元素结尾的递增子序列的最长长度。F[i]=max{1,F[j]+1|aj<ai&&j<i},复杂度是O(n2)。基本模型可以考虑导弹的例题,原序列是有序的,在固定序列上求最长递增子序列。对于有多个属性的无需元素,可以先按某一属性排序。

对于求LIS长度,有O(nlgn)的方法,通过贪心维护子序列长度为i时,第i个元素的最小值.

int loc=0;
for(int i=1;i<=n;i++){
    if(nodes[i].wid>dp[loc]) dp[++loc]=nodes[i].wid;
    else *(lower_bound(dp+1,dp+loc+1,nodes[i].wid))=nodes[i].wid;
}

还有一个拓展,求最长递增子序列的个数,使各个子序列能够覆盖整个区间。可以将该问题转化为求最长不递增子序列的长度。

LCS问题,最长公共子序列,有两个字符串s1和s2,求一个字符串s3同时是s1和s2的子串,同时长度最大。dp[i][j]表示到s1的第i个元素,到s2的第j个元素时的最长子串的长度。dp[i][0]=dp[0][j]=0;dp[i][j]=dp[i-1][j-1]+1|s1[i]==s2[j];dp[i][j]=max{dp[i-1][j],dp[i][j-1]}|s1[i]!=s2[j]


背包问题,在一个有容积限制或重量限制的背包中放入物品,物品拥有体积、重量和价值等属性,需要求一种满足背包限制的放置物品的方式,使背包中物品的价值和最大。

01背包,每种物品有且仅有一个,有权值和体积两个属性。只需要考虑每种物品选与不选这两种情况。dp[i][j]表示前i个物品占用j体积时的最大价值,有转移方程:dp[i][j]=max(dp[i-1][j],dp[i-1][j-c[i]]+w[i]),j>=c[i]。可以发现dp[i]-1[j]只可能对dp[i][x](x>=j)有影响,因此dp[i][j]可以覆盖dp[i-1][j],从而实现空间的优化。时间复杂度为O(VN)

for(int i=0;i<n;i++){
    for(int j=v;j>=c[i];j--){
        dp[j]=max(dp[j],dp[j-c[i]]+w[i]);
    }
}

01背包的简单变形,要求最终重量恰好装满背包。状态量和转移方程还是一样的,但是状态初始值发生变化。dp[0][0]=0,dp[0][i]=-1表示当前状态不可到达。

完全背包,每种物品的数量没有限制。考虑枚举每种物品的个数,从而转化为01背包问题,但是复杂度太高。可以直接修改01背包第二层循环的方向,因为01背包要求每种物品要么选要么不选,而完全背包下dp[i][j]可能是由dp[i][j-w[i]]转移得来的,而dp[i][j-w[i]]对物品个数没有要求,可能已经拿过第i个物品。时间复杂度为O(VN)

for(int i=0;i<n;i++){
    for(int j=c[i];j<=v;j++){
        dp[j]=max(dp[j],dp[j-c[i]]+w[i]);
    }
}

多重背包,每种物品可以有多个但有个数限制。尝试用分组的方式转化为01背包,若第i中物品为数量k[i],那么新的每一个物品价值w[i]*1、w[i]*2、...,体积c[i]*1、c[i]*2、...时间复杂度变为O(V*∑log2(k[i]))。对于部分物品,若num[i]*w[i]>=v,可以认为这种物品是无限的,以完全背包处理

for(int i=1;i<=n;i++){
    int d=1,k=num[i];
    while(k-d>0){
        k-=d;
        item[++loc].w=w[i]*d;
        item[loc].v=v[i]*d;
        d*=2;
    }
    item[++loc].w=w[i]*d;
    item[loc].v=v[i]*d;
}

for(int i=1;i<=loc;i++){
    for(int j=v;j>=item[i].w;j--){
        dp[j]=max(dp[j],dp[j-item[i].w]+item[i].v);
    }
}

使用单调队列,可以将多重背包的复杂度优化为O(VN)。这个做法是学习了《挑战程序设计实践》。这里先不讲解了(说实话对于推导过程还是有点懵...),只贴代码。但是多重背包这样的做法是很强的.例题:POJ1276,POJ1742,POJ1014,POJ3260

//POJ1276
#include<stdio.h>
#include<string.h>
int n,m,num[15],val[15],dp[100005];
int main(){
    while(scanf("%d",&m)!=EOF){
        scanf("%d",&n);
        for(int i=0;i<n;i++){
            scanf("%d%d",&num[i],&val[i]);
        }
        memset(dp,-1,sizeof(dp));
        dp[0]=0;
        for(int i=0;i<n;i++){
            for(int j=0;j<=m;j++){
                if(dp[j]>=0) dp[j]=num[i];
                else if(j<val[i]||dp[j-val[i]]<=0) dp[j]=-1;
                else dp[j]=dp[j-val[i]]-1;
            }
        }
        for(int i=m;i>=0;i--){
            if(dp[i]>=0){
                printf("%d\n",i);
                break;
            }
        }
    }
    return 0;
}
//POJ3260
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
const int maxn=30005,inf=0x3f3f3f3f;
int n,t,d1[maxn],d2[maxn],deq[maxn],deqv[maxn],v[105],c[105];
int main(){
    while(scanf("%d%d",&n,&t)!=EOF){
        memset(d1,0x3f,sizeof(d1));
        memset(d2,0x3f,sizeof(d2));
        int maxv=0;
        for(int i=0;i<n;i++){
            scanf("%d",&v[i]);
            maxv=max(maxv,v[i]);
        }
        for(int i=0;i<n;i++){
            scanf("%d",&c[i]);
        }
        maxv*=maxv;

        d1[0]=d2[0]=0;
        for(int i=0;i<n;i++){
            for(int a=0;a<v[i];a++){
                int s=0,t=0;
                for(int j=0;j*v[i]+a<=t+maxv;j++){
                    int val=d1[j*v[i]+a]-j;
                    while(s<t&&val<=deqv[t-1]) t--;
                    deq[t]=j;
                    deqv[t++]=val;
                    d1[j*v[i]+a]=deqv[s]+j;
                    if(deq[s]==j-c[i]) s++;
                }
            }
        }
        for(int i=0;i<n;i++){
            for(int j=v[i];j<=t+maxv;j++){
                d2[j]=min(d2[j],d2[j-v[i]]+1);
            }
        }

        int ans=inf;
        for(int i=t;i<=t+maxv;i++){
            if(d1[i]==inf||d2[i-t]==inf) continue;
            ans=min(ans,d1[i]+d2[i-t]);
        }
        if(ans==inf) printf("-1\n");
        else printf("%d\n",ans);
    }
}

背包经常会以变体的形式考察,不会是可以直接处理的背包问题,需要对数据进行一定处理之后才能变为背包问题,主要是一个思维的过程。例题洛谷P1064


多重部分和问题(感觉也算是一种特殊的多重背包),POJ1276POJ1742。可以以O(n*K)的复杂度解决

//POJ1742
#include<stdio.h>
#include<string.h>
int n,m,num[15],val[15],dp[100005];
int main(){
    while(scanf("%d",&m)!=EOF){
        scanf("%d",&n);
        for(int i=0;i<n;i++){
            scanf("%d%d",&num[i],&val[i]);
        }
        memset(dp,-1,sizeof(dp));
        dp[0]=0;
        for(int i=0;i<n;i++){
            for(int j=0;j<=m;j++){
                if(dp[j]>=0) dp[j]=num[i];
                else if(j<val[i]||dp[j-val[i]]<=0) dp[j]=-1;
                else dp[j]=dp[j-val[i]]-1;
            }
        }
        for(int i=m;i>=0;i--){
            if(dp[i]>=0){
                printf("%d\n",i);
                break;
            }
        }
    }
    return 0;
}

划分数,有n个无区别的物品,将它们划分为不超过m组,求总的划分方法数。POJ1664

//POJ1664
#include<stdio.h>
int m,n,dp[100][100];
int main(){
    int t;
    scanf("%d",&t);
    while(t--){
        scanf("%d%d",&m,&n);
        dp[0][0]=1;
        for(int i=1;i<=n;i++){
            for(int j=0;j<=m;j++){
                if(j>=i) dp[i][j]=dp[i-1][j]+dp[i][j-i];
                else dp[i][j]=dp[i-1][j];
            }
        }
        printf("%d\n",dp[n][m]);
    }
}

多重集组合数,有n种物品,第i种物品有ai个,不同种类的物品可以互相区分但相同种类的无法区分。从这些物品中取出m个的话,有多少种取法?POJ3046

//POJ3046
#include<stdio.h>
int n,m,a,b,num[1005],dp[1005][100005],mod=1000000;
int main(){
    scanf("%d%d%d%d",&n,&m,&a,&b);
    for(int i=1;i<=m;i++){
        int c;
        scanf("%d",&c);
        num[c-1]++;
    }
    for(int i=0;i<=n;i++){
        dp[i][0]=1;
    }
    for(int i=0;i<n;i++){
        for(int j=1;j<=m;j++){
            if(j-1-num[i]<0) dp[i+1][j]=(dp[i][j]+dp[i+1][j-1])%mod;
            else dp[i+1][j]=(dp[i][j]+dp[i+1][j-1]-dp[i][j-1-num[i]]+mod)%mod;
        }
    }
    int ans=0;
    for(int i=a;i<=b;i++){
        ans=(ans+dp[n][i])%mod;
    }
    printf("%d\n",ans);
}

 

posted @ 2020-10-09 15:49  太山多桢  阅读(224)  评论(0编辑  收藏  举报