什么样的问题可以被称作为背包问题?换言之,我们拿到题目如何透过题目的不同包装形式看到里面背包问题的不变内核呢?
给定一个背包容量target,再给定一个数组nums(物品),能否按一定方式选取nums中的元素得到target。
1、背包容量target和物品nums的类型可能是数,也可能是字符串
2、target可能题目已经给出(显式),也可能是需要我们从题目的信息中挖掘出来(非显式)(常见的非显式target比如sum/2等)
3、选取方式有常见的一下几种:每个元素选一次/每个元素选多次/选元素进行排列组合 对应背包问题。
动态规划要考虑两个方面:状态表示,状态计算。
分类
背包问题可以总结为三类:01背包问题、完全背包问题以及分组背包问题。
01背包问题:每个元素最多取1次。具体来讲:一共有 N 件物品,第 i(i 从 1 开始)件物品的重量为 w[i],价值为 v[i]。在总重量不超过背包承载上限 W 的情况下,能够装入背包的最大价值是多少?
完全背包问题:每个元素可以取多次。具体来讲:完全背包与 01 背包不同就是每种物品可以有无限多个:一共有 N 种物品,每种物品有无限多个,第 i(i 从 1 开始)种物品的重量为 w[i],价值为 v[i]。在总重量不超过背包承载上限 W 的情况下,能够装入背包的最大价值是多少?
分组背包问题:有多个背包,需要对每个背包放入物品,每个背包的处理情况与完全背包完全相同。
在完全背包问题当中根据是否需要考虑排列组合问题(是否考虑物品顺序),可分为两种情况,我们可以通过内外循环的调换来处理排列组合问题,如果题目不是排列组合问题,则这两种方法都可以使用(推荐使用组合来解决)
而每个背包问题要求的也是不同的,按照所求问题分类,又可以分为以下几种:
模板
背包问题大体的解题模板是两层循环,分别遍历物品nums和背包容量target,然后写转移方程,根据背包的分类我们确定物品和容量遍历的先后顺序,根据问题的分类我们确定状态转移方程的写法。
首先是背包分类的模板:
贪心问题: 局部最优解, 需证明该解就是该题的最优解无其他情况,则能直接用贪心法。典型例题: 部分背包问题, 直接用贪心法求的解即最优解,;而0-1背包问题额不能用贪心法去求最优解。
01背包是一种动态规划问题。动态规划的核心就是状态转移方程。
问题描述
有一个容量为V的背包,还有n个物体。只要背包的剩余容量大于等于物体体积,那就可以装进背包里。每个物体都有两个属性,即体积w和价值v。问:如何向背包装物体才能使背包中物体的总价值最大?
为什么不用贪心?
所谓贪心问题,就是每一步决策都采取最优解,按照此方案最后结果也是最优解。
举个例子
我的背包容量为10,而且有4个物体,它们的体积和价值分别为
w1 = 8, v1 = 9
w2 = 3, v2 = 3
w3 = 4, v3 = 4
w4 = 3, v4 = 3
贪心是每一步采取最优拿法,即每一次都优先拿价值与体积比值最大的物体
c1 = v1/w1 = 1.125(最大)
c2 = v2/w2 = 1
c3 = v3/w3 = 1
c4 = v4/w4 = 1
所以优先拿第一个物体,随后背包再也装不下其他物体了,则最大价值为9。
但是这个问题的最优解是取物体2,3,4装进背包,最大价值为3+4+3=10!!!
所以这个问题不可以用贪心法来处理。
思路:
原始的 01背包
01背包的状态转移方程为
f [i] [j] = max( f[i - 1][ j ], f [i - 1] [ j - w[i] ] + v[ j ] )
i代表对i件物体做决策,有两种方式—放入背包和不放入背包。 j 表示当前背包剩余的容量。
转移方程的解释:
创建一个状态矩阵 f,横坐标 i 是物体编号,纵坐标 j 为背包容量。
首先将 f 第 0 行和第 0 列初始化为0 (代码里面将整个f初始化为0了,其实只初始化第0行和第0列就够了)。这个表示不放物体时最大价值为0 。(物体编号从1开始)
接下来依次遍历f的每一行。如下所示。
for (int i = 1; i <= n; i++)
{
for (int j = 0; j <= m; j++)
{
if (j >= w[i])//如果背包装得下当前的物体
{
f[i][j] = max(f[i - 1][j], f[i - 1][j - w[i]] + v[i]);
}
else//如果背包装不下当前物体
{
f[i][j] = f[i - 1][j];
}
}
}
如果背包装得下当前的物体,在遍历过程中分别计算第i件物体放入和不放入背包的价值,取其中大的做为当前的最大价值。
如果背包装不下当前物体,那么第i个物体只有不放入背包一种选择。
不放入背包时:第 i 次决策后的最大价值和第 i-1 次决策时候的价值是一样的(还是原来的那些物体,没多没少)。
放入背包时:第 i 次决策后的价值为:第 i-1 次决策时候的价值 + 当前物体的价值 v[j]。物体放入背包后会使背包容量变为 j ,即没放物体之前背包的容量为 j - w[i]。
用二维来表示的代码:
#include
#include
using namespace std;
const int N = 10010;
int n, m; // 物品数量n,背包容量m
int v[N], w[N]; // v数组存储物品的体积,w数组存储物品的权重
int f[N][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 = 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]); // 如果当前背包容量可以放下第i个物品,则更新当前状态
}
}
cout << f[n][m] <
动态规划的计算过程:
外层循环开始,遍历每个物品。在这个例子中,从i=1
到i=4
。
内层循环开始,遍历背包容量。在这个例子中,从j=1
到j=5
。
对于每个物品和背包容量的组合,判断是否能将该物品放入背包中。
j
小于第 i
个物品的体积v[i-1]
,则无法放入该物品,最大权重与前 i-1
个物品相同,即f[i][j] = f[i-1][j]
。f[i-1][j]
。f[i-1][j-v[i]] + w[i]
,其中f[i-1][j-v[i]]
表示考虑前i-1
个物品,在剩余背包容量为j-v[i]
的情况下的最大权重,加上当前物品的权重w[i].(前 i-1 个物品占容量j-v[i],第 i
个物品占容量v[i])
f[i][j] = max(f[i-1][j], f[i-1][j-v[i]] + w[i])
= max( 不放, 放 )。当所有物品和背包容量都遍历完毕后,f[n][m]
即为问题的最优解,表示在给定物品和背包容量下的最大权重。
其中 f[i][j] 表示前 i 个物品放入容量为 j 的背包中能够得到的最大权重。通过遍历物品和背包容量,根据当前背包容量是否可以放下物品来更新状态。最后输出f[n][m]即为答案,表示将前n个物品放入容量为m的背包中能够得到的最大权重。通过填表格的方式逐步求解子问题,最终得到全局最优解。
接下来,开始进行动态规划。
2. 外层循环i = 2,考虑第2个物品。
3.外层循环i = 3,考虑第3个物品。
4. 外层循环i = 4,考虑第4个物品。
优化
通过分析可以发现,f[i][j]的值仅仅与依赖i-1时的状态,因此,代码中无需使用二维的f[i][j]进行存储,使用一维的f[j]进行存储,减少程序的空间。
使用一维f,可能会有一个疑问:如果j从小到大的顺序依次更新f[j]的值,因此,在计算 f[j] = max( f[j] , f[ j - v[i] ] + w[j] )时,f[j] 已经是 i 时刻的值了,而非 i-1 时刻的值,这个问题怎么解决呢?那就是当 f[j] 的值在更新的时候,按照 j 从大到小的顺序依次更新。
代码:
#include
#include
using namespace std;
const int N = 10010;
int n, m; // 物品数量n,背包容量m
int v[N], w[N]; // v数组存储物品的体积,w数组存储物品的权重
int f[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 = m; j >= v[i]; j--){
f[j] = max(f[j], f[j-v[i]] + w[i]); // 如果当前背包容量可以放下第i个物品,则更新当前状态
}
}
cout << f[m] <
外层循环开始,遍历每个物品。在这个例子中,从 i=1 到 i=4。
内层循环开始,从背包容量 m 逆序遍历到当前物品体积 v[i] ,更小的 j 不再考虑。这是因为逆序遍历可以保证每个物品只被考虑一次,避免重复放入。
对于每个背包容量 j,在当前的背包容量下,判断是否可以放入第 i 个物品。
初始化数组f为全0: f = [0, 0, 0, 0, 0]
第一个物品,体积v[1]=1, 权重w[1]=2
当前状态下,数组f变为:f = [2, 2, 2, 2, 2]
第二个物品,体积v[2]=2, 权重w[2]=4
当前状态下,数组f变为:f = [2, 4, 6, 6, 6]
第三个物品,体积v[3]=3, 权重w[3]=4
当前状态下,数组f变为:f = [2, 4, 6, 6, 8]
第四个物品,体积v[4]=4, 权重w[4]=5
当前状态下,数组f变为:f = [2, 4, 6, 6, 8]
0/1背包与完全背包对比:
01背包问题:有一个背包的容积为V,有N个物品,每个物品的体积为v[i],权重为w[i],每个物品只能取1次放入背包中,背包所有物品权重和最大是多少?
完全背包 :有一个背包的容积为V,有N个物品,每个物品的体积为v[i],权重为w[i],每个物品可以取无限次放入背包中,背包所有物品权重和最大是多少?
01背包问题和完全背包问题的区别就在于,每个物品取的最大次数是1次还是无限次。要考虑集合的划分问题。
状态转移方程:
回想01背包问题的状态和转移方程:
状态: f[i][j] 选择前i个物品,体积为j时的最优方案,即所选物品的最大权重和。
状态转移:f[i][j] = max(f[i-1][j-v[i]]+w[i], f[i-1][j])
现在物品可以取无数次,那么f[i][j]怎么构造合适呢?
01背包问题每次增加第i物品时,因为只能取一次,从 j 中减去 v[i] 再加上w[i](取 i - 1 个物体的权重 + 当前物体的权重),这个操作只进行了一次。现在可以增加无数次。设为:第 i 个物品可以加入0次(即f[i-1][j]),加入1次,加入2次,…加入k-1次,(这里限制体积 k*v[i] <= j),然后求所有值的最大值作为f[i][j]的值。
状态: f[i][j] 选择前i个物品,体积为j时的最优方案,即所选物品的最大权重和。
状态转移:f[i][j] = max(f[i-1][j- k * v[i]]+ k * w[i] (k= 0, 1, 2, ...))
优化点1:
if (v[i] > j) f[i][j] = ....
可以优化到 for (int j = 1; j <= m; j ++ ) 中,优化后为:
for (int j = v[i]; j <= m; j ++ ) {
......
}
优化点2:
二维数组 f [ ] [ ] 可以优化成一维的 f [ ]。
优化点3:
从状态转移方程下手:f[i][j] = max(f[i-1][j- k * v[i]]+ k * w[i] (k= 0, 1, 2, 3, 4,…))
方程拆开:
f[i][j] = max(f[i-1][j], f[i-1][j - v[i]]+w[i], f[i-1][j-2*v[i]]+2*w[i], f[i-1][j-3*v[i]]+3*w[i] ,......)
使用代入法,将j = j-v[i]带入上面的方程中得到:
f[i][j-v[i]] = max(f[i-1][j-v[i]], f[i-1][j - 2*v[i]]+w[i], f[i-1][j-3*v[i]]+2*w[i], f[i-1][j-3*v[i]]+3*w[i] ,......)
对比第二个和第一个方程,我们发现,方程1可以简化成:
f[i][j] = max(f[i-1][j], f[i][j - v[i]] + w[i])
对比01背包模型的:
f[i][j] = max(f[i-1][j], f[i-1][j - v[i]] + w[i]) // 01背包
f[i][j] = max(f[i-1][j], f[i ][j - v[i]] + w[i]) // 完全背包
发现了区别在于数组的一维下标从 i-1 变为 i。为什么呢?
f[i][j - v[i]] 已经将除去物品 i 时的所有最优解已经求出来了,因此在计算 f[i][j] 时,无需再重复计算 k=2,3,4,…时的值了。
区别:
题目:
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。
输出格式
输出一个整数,表示最大价值。数据范围
00 输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
10
暴力写法:(350ms)
#include
#include
using namespace std;
const int N = 10010;
int n, m; // 物品数量n,背包容量m
int v[N], w[N]; // v数组存储物品的体积,w数组存储物品的权重
int f[N][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++){
for(int k = 0; k * v[i] <= j; k++){
f[i][j] = max(f[i-1][j], f[i-1][j-v[i]*k] + w[i]*k);
}
}
}
cout << f[n][m] <
三层for循环,第一层循环行数n次,第二层循环列数m次,第三次取决于v[i],比较随机。
优化后的二维写法:(60ms)
#include
#include
using namespace std;
const int N = 10010;
int n, m; // 物品数量n,背包容量m
int v[N], w[N]; // v数组存储物品的体积,w数组存储物品的权重
int f[N][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++){
f[i][j] = f[i-1][j];
if(j >= v[i]) f[i][j] = max(f[i][j],f[i][j-v[i]] + w[i]);
}
}
cout << f[n][m] <
优化到一维:(40ms)
#include
#include
using namespace std;
const int N = 10010;
int n, m; // 物品数量n,背包容量m
int v[N], w[N]; // v数组存储物品的体积,w数组存储物品的权重
int f[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 = v[i]; j <= m; j++){
f[j] = max(f[j], f[j-v[i]] + w[i]); // 直接把第一维删掉
}
}
cout << f[m] <
做几道题吧:背包问题大全(动态规划)_动态规划_小轩爱学习-华为云开发者联盟 (csdn.net)
例题:
暴力写法:
#include
#include
using namespace std;
const int N = 10010;
int n, m; // 物品数量n,背包容量m
int v[N], w[N], s[N];
int f[N]; // 动态规划数组
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> v[i] >> w[i] >> s[i];
// 动态规划过程
for(int i = 1; i <= n; i++){
for(int j = 0; j <= m; j++){
for(int k = 0; k <= s[i] && k * v[i] <= j; k++)
f[i][j] = max(f[i][j], f[i-1][j-v[i]*k] + w[i] * k);
}
}
cout << f[n][m] <
动态规划的过程。使用三层循环,外层循环遍历每个物品,中间循环遍历背包容量(从0到m),内层循环遍历当前物品的数量(从0到s[i])。 在每次循环中,计算将当前物品的k个放入背包时的最大价值,并更新动态规划数组f[i][j]的值。
第三层循环的作用是处理物品数量超过1个时的情况,即处理每个物品在背包中可能放入多个的情况。变量k表示当前物品i选择放入背包的数量(从0到s[i]),并且满足k * v[i] <= j,其中j表示当前的背包容量。通过这个循环,我们可以枚举当前物品i能够选择放入的数量,并计算出相应的总价值。在每次循环中,我们比较当前选择放入k个物品i和不放入物品i的情况下的总价值,取最大值更新动态规划数组f[i][j]的值。这样就考虑了每个物品在背包中放入多个的情况。
无法直接用完全背包的优化方法。
优化代码:
#include
#include
using namespace std;
const int N = 10010; // 物品数量上限
int n, m; // 物品数量n,背包容量m
int v[N], w[N]; // 物品体积v和价值w数组
int f[N]; // 动态规划数组f
int main(){
cin >> n >> m; // 输入物品数量n和背包容量m
int cnt = 0; // 计数器,用于记录物品总数
for(int i = 1; i <= n; i++){
int a, b, s;
cin >> a >> b >> s;
int k = 1;
while(k <= s){
cnt++;
v[cnt] = a * k; // 将物品i拆分为k个体积为a,价值为b的部分
w[cnt] = b * k;
s -= k; // 更新剩余物品数量
k *= 2; // 以指数倍增长
}
if(s > 0){
cnt++;
v[cnt] = a * s; // 将剩余数量s作为一个单独的物品
w[cnt] = b * s;
}
}
n = cnt; // 更新实际物品数量
// 动态规划过程
for(int i = 1; i <= n; i++)
for(int j = m; j >= v[i]; j--)
f[j] = max(f[j], f[j-v[i]] + w[i] ); // 计算放入和不放入当前物品的最大价值
cout << f[m] <
这段代码使用了动态规划的思想来解决背包问题,以下是其详细的逻辑说明:
定义一个计数器cnt,用于记录实际的物品总数。
使用一个循环遍历每个物品:
更新实际物品数量n为计数器cnt的值。
动态规划过程:
输出背包容量为m时的最大价值,即输出f[m]的值。
问题: 每组中只能选一个 ,完全背包问题的集合划分是对于第i个物品选几个 ,而分组背包问题我们是要枚举第 i 组中选哪个物品。
类似于多重背包问题,多重背包是可以选第i个物品至多s[i]件,此题只允许选一件,每件体积和重量不同。最终还是转化为01背包问题,选不选第x个的问题
和01背包一样,都是从i-1层更新,所以一维优化要从后往前更新。
状态转移方程:
题目
有 N 组物品和一个容量是 V 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行有两个整数 N,V,用空格隔开,分别表示物品组数和背包容量。
接下来有 N 组数据:
- 每组数据第一行有一个整数 Si,表示第 i 个物品组的物品数量;
- 每组数据接下来有 Si 行,每行有两个整数 vij,wij,用空格隔开,分别表示第 i 个物品组的第 j 个物品的体积和价值;
输出格式
输出一个整数,表示最大价值。
数据范围
0
0 0 输入样例
3 5 2 1 2 2 4 1 3 4 1 4 5
输出样例:
8
状态转移:注意如果用的是上一次状态,从大到小枚举体积,保证这一层计算的体积还没有被用到过。如果用的是这一层的状态,从小到大枚举体积。
代码:
#include
#include
using namespace std;
const int N = 110; // 最大物品数量
int n, m; // n为物品种类数量,m为背包容量
int v[N][N], w[N][N], s[N]; // v为每个物品的体积,w为每个物品的价值,s为每个物品的数量
int f[N]; // f数组用于记录当前背包容量下能获得的最大价值
int main(){
cin >> n >> m; // 输入物品种类数量和背包容量
for(int i = 1; i <= n; i++){
cin >> s[i]; // 输入第i种物品的数量
for(int j = 0; j < s[i]; j++)
cin >> v[i][j] >> w[i][j]; // 输入第i种物品的体积和价值
}
// 动态规划过程
for(int i = 1; i <= n; i++) // 遍历每一种物品
for(int j = m; j >= 0; j--) // 从背包容量m开始递减,计算当前背包容量下的最大价值
for(int k = 0; k < s[i]; k++) // 遍历第i种物品的所有数量
if(v[i][k] <= j) // 如果第i种物品的体积小于等于当前背包容量
f[j] = max( f[j], f[j-v[i][k]] + w[i][k]); // 更新当前背包容量下能获得的最大价值
cout << f[m] <
这段代码是一种背包问题的动态规划解法。给定n种物品和一个背包,每种物品有不同的数量、体积和价值。要求选择一些物品放入背包中,使得背包的总体积不超过m,并且所选物品的总价值最大。
逻辑如下:
该算法的时间复杂度为O(nmmax(s)),其中n为物品种类数量,m为背包容量,max(s)为每种物品的最大数量。
01背包问题详解(浅显易懂)_Iseno_V的博客-CSDN博客
背包问题大全(动态规划)_动态规划_小轩爱学习-华为云开发者联盟 (csdn.net)
算法之动态规划(DP)求解完全背包问题_完全背包问题 动态规划_PRML_MAN的博客-CSDN博客
算法之动态规划(DP)求解01背包问题_选择防晒霜问题变背包问题_PRML_MAN的博客-CSDN博客