本文主要内容来自网上流传的背包九讲 ,非常的经典,现以在github上开源,记录一下个人学习笔记和理解。
0 / 1背包
实现代码:
1 2 3 4 5 6 7 8 int cost[N];int w[N];int dp[N][N];for (int i=1 ;i<=n;i++) for (int j=1 ;j<=v;j++) if (j>cost[i])dp[i][j]=max(dp[i-1 ][j],dp[i-1 ][j-cost[i]]+w[i]); else dp[i][j]=dp[i-1 ][j];
代码:
1 2 3 4 5 6 7 8 9 10 11 12 for (int i=1 ;i<=n;i++) for (int j = v; j >= cost[i]; j--) { dp[j] = max(dp[j], dp[j - cost[i]] + weight[i]); }
这个常数优化代码待添加
01背包是所有背包问题的基础,其它背包问题都能转化为01背包,因此必须牢记算法思想原理。
完全背包 在原问题(本文中的原问题均指代基础的01背包问题)中,每个物品都只有一件,而当每个物品都有无数件可取时,这个问题就变成了完全背包。
解决方法就是在01背包的基础上,把内层循环的遍历顺序倒置。
1 2 3 for (int i=1 ;i<=n;i++) for (int j = cost[i]; j <= v; j++) dp[j] = max(dp[j], dp[j - cost[i]] + weight[i]);
考虑为什么倒置可以解决?
在01背包问题中,二维数组优化为一维数组时d[ j ]倒序填写,目的是不覆盖dp [ i-1 ] [ j]和dp[ i -1] [ j-cost[ i ] ],因为dp[ i ] [ j ]的结果要依赖于dp[ i-1 ] [ … ](即第i 个物品还没有被考虑到取和不取),而若正序填dp[ j ],当填到dp[ j ]时,dp[ k ] k<j 的内容已经被覆盖了 ,也就是说在考虑第i件物品是否取的时候,dp[ k ]已经包含了该物品被取过的情况,而此时dp[ j ]依旧在考虑是否要取物品i,因此,此情况下物品可以无限取。
多重背包 在原问题中,当每个物品的数量可以为任意正整数时,求背包的最大价值,该问题就演变为了多重背包。
一个直观的想法就是,当该种物品有m件时,把它分割成m种物品,并且他们的数量均为1,话句话说,就是把每件物品都看成一个独一无二的种类,这样,当有a件A物品,b件B物品,c件C物品时进行分割,得到了a+b+c种物品,每种物品的数量为1,这就转化为了最基本的01背包问题。
代码:
1 2 3 4 5 6 7 8 cin >> v >> n;for (int i = 1 ; i <= n; i++) cin >> cost[i] >> weight[i] >> kinds[i]; for (int i = 1 ; i <= n; i++) for (int k = 1 ; k <= kinds[i]; k++) for (int j = v; j >= cost[i]; j--) dp[j] = max(dp[j], dp[j - cost[i]] + weight[i]);
例题:HDU2191
这种情况下,当一种物品的数量非常大时,实际上中间一层循环包含了很多无效的考虑。时间复杂度很高,在很多题目中,都可能出现超时,有两种优化的方式,分别是二进制思想优化和单调队列
二进制优化
二进制优化的原理 当一个物品有k件时,即,1,1,1…1 共k个1,需要考虑每件物品的取和不取的组合情况,也就是最终可能取得0到k 以19为例,如果我们拆分成1,2,4,8,3,每个数字有取和不取两种情况,那么可以组成0到19中的任意数,但是这样就可以把k个物品转化成大约logk个,极大降低了时间复杂度,但同时也增加了空间复杂度 那么五个数是怎么来的呢 其实就是相加小于等于该数的2的幂次方(1,2,4,8)和那个数与和的差值(3)
当一件物品的 件数X单件花费>总限制花费 时该物品相当于完全背包了,不需要优化,因每件物品最多取 总限制花费/单件花费 的数量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 int dp[1000005 ];int cost[1000005 ], weight[1000005 ];int main () { int n, v, ans, cnt = 1 ; scanf ("%d%d" , &n, &v); for (int i = 1 ; i <= n; i++) { int c, w, num; scanf ("%d%d%d" , &c, &w, &num); for (int j = 1 ; j <= num; j <<= 1 ) { cost[cnt] = j*w; weight[cnt] = j * c; cnt++; num -= j; } if (num) { cost[cnt] = num * w; weight[cnt] = num * c; cnt++; } } for (int i = 1 ; i < cnt; i++) for (int j = v; j >= cost[i]; j--) dp[j] = max(dp[j], dp[j - cost[i]] + weight[i]); printf ("%d\n" , dp[v]); return 0 ; }
例题:P1776 宝物筛选
单调队列优化,还没搞懂
混合背包 原问题中,当每种物品的数量既有1个也有多个,还可以有无限个的情况下,该问题就演变成了混合背包,实质上是前面三种背包情况的一个综合。
一个直观的想法就是分情况讨论,当在考虑某种物品取和不取时,
搞懂了01背包、完全背包和多重背包后,剩下的就都是以上三种情况的运用和变形,我就直接简单的记录例题和题解。基本上每种做上两三个题目就明白了。
例题:p1833 樱花
题解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 #include <iostream> #include <algorithm> #include <cstdio> #include <malloc.h> using namespace std ;int dp[1005 ];int cost[1000000 ]; int value[1000000 ];int kinds[1000000 ];int main () { int v, n; int h1, m1, h2, m2; char c; scanf ("%d%c%d%d%c%d%d" , &h1, &c, &m1, &h2, &c, &m2, &n); if (h2 > h1)v = (h2 - h1) * 60 + m2 - m1; else v = m2 - m1; int cnt = 1 ; for (int i = 1 ; i <= n; i++) { int c, v, k; scanf ("%d%d%d" , &c, &v, &k); if (k == 0 ) { cost[cnt] = c, value[cnt] = v, kinds[cnt] = 0 ; cnt++; continue ; } for (int j = 1 ; j <= k; j <<= 1 ) { cost[cnt] = j * c, value[cnt] = j * v, kinds[cnt] = 1 ; k -= j; cnt++; } if (k) { cost[cnt] = k * c, value[cnt] = k * v, kinds[cnt] = 1 ; cnt++; } } for (int i = 1 ; i < cnt; i++) { if (kinds[i] == 0 ) { for (int j = cost[i]; j <= v; j++) dp[j] = max(dp[j], dp[j - cost[i]] + value[i]); } else { for (int j = v; j >= cost[i]; j--) dp[j] = max(dp[j], dp[j - cost[i]] + value[i]); } } printf ("%d\n" , dp[v]); return 0 ; }
二维费用背包 在原问题中,当限制物品取和不取的条件除了背包容量外,还增加了一个新的约束条件时,原问题就演变为二维费用背包问题,即对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)
解决方法:
例题: p1855 榨取kkksc03 HDU2159 FATE
题解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <iostream> #include <algorithm> #include <cstdio> #include <malloc.h> using namespace std ;int dp[201 ][201 ];int money[101 ];int times[101 ];int main () { int n, m, t; cin >> n >> m >> t; for (int i = 1 ; i <= n; i++) { cin >> money[i] >> times[i]; } for (int i = 1 ; i <= n; i++) { for (int j = m; j >= money[i]; j--) { for (int k = t; k >= times[i]; k--) dp[j][k] = max(dp[j][k], dp[j - money[i]][k- times[i]]+1 ); } } printf ("%d\n" , dp[m][t]); return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <iostream> #include <algorithm> #include <cstdio> using namespace std ;int dp[101 ][101 ]; int expe[101 ];int tor[101 ];int main () { int e, t, k, m; while (scanf ("%d%d%d%d" , &e, &t, &k, &m) != EOF) { memset (dp, 0 , sizeof (dp)); for (int i = 1 ; i <= k; i++) { scanf ("%d%d" , &expe[i], &tor[i]); } for (int i = 1 ; i <= k; i++) for (int j = tor[i]; j <= t; j++) for (int k = 1 ;k <= m; k++) dp[j][k] = max(dp[j][k], dp[j - tor[i]][k - 1 ]+expe[i]); if (dp[t][m] < e) printf ("-1\n" ); else { int i; for (i = 1 ; i <= t; i++) if (dp[i][m] >= e)break ; printf ("%d\n" , t - i); } } return 0 ; }
分组背包问题 在原问题中,当所有的物品被划分为若干组,每组中的物品互相冲突,最多选一件时,原问题就演变为分组的背包问题。
例题 P1757 通天之分组背包
题解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include <cstdio> #include <vector> #include <algorithm> #define N 1001 using namespace std ;struct Node { int c; int v; }; int dp[N];vector <Node> cost[N];int main () { int v,n; int numOfGroup = -1 ; scanf ("%d%d" , &v, &n); for (int i = 1 ; i <= n; i++) { int c, v, g; scanf ("%d%d%d" , &c, &v, &g); if (g > numOfGroup)numOfGroup = g; Node t; t.c = c, t.v = v; cost[g].push_back(t); } for (int i=1 ;i<=numOfGroup;i++) for (int j=v;j>=0 ;j--) for (vector <Node>::iterator it = cost[i].begin(); it != cost[i].end(); it++) { if (j>=it->c) dp[j] = max(dp[j], dp[j - it->c] + it->v); } printf ("%d" ,dp[v]); }
有依赖关系的背包 例题: 金明的预算方案
题解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include <bits/stdc++.h> using namespace std ;struct Node { int c, v, k; }node[60 ]; int dp[32005 ];int group[32005 ];int main () { int n, v; cin >> v >> n; for (int i = 1 ; i <= n; i++) { cin >> node[i].c >> node[i].v >> node[i].k; node[i].v *= node[i].c; } for (int i = 1 ; i <= n; i++) { if (node[i].k == 0 ) { memset (group, 0 , sizeof (group)); for (int j = v; j >= node[i].c; j--) group[j] = dp[j - node[i].c] + node[i].v; for (int k = 1 ; k <= n; k++) if (node[k].k == i) { for (int j = v; j >= node[k].c + node[i].c; j--) group[j] = max(group[j], group[j - node[k].c] + node[k].v); } for (int k = node[i].c; k <= v; k++) dp[k] = max(group[k], dp[k]); } } cout << dp[v] << endl ; return 0 ; }
泛化物品
背包问题问法的变化
最后两章以及前几章中的其它优化方案,自行阅读背包九讲 原文
本文大部分内容来源于背包九讲,并根据一年前做过的题解,结合个人对的学习理解和网上的思路整体而成,可能有少许内容,当时参考了很多博客,链接也没保存,侵删。