复健计划(3)动态规划(线性DP的模型和模板)

复健计划(3)动态规划(线性DP的模型和模板)

写在前面:关于DP的内容,复习的时候正好看到了y总的DP分析法,觉得很好理解,就都尝试拿来理解DP题目,同时我对一些典型的DP模型也有总结。不过,因为做的DP题目实在太少,总结内容不免出现错误,在学习做题中也会及时改正所写内容.
同时,我也建议每一个人都可以去 ACwing学习到y总的DP分析法(.

自己的总结:DP的过程相当于在走拓扑图,每个节点就是一个状态,对于一个结点会有几条入边和出边;一般设置的f/dp[]…就是对节点状态的一个表示,考虑入边,就是划分形成该状态集合的方式;考虑出边,就是考虑该状态能递推到其他状态的规则;两种思想都有用,根据题目采用.
不过往往DP题目难是难在状态的表示,这一点还需要我们多做题来积累经验.

复健题目:acwing2~12,acwing895,896,897,902共15道模板题.


一、线性DP(包括背包)
1、01背包,完全背包,多重背包

这三个背包模型是所有背包问题的基础,同时他们时间和空间上的优化方法也是值得借鉴的.

a) 01背包(f[i][j] ——> f[j], 其中f[i][j]表示选前 i 件物品且体积不超过 j 的最大价值)

优化原理:因为每一次 f 在计算时只与上一层 i-1 有关,因此倒序体积更新,刚好就可以用 i-1 层的小体积状态更新到 i 层的大体积状态

代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
const int N=1010;
int w[N],v[N],n,V;
int f[N*N];
using namespace std;
int main(){
    cin>>n>>V;
    for(int i=1;i<=n;i++){
        cin>>v[i]>>w[i];
    }
    for(int i=1;i<=n;i++)
        for(int j=V;j>=v[i];j--){
            f[j]=max(f[j],f[j-v[i]]+w[i]);
        }
    int res=0;
    for(int i=1;i<=V;i++) res=max(res,f[i]);
    cout<<res<<endl;
    return 0;
}
b) 完全背包(f[i][j] O(nm^2) ——> f[i][j] O(nm) ——> f[j], 其中f[i][j]表示选前 i 件物品且体积恰好为 j 的最大价值)

优化原理:
1、因为 f [ i ] [ j ] = m a x ( f [ i ] [ j − v ] + w , f [ i ] [ j − 2 v ] + 2 w , . . . , f [ i ] [ j − s v ] + s w ) = m a x ( f [ i ] [ j − v ] , f [ i ] [ j − 2 v ] + w , . . . , f [ i ] [ j − s v ] + ( s − 1 ) w ) + w = f [ i ] [ j − v ] + w f[i][j]=max(f[i][j-v]+w, f[i][j-2v]+2w, ..., f[i][j-sv]+sw)=max(f[i][j-v], f[i][j-2v]+w,..., f[i][j-sv]+(s-1)w)+w=f[i][j-v]+w f[i][j]=max(f[i][jv]+w,f[i][j2v]+2w,...,f[i][jsv]+sw)=max(f[i][jv],f[i][j2v]+w,...,f[i][jsv]+(s1)w)+w=f[i][jv]+w
2、原理类似于01背包,不用倒序体积的原因是因为正好可以无限用物品,所以需要用 i 层的小体积状态更新同样是第 i 层的大体积状态.

代码:
#include <iostream>
#include <cstdio>
using namespace std;
const int N=1010;
int w[N],v[N],n,V,f[N*N];
int main(){
    cin>>n>>V;
    for(int i=1;i<=n;i++)
        cin>>v[i]>>w[i];
    for(int i=1;i<=n;i++)
        for(int j=v[i];j<=V;j++)
            f[j]=max(f[j],f[j-v[i]]+w[i]);
    cout<<f[V]<<endl;
    return 0;
}
c) 多重背包(朴素做法——>二进制拆分物品再用01背包——>单调队列维护做法)

优化原理:
1、二进制拆分物品个数后,来一遍01背包;
2、类似于完全背包的优化原理1,只不过这一次s不是固定的,所以不能用算过的 f 直接表示,而是一个类似“滑动窗口”求最值的问题,细节上要注意加上“价值*物品个数”这一偏移量.

二进制拆分物品代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>

using namespace std;

const int N=2010;
int n,V;
int W[N],VO[N];
int f[N];

int main(){
    cin>>n>>V;
    for(int i=1;i<=n;i++){
        int v,w,s;
        int cnt=0,k=1;
        scanf("%d%d%d",&v,&w,&s);
        while(k<=s){
            cnt++;
            W[cnt]=k*w;
            VO[cnt]=k*v;
            s-=k;
            k*=2;
        }
        if(s>0){
            W[++cnt]=s*w;
            VO[cnt]=s*v;
        }
        for(int k=1;k<=cnt;k++)
            for(int j=V;j>=VO[k];j--){
                f[j]=max(f[j],f[j-VO[k]]+W[k]);
            }
    }
    int res=0;
    for(int i=0;i<=V;i++) res=max(res,f[i]);
    cout<<res<<endl;
    return 0;
}
单调队列维护做法:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int N=20010;
int f[N],g[N],q[N],n,V;
int main(){
    cin>>n>>V;
    for(int i=1;i<=n;i++){
        int v,w,s;
        cin>>v>>w>>s;
        memcpy(g,f,sizeof(f));
        for(int j=0;j<v;j++){ //mod v 的余数  *
            int hh=0,tt=-1;
            for(int k=j;k<=V;k+=v){    //单调队列里存储的相当于是使g[体积]单调下降的体积,因为一般情况体积剩的越多,g[]越小
                if(hh<=tt && q[hh]<k-s*v) hh++;
                while(hh<=tt && g[q[tt]]-q[tt]/v*w<g[k]-k/v*w) tt--;
                q[++tt]=k;
                f[k]=g[q[hh]]+(k-q[hh])/v*w;  //*
            }
        }
    }
    cout<<f[V]<<endl;
    return 0;
}
2、最长公共子串,最长上升子列,最短编辑距离
a)最长上升子序列
朴素做法代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

const int N=1010;

int a[N],n;
int f[N];

int main(){
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i],f[i]=1;
    
    for(int i=1;i<=n;i++)   
        for(int j=1;j<i;j++)
            if(a[i]>a[j])
                f[i]=max(f[i],f[j]+1);
    int res=0;
    for(int i=1;i<=n;i++) res=max(res,f[i]);
    cout<<res<<endl;
    return 0;
}
nlogn做法代码:

优化原理:其实是贪心的思想,这里的 q[i] 储存的是长度为 i 的上升子列结尾最小的数字,根据性质,也可以知道 q 本身也是一个单调的数组,因此在更新 q 的时候可以二分查找更新,提高效率.

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

const int N=100010;
int n,a[N];

int main(){
    cin>>n;
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    int len=0;
    int q[N];
    q[0]=-2e9;
    for(int i=1;i<=n;i++){
        int l=0,r=len;
        while(l<r){
            int mid=(l+r+1)>>1;
            if(q[mid]<a[i]) l=mid;
            else r=mid-1;
        }
        len=max(len,r+1);
        q[r+1]=a[i];
    }
    cout<<len<<endl;
    return 0;
}
b) 最长公共子串
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

const int N=1010;
int f[N][N];
int n,m;
char a[N],b[N];

int main(){
    cin>>n>>m;
    cin>>a+1>>b+1;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            if(a[i]==b[j]) f[i][j]=f[i-1][j-1]+1;
            else f[i][j]=max(f[i-1][j],f[i][j-1]);
    cout<<f[n][m]<<endl;
    return 0;
}
c) 最短编辑距离

题目的一般描述:给定两个字符串A和B,现在要将A经过若干操作变为B,可进行的操作有:
1.删除–将字符串A中的某个字符删除.
2.插入–在字符串A的某个位置插入某个字符.
3.替换–将字符串A中的某个字符替换为另一个字符。
现在请你求出,将A变为B至少需要进行多少次操作.

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

const int N=1010;
int n,m;
char a[N],b[N];
int f[N][N];

int main(){
   scanf("%d%s",&n,a+1);
   scanf("%d%s",&m,b+1);
   for(int i=0;i<=n;i++) f[i][0]=i;
   for(int i=0;i<=m;i++) f[0][i]=i;
   for(int i=1;i<=n;i++)
       for(int j=1;j<=m;j++){
           f[i][j]=min(f[i-1][j]+1,f[i][j-1]+1);
           int t=!(a[i]==b[j]);
           f[i][j]=min(f[i-1][j-1]+t,f[i][j]);
       }
   cout<<f[n][m]<<endl;
   return 0;
}
二、背包问题的一些扩展

除了上述三种背包,背包问题的扩展应用大多也很经典,例如分组背包,有依赖的背包,背包方案数,背包具体方案,二维费用背包,还有各种背包的组合.头大

背包方案数:
#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N=1010,mod=1e9+7;
int g[N],f[N],n,V;

int main(){
   cin>>n>>V;
   //memset(f,-0x3f,sizeof(f));
   //f[0]=0;
   g[0]=1;
   for(int i=1;i<=n;i++){
       int v,w;
       cin>>v>>w;
       for(int j=V;j>=v;j--){
           int mx=max(f[j],f[j-v]+w);
           int cnt=0;
           if(f[j]==mx) cnt=g[j];
           if(f[j-v]+w==mx) cnt=(cnt+g[j-v])%mod;
           f[j]=mx;
           g[j]=cnt;
           }
   }
   int res=0;
   for(int i=1;i<=V;i++) res=max(f[i],res);
   int sum=0;
   for(int i=1;i<=V;i++)
       if(res==f[i])  sum=(sum+g[i])%mod;
       
   cout<<sum<<endl;
   return 0;
}
背包具体方案:
#include <iostream>
#include <algorithm>

using namespace std;

const int N=1010;
int f[N][N],v[N],w[N],n,V;

int main(){
    cin>>n>>V;
    for(int i=1;i<=n;i++){
        cin>>v[i]>>w[i];
    }
    for(int i=n;i>=1;i--)
        for(int j=0;j<=V;j++){
            f[i][j]=f[i+1][j];
            if(j>=v[i]) f[i][j]=max(f[i][j],f[i+1][j-v[i]]+w[i]);
        }
    
    for(int j=V,i=1;i<=n;i++){
        if(j>=v[i] && f[i][j]==f[i+1][j-v[i]]+w[i]){
        j-=v[i];
        cout<<i<<" ";
        }
    }
    return 0;
}
二位费用背包问题:
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N=110,M=1010;
int f[N][M],n,V,mm;
int main(){
    cin>>n>>V>>mm;
    for(int i=1;i<=n;i++){
        int v,m,w;
        cin>>v>>m>>w;
        for(int j=mm;j>=m;j--)
            for(int k=V;k>=v;k--)
                f[j][k]=max(f[j][k],f[j-m][k-v]+w);
    }
    int res=0;
    for(int j=0;j<=mm;j++)
        for(int k=0;k<=V;k++)
            res=max(res,f[j][k]);
    cout<<res<<endl;
    return 0;
}
分组背包问题:
#include <iostream>
#include <algorithm>
using namespace std;

const int N=110;
int f[N],c[N],n,V;
int w[N][N],v[N][N];

int main(){
    
    cin>>n>>V;
    for(int i=1;i<=n;i++){
        cin>>c[i];
        for(int j=1;j<=c[i];j++)
            cin>>v[i][j]>>w[i][j];
    }
    
    for(int i=1;i<=n;i++)
        for(int j=V;j>=0;j--)
            for(int k=1;k<=c[i];k++)
                if(v[i][k]<=j)
                    f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
    
    cout<<f[V]<<endl;
    return 0;
}

至于有依赖的背包问题在树形DP复习时会复习到,到时候再说.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值