背包问题
记录最近两天在AcWing上学习背包问题的总结与思考 原文链接
贪心算法
在第一次遇到背包问题的时候,我相信很多人的想法和我一样,就是使用贪心算法,我们只需要算出每件物品的性价比,也就是物品权重(价值/重量),然后排序,从高向低选做局部最优解,由此得出整体最优解,求出最大价值。
这种方法对于可拆分背包当然可以,但是对于01背包,貌似就没那么简单了。
举个例子:求 N = 3 , V = 15 , v = [ 6 , 7 , 10 ] , w = [ 8 , 9 , 15 ] N~=~3,~V~=~15,~v~=~[~6,~7,~10~],~w~=~[~8,9,15~] N = 3, V = 15, v = [ 6, 7, 10 ], w = [ 8,9,15 ] 时的最优解,如果使用贪心那么得出来的答案会是 w [ 2 ] = 15 w~[~2~]~=~15 w [ 2 ] = 15,显然答案是错误的,应该是 w [ 0 ] + w [ 1 ] = 17 w~[~0~]~+~w~[~1~]~=~17 w [ 0 ] + w [ 1 ] = 17。那为什么呢?
因为对于贪心算法,他只关注性价比,忽视了背包的空间,在空间上的浪费就会增加,导致单位体积物品的价值降低了,最后反而并不是整体最优解。
对于这种问题,我们使用动态规划算法来解决(当然暴力枚举也可以,这里不考虑),动态规划与贪心算法其实很相似,他们唯一的不同在于:贪心每次只做一个局部最优决策,每往下的决策越少;而动态规划则是重叠子问题,将原始问题分成若干个相关子问题。虽然都是通过获得每一个最优子结构来获得整体最优。但是这么对比起来,贪心是不是就显得有点鼠目寸光了。而动态规划则是统筹全局呢。
动态规划
那么现在我们来学习动态规划的使用,首先我们要知道,既然叫动态规划,那么他一定是动态的,同时他的每个状态依赖于前一次的状态。为了方便描述这种状态,现在我们可以给他一个二维表:dp[N][V]
状态:dp[i][j] 表示选择前 i 个物品,体积为 j 时的最优方案
状态转移方程:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]) (_当作递推公式看就行了_)
dp[i - 1][j]:上一个状态体积为 j 的价值
dp[i - 1][j - v[i]] + w[i]:上一状态体积为 {j - v[i] (第 i 件物品体积)}的价值 + w[i](第 i 件物品的价值)
这样我们就可以看到每个状态下各种体积的最优解。
01 01 01背包
问题描述
有 N N N 件物品和一个容量是 V V V 的背包。每件物品只能使用一次。
第 i i i 件物品的体积是 v i v_i vi,价值是 w i w_i wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
数据范围
0
<
N
,
V
≤
1000
0<N,V≤1000
0<N,V≤1000
0
<
v
i
,
w
i
≤
1000
0<v_i,w_i≤1000
0<vi,wi≤1000
算法:动态规划二维表
时间和空间复杂度: O ( n ⋅ m ) ~O(n·m) O(n⋅m)
现在我们用上述方法解决 01 01 01背包问题。
核心代码
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; 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]); // 状态转移
}
}
好了,这样就可以解决 01 01 01 背包的简单问题了。
不过,到这里还没有结束,如果真的理解了动态规划,那么应该很容易发现,上述代码是可以优化的。
我们每多一个物品做状态转移,只需要上一次的全部状态,并不需要之前所有状态;所以我们可以将二维表转化为一维表来节省空间的使用。空间复杂度降到 O ( m ) ~O(m) O(m)
注意,这里的j
循环是从高位到低位,因为dp[j-v[i]]
表示的是容量为j-v[i]
的背包装前i-1
个物品的最大价值。物品容量i
最大价值循环,从高位到低位同一物品至多拿一次;反之,则会出现多次。
算法:动态规划一维表
C++ 代码
#include <bits/stdc++.h>
using namespace std;
#define N 1007
int n, m, v, w, dp[N];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> v >> w;
for (int j = m; j >= v; j--) {
dp[j] = max(dp[j], dp[j-v] + w);
}
}
cout << dp[m];
return 0;
}
现在 01 01 01 背包问题结束了,我们来看完全背包问题。
完全背包
完全背包是在 01 01 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 - k * v[i]] + k * w[i]) (k = 0, 1, ··· , n)
dp[i][j - v[i]] = max(dp[i - 1][j - k * v[i]] + k * w[i]) (k = 1, 2, ··· , n)
将上述两式相减就可以得出完全背包的状态转移方程了。
dp[i][j] = max(dp[i - 1][j], dp[i][j - v[i]] + w[i])
同 01 01 01 背包一样,我们将二维表进行优化得出代码:
C++ 代码
#include <bits/stdc++.h>
using namespace std;
#define N 1007
int n, m, v, w, dp[N];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> v >> w;
for (int j = v; j <= m; j++) {
dp[j] = max(dp[j], dp[j-v] + w);
}
}
cout << dp[m];
return 0;
}
注意这里完全背包与 01 01 01 背包的的区别!!!
我们现在将两个方程放在一起,并对核心代码进行比较。
状态转移方程
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]) // 01背包
dp[i][j] = max(dp[i - 1][j], dp[i][j - v[i]] + w[i]) // 完全背包
核心代码
// 01背包
for (int i = 1; i <= n; i++)
for (int j = m; j <= v[i]; j++) // 从高位到低位
dp[j] = max(dp[j], dp[j-v[i]] + w[i]);
// 完全背包
for (int i = 1; i <= n; i++)
for (int j = v[i]; j <= m; j++) // 从低位到高位
dp[j] = max(dp[j], dp[j-v[i]] + w[i]);
还记得之前二维表优化为一维表的方法吗?在这里我们也可以这样理解,dp[j-v[i]]
表示的是容量为j-v[i]
的背包装前i-1
个物品的最大价值。物品容量i
最大价值循环,从高位到低位同一物品至多出现一次,而从低位到高位则同一物品可以出现多次。
学完 01 01 01 背包和完全背包,趁着脑子还热,我们继续来学习一下多重背包问题吧!!!先来看看问题描述。
多重背包问题 I
问题描述
有 N N N 件物品和一个容量是 V V V 的背包。
第 i i i 件物品最多有 s i s_i si 件,每件体积是 v i v_i vi,价值是 w i w_i wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
数据范围
0
<
N
,
V
≤
100
0<N,V≤100
0<N,V≤100
0
<
v
i
,
w
i
,
s
i
≤
100
0<v_i,w_i,s_i≤100
0<vi,wi,si≤100
算法:动态规划 + 暴力
时间复杂度: O ( m ⋅ n ⋅ s ) ~O(m·n·s) O(m⋅n⋅s)
我们先来看一下多重背包问题的状态转移方程,用 k k k 表示每个物品选中次数,
f[i][j] = max(f[i - 1][j - k * v[i]] + k * w[i]) (k = 0, 1, ···, s[i])
想要得到最终价值并不难,我们只要在 01 01 01 背包的 j j j 循环下加个 k k k 循环,循环给出两个条件:
1
)
k
≤
s
1)~k~≤~s
1) k ≤ s
2
)
k
∗
v
≤
j
2)~k~*~v~≤~j
2) k ∗ v ≤ j
这样就可以得出最大价值了。
C++ 代码
#include <bits/stdc++.h>
using namespace std;
#define N 107
int n, m, v, w, s, dp[N];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> v >> w >> s;
for (int j = m; j >= v; j--) {
for (int k = 1; k <= s && k * v <= j; k++) {
dp[j] = max(dp[j], dp[j - k * v] + k * w);
}
}
}
cout << dp[m];
return 0;
}
这样就结束了吗?并没有,如果我们将范围扩大到: 0 < N ≤ 1000 0<N≤1000 0<N≤1000, 0 < V ≤ 2000 0<V≤2000 0<V≤2000, 0 < v i , w i , s i ≤ 2000 0<v_i,w_i,s_i≤2000 0<vi,wi,si≤2000 会怎么样呢?显然 n ⋅ m ⋅ s > 1 0 9 n·m·s~>~10^9 n⋅m⋅s > 109,这样提交一定是会 T L E TLE TLE 的。那么我们该怎么优化呢?
多重背包问题 II
数据范围
0
<
N
≤
1000
0<N≤1000
0<N≤1000
0
<
V
≤
2000
0<V≤2000
0<V≤2000
0
<
v
i
,
w
i
,
s
i
≤
2000
0<v_i,w_i,s_i≤2000
0<vi,wi,si≤2000
算法:动态规划 + 二进制
时间复杂度: O ( m ⋅ n ⋅ l o g s ) ~O(m·n·logs) O(m⋅n⋅logs)
原来的做法是,对于每件物品,我们一次拿一个。现在我们把每种物品的总件数 s s s,分成一个又一个堆,我们每次都拿其中的一个堆,那么我们该怎么样分配呢?
我们可以利用二进制的性质对其进行优化,比如我们现在有 7 7 7 件物品,原来我们最多可能要拿 7 7 7次,现在我们使用二进制换算 7 B = 111 7B~=~111 7B = 111,我们可以把他拆解成 100 010 001 100~010~001 100 010 001 这三个堆,他们可以组合成任意 ≤ 7 ≤7 ≤7 的数,而且每种组合都会得到不同的数,那么我至多只需要拿 3 3 3 次就可以拿完。
假设我要拿 6 6 6 件,原来我们一个一个拿,需要拿 6 6 6 次才能全部取完。现在我只需要拿 2 2 2 次,我们只要拿 2 , 4 ~2,~4 2, 4 这两堆就可以了。这样就可以将时间复杂度 O ( m ⋅ n ⋅ s ) ~O(m·n·s) O(m⋅n⋅s) 降到了 O ( m ⋅ n ⋅ l o g s ) ~O(m·n·logs) O(m⋅n⋅logs),大概从 4 ⋅ 1 0 9 4·10^9 4⋅109 降到了 2 ⋅ 1 0 7 2·10^7 2⋅107,可以通过。
C++ 代码
#include <bits/stdc++.h>
using namespace std;
#define N 2007
#define M 12007
// 多重背包问题
// 二进制优化
int n, m, vi, wi, si, k, ans;
int w[M], v[M], dp[N];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> vi >> wi >> si;
// 进行二进制拆解,每次左移一位
for (int k = 1; k <= si; k <<= 1) {
w[++ans] = wi * k; // 这一位的全部价值
v[ans] = vi * k; // 这一位的全部体积
si -= k; // 减去这一位的全部
}
// 最后一位放剩余部分
if (si > 0) {
w[++ans] = wi * si;
v[ans] = vi * si;
}
}
// 因为已经进行了二进制优化,所以此时 n 变为 ans
for (int i = 1; i <= ans; i++) {
for (int j = m; j >= v[i]; j--) {
dp[j] = max(dp[j], dp[j-v[i]] + w[i]);
}
}
cout << dp[m];
return 0;
}
思考?
在 01 01 01 背包中我们对【空间】进行了优化,空间复杂度从 O ( n ⋅ m ) ~O(n·m) O(n⋅m) 降到了 O ( m ) ~O(m) O(m);
在多重背包中我们利用二进制思想
对同种【物品的分类】进行了优化,时间复杂度从 O ( m ⋅ n ⋅ s ) ~O(m·n·s) O(m⋅n⋅s) 降到了 O ( m ⋅ n ⋅ l o g s ) ~O(m·n·logs) O(m⋅n⋅logs);
到此已经是最优了吗?我们还可以对哪些部分进行优化?