Knapsack Problem 就是大名鼎鼎的“背包问题”,其中又以“01背包”为大家所熟知。这里记录一下在AcWing上学习的一些背包问题相关的解法。
先来看一段来自维基百科的介绍:
背包问题(Knapsack problem)是一种组合优化的NP完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。问题的名称来源于如何选择最合适的物品放置于给定背包中。
相似问题经常出现在商业、组合数学,计算复杂性理论、密码学和应用数学等领域中。
也可以将背包问题描述为决定性问题,即在总重量不超过W的前提下,总价值是否能达到V。
定义
我们有 n 种物品,物品 j 的重量为\(w_j\),价格为\(p_j\)。 我们假定所有物品的重量和价格都是非负的。背包所能承受的最大重量为W。 如果限定每种物品只能选择0个或1个,则问题称为0-1背包问题。
可以用公式表示为: \[max: \sum_{j = 1}^{n} p_ix_i\] \[ s.t.: \sum_{j=1}^n w_ix_i \leqslant W, x_j \in \{0,1\}\]
如果限定物品 j 最多只能选择 \(b_j\)个,则问题称为有界背包问题
可以用公式表示为: \[max: \sum_{j = 1}^{n} p_ix_i\] \[ s.t.: \sum_{j=1}^n w_ix_i \leqslant W, x_j \in \{0,1,...,b_j\}\]
如果不限定每种物品的数量,则问题称为无界背包问题。
各类复杂的背包问题总可以变化为简单的0-1背包问题进行求解。
计算复杂度
在计算机科学领域,人们对背包问题感兴趣的原因在于:
背包问题在我们的生活中是那么的常见(在面试题中也一样),直白的问题和经典的动态规划解法,使得背包问题显得那么重要而基础。
先来看来自于左程云的《程序员代码面试指南》中的例子
1. 换钱的最少货币数 I
给定货币面值,存放于 cash
数组中。求对于指定的钱数
aim
, 最少需要几张货币。若不能换,则返回-1。
1 | Input: |
由于每个面值的货币都可以使用无数次(即无界背包问题),所以对于每一个面值的货币,我们都可以从最小的钱数开始,每次判断这个数减去面值后所需要的货币数,同时更新该钱数需要的最少货币数。
即转移公式为:
\[ dp[k] = min(dp[k], dp[k-cash[i]]+ 1) \]
如若输入为cash = {2,5}, aim = 6
:
最开始时,面值为2的货币,钱为2需要1张,3不行,4需要2张,5不行,6需要3张
然后面值为5的货币,则钱为5需要1张,6需要2张,这个时候就更新6元的货币数,从3改为2。
代码:
1 |
|
上面的例题是货币可以无限取,当只能有限取(即有界背包问题)时,又改怎么变化呢?
再来个例子:
2. 换钱的最少货币数 II
给定你现在有限的钱数,存放于 cash
数组中。求对于指定的钱数 aim
,
最少需要几张货币。若不能换(或钱不够),则返回 -1。
1 | Input: |
其实解答的思路和上个例子一模一样,也是用一个数组来存放已经计算好的最少需要的货币数。唯一的不同在于,我们的钱只能用一次,很现实。
注意在上一个例子中,我们重复使用钱的关键在于这一句:
1 | for (int sum = cash[i]; sum <= aim; ++sum) |
即我们是从小加到大,从而保证了使用某货币c
时,大的钱数sum
可以由小的钱数sum-c
那跳转,而sum-c
我们已经计算过了,所以大的钱数sum
可以重复使用该货币。
那么如果要让钱不能重复使用,我们就从大往小算就行了,这样,每次计算,前面的最小货币数必定是没有使用当前货币的情况。
代码:
1 | int minCash(const vector<int> &cash, int aim) { |
看,其实差距只有一行。
3. 换钱的方法数
假定你现在有面值不同的货币,存放在 cash
数组中,每种货币可以用无数次。求对于一个给定的钱数 aim
,有多少中换钱的方法。
1 | Input: |
题目和第一个例子很相似,不同之处在于求解的目标不一样,所以我们动态规划使用的
dp
数组的含义也不一样。
对于例子“换钱的最少货币数 I”, dp
数组的含义是当前状态下,每个数需要的最少货币数;
对于例子“换钱的方法数”, dp
数组的含义是当前状态下,每个数对应的换钱方法数。
转移公式:
\[dp[k] = dp[k]+dp[k-cach[i]]\]
代码:
1 | int countChanges(const vector<int> &cash, int aim) { |
可以看到这里的转移公式和经典的爬梯子问题很像。
讲的很浅,以后有机会再补充例子。
最后再用一道实际的腾讯笔试题来结尾吧。
数字拼凑 小Q有一个长度为n的序列S,序列中有n个正整数。小Q现在和你玩数字游戏,每次小Q说出一个正整数X,你需要从序列中挑选出若干个数字出来进行相加,使结果等于X,如果你不能完成拼凑你就输了。
小Q现在想知道能让你输掉游戏的最小的正整数X是多少?
1 | 输入描述: |