AcWing笔记——背包问题

背包问题

01背包

原题链接🔗

问题描述

有N件物品和一个容量为V的背包, 每件物品只能用一次 \textcolor{red}{每件物品只能用一次} 每件物品只能用一次

第i件物品的体积为vi,价值为wi

求解将哪些物品装入背包,可以使这些物品的总体积不超过背包容量,且总价值最大。

输出该最大的总价值

问题分析

一般来说动态规划中规划的是可变的量,对于这个问题来讲

N和V是可变的,也就是说,我们是也可以求数量为N-1,体积为V-1的情况

所以我们可以建立一个二维动态数组dp[n][v],用来表示在数量为n,最大体积为v的情况下,最大的总价值

那么如何建立dp[n1][v1]到dp[n2][v2]的状态转移方程呢

我们对n1和v1逐渐递增

令n2=n1+1

设标号为n2的物品的体积为v,在v2>v的情况下

我们会有两种方案:选择第n2件物品和不选择第n2件物品

选择第n2件物品:dp[n2][v2]=dp[n1][v2-v]+w

不选择第n2件物品:dp[n2][v2]=dp[n1][v2]

因为我们要选择最大值,所以最终状态转移方程为:
d p [ n 2 ] [ v 2 ] = m a x ( d p [ n 1 ] [ v 2 − v ] + w , d p [ n 1 ] [ v 2 ] ) dp[n2][v2]=max(dp[n1][v2-v]+w,dp[n1][v2]) dp[n2][v2]=max(dp[n1][v2v]+w,dp[n1][v2])
所以我们的最大值就为dp[n][m]

原始代码

#include<bits/stdc++.h>
using namespace std;
int main(){
    int N,V;
    cin>>N;
    cin>>V;
    int dp[N+1][V+1];
    int v[N+1];
    int w[N+1];
    for(int i=0;i<=N;i++){
        for(int j=0;j<=V;j++){
            dp[i][j]=0;
        }
    }
    for(int i=1;i<=N;i++){
        cin>>v[i];
        cin>>w[i];
    }
    for(int i=1;i<=N;i++){
        for(int j=1;j<=V;j++){
            dp[i][j]=dp[i-1][j];
            if(j>=v[i]){
                dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
            }
		}
    }
    cout<<dp[N][V];
    return 0;
}

时间复杂度:O(N*V)

空间复杂度:O(N*V)

空间优化过程

由状态转移方程
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − v [ i ] ] + w [ i ] ) dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]) dp[i][j]=max(dp[i1][j],dp[i1][jv[i]]+w[i])
我们可以看出dp[i]的状态只和dp[i-1]的状态有关

所以我们没有必要保存之前的状态

二维动态规划过程:

在这里插入图片描述

一维动态规划过程:

这里面有一个很重要的点,需要注意到在二维规划过程中 在更新 d p [ i ] [ j ] 所用到的 d p [ i − 1 ] [ j − v ] 和 d p [ i − 1 ] [ j ] 都是在 i − 1 中的 \textcolor{red}{在更新dp[i][j]所用到的dp[i-1][j-v]和dp[i-1][j]都是在i-1中的} 在更新dp[i][j]所用到的dp[i1][jv]dp[i1][j]都是在i1中的所以在一维动态

规划中更新dp[j]时不能用已经更新过的dp[j-v],而要用未被更新过的dp[j-v],所以我们不能顺序更新j,因为在顺序执行下,在更新dp[j]时,dp[j-v]已经被更新过了,所以我们需要逆序更新j

在这里插入图片描述

空间优化代码

#include<bits/stdc++.h>
using namespace std;
int main(){
    int N,V;
    cin>>N;
    cin>>V;
    int dp[V+1];
    int v[N+1];
    int w[N+1];
    for(int i=0;i<=V;i++){
        dp[i]=0;
    }
    for(int i=1;i<=N;i++){
        cin>>v[i];
        cin>>w[i];
    }
    for(int i=1;i<=N;i++){
        for(int j=V;j>=1;j--){
            if(j>=v[i]){
                dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
            }
		}
    }
    cout<<dp[V];
    return 0;
}

时间复杂度:O(N*V)

空间复杂度:O(V)

完全背包

原题链接🔗

题目描述

有N种物品和一个容量为V的背包, 每种物品都可以无限制的使用 \textcolor{red}{每种物品都可以无限制的使用} 每种物品都可以无限制的使用

第i种物品的体积是vi,价值是wi

求解将哪些物品装入背包,可使这些物品总体积不超过背包容量,且总价值最大

输出该最大价值

问题分析

完全背包问题和01背包问题的区别在于完全背包可以将物品使用无限次

所以我们的状态转移方程是
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − v ] + w , d p [ i − 1 ] [ j − 2 ∗ v ] + 2 ∗ w . . . . . ) ( 1 ) dp[i][j]=max(dp[i-1][j],dp[i-1][j-v]+w,dp[i-1][j-2*v]+2*w.....) (1) dp[i][j]=max(dp[i1][j],dp[i1][jv]+w,dp[i1][j2v]+2w.....)(1)
又因为
d p [ i ] [ j − v ] = m a x ( d p [ i − 1 ] [ j − v ] , d p [ i − 1 ] [ j − 2 ∗ v ] + w , d p [ i − 1 ] [ j − 3 ∗ v ] + 2 ∗ w . . . . . . ) ( 2 ) dp[i][j-v]=max(dp[i-1][j-v],dp[i-1][j-2*v]+w,dp[i-1][j-3*v]+2*w......)(2) dp[i][jv]=max(dp[i1][jv],dp[i1][j2v]+w,dp[i1][j3v]+2w......)(2)

所以
d p [ i ] [ j − v ] + w = m a x ( d p [ i − 1 ] [ j − v ] + w , d p [ i − 1 ] [ j − 2 ∗ v ] + 2 ∗ w , d p [ i − 1 ] [ j − 3 ∗ v ] + 3 ∗ w . . . . . . ) ( 3 ) dp[i][j-v]+w=max(dp[i-1][j-v]+w,dp[i-1][j-2*v]+2*w,dp[i-1][j-3*v]+3*w......)(3) dp[i][jv]+w=max(dp[i1][jv]+w,dp[i1][j2v]+2w,dp[i1][j3v]+3w......)(3)
将公式1和3连接起来得到
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − v ] + w ) dp[i][j]=max(dp[i-1][j],dp[i][j-v]+w) dp[i][j]=max(dp[i1][j],dp[i][jv]+w)

原始代码

#include<bits/stdc++.h>
using namespace std;
int main(){
    int N,V;
    cin>>N;
    cin>>V;
    int dp[N+1][V+1];
    int v[N+1];
    int w[N+1];
    for(int i=0;i<=N;i++){
        for(int j=0;j<=V;j++){
			dp[i][j]=0;
        }
    }
    for(int i=1;i<=N;i++){
        cin>>v[i];
        cin>>w[i];
	}
    for(int i=1;i<=N;i++){
        for(int j=1;j<=V;j++){
            dp[i][j]=dp[i-1][j];
            if(j>=v[i]){
                dp[i][j]=max(dp[i-1][j],dp[i][j-v[i]]+w[i]);   
            }
        }
    }
    cout<<dp[N][V];
    return 0;
}

时间复杂度:O(N*V)

空间复杂度:O(N*V)

空间优化过程

看一下二维时是如何进行动态规划的

在这里插入图片描述

可以看出,与01背包最本质的区别在于用于更新dp[i][j]所用到的dp[i-1][j]和dp[i][j-v]是已经被更新过的了,所以按照和分析01背包的思路可以得出,应该对j顺序递增

空间优化代码

#include<bits/stdc++.h>
using namespace std;
int main(){
    int N,V;
    cin>>N;
    cin>>V;
    int dp[V+1];
    int v[N+1];
    int w[N+1];
    for(int i=0;i<=V;i++){
        dp[i]=0;
    }
    for(int i=1;i<=N;i++){
        cin>>v[i];
        cin>>w[i];
	}
    for(int i=1;i<=N;i++){
        for(int j=1;j<=V;j++){
            if(j>=v[i]){
                dp[j]=max(dp[j],dp[j-v[i]]+w[i]);   
            }
        }
    }
    cout<<dp[V];
    return 0;
}

多重背包

原题链接🔗

问题描述

有N种物品和一个容量为V的背包

第i件物品最多有si件,每件的体积是vi,价值是wi

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大

输出该最大总价值

问题分析

多重背包和01背包的区别在于多重背包可以将同一个物品取s次

根据01背包的思路,可以得到状态转移方程如下
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − v ] + w , d p [ i − 1 ] [ j − 2 ∗ v ] + 2 ∗ w + . . . . . + d p [ i − 1 ] [ j − s ∗ v ] + s ∗ w ) dp[i][j]=max(dp[i-1][j],dp[i-1][j-v]+w,dp[i-1][j-2*v]+2*w+.....+dp[i-1][j-s*v]+s*w) dp[i][j]=max(dp[i1][j],dp[i1][jv]+w,dp[i1][j2v]+2w+.....+dp[i1][jsv]+sw)

原始代码

#include<bits/stdc++.h>
using namespace std;
int main(){
    int N,V;
    cin>>N;
    cin>>V;
    int dp[N+1][V+1];
    int v[N+1];
    int w[N+1];
    int s[N+1];
    for(int i=0;i<=N;i++){
        for(int j=0;j<=V;j++){
            dp[i][j]=0;
        }
    }
    for(int i=1;i<=N;i++){
        cin>>v[i];
        cin>>w[i];
        cin>>s[i];
    }
    for(int i=1;i<=N;i++){
        for(int j=1;j<=V;j++){
            dp[i][j]=dp[i-1][j];
            for(int k=0;k<=s[i];k++){
                if(j>=k*v[i]){
                    dp[i][j]=max(dp[i][j],dp[i-1][j-k*v[i]]+k*w[i]);
                }
            }
        }
    }
    cout<<dp[N][V];
    return 0;
}

时间复杂度:O(N*V*K)

空间复杂度:O(N*V)

然后根据01背包的空间优化可以写出

空间优化代码

#include<bits/stdc++.h>
using namespace std;
int main(){
    int N,V;
    cin>>N;
    cin>>V;
    int dp[V+1];
    int v[N+1];
    int w[N+1];
    int s[N+1];
    for(int i=0;i<=V;i++){
        dp[i]=0;
    }
    for(int i=1;i<=N;i++){
        cin>>v[i];
        cin>>w[i];
        cin>>s[i];
    }
    for(int i=1;i<=N;i++){
        for(int j=V;j>=1;j--){
            
            for(int k=0;k<=s[i];k++){
                if(j>=k*v[i]){
                    dp[j]=max(dp[j],dp[j-k*v[i]]+k*w[i]);
                }
            }
        }
    }
    cout<<dp[V];
    return 0;
}

时间复杂度:O(N*V*K)

空间复杂度:O(V)

二进制优化过程

首先我们考虑一个问题:

在选择k件物品的时候,除了顺序遍历[0,k]外,还有没有其他的方法

答案是二进制法

举个例子

假设我们要取1000件物品,我们可以将1000件物品提前分成1,2,4,8,16,32,64,128,256,512这10堆,然后我们只需要遍历这10堆,然后选与不选任意选择,就也可以达到顺序遍历[0,k]的目的(注意不要让最后的值大于1000)

二进制优化代码

#include<bits/stdc++.h>
using namespace std;
int main(){
    int N,V;
    cin>>N;
    cin>>V;
    int dp[V+1];
    vector<int> v;
    vector<int> w;
    vector<int> s;
    v.push_back(0);
    w.push_back(0);
    s.push_back(0);
    for(int i=0;i<=V;i++){
        dp[i]=0;
    }
    for(int i=1;i<=N;i++){
        int a,b,c;
        int f=1;
        int sum=1;
        int pre=0;
        cin>>a;
        cin>>b;
        cin>>c;
        while(sum<=c){
            pre+=f;
            v.push_back(a);
            w.push_back(b);
            s.push_back(f);
            f=2*f;
            sum+=f;
        }
        sum=sum-f;
        int cnt=c-sum;
        if(cnt>0){
            v.push_back(a);
            w.push_back(b);
            s.push_back(cnt);
        }
    }
    N=v.size();
    for(int i=1;i<=N;i++){
        for(int j=V;j>=1;j--){
            if(j>=v[i]*s[i]){
             	dp[j]=max(dp[j],dp[j-s[i]*v[i]]+s[i]*w[i]);   
            }
        }
    }
    cout<<dp[V];
    return 0;
}

时间复杂度O(N*V*log(k))

空间度杂度O(V)

单调队列优化过程

根据原始的一维状态转移方程可以求得
d p [ j ] = d p [ j ] dp[j]=dp[j] dp[j]=dp[j]

d p [ j + v ] = m a x ( d p [ j + v ] , d p [ j ] ) dp[j+v]=max(dp[j+v],dp[j]) dp[j+v]=max(dp[j+v],dp[j])

d p [ j + 2 ∗ v ] = m a x ( d p [ j + 2 ∗ v ] , d p [ j + v ] + w , d p [ j ] + 2 ∗ w ) dp[j+2*v]=max(dp[j+2*v],dp[j+v]+w,dp[j]+2*w) dp[j+2v]=max(dp[j+2v],dp[j+v]+w,dp[j]+2w)

d p [ j + 3 ∗ v ] = m a x ( d p [ j + 3 ∗ v ] , d p [ j + 2 ∗ v ] + w , d p [ j + v ] + 2 ∗ w , d p [ j ] + 3 ∗ w ) dp[j+3*v]=max(dp[j+3*v],dp[j+2*v]+w,dp[j+v]+2*w,dp[j]+3*w) dp[j+3v]=max(dp[j+3v],dp[j+2v]+w,dp[j+v]+2w,dp[j]+3w)

. . .

. . .

. . .

d p [ j + s ∗ v ] = m a x ( d p [ j + s ∗ v ] + . . . . + d p [ j ] + s ∗ w ) dp[j+s*v]=max(dp[j+s*v]+....+dp[j]+s*w) dp[j+sv]=max(dp[j+sv]+....+dp[j]+sw)

使变量统一,给每一个方程左右两边减去i*w得到
d p [ j ] = d p [ j ] dp[j]=dp[j] dp[j]=dp[j]

d p [ j + v ] − w = m a x ( d p [ j ] , d p [ j + v ] − w ) dp[j+v]-w=max(dp[j],dp[j+v]-w) dp[j+v]w=max(dp[j],dp[j+v]w)

d p [ j + 2 ∗ v ] − 2 ∗ w = m a x ( d p [ j ] , d p [ j + v ] − w , d p [ j + 2 ∗ v ] − 2 ∗ w ) dp[j+2*v]-2*w=max(dp[j],dp[j+v]-w,dp[j+2*v]-2*w) dp[j+2v]2w=max(dp[j],dp[j+v]w,dp[j+2v]2w)

d p [ j + 3 ∗ v ] − 3 ∗ w = m a x ( d p [ j ] , d p [ j + v ] − v , d p [ j + 2 ∗ v ] − 2 ∗ w , d p [ j + 3 ∗ v ] − 3 ∗ w ) dp[j+3*v]-3*w=max(dp[j],dp[j+v]-v,dp[j+2*v]-2*w,dp[j+3*v]-3*w) dp[j+3v]3w=max(dp[j],dp[j+v]v,dp[j+2v]2w,dp[j+3v]3w)

. . .

. . .

. . .

d p [ j + s ∗ v ] − s ∗ w = m a x ( d p [ j ] , . . . . . . d p [ j + s ∗ v ] − s ∗ w ) dp[j+s*v]-s*w=max(dp[j],......dp[j+s*v]-s*w) dp[j+sv]sw=max(dp[j],......dp[j+sv]sw)

所以可以根据余数j将[0,k]划分为x个单调队列,然后求出每个队列的最大值即可

注意每次入队的值为 j + k ∗ v j+k*v j+kv,从队尾弹出元素的判断条件是 p r e [ k ] − ( k − j ) / v ∗ w > p r e [ q [ t ] ] − ( q [ t ] − j ) / v ∗ w pre[k]-(k-j)/v*w>pre[q[t]]-(q[t]-j)/v*w pre[k](kj)/vw>pre[q[t]](q[t]j)/vw

p r e pre pre数组是当前步骤还未被更新的数组

单调队列优化代码

#include<bits/stdc++.h>
using namespace std;
int main(){
    int N,V;
    scanf("%d %d",&N,&V);
    vector<int> dp(V+1);
    vector<int> q(V+1);
    for(int i=1;i<=N;i++){
        int v,w,s;
        scanf("%d %d %d",&v,&w,&s);
        vector<int> pre=dp;
        for(int j=0;j<v;j++){
            int h=0,t=-1;
            for(int k=j;k<=V;k+=v){
                if(t>=h&&k-q[h]>s*v){
                    h++;
                }
                if(t>=h){
                    dp[k]=max(dp[k],pre[q[h]]+(k-q[h])/v*w);
                }
                while(t>=h&&pre[q[t]]-(q[t]-j)/v*w < pre[k]-(k-j)/v*w){
                    t--;
                }
                q[++t]=k;
            }
        }
    }
    cout<<dp[V];
    return 0;
}
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值