2021-07-02 暑训开端——重见dp

一、背包问题全集

1、01背包
每个物品只有一个
代码有注释行,放上普通版与滚动数组优化版
普通版:
01背包问题

#define ll long long

using namespace std;
const int maxn=1e3+7;

int v[maxn];
int w[maxn];
int dp[maxn][maxn];//选到第几件物品,当前背包内装的物品体积总和,当前价值为
int main(){
    int N,V;
    read(N);//快读,代码省略了
    read(V);
    for(int i=1;i<=N;i++){
        read(v[i]);
        read(w[i]);
    }
    dp[0][0]=0;
    for(int i=1;i<=N;i++){
        for(int j=0;j<=V;j++){
            if(j<v[i]){
                dp[i][j]=dp[i-1][j];
            }
            else{
                dp[i][j]=max(dp[i-1][j-v[i]]+w[i],dp[i-1][j]);
            }
            //cout<<dp[i][j]<<' ';
        }
        //cout<<endl;
    }
    int ans=0;
    for(int i=0;i<=V;i++){
        ans=max(ans,dp[N][i]);
    }
    cout<<ans<<endl;
    
}

滚动数组版:


#define ll long long

using namespace std;

const int maxn=1e3+7;

int v[maxn];
int w[maxn];
int last[maxn];//当前背包内装的物品总体积为,当前价值为
int now[maxn];
int main(){
    int N,V;
    read(N);
    read(V);
    for(int i=1;i<=N;i++){
        read(v[i]);
        read(w[i]);
    }
    last[0]=0;
    for(int i=1;i<=N;i++){
        for(int j=0;j<=V;j++){
            if(j+v[i]>V){
                now[j]=last[j];
            }
            else{
                now[j]=max(last[j+v[i]]+w[i],last[j]);
            }
            last[j]=now[j];
            //cout<<now[j]<<' ';
        }
        //cout<<endl;
        
    }
    int ans=0;
    for(int i=0;i<=V;i++){
        ans=max(now[i],ans);
    }
    cout<<ans<<endl;
    
}

2、完全背包
每个物品有无限个
一般思路:(在我没学之前想到的,n^3写法)
因为每一种物品都有无限个,那么选到每种物品,可以有多种状态转移的办法。
可以考虑这样的状态转移:

 for(int i=1;i<=N;i++){
        for(int j=0;j<=V;j++){
            //枚举第i个物品选几个
            for(int k=0;k*v[i]<=j;k++){
                dp[i][j]=max(dp[i-1][j-k*v[i]]+k*w[i],dp[i][j]);
            }
        }
    }

但是由于复杂度过大,会TLE。

于是分析状态转移过程,上段代码的

dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i],dp[i-1][j-2*v[i]+2*v[i] ... );
dp[i][j-v]=max(.           dp[i-1][j-v[i],dp[i-1][j-2*v[i]]+w[i] ... );

可知,dp[i][j]=max(dp[i-1][j],dp[i-1][j-v]+w[i])

这样以来,复杂度被压缩到n^2

using namespace std;

const int maxn=1e3+7;
int v[maxn];
int w[maxn];
int dp[maxn][maxn];//选到第几类物品,当前背包内已有的物品总体积,当前价值为
int main(){
    int N,V;
    read(N);
    read(V);
    for(int i=1;i<=N;i++){
        read(v[i]);
        read(w[i]);
    }
    for(int i=1;i<=N;i++){
        //枚举第i个物品选几个
        for(int j=0;j<=V;j++){
            dp[i][j]=dp[i-1][j];
            if(j>=v[i]){
                dp[i][j]=max(dp[i][j],dp[i][j-v[i]]+w[i]);
            }
        }
    }
    int ans=0;
    for(int i=0;i<=V;i++){
        ans=max(dp[N][i],ans);
    }
    cout<<ans<<endl;
}

这个也可以优化成滚动数组版。

3、多重背包
每个物品有有给定数目个
这类问题,一般思路可以想出一个n^3的写法,但是这种显然会在数据量稍稍上去一点就TLE。

这个时候可以用到一种特殊的优化方式——二进制优化。
设每种物品有S个。
假如S=100个,考虑将这个物品打包成1,2,4,…,32以及(100-63)个一组,每个包裹最多被选择一次。(2n+1-1 <= S)
运用数学归纳法可以证明存在一种选法,能凑出0~100中任意一个数。

所以,可以考虑将每一种物品打包成1,…,
2n ,
s-((2n+1)-1),转化为01背包问题。
多重背包问题 II
代码实现:


using namespace std;

const int maxn=2e4+5000;

int dp[maxn];

int v[maxn];
int w[maxn];
int s[maxn];

int nv[maxn];
int nw[maxn];

int main(){
    int N,V;
    cin>>N>>V;
    int cnt=0;
    for(int i=1;i<=N;i++){
        cin>>v[i]>>w[i]>>s[i];
        int k=1;
        while(k<=s[i]){
            cnt++;
            nv[cnt]=v[i]*k;
            nw[cnt]=w[i]*k;
            s[i]-=k;
            cout<<k<<endl;
            k*=2;
        }
        if(s[i]>0){
            cout<<s[i]<<endl;
            cnt++;
            nv[cnt]=v[i]*s[i];
            nw[cnt]=w[i]*s[i];
        }
    }
    //01背包
    for(int i=1;i<=cnt;i++){
        for(int j=V;j>=nv[i];j--){
            dp[j]=max(dp[j],dp[j-nv[i]]+nw[i]);
        }
    }
    cout<<dp[V]<<endl;
}

其实这个还可以继续优化,下面引入单调队列优化:
最终复杂度降低至O(NV)!
详细见我的另一篇博客~
单调队列及其应用

4、分组背包
每个小组内物品相互冲突,最多只能选1个(基础版),求在物品体积不超过背包容量能获得的最大价值。
分组背包问题

const int maxn=110;
int dp[maxn][maxn];

int main(){
    int N,V;
    cin>>N>>V;
    for(int i=1;i<=N;i++){
        int s;
        cin>>s;
        for(int j=1;j<=s;j++){
            int v,w;
            cin>>v>>w;
            //每组选一个,看状态转移
            for(int k=0;k<=V;k++){
                dp[i][k]=max(dp[i-1][k],dp[i][k]);
                if(k>=v)
                    dp[i][k]=max(dp[i][k],dp[i-1][k-v]+w);
            }
        }
    }
    cout<<dp[N][V]<<endl;
}

5、背包问题方案相关
求方案数:
背包问题求方案数

//背包问题求方案数
const int maxn=1e3+7;
const int mod=1e9+7;
int dp[maxn][maxn];
ll cnt[maxn][maxn];
int v[maxn],w[maxn];
int main(){
    int N,V;
    cin>>N>>V;
    for(int i=1;i<=N;i++){
        cin>>v[i]>>w[i];
    }
    for(int i=0;i<=V;i++){
        cnt[0][i]=1;
    }
    for(int i=1;i<=N;i++){
        for(int j=0;j<=V;j++){
            dp[i][j]=dp[i-1][j];
            cnt[i][j]=cnt[i-1][j];
            if(j>=v[i]&&dp[i][j]<dp[i-1][j-v[i]]+w[i]){
                //需要更新
                dp[i][j]=dp[i-1][j-v[i]]+w[i];
                cnt[i][j]=cnt[i-1][j-v[i]];
            }
            else if(j>=v[i]&&dp[i][j]==dp[i-1][j-v[i]]+w[i]){
                cnt[i][j]=(cnt[i][j]+cnt[i-1][j-v[i]])%mod;
            }
        }
    }
    cout<<cnt[N][V]<<endl;
}

求具体方案:
背包问题求具体方案

const int maxn=1e3+7;
int dp[maxn][maxn];//必须记录所有物品的状态,选择方案时才可以判断这个物品是不是方案中的一种
int v[maxn],w[maxn];
int last[maxn];//记录达到该方案的
int main(){
    int N,V;
    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++){
            dp[i][j]=dp[i+1][j];
            if(j>=v[i]){
                dp[i][j]=max(dp[i+1][j-v[i]]+w[i],dp[i][j]);
            }
        }
    }
    int vol=V;
    for(int i=1;i<=N;i++){
        //从小到大枚举,保证字典序最小
        //cout<<"Vol="<<vol<<endl;
        if(vol-v[i]>=0&&dp[i][vol]==dp[i+1][vol-v[i]]+w[i]){
            cout<<i<<' ';
            vol-=v[i];
        }
    }
    cout<<endl;
}

二、线性dp的一些优化
1、最长上升子序列(nlgn)优化
考虑另外一种描述状态的方法,dp数组具有单调性,就可以使用二分来优化了。
详见代码:

const int maxn=1e5+7;
int a[maxn];
int dp[maxn];//dp[i]=minn,长度为i的上升子序列的最小结尾
int main(){
    //nlogn的最长上升子序列
    memset(dp,INF,sizeof dp);
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
    int ans=0;
    dp[0]=-INF;
    for(int i=1;i<=n;i++){
        //二分
        int l=0,r=i;
        int maxx=0;
        while(l<=r){
            int mid=(l+r)>>1;
            if(dp[mid]<a[i]){
                maxx=mid;
                l=mid+1;
            }
            else{
                r=mid-1;
            }
        }
        if(dp[maxx+1]>a[i]){
            dp[maxx+1]=a[i];
        }
        ans=max(maxx+1,ans);
    }
    cout<<ans<<endl;
}

三、树形dp
没有上司的舞会
详解在代码中,可以作为树形dp参考模版

#define pb push_back

const int maxn=6e3+7;

vector<int> mp[maxn];

int con[maxn];//欢乐程度

int dp[maxn][2];//记录每个节点在选或者不选的情况下的最大权值(不包含当前节点权值)

int vvis[maxn];//防止多次递归导致tle
void solve(int v){
    //v为当前节点,从树根开始
    int ans0=0;
    for(int i=0;i<mp[v].size();i++){
        if(!vvis[mp[v][i]])
            solve(mp[v][i]);
        vvis[mp[v][i]]=1;
        ans0+=max(dp[mp[v][i]][1]+con[mp[v][i]],dp[mp[v][i]][0]);
    }
    dp[v][0]=ans0;
    int ans=0;
    for(int i=0;i<mp[v].size();i++){
        if(!vvis[mp[v][i]])
            solve(mp[v][i]);
        vvis[mp[v][i]]=1;
        ans+=dp[mp[v][i]][0];
    }
    dp[v][1]=ans;
}
int vis[maxn];
int main(){
    //建图
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>con[i];
    }
    int a,b;
    while(~scanf("%d%d",&a,&b)){
        if(a==0&&b==0){
            break;
        }
        vis[a]=1;
        mp[b].pb(a);//记录相关节点(从上往下)
    }
    //寻找根节点。
    int root=0;
    for(int i=1;i<=n;i++){
        if(!vis[i]){
            root=i;
            break;
        }
    }
    //cout<<"root="<<root<<endl;
    solve(root);
    cout<<max(dp[root][1]+con[root],dp[root][0])<<endl;
}

四、状压dp
蒙德里安的梦想

//状态压缩dp
const int maxn=1e4+7;
ll c[20][maxn];//记录每列的某种情况可行的次数,0为不放,1为放
//基本思路:枚举横着放的小方块的个数
//dp数组表示该列xx行放了一个横着的小方块
bool st[maxn];
int main(){
    int n,m;
    while(cin>>n>>m){
        if(n==0&&m==0)
            break;
        memset(c,0,sizeof c);
        int up=1<<n;
        //st数组必须预处理,否则会t
        for(int i=0;i<=up-1;i++){
            int t=i;
            int cnt=0;
            st[i]=1;
            for(int j=1;j<=n;j++){
                if((t&1)==0){
                    cnt++;
                }
                else{
                    if(cnt%2!=0){
                        st[i]=0;
                    }
                    cnt=0;
                }
                t>>=1;
            }
            if(cnt%2!=0){
                st[i]=0;
            }
        }
        c[0][0]=1;
        for(int i=1;i<=m;i++){
            for(int j=0;j<=up-1;j++){
                for(int k=0;k<=up-1;k++){
                    //1不能左右相邻
                    //同一列连续的0都为偶数
                   
                    if((j&k)==0&&st[j|k]==1){
                        c[i][j]+=c[i-1][k];
                    }
                }
            }
        }
        cout<<c[m][0]<<endl;//答案就是最后一列什么都不放的方案数
    }
}

还有另外一种状压dp,往往应用于一张图上。
最短Hamilton路径

//求最小Hamilton路径
const int maxn=(1<<20)+7;

int mp[25][25];

int dp[maxn][25];

int main(){
    int n;
    cin>>n;
    for(int i=0;i<=n-1;i++){
        for(int j=0;j<=n-1;j++){
            cin>>mp[i][j];
        }
    }
    memset(dp,INF,sizeof dp);
    dp[0][0]=0;
    for(int i=0;i<=((1<<n)-1);i++){
        //枚举状态
        //枚举这个状态可以由哪些状态转移过来
        for(int j=0;j<=n-1;j++){
            //到达了哪些点
            if((i>>j)&1){
                //这个点到达了
                int k=i-(1<<j);
                for(int x=0;x<=n-1;x++){
                    if(dp[k][x]!=INF)
                        dp[i][j]=min(dp[i][j],dp[k][x]+mp[x][j]);
                }
            }
        }
    }
    int fin=0;
    for(int i=0;i<=n-1;i++){
        fin+=(1<<i);
    }
    cout<<dp[fin][n-1]<<endl;
}

五、区间dp
codefoeces D. Flood Fill

const int maxn=5e3+7;
int dp[maxn][maxn][2];//dp[l][r][0/1]=达到该状态的最小操作数,l,r分别为左右区间,0/1表示当前区间颜色与左边界/右边界相同
int a[maxn];
int main(){
    int n;
    cin>>n;
    int x;
    int len=0;
    for(int i=1;i<=n;i++){
        cin>>x;
        if(x!=a[len]&&len>0){
            a[++len]=x;
        }
        if(len==0){
            a[++len]=x;//把同色的区间直接合并
        }
    }
    //跑一遍区间dp
    //初始化
    memset(dp,INF,sizeof dp);
    for(int i=1;i<=len;i++){
        dp[i][i][1]=0;
        dp[i][i][0]=0;
    }
    //cout<<"len="<<len<<endl;
    for(int i=2;i<=len;i++){
        //枚举区间长度
        //cout<<"i="<<i<<endl;
        for(int j=1;j<=len-i+1;j++){
            int l=j,r=j+i-1;
            //枚举区间起点
            //那么区间终点已知
            //往左边转移得到当前区间
            //只可能从dp[l+1][r][0/1]得到
            //cout<<"j="<<j<<endl;
            if(a[l]==a[r]){
                dp[l][r][0]=min(dp[l][r][0],dp[l+1][r][1]);
            }
            else{
                dp[l][r][0]=min(dp[l][r][0],dp[l+1][r][1]+1);
            }
            dp[l][r][0]=min(dp[l][r][0],dp[l+1][r][0]+1);
            //cout<<"dp="<<dp[j][j+i-1][0]<<endl;
            //往右边转移得到当前区间
            //只可能从dp[l][r-1][0/1]的搭配
            if(a[l]==a[r]){
                dp[l][r][1]=min(dp[l][r][1],dp[l][r-1][0]);
            }
            else{
                dp[l][r][1]=min(dp[l][r][1],dp[l][r-1][0]+1);
            }
            dp[l][r][1]=min(dp[l][r][1],dp[l][r-1][1]+1);
            
        }
        //cout<<endl;
    }
    cout<<min(dp[1][len][1],dp[1][len][0])<<endl;
}

六、数位dp
P2657 [SCOI2009] windy 数

//可以通过预处理dp数组,然后再运用数位dp的思想去做

const int maxn=12;

int dp[maxn][maxn];

void init(){
    for(int i=0;i<=9;i++) dp[1][i]=1;
    
    for(int i=2;i<maxn;i++){
        for(int j=0;j<=9;j++){
            for(int k=0;k<=9;k++){
                if(abs(k-j)>=2)
                    dp[i][j]+=dp[i-1][k];
            }
        }
    }
}

int DP(int n){
    if(!n) return 0;
    
    vector<int>dig;
    while(n){
        dig.pb(n%10);
        n/=10;
    }
    
    int res=0;
    int last=-2;
    
    for(int i=(int)dig.size()-1;i>=0;i--){
        int x=dig[i];
        
        for(int j=(i==(int)dig.size()-1);j<x;j++){
            if(abs(j-last)>=2){
                res+=dp[i+1][j];
            }
        }
        
        if(abs(last-x)>=2) last=x;
        else break;
        
        if(!i) res++;
    }
    //单独处理前导0的情况
    for(int i=1;i<dig.size();i++){
        for(int k=1;k<=9;k++){
            res+=dp[i][k];
        }
    }
    return res;
}


int main(){
    
    init();
    
    int a,b;
    cin>>a>>b;
    
    cout<<DP(b)-DP(a-1)<<endl;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

KaaaterinaX

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值