写在前面
为提高我写博客的效率,前面曾提过的基础概念在此处将不再赘述,有需要的同学可以前往我的上一篇博客:背包问题入门 #1 | 01背包问题的求解与优化 查看,理解万岁!
在了解了01背包问题的求解与优化后,本文我们将继续探讨完全背包问题的相关内容,完全背包问题与01背包问题的不同就在于01背包问题,顾名思义,对每种物品最多只有两种选择,要么0要么1,即要么选要么不选;而完全背包问题则在此基础上解除了数量的限制,要么不选,如果选,在背包容量允许的前提下,可以选任意多个该物品,在此条件下求解能够获取的最大价值。
同样地,暂时理解不了没关系,继续往下看就行。
完全背包问题
原题链接:https://www.acwing.com/problem/content/3/
基本描述
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量且总价值最大,并将最大的价值输出。
输入格式第一行两个整数 N,V 用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行两个整数 vi,wi 用空格隔开,分别表示第 i 种物品的体积和价值。
数据范围0< N , V ≤ 10000 < N , V ≤1000
0< vi , wi ≤ 10000 < vi , wi ≤ 1000
输入样例4 5 1 2 2 4 3 4 4 5
输出样例
10
题意解析
本题题意与01背包问题基本相同,接下来我们以测试样例为例来理解一下本题的意思。
在本题中给出的数据有:
-
物品的数量N(4)
-
背包的容量V(5)
-
每件物品的体积与价值(下面以表格的形式呈现)
体积 | 价值 | |
---|---|---|
物品1 | 1 | 2 |
物品2 | 2 | 4 |
物品3 | 3 | 4 |
物品4 | 4 | 5 |
据此我们理解题目要求在满足物品的总体积不大于背包的容量V(5)的前提下所能获取的最大价值
与01背包问题唯一不同的是,本题中每件物品的数量没有上限,在不超出背包容量的前提下,可以任意选择每件物品的数量。
例如:
-
物品1的体积为1,价值为2,背包的最大容量为V(5),那么我们就可以把用五个物品1塞满背包,得到价值10;
-
物品2的体积为2,价值为2,背包的最大容量为V(5),那么我们就可以先选用两个物品2,再补一个物品1,得到的价值也是10
对多种符合条件的可能性进行分析计算,最终得出所能获取的最大价值为10。
思路点拨
由于完全背包问题与01背包问题的出发点基本相同,所以对于本题,我们依旧
定义两个变量n,m
来获取物品的数量以及背包的容量;
定义两个全局数组v[i]
来分别存放物品的体积,w[i]
来分别存放物品的价值;
定义一个二维数组dp[i][j]
以表示只考虑前i
个物品且背包的容量为j
时所能获取的最大价值。
完成了这些基本的定义后我们首先回顾一下01背包问题的核心代码句:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]);
在这段代码中,由于我们只有选与不选两种情况,所以仅需对这两种情况的结果进行判断,选取其中较大的值即可。
如果我们用同样的思路分析完全背包问题,应当知道需要对比的情况远不止两种,因为选一个、选两个以至选 N 个都是一种独立的决策。
因此,我们仿照01背包问题的写法得到的递推式应该为(由于式子较长,用v
来代替v[i]
、w
代替w[i]
,理解意思即可):
dp[i][j] = max(dp[i-1][j] , dp[i-1][j-v]+w , dp[i-1][j-2v]+2w ,..., dp[i-1][j-kv]+2kw)
有些同学想到这里可能会觉得束手无策,因为中间省略的部分是不确定的,它随着具体的背包容量的大小变化而变化,看上去在我们的程序中根本无法写出这样的式子。
的确我们无法直接在程序中写出这样的递推式,但这并不意味着我们不能用其他的方式将它表示出来。
通过观察,我们不难发现对于每一种决策来说,dp[i - 1][j - kv] + 2kw)
中在不断变化的其实只有k,那么,如果我们将每一项中的j
都替换成j - v
的话,上述的递推式就变成了:
dp[i][j-v] = max(dp[i-1][j-v] , dp[i-1][j-2v]+w , dp[i-1][j-3v]+2w ,..., dp[i-1][j-(k+1)v]+2kw)
我们观察上述这两个式子,是不是觉得有些熟悉?没错,这就是我们在高中时都一定学过的错位相消。
我们首先对图中最后的蓝色部分进行讨论,蓝色部分的式子为dp[i-1][j-(k+1)v]+2kw
,我们回想一下,在推导第一个式子的时候,我们实际上是把背包容量允许范围内的所有可能都考虑进来了,那么显而易见的是j-(k+1)v
一定已经小于0
了,因此就没有再讨论的意义、可以直接省略。
接着,我们从图中可以直观地看到,用黑色斜线连接的两边式子是一样的,唯一的不同在于上面的每一项与下面相比都多加了一个w
,也就是说上面的这些式子完全就可以用下面的式子加上一个w
来代替,上式也就变成了:
dp[i][j] = max(dp[i - 1][j], dp[i][j - v](下式的左边) + w(补加的w))
即
dp[i][j] = max(dp[i - 1][j], dp[i][j - v[i]]+ w[i])
状态转移
其实理解了01背包问题的状态转移图后,完全背包问题的状态转移图大同小异
这里以dp[2][5]
为例,因为题中给出 物品2 的体积为2
,所以dp[2][5]
依赖于dp[1][5]
、dp[1][3]
、dp[1][1]
三项,即分别对应着 物品2 不选、选一个、选两个三种情况。
我们也可以用状态转移图来理解上面推导得出的式子,即如下图所示:
我们可以看到蓝色五角星所标记的位置,即dp[2][3]
,实际上是依赖于dp[1][3]
和dp[1][1]
的,也就是 物品2 不选和选一个的情况。
而上面已经分析过红色五角星所标记的位置,即dp[2][5]
,依赖于dp[1][5]
、dp[1][3]
、dp[1][1]
三项.
我们不难发现,红色五角星所依赖的与蓝色五角星所依赖的其实只差了一个dp[1][5]
,也就是说,我们可以将红色五角星的依赖关系改为图中绿色箭头所示,依赖于dp[1][5]
和dp[2][3]
,也就得出了我们上面推导得到的结果:
dp[i][j] = max(dp[i - 1][j], dp[i][j - v[i]]+ w[i])
代码实现
C++代码实现如下:
带注释版:
#include <iostream>
using namespace std;
const int N = 1010; // 数组的大小,取值稍大于数据范围即可
int n, m; // n表示数组中元素的个数,m表示背包的容量大小
int dp[N][N]; // 二维数组dp[i][j]只从前i个物品中选,总体积不超过j的最大价值
int v[N], w[N]; // 数组v[i]用于表示第i件物品的重量,w[i]用于表示其价值
int main()
{
cin >> n >> m; // n与m分别录入物品的个数以及背包的最大容量
for (int i = 1; i <= n; i++)
{
cin >> v[i] >> w[i]; // 循环录入每一件物品的体积与价值,也可以不用数组,写在循环里边更新边处理
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
if(j < v[i]) // 若容量不够,则dp[i][j]只能等于dp[i - 1][j];
{
dp[i][j] = dp[i - 1][j];
}
else // 若容量足够,则参照上面推导的递推式进行
{
dp[i][j] = max(dp[i - 1][j], dp[i][j - v[i]] + w[i]);
}
}
}
cout << dp[n][m] <<endl; // 输出考虑所有的物品且背包容量最大时的结果,即dp[n][m]
return 0;
}
纯净版:
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int dp[N][N];
int v[N], w[N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
cin >> v[i] >> w[i];
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
if(j < v[i])
{
dp[i][j] = dp[i - 1][j];
}
else
{
dp[i][j] = max(dp[i - 1][j], dp[i][j - v[i]] + w[i]);
}
}
}
cout << dp[n][m] <<endl;
return 0;
}
优化方案
与01背包问题一样,本题也需要让考虑物品的数量与背包的容量一点点变大,时间复杂度无法优化。
与01背包问题一样,本题也可以使用一维数组来代替二维数组来实现空间复杂度上的优化。
我们先来对比一下01背包问题与完全背包问题递推式的异同:
01背包问题:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i])
完全背包问题:dp[i][j] = max(dp[i - 1][j], dp[i][j - v[i]]+ w[i])
可以看出唯一的区别在于在选择要的情况下,
01背包问题考虑的是前i - 1
项,而完全背包问题考虑的是前i
项.
在01背包问题的优化里曾说过,若要将二维数组优化为一维数组,需要将内循环的j
由大到小变化,因为每一个位置的状态所依赖的来自上一层i
循环,即i - 1
;
而从图中我们可以清楚地看到,在完全背包问题中,选择要该物品的时候,所依赖的状态在同一层,即第i
层,所以我们应当让这时候的j
从小到大变化,若从大到小则会错误地依赖箭头②所指的位置。
优化后的代码实现
C++代码实现如下:
带注释版:
#include <iostream>
using namespace std;
const int N = 1010;
int dp[N]; // 定义一维数组用于表示背包最大容量为N时所能获取的最大价值
int main()
{
int n , m;
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
int v , w;
cin >> v >> w;
for (int j = v; j <= m; j++) // j从小到大变化,最小不能小于v,否则将无意义
{
dp[j] = max(dp[j], dp[j - v] + w);
}
}
cout << dp[m] <<endl;
return 0;
}
纯净版:
#include <iostream>
using namespace std;
const int N = 1010;
int dp[N];
int main()
{
int n , m;
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
int v , w;
cin >> v >> w;
for (int j = v; j <= m; j++)
{
dp[j] = max(dp[j], dp[j - v] + w);
}
}
cout << dp[m] <<endl;
return 0;
}
参考资料
【1】崔添翼 . 背包问题九讲