时隔半年,再次回过头来看背包问题,突然感觉十分明朗了,不觉得空间降维很难理解了,以前死活理解不了这里为什么可以空间降维。
01背包问题:
如果使用dfs, 每件物品都有选和不选两种可能,把所有的情况都枚举一遍,可以得出一个最大价值,但是时间复杂度为O(2n),这个复杂度太高了。所以尝试使用动态规划来降低复杂度,使用动态规划可以将时间复杂度降低至O(nV),
算法实现过程
创建一个dp[][]二维数组,每个元素dp[i][v]表示前i件物品,恰好装入容量为V的背包所能得到的最大价值,
对于每一件物品都有选和不选两种可能,
- 如果不选这件物品,那dp[i][v] = dp[i-1][v], 因为没选这件物品,容量没有增大,价值也没有增大,价值仍然等于前(i-1)件物品刚好装入容量为V的背包所能获得的最大价值;
- 如果选这件物品,那前 i 件物品装入容量为v的背包所能获得最大价值就等于 把前(i-1)件物品刚好装入容量为(v-w[i])的背包所能获得最大价值加上当前物品的价值 c[i], 即dp[i][v] = d[i-1][v-w[i]] + c[i];
所以综合选和不选,就是要在两种情况中选一个最大值,即dp[i][v] = max(dp[i-1][v], d[i-1][v-w[i]] + c[i])
所以该问题的状态转移方程为dp[i][v] = max(dp[i-1][v], d[i-1][v-w[i]] + c[i])
边界初始值为dp[0[v] = 0, 对于前0件物品,无论背包容量有多大,所能获得最大价值都是0; 最终的结果为dp[n][v]
代码实现如下:
int[][] dp = new int[n+1][V+1];
// 初始化边界
for(int v = 0; v <= V; v++)
dp[0][v] = 0;
}
// 使用状态转移方程进行迭代
for(int i = 1; i <= n; i++){
for(int v = w[i]; v <= V; v++){
dp[i][v] = Math.max(dp[i-1][v], dp[i-1][v-w[i]] + c[i]);
}
}
使用滚动数组优化空间
上面动态规划的解法时间复杂度已经达到最优,但是空间复杂度还可以优化,使用一个滚动的一维数组可以代替原来的二维数组。
创建一个大小为 (v + 1)的 的一维数组代替元素的二维dp[][]数组,
通过观察发现,d[i][v]始终只和dp[i-1]有关,和dp[i-2]无关,所以上面的dp[]数组就是用来存储dp[i-1], dpp[i]从从dp[i-1]中获取到数据后对dp[i-1]直接进行修改,把dp[i-1]变成dp[i]
变成一维数组后,原来的dp[i-1][v]变成了dp[v], 原来的dp[i-1][v-w[i]]变成了dp[v-w[i]], 因为计算当前dp[v]的过程中用到了上一层的dp[v-w[i]], 所以我们必须从dp[V]开始逆向遍历,这样使用的dp[v-w[i]]才是上一层的dp[v-w[i]], 而非本层修改后的。
改成一维数组后,状态转移方程变为 dp[v] = max(dp[v], dp[v-w[i]] + c[i]), 代码实现修改如下:
int[] dp = new int[V+1];
// 初始化边界
for(int v = 0; v <= V; v++)
dp[0][v] = 0;
}
// 使用状态转移方程进行迭代
for(int i = 1; i <= n; i++){
for(int v = V; v >= w[i]; v--){ // v从大到小倒序遍历
dp[v] = Math.max(dp[v], dp[v-w[i]] + c[i]);
}
}
完全背包问题:
区别和01背包就是每件物品可以被选不止一次,可以反复选择这件物品。
状态转移方程变为dp[i][v] = max(dp[i-1][v], dp[i][v - w[i]] + c[i]); 标红的地方就是区别。其中的道理我也不是很明白。暂时先记住吧
代码实现为:
int[][] dp = new int[n+1][V+1];
// 初始化边界
for(int v = 0; v <= V; v++)
dp[0][v] = 0;
}
// 使用状态转移方程进行迭代
for(int i = 1; i <= n; i++){
for(int v = w[i]; v <= V; v++){
dp[i][v] = Math.max(dp[i=1][v], dp[i][v-w[i]] + c[i]);
}
}
空间优化
同样可以使用滚动数组来对二维数组进行空间优化。但是注意这次的dp[v]要正序遍历,dp[v]使用了上一层的dp[v]和本层dp[v-w[i]], 所以计算dp[v]之前,必须先计算出本层的dp[v-w[i]], 所以遍历v的时候必须从小到大正序遍历。
代码实现为:
int[] dp = new int[V+1];
// 初始化边界
for(int v = 0; v <= V; v++)
dp[0][v] = 0;
}
// 使用状态转移方程进行迭代
for(int i = 1; i <= n; i++){
for(int v = w[i]; v <= V; v++){ // v从小到大正序遍历
dp[v] = Math.max(dp[v], dp[v-w[i]] + c[i]);
}
}
下面是 c 语言实现的动态规划的01背包问题和完全背包问题模板
01背包问题模板:
// 01背包问题
#include
#include
using namespace std;
const int maxn = 100; // 物品的最大件数
const int maxv = 1000; // V的上限
int w[maxn], c[maxn], dp[maxv];
int main()
{
// 边界
for (int v = 0; v <= V; v++){
dp[v] = 0;
}
for (int i = 1; i <= n; i++){
for (int v = V; v >= w[i]; v--){ // 逆向枚举v
// 状态转移方程
dp[v] = max(dp[v], dp[v - w[i]] + c[i]);
}
}
// 寻找dp[0] ... dp[V]中的最大值即为答案
int max = 0;
for (int v = 0; v <= V; v++){
if (dp[v] > max){
max = dp[v];
}
}
}
完全背包问题模板:
for (int i = 1; i <= n; i++){
for (int v = w[i]; v <= V; v++){
// 状态转移方程
dp[v] = max(dp[v], dp[v - w[i]] + c[i]);
}
}
01背包问题实战:
Eva loves to collect coins from all over the universe, including some other planets like Mars. One day she visited a universal shopping mall which could accept all kinds of coins as payments. However, there was a special requirement of the payment: for each bill, she must pay the exact amount. Since she has as many as 104 coins with her, she definitely needs your help. You are supposed to tell her, for any given amount of money, whether or not she can find some coins to pay for it.
Input Specification:
Each input file contains one test case. For each case, the first line contains 2 positive numbers: N
(≤104, the total number of coins) and M
(≤102, the amount of money Eva has to pay). The second line contains N
face values of the coins, which are all positive numbers. All the numbers in a line are separated by a space.
Output Specification:
For each test case, print in one line the face values V1≤V2≤⋯≤Vk such that V1+V2+⋯+Vk=M
. All the numbers must be separated by a space, and there must be no extra space at the end of the line. If such a solution is not unique, output the smallest sequence. If there is no solution, output "No Solution" instead.
Note: sequence {A[1], A[2], ...} is said to be "smaller" than sequence {B[1], B[2], ...} if there exists k≥1 such that A[i]=B[i] for all i<k, and A[k] < B[k].
Sample Input 1:
8 9
5 9 8 7 2 3 4 1
Sample Output 1:
1 3 5
Sample Input 2:
4 8
7 2 4 3
Sample Output 2:
No Solution
分析:这题的价值和容量数组是同一个数组,但是还需要记录下路径,所以多了一个choice[][]数组
完整代码:
1 #include
2 #include
3 using namespace std;
4
5 const int maxn = 10010;
6 const int maxv = 110;
7
8 int w[maxn], dp[maxv] = { 0 }; // w[i]为钱币的价值
9 bool choice[maxn][maxv], flag[maxn];
10 bool cmp(int a, int b){ // 从大到小排序
11 return a > b;
12 }
13
14 int main()
15 {
16 // freopen("in.txt", "r", stdin);
17 int n, m;
18 scanf("%d %d", &n, &m);
19 for (int i = 1; i <= n; i++){
20 scanf("%d", &w[i]);
21 }
22
23 sort(w + 1, w + n + 1, cmp); // 从大到小排序
24 for (int i = 1; i <= n; i++){
25 for (int v = m; v >= w[i]; v--){
26 // 状态转移方程
27 if (dp[v] <= dp[v - w[i]] + w[i]){
28 dp[v] = dp[v - w[i]] + w[i];
29 choice[i][v] = 1; // 放入第i 件物品
30 }
31 else{
32 choice[i][v] = 0; // 不放入第i 件物品
33 }
34 }
35 }
36 if (dp[m] != m)
37 printf("No Solution"); // 无解
38 else{
39 // 记录最优路径
40 int k = n, num = 0, v = m;
41 while (k >= 0){
42 if (choice[k][v] == 1){
43 flag[k] = true;
44 v -= w[k];
45 num++;
46 }
47 else{
48 flag[k] = false;
49 }
50 k--;
51 }
52
53 // 输出方案
54 for (int i = n; i >= 1; i--){
55 if (flag[i] == true){
56 printf("%d", w[i]);
57 num--;
58 if (num > 0)
59 printf(" ");
60 }
61 }
62 }
63
64 // fclose(stdin);
65 return 0;
66 }
参考:胡凡《算法笔记》