导语:java
背包问题能够说是一个老生常谈的问题,一般被用做面试题来考查面试者对动归的理解,咱们常常说学算法,初学者最难理解的就是 “二归”,一个叫递归,另外一个叫动归。背包问题属于特殊的一类动归问题,也就是按值动归,这篇文章我会列举一些常见的背包问题,涵盖 0-1 背包,彻底背包,以及 多重背包。我同时会分享一些经典的题目帮助理解其中的思路与解题技巧。本文是参考了网上广为人知的 “背包九讲” 一书,文末会有下载连接。git
一般背包这一类题目,题目大概就是给你一个容量或者大小固定的背包,而后要求你去用这个背包去装物品,通常来讲这些物品都是大小固定的,可是题目对物品的限定不一样,衍生出来多种背包问题,例如 0-1 背包 问题中,物品个数有且仅有一个;彻底背包 问题中的物品个数是无限的;多重背包 问题中的针对不一样的物品,个数不同。一般题目会要你求出背包能装的最大价值(每一个物品都会有容量和价值),固然也会有不同的问法,相似背包可否被装满,还有背包能装的最大容量是多少,多少种方式填满背包。可是这些并非背包问题的全部,还有 分组背包 问题,依赖背包 问题等等,由于考虑到这篇文章主要是针对面试,而不是竞赛,这些有机会再去介绍。github
题目:
面试
有N件物品和一个容量为V的背包。放入第i件物品耗费的费用是C[i] ,获得的价值是W[i]。求解将哪些物品装入背包可以使价值总和最大。求出最大总价值算法
分析:
数组
对于每个物品能够考虑放,或者不放;若是当前是第 i 个物品,当前背包里面物品总价值是,背包当前容量是
,若是取这个物品,背包总价值会变成
,背包容量会变成
。以前咱们提到过,背包是属于按值动归,咱们把背包划分为 1-V 个区间,也就是背包全部可能的大小,而后针对全部的物品,看看每一个背包容量下能存放的最大价值,代码以下:app
public static int zeroOnePack(int V, int[] C, int[] W) {
// 防止无效输入
if ((V <= 0) || (C.length != W.length)) {
return 0;
}
int n = C.length;
// dp[i][j]: 对于下标为 0~i 的物品,背包容量为 j 时的最大价值
int[][] dp = new int[n + 1][V + 1];
// 背包空的状况下,价值为 0
dp[0][0] = 0;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= V; ++j) {
// 不选物品 i 的话,当前价值就是取到前一个物品的最大价值,也就是 dp[i - 1][j]
dp[i][j] = dp[i - 1][j];
// 若是选择物品 i 使得当前价值相对不选更大,那就选取 i,更新当前最大价值
if ((j >= C[i - 1]) && (dp[i][j] < dp[i - 1][j - C[i - 1]] + W[i - 1])) {
dp[i][j] = dp[i - 1][j - C[i - 1]] + W[i - 1];
}
}
}
// 返回,对于全部物品(0~N),背包容量为 V 时的最大价值
return dp[n][V];
}
复制代码
优化:
less
空间优化:
ide
仅仅看代码就能够发现,其实 dp 数组当前行的计算只用到了前一行,咱们能够利用 滚动数组 来优化,可是再仔细看下去的话,你就会发现其实还能够更优,当前行的遍历用到的值是上一行的前面列的值,若是咱们第二层 for 循环遍历的时候倒着遍历的话,保证了前面更新的值不会被新计算的值覆盖掉,咱们仅仅用一维数组就能够完美解决问题,代码以下:优化
public static int zeroOnePackOpt(int V, int[] C, int[] W) {
// 防止无效输入
if ((V <= 0) || (C.length != W.length)) {
return 0;
}
int n = C.length;
int[] dp = new int[V + 1];
// 背包空的状况下,价值为 0
dp[0] = 0;
for (int i = 0; i < n; ++i) {
for (int j = V; j >= C[i]; --j) {
dp[j] = Math.max(dp[j], dp[j - C[i]] + W[i]);
}
}
return dp[V];
}
复制代码
极端状况优化:
当背包的 V 特别大的时候,对于每个物品都去遍历一遍没有意义,经过阈值来进行优化,优化的同时能够考虑将数组从大到小排个序:
public static int zeroOnePackOpt(int V, int[] C, int[] W) {
// 防止无效输入
if ((V <= 0) || (C.length != W.length)) {
return 0;
}
int n = C.length;
int[] dp = new int[V + 1];
int bound, sum = 0, total = 0;
for (int i : C) {
total += i;
}
for (int i = 0; i < n; ++i) {
bound = Math.max(V - total + sum, C[i]);
sum += C[i];
for (int j = V; j >= bound; --j) {
dp[j] = Math.max(dp[j], dp[j - C[i]] + W[i]);
}
}
return dp[V];
}
复制代码
0-1 背包 基本概况就是这些,固然可能问题的问法会不同,例如:
背包能不能被装满
解题思路就是将 int 数组换成 boolean 数组,也不用去考虑物品的价值来,直接看容量够不够,能不能装进背包便可
背包能装的最大容量
也很简单,解法和上面 “背包能不能被装满” 同样,只不过最后须要从后往前遍历 dp 数组,直到找到 true
多少种方式塞满背包
一样是不用考虑物品的价值,用 int 数组,可是里面记录的是个数,背包被填充的个数,也就是把这里的个数看成价值来看待,只不过 W[i] = 1。
下面是以前作过的一些关于 0-1 背包 的题目,我会给出相应的思路,能够试着写写,但愿对你有帮助:
1、Need A offer
Speakless很早就想出国,如今他已经考完了全部须要的考试,准备了全部要准备的材料,因而,便须要去申请学校了。要申请国外的任何大学,你都要交纳必定的申请费用,这但是很惊人的。Speakless没有多少钱,总共只攒了n万美圆。他将在m个学校中选择若干的(固然要在他的经济承受范围内)。每一个学校都有不一样的申请费用a(万美圆),而且Speakless 估计了他获得这个学校offer的可能性b。不一样学校之间是否获得offer不会互相影响。“I NEED A OFFER”,他大叫一声。帮帮这个可怜的人吧,帮助他计算一下,他能够收到至少一份offer的最大几率。(若是Speakless选择了多个学校,获得任意一个学校的offer均可以)。
第一题思路:
0-1 背包 基础问题,这里背包大小就是 n 万美圆,物品就是 m 所学校,申请费 a 是物品的容量,可能性 b 是物品的价值。可是有一点须要注意的是计算可能性的时候,不能仅仅取最大值,正确的方法应该是
2、饭卡
电子科大本部食堂的饭卡有一种很诡异的设计,即在购买以前判断余额。若是购买一个商品以前,卡上的剩余金额大于或等于5元,就必定能够购买成功(即便购买后卡上余额为负),不然没法购买(即便金额足够)。因此你们都但愿尽可能使卡上的余额最少。某天,食堂中有n种菜出售,每种菜可购买一次。已知每种菜的价格以及卡上的余额,问最少可以使卡上的余额为多少。
第二题思路:
背包大小是卡里面的余额,物品大小就是菜的价格。从 “每种菜可购买一次” 看出这实际上是一个 0-1 背包 问题,这里比较诡异的一点是,背包容量能够超,可是仅限于最后一个物品。因而这里有一个思路上的变迁就是如何把这个诡异的背包问题变成正常的背包问题,若是能想到的话,其实不难,就是用 5 块钱去买最贵的那道菜,而后 “卡上余额 - 5” 做为背包的容量,其他的菜做为物品,而后这道题的问法就是 “背包能装的最大容量是多少”,最后的答案是 “(5 - 最贵的菜) + (卡上总余额 - 5 - 该背包问题的解)”
你能够看得出来得是,这里花了很大得篇幅来将 0-1背包 问题,的确,0-1 背包 是最简单的背包问题,可是是其余背包问题的基础,这种解题思路,以及这里提到的一些写代码上面的优化技巧是能够在其余的背包问题上复用的。
题目
有 N 种物品和一个容量为 V 的背包,每种物品都有无限件可用。放入第 i 种物品 的费用是 C[i],价值是 W[i]。求解:将哪些物品装入背包,可以使这些物品的耗费的费用总和不超过背包容量,且价值总和最大。求解这个最大价值
分析
和以前的 0-1 背包 不一样的是,彻底背包 中的物品能够任意取多少都行,若是对 0-1 背包 理解的话,这里在写代码的时候只须要作一点的改变,就是在决定取不取当前物品的时候,以前 0-1 背包 是和以前不考虑当前物品的子问题结果作对比和更新,而 彻底背包 相反,是和考虑当前物品的子问题作对比,代码实现以下:
public static int completePack(int V, int[] C, int[] W) {
// 防止无效输入
if (V == 0 || C.length != W.length) {
return 0;
}
int n = C.length;
// dp[i][j]: 对于下标为 0~i 的物品,背包容量为 j 时的最大价值
int[][] dp = new int[n + 1][V + 1];
// 背包空的状况下,价值为 0
dp[0][0] = 0;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= V; ++j) {
// 不取该物品
dp[i][j] = dp[i - 1][j];
// 取该物品,可是是在考虑过或者取过该物品的基础之上(dp[i][...])取
// 0-1背包则是在尚未考虑过该物品的基础之上(dp[i - 1][...])取
if ((j >= C[i - 1]) && (dp[i][j - C[i - 1]] + W[i - 1] > dp[i][j])) {
dp[i][j] = dp[i][j - C[i - 1]] + W[i - 1];
}
}
}
// 返回,对于全部物品(0~N),背包容量为 V 时的最大价值
return dp[n][V];
}
复制代码
优化
空间和以前同样,也是能够优化到一维数组,可是这里要注意的是,彻底背包 考虑的值再也不是更新前的值了(dp[i - 1][...]),而是更新后的值(dp[i][...]),所以这时咱们的第二层 for 循环须要从前日后遍历,保证当前考虑的值都是更新事后的值,代码以下:
public static int completePackOpt(int V, int[] C, int[] W) {
if (V == 0 || C.length != W.length) {
return 0;
}
int n = C.length;
int[] dp = new int[V + 1];
for (int i = 0; i < n; ++i) {
for (int j = C[i]; j <= V; ++j) {
dp[j] = Math.max(dp[j], dp[j - C[i]] + W[i]);
}
}
return dp[V];
}
复制代码
对于 彻底背包 问题,也会有不同的问法,可是和 0-1背包 问题相似,能够参照前面的内容,这里不作赘述,这里还有一道关于 彻底背包 的题目能够巩固练习:
Piggy-bank
Before ACM can do anything, a budget must be prepared and the necessary financial support obtained. The main income for this action comes from Irreversibly Bound Money (IBM). The idea behind is simple. Whenever some ACM member has any small money, he takes all the coins and throws them into a piggy-bank. You know that this process is irreversible, the coins cannot be removed without breaking the pig. After a sufficiently long time, there should be enough cash in the piggy-bank to pay everything that needs to be paid. But there is a big problem with piggy-banks. It is not possible to determine how much money is inside. So we might break the pig into pieces only to find out that there is not enough money. Clearly, we want to avoid this unpleasant situation. The only possibility is to weigh the piggy-bank and try to guess how many coins are inside. Assume that we are able to determine the weight of the pig exactly and that we know the weights of all coins of a given currency. Then there is some minimum amount of money in the piggy-bank that we can guarantee. Your task is to find out this worst case and determine the minimum amount of cash inside the piggy-bank. We need your help. No more prematurely broken pigs!
解题思路:
最基本的 彻底背包 问题,背包大小是存钱罐的重量减去猪的重量,也就是里面装的钱币的总重量,钱币就是物品,每种钱币能够有无数个,可是注意这里要求的是总价值的最小值。
题目
有 N 种物品和一个容量为 V 的背包。第 i 种物品最多有 M[i] 件可用,每件耗费的 空间是 C[i],价值是 W[i]。求解将哪些物品装入背包可以使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。求解该价值
分析
若是仅仅是要解决问题的话,其实很是简单,咱们能够把这个问题变成 0-1 背包 问题去作,我这里就直接套用 0-1 背包 优化的版本,代码以下:
public static int multiplePack1(int V, int[] C, int[] W, int[] M) {
int n = C.length;
int[] dp = new int[V + 1];
for (int i = 0; i < n; ++i) {
for (int k = 0; k < M[i]; ++k) {
// consider about zeroOnePack
for (int j = V; j >= C[i]; --j) {
dp[j] = Math.max(dp[j], dp[j - C[i]] + W[i]);
}
}
}
return dp[V];
}
复制代码
优化
上面代码的时间复杂度是,就是咱们其实对每一个物品,无论相同与否,都遍历了一遍。优化的话,能够这样思考,若是某个物品的总重量(C[i] * M[i] > V),那咱们其实就能够考虑使用 彻底背包 了,这样的话,对于这个物品的计算时间上从以前的 V * M[i] 变成了 V,若是 M[i] 很大的话,时间上的节省仍是很可观的;另一个优化就是在作 0-1 背包 的时候,这里有一个物品拆分的小优化,利用的是二进制的思想,假如 M[i] = 16,按照以前的思路,作 0-1 背包 须要的时间是 16 * V,可是这里的 16 能够拆成,1 + 2 + 4 + 8 + 1,也就是说把这 16 个物品缩减成了 5 个物品,固然对应物品的价值等于分配的数量乘上单个物品的价值,这样遍历时间缩减为 5 * V,其实就是将以前时间中的 M 变成了
。这两个优化把时间复杂度降为
,代码以下:
public static int multiplePackOpt(int V, int[] C, int[] W, int[] M) {
int n = C.length;
int[] dp = new int[V + 1];
for (int i = 0; i < n; ++i) {
// go completePack
if (C[i] * M[i] >= V) {
for (int j = C[i]; j <= V; ++j) {
dp[j] = Math.max(dp[j], dp[j - C[i]] + W[i]);
}
}
// go zeroOnePack
else {
// 1, 2, 2^2, 2^3,..., 2^(k-1), M[i] - 2^k + 1
int k = 1, tmp = M[i];
while (k < tmp) {
for (int j = V; j >= k * C[i]; --j) {
dp[j] = Math.max(dp[j], dp[j - k * C[i]] + k * W[i]);
}
tmp -= k;
k *= 2;
}
for (int j = V; j >= tmp * C[i]; --j) {
dp[j] = Math.max(dp[j], dp[j - tmp * C[i]] + tmp * W[i]);
}
}
}
return dp[V];
}
复制代码
你能够看到,对于 多重背包 问题,难点是在优化上面,该问题综合考虑了前面提到的 0-1 背包 问题和 彻底背包 问题,这样的优化思路第一次看上去很陌生,可是写的多了就不奇怪了,其实就是数学上面拆分数字的小技巧,同时这里也有几道题想和你分享,但愿能帮你巩固对背包问题的认识:
1、Coins
Whuacmers use coins. They have coins of value A1,A2,A3...An Silverland dollar. One day Hibix opened purse and found there were some coins. He decided to buy a very nice watch in a nearby shop. He wanted to pay the exact price(without change) and he known the price would not more than m. But he didn't know the exact price of the watch. You are to write a program which reads n,m,A1,A2,A3...An and C1,C2,C3...Cn corresponding to the number of Tony's coins of value A1,A2,A3...An then calculate how many prices(form 1 to m) Tony can pay use these coins.
第一题思路:
基本的多重背包问题,可是参考以前在 0-1 背包 问题中讲到的变形问法 “背包能不能被装满”
2、Are You Busy
As having become a junior, xiaoA recognizes that there is not much time for her to AC problems, because there are some other things for her to do, which makes her nearly mad. What's more, her boss tells her that for some sets of duties, she must choose at least one job to do, but for some sets of things, she can only choose at most one to do, which is meaningless to the boss. And for others, she can do of her will. We just define the things that she can choose as "jobs". A job takes time , and gives xiaoA some points of happiness (which means that she is always willing to do the jobs). So can you choose the best sets of them to give her the maximum points of happiness and also to be a good junior(which means that she should follow the boss's advice)?
第二题思路:
算是多重背包的变形题目,xiaoA 全部的时间就是背包的容量,有三种物品,第一种是必须至少选一件,第二种是必须最多选一件,第三种是随便怎么选均可以,每一个物品也会有价值,价值就是成就感。这里遍历的顺序很重要,先是遍历必须至少选一件的状况,而后是随便选,最后是最多选一件的,要注意的是在后面两次(随便选,最多选一件)的遍历中,要明确当前的背包值是否选了至少选一件这一类中的物品。
以上即是我想要分享的全部背包问题,其实背包问题仍是比较常见的一类动态规划问题,这样子的分类或许对理解题目,寻找突破口颇有帮助,固然别忘了勤于练习,这里的分享是基于背包九讲这一书的,最后会有下载连接,还有,若是以为我有讲的不到位的地方,欢迎指出,谢谢