背包问题其二(动态规划)

01背包

题目内容

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

第 i 件物品的体积是 v i v_i vi ,价值是 w i w_i wi

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

输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。

接下来有 N 行,每行两个整数 v i , w i v_i,w_i vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式
输出一个整数,表示最大价值。

数据范围
0 < N , V ≤ 1000 0<N,V≤1000 0<N,V1000
0 < v i , w i ≤ 1000 0<v_i,w_i≤1000 0<vi,wi1000

输入样例

4 5
1 2
2 4
3 4
4 5

输出样例:

8

题目分析

本题的含义比较明确,即在拿取的物品不超过背包的总体积的情况下,尽可能使得背包内的物品总价值最大。
首先我们想到的就是最朴素的想法——枚举每一种可能,假设一共有n件物品,那么我们有如下的几种情况:
C n 0 + C n 1 + C n 2 + ⋯ + C n n = 2 n C^0_n + C_n^1 + C_n^2 + \dots + C_n^n = 2^n Cn0+Cn1+Cn2++Cnn=2n
枚举出每一种情况以后,我们判断当前所拿去的物品是否超过了背包的容量限制,在所有不超过背包容量限制的情况下取最大的价值。显然这种方法的时间复杂度是很高的,即 O ( 2 n ) O(2^n) O(2n)

由于直接暴力枚举求解的方法复杂度极高,在面对很多物品的情况下速度比较慢,因此我们思考进一步的优化方法。我们考虑贪心的策略,即我们每一次都取价值最大的物品,直到背包装满。根据题目中的样例,按照贪心策略取出的物品应该是价值为5的商品,显然并不是最优的策略。因此贪心在保证了局部最优的情况下并不能保证全局最优,这种算法并不正确。

进一步分析可以发现,对于每一件物品,我们都有两种选择,即选这件物品,或者不选这件物品(这也是这道题目被称作01背包的原因),那么我们在这两种选择中挑选最优的一种即可,通过这种方法,我们可以保证在考虑到每一件物品的时候保证当前情况是全局最优的。

用一个二维数组f[i][j]表示考虑前i件物品,总体积不超过j的情况,f[i][j]中储存的事=是当前的最大值,根据上面的分析,对于每一件物品有两种选择,若不选择第i件物品,则有
f [ i ] [ j ] = f [ i − 1 ] [ j ] f[i][j] = f[i - 1][j] f[i][j]=f[i1][j]
若选择第i件物品,则有
f [ i ] [ j ] = m a x ( f [ i ] [ j ] , f [ i − 1 ] [ j − w [ i ] ] + v [ i ] ) f[i][j] = max(f[i][j], f[i -1 ][j - w[i]] + v[i]) f[i][j]=max(f[i][j],f[i1][jw[i]]+v[i])
画图分析即为
集合分析法

其中max表示在两个数字之间选择最大的一个,那么我们可以画出如下表格

01234
000000
1: v = 1, w = 202222
2: v = 2, w = 402466
3: v = 3, w = 402466
4: v = 4, w = 502466

显然所求的最大值即为表格右下角的值 f [ n ] [ m ] f[n][m] f[n][m]

完整代码

#include <bits/stdc++.h>

using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N][N];

int main()
{
    cin >> n >> m;
    for(int i = 1 ; i <= n; i ++) scanf("%d%d", &v[i], &w[i]);
    
    for(int i = 1 ; i <= n ; i ++ )
        for(int j = 0 ; j <= m ; 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]);
        }
        
    cout << f[n][m] << endl;
    
    return 0;
}

由于每次都只用到了 f [ i − 1 ] [ ] f[i-1][] f[i1][],因此还可以使用滚动数组优化空间复杂度,从 O ( n 2 ) O(n^2) O(n2)优化为 O ( n ) O(n) O(n)(其实一定程度上可以减少常数,也能起到优化时间复杂度的作用)

滚动数组优化
#include <bits/stdc++.h>

using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N];

int main()
{
    cin >> n >> m;
    for(int i = 1 ; i <= n; i ++) scanf("%d%d", &v[i], &w[i]);
    
    for(int i = 1 ; i <= n ; i ++ )
        for(int j = m ; j >= v[i] ; j --) //j从大到小循环
            if(j >= v[i]) f[j] = max(f[j] , f[j - v[i]] + w[i]);
        
    cout << f[m] << endl;
    
    return 0;
}

完全背包

题目内容

有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。

第 i 种物品的体积是 v i v_i vi ,价值是 w i w_i wi

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

输入格式
第一行两个整数,N,V ,用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行两个整数 v i , w i v_i,w_i vi,wi ,用空格隔开,分别表示第 i 种物品的体积和价值。

输出格式
输出一个整数,表示最大价值。

数据范围
0 < N , V ≤ 1000 0<N,V≤1000 0<N,V1000
0 < v i , w i ≤ 1000 0<v_i,w_i≤1000 0<vi,wi1000

输入样例

4 5
1 2
2 4
3 4
4 5

输出样例:

10

题目分析

这道题目和上面的01背包问题十分相似,不同的地方仅在于每一件物品的数量是没有上限的,我们可以拿去任意的数量。如此一来我们就直接跳过了暴力的思维,因为数量没有上限,所以无法暴力枚举。我们直接考虑贪心的策略,显然这种做法也是仅能保证局部最优而无法保证全局最优的,任意举例就能否定这种做法。

三重循环朴素做法

仿照01背包的做法,我们仍然用一个二维的数组 f [ i ] [ j ] f[i][j] f[i][j]表示考虑前i件物品,并且总体积不超过j, f [ i ] [ j ] f[i][j] f[i][j]储存数值是当前的物品价值之和的最大值。
仍然用画图的方法来分析 f [ i ] [ j ] f[i][j] f[i][j]的含义和计算方法
集合分析法

因此发现与01背包不同之处仅在于状态的划分,01背包对于每件物品只有选或者不选的情况,而完全背包则存在选择的数量从0到k个的情况(当选择k+1个当前的物品时体积超过限制),所以我们可以发散的思考这道题目是01234……背包问题(笑),我们需要做的就是找出从0到k种情况中价值之和最大的情况,即在01背包的代码上多加一层循环,枚举选取当前物品的数量,可见这种做法的算法复杂度为 O ( n 3 ) O(n^3) O(n3),代码如下

朴素代码

#include <bits/stdc++.h>

using namespace std;
const int N = 1010;

int n, m;
int f[N][N];
int v[N], w[N];

int main()
{
    scanf("%d%d", &n, &m);
    for(int i = 1 ; i <= n ; i ++) scanf("%d %d", &v[i], &w[i]);
    
    for(int i = 1 ; i <= n ; i ++)
        for(int j = 1 ;  j <= m ; j++)
            for(int k = 0 ; k * v[i] <= j ; k ++) //增加一层循环枚举当前物品的数量
                f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
    
    printf("%d", f[n][m]);
    
    return 0;
}

优化分析

设第i件物品的体积为v,价值为w,我们把 f [ i ] [ j ] f[i][j] f[i][j]的计算公式列出来
f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − v ] + w , f [ i − 1 ] [ j − 2 v ] + 2 w , … , + f [ i − 1 ] [ j − k v ] + k w ) f[i][j] = max(f[i - 1][j], f[i - 1][j - v] + w, f[i - 1][j - 2v] + 2w, \dots,\\ +f[i - 1][j - kv] + kw) f[i][j]=max(f[i1][j],f[i1][jv]+w,f[i1][j2v]+2w,,+f[i1][jkv]+kw)
观察可以发现,从 f [ i − 1 ] [ j − v ] f[i - 1][j - v] f[i1][jv]开始,后面的每一项都是有规律的,我们考虑 f [ i ] [ j − v ] f[i][j - v] f[i][jv]的计算
f [ i ] [ j − v ] = m a x ( f [ i − 1 ] [ j − v ] , f [ i − 1 ] [ j − 2 v ] + w , f [ i − 1 ] [ j − 3 v ] + 2 w , … , + f [ i − 1 ] [ j − k v ] + ( k − 1 ) w ) f[i][j - v] = max(f[i - 1][j - v] ,f[i - 1][j - 2v] + w, f[i - 1][j - 3v] + 2w,\dots, \\+ f[i - 1][ j - kv] + (k - 1)w) f[i][jv]=max(f[i1][jv],f[i1][j2v]+w,f[i1][j3v]+2w,,+f[i1][jkv]+(k1)w)
因此从 f [ i − 1 ] [ j − v ] f[i - 1][j - v] f[i1][jv]开始,可以表示为
m a x ( f [ i − 1 ] [ j − v ] + w , f [ i − 1 ] [ j − 2 v ] + 2 w , … , + f [ i − 1 ] [ j − k v ] + k w ) = f [ i ] [ j − v ] + w max(f[i - 1][j - v] + w, f[i - 1][j - 2v] + 2w, \dots,\\ +f[i - 1][j - kv] + kw) = f[i][j - v] + w max(f[i1][jv]+w,f[i1][j2v]+2w,,+f[i1][jkv]+kw)=f[i][jv]+w
则原本的 f [ i ] [ j ] f[i][j] f[i][j]就可以优化成
f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i ] [ j − v ] + w ) f[i][j] = max(f[i - 1][j],f[i][j - v] + w) f[i][j]=max(f[i1][j],f[i][jv]+w)
如此一来,直接优化掉了第三重循环,把问题转化为了类似01背包的做法,时间复杂度也从 O ( n 3 ) O(n^3) O(n3)优化到了 O ( n 2 ) O(n^2) O(n2)

优化代码
#include <bits/stdc++.h>

using namespace std;
const int N = 1010;

int n, m;
int f[N][N];
int v[N], w[N];

int main()
{
    scanf("%d%d", &n, &m);
    for(int i = 1 ; i <= n ; i ++) scanf("%d %d", &v[i], &w[i]);
    
    for(int i = 1 ; i <= n ; i ++)
        for(int j = 1 ;  j <= m ; j++)
        {
            //选0个
            f[i][j] = f[i - 1][j]; 
            //选1个及以上
            if(j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
        }
            
    
    printf("%d", f[n][m]);
    
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值