前言:背包问题是一类很经典的动态规划问题,知识涵盖可以占常见动态规划类型里的10%。
由于前辈们总结的太好,现在基本上见不到单纯的模板背包问题了,命题人多会结合一些其他知识点进行综合考察,当然背包问题本身就涉及了许多不同的算法。
然后就吉林省赛而言,我认为背包问题的难度覆盖在银牌区间,也就是说,赛场上你能确定这一道背包题并且顺风做出来就是可以拿下省二的。
经典背包问题讲的是给定一个有限承重为W的背包,再给任意个物品。每个物品有其价值m和重量w。计算在背包不超重前提下所能得到最大价值和。
经典背包问题:
01背包问题:每件物品只有一个,故只有选和不选两种情况。
入门思想:暴力枚举——开辟二维数组,初始化,行标 i 作为物品编号,列标 j 作为背包当前重量,数组值存放当前所能得到最大价值。
从数组左上角起始点,开始依次尝试放入每一件物品,按照遍历顺序,每一个位置得到的就都会是编号至 i 重量为 j 时所能得到的最大价值。
如此,在我们遍历完成整个二维数组之后,最后的[n][W]位置必然为全局最优解。
看完过程后,想必大家都发现了实际作用的其实只有对角线附近的那层阶梯式,每一行价值其实都是在上一行价值的基础上增添过来的,我们对列标的使用其实是一维的,那不妨就用数组下标来替代掉,反复处理一个一维数组就好了。
而实际上代码就应该是线性的dp,开二维数组是对空间的严重浪费,只是为了帮助大家理解。但这个任务我要布置给学弟学妹们自行研究(一定要研究,搞明白后的顺便去看看滚动数组,完事来和我说理解)。
for (int i = 0; i < n; i++) for (int j = 0; j <= W; j++) if (j < w[i]) dp[i + 1][j] = dp[i][j]; else dp[i + 1][j] = max(dp[i][j], dp[i][j - w[i]] + v[i]); printf("%d", dp[n][W]);
打表
#include<iostream> using namespace std; int dp[505][505],v[505], w[505]; int main() { int n, W; cin >> n >> W; for (int i = 0; i < n; i++) cin >> w[i] >> v[i]; for (int i = 0; i < n; i++) { for (int j = 0; j <= W; j++) if (j < w[i]) //放不下时不改变重量,价值传递给编号第i+1件物品 dp[i + 1][j] = dp[i][j]; else dp[i + 1][j] = max (dp[i][j], dp[i][j - w[i]] + v[i]); //放的下时就记录最大值 cout<<"当前判断至第"<<i+1<<"个物品:重量为 "<<w[i]<<" 价值为 "<<v[i]<<endl; for (int k1 = 0; k1 <= W; k1++) cout<<k1<<" "; cout<<"(此行为背包当前重量)"<<endl; for (int k1 = 0; k1 <= n; k1++) { for (int k2 = 0; k2 <= W; k2++) cout << dp[k1][k2] << " "; cout << endl; } cout << endl << endl; } cout<<dp[n][W]; return 0; }
完全背包问题:每件物品有无限个。
入门思想:对于完全背包问题,我知道大家很容易想要去用贪心做,因为好像优先取 价值与重量比最大 的策略呼之欲出。这里强调一下千万不要!贪心解决背包问题不可取,可以说是必漏情况。赛场上造成的结果就会是大家WA一发之后就不再看了。
举一个经典的例题:你有面值2元、5元、7元的三种纸币,每种纸币你有无限张。要购买一件27元的商品,如何用最少的纸币数付清?
我们第一反应策略就是一直用最大面值的纸币:结果是7 7 7 2 2 2总共用了6张,而最优解实际为7 5 5 5 5只用了5张。所以,贪心需谨慎~~
然而我们考虑这样一种背包问题:在选择一件物品装入背包时,可以只装入物品的一部分,而不一定要全部装入背包。这是延伸出的另一类背包问题——部分背包,这时就是使用贪心算法求解了,问题也就变成了考贪心算法的基本题,今天不讲。
好,回归正题,回顾我们之前01背包的思想,发现面对完全背包依旧可行。所以我们继续暴力枚举,遍历出每个位置所能得到最大价值,最后的[n][W]位置必然为全局最优解。和01背包一模一样的原因,代码同样是该优化成一维的,交给大家了。
for (int i =0; i < n ; i ++) for (int j =0; j <= W ; j ++) if(j < w [ i ]) dp [ i +1][ j ]= dp [ i ][ j ]; else dp [ i +1][ j ]= max ( dp [ i ][ j ] , dp [ i +1][ j - w [ i ]]+ v [ i ]); printf ( "%d" , dp [ n ][ W ]);
打表
#include<iostream> using namespace std; int dp[505][505], v[505], w[505]; int main() { int n, W; cin >> n >> W; for (int i = 0; i < n; i++) cin >> w[i] >> v[i]; for (int i = 0; i < n; i++) { for (int j = 0; j <= W ; j ++) if (j < w [ i ]) dp [ i + 1][ j ] = dp [ i ][ j ]; else dp [ i + 1][ j ] = max ( dp [ i ][ j ], dp [ i + 1][ j - w [ i ]] + v [ i ]); cout << "当前判断至第" << i + 1 << "个物品:重量为 " << w[i] << " 价值为 " << v[i] << endl; for (int k1 = 0; k1 <= W; k1++) cout << k1 << " "; cout << "(此行为背包当前重量)" << endl; for (int k1 = 0; k1 <= n; k1++) { for (int k2 = 0; k2 <= W; k2++) cout << dp[k1][k2] << " "; cout << endl; } cout << endl << endl; } cout << dp[n][W]; return 0; }
多重背包问题:每种物品有有限不同个。
入门思想:最朴素的思想——既然每种物品有限个,那我们就能把每种物品都摆开,诶嘿,你看它就又变成了标准的01背包问题!所以多重背包的实现就是在01背包的基础上再加一内层循环,但是这样我们的代码就已经有了三层循环,如果某种物品虽然有限但是个数很多,我们交一发就会容易得到一个时间超限。
for(int i = 0; i < n; i++) { int v, w, m; cin >> w >> v >> m; for(int j = W; j >= 0; j--) for(int k = 1; k <= m && k * w <= j; k++) dp[j] = max(dp[j], dp[j - k * w] + k * v); }
printf("%d", dp[W]);
如何优化?——摆开物品的时候别摆太开,从而将一堆物品作为一个整体,进行整体判断选与不选。而为了避免出现之前完全背包提到的问题,具体如何摆是个十分巧妙的活。前辈们总结出来的方法是:摆成log2n份。
问题其实已经抽象成——给定一个数s,问最少可以把s分成多少个数,使得这些数能组成1~s 的所有数。
这个点我觉得说来话长,难以简单的给大家说明白,怕造成误解,所以大家之后有兴趣自己去看吧。然后还有另一种终极的优化方法,用到了单调队列,这里就提一下,超纲了。等大家学到了再说,今天不讲。(但是我要一定要吹一波单调栈和单调队列,涉及线性数据结构的题,这俩学好了基本可以乱杀)
#include<iostream> using namespace std; int dp[50005]; int main() { int n, W; cin >> n >> W; for (int i = 0; i < n; i++) { int v, w, m; cin >> w >> v >> m; for (int k = 1; m > 0; k <<= 1) { int mul = min (k, m); for (int j = W; j >= w * mul; j--) dp[j] = max (dp[j], dp[j - w * mul] + v * mul); m -= mul; } } cout << dp[W]; return 0; }
混合背包问题:物品有多种,每种什么样看题。
当前三种经典背包问题我们都可手撕,那么即便他们混合起来我们也不怕了!
之前提到:多重背包实际上就是01背包的变形,因此我们可以把多重背包和01背包一起处理。若是完全背包直接按照完全背包的模板去做。
那么我们应该做怎样一个处理顺序呢?很容易得到——一定是遇到完全背包就先遍历完把值放在那,反正后续处理出现更大价值的也会给覆盖掉,然后再做一遍01背包,就大功告成啦~
#include<iostream> using namespace std; int n, W, s; int dp[10005], w[1005], v[1005]; int main() { cin >> n >> W; for (int i = 1; i <= n; i++) { int wx, vx, f; cin >> wx >> vx >> f; if (!f) //完全背包 { for (int j = wx; j <= W; j++) dp[j] = max (dp[j], dp[j - wx] + vx); } else { if (f == -1) f = 1; for (int k = 1; k <= f; k <<= 1) //记录01背包 w[++s] = wx * k, v[s] = vx * k, f -= k; if (f) w[++s] = f * wx, v[s] = f * vx; } } for (int i = 1; i <= s; i++) //处理01背包 for (int j = W; j >= w[i]; j--) dp[j] = max (dp[j], dp[j - w[i]] + v[i]); cout << dp[W]; return 0; }
变种背包问题:
二维费用背包问题:经典背包问题上又加一维限制
比如本来我们的背包只有重量的限制,现在又增加了一维体积的限制。入门思想:开辟二维数组,行标和列表分别记录二维的限制条件,在经典背包基础上再加一层遍历循环。
#include<iostream> using namespace std; int dp[505][505]; int main() { int n, W, L; cin >> n >> W >> L; for (int i = 0; i < n; i++) { int v, w, l; cin >> v >> w >> l; for (int j = W; j >= w; j--) for (int k = L; k >= l; k--) dp[j][k] = max (dp[j][k], dp[j - w][k - l] + v); } cout << dp[W][L]; return 0; }
分组背包问题:物品多了组别,每组物品只能选一个
有依赖背包问题:物品能不能选,多了限制条件
背包方案数问题
求背包方案问题
测试数据
4 5 1 2 2 4 3 4 4 5 8
01背包
4 5 1 2 2 4 3 4 1 5 25
完全背包
4 5 1 2 3 2 4 1 3 4 3 4 5 2 10
多重背包
4 5 1 2 -1 2 4 1 3 4 0 4 5 2 8
混合背包 -1对应01、0对应完全
4 5 6 3 1 2 4 2 4 5 3 4 6 4 5 8
二维费用背包
模板:
for(int i = 0; i < n; i++) for(int j = W; j >= w[i]; j--) dp[j] = max(dp[j], dp[j - w[i]] + v[i]); printf("%d", dp[W]);
01背包
for (int i = 0; i < n; i++) { cin >> w >> v; for (int j = w; j <= W ; j++) dp [ j ] = max ( dp[ j ], dp[ j - w] + v); } printf ("%d", dp[W]);
完全背包
for (int i = 0; i < n; i++) { int v, w, m; cin >> w >> v >> m; for (int k = 1; m > 0; k <<= 1) { int mul = min(k, m); for (int j = W; j >= w * mul; j--) dp[j] = max(dp[j], dp[j - w * mul] + v * mul); m -= mul; } } printf("%d", dp[W]);
多重背包