导读
背包问题(Knapsack Problem) 是动态规划中非常经典的一类,一般的解题思路是:状态为前 i 中物品在容量为 j 的背包下,最多可以装的物品总价值,然后遍历物品和背包容量,根据之前的状态不断更新当前状态。本文是背包问题(Knapsack)全解析(上)的后续,主要分析Leetcode上用背包问题思路解的几道例题。
相关题目
322. 零钱兑换:给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
解法如下,设dp[i]表示凑满i元的最少硬币个数,遍历所有的硬币,转移方程就是dp[i] = min(dp[i], dp[i-c]+1)。
var coinChange = function (coins, amount) {
let dp = Array(amount + 1).fill(Infinity);
dp[0] = 0;
for (let i = 1; i <= amount; i++) {
for (let c of coins) {
if (i - c >= 0) {
dp[i] = Math.min(dp[i], dp[i - c] + 1);
}
}
}
return dp[amount] === Infinity ? -1 : dp[amount];
};
474. 一和零:现在,假设你分别支配着 m 个 0 和 n 个 1。另外,还有一个仅包含 0 和 1 字符串的数组。 你的任务是使用给定的 m 个 0 和 n 个 1 ,找到能拼出存在于数组中的字符串的最大数量。每个 0 和 1 至多被使用一次。
输入: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3
输出: 4
解释: 总共 4 个字符串可以通过 5 个 0 和 3 个 1 拼出,即 "10","0001","1","0" 。
定义dp[i][j][k]表示,前k个字符串,给定分别给i个0和j个1,最多可以拼出的字符串数量。对于当前这个字符串,我们可以选择拼或者不拼,转移方程如下,其中假设当前这个字符串需要a个0和b个1,
dp[i][j][k] = Math.max(dp[i-a][j-b][k-1]+1,dp[i][j][k-1])
代码如下:
/**
* @param {string[]} strs
* @param {number} m
* @param {number} n
* @return {number}
*/
var findMaxForm = function(strs, m, n) {
let len = strs.length;
let dp = Array(m+1).fill().map(()=>Array(n+1).fill().map(()=>Array(len).fill(0)));
// count数组记录了每个字符串中包含了多少个0和1,
// count[i][0]表示的是0的数量,count[i][1]表示的是1的数量
let count = [];
for(let i=0;i<strs.length;i++){
let cur = strs[i];
let tmp = [0,0];
for(let k=0;k<cur.length;k++){
if(cur[k] === "0"){
tmp[0]++;
}else{
tmp[1]++;
}
}
count.push(tmp);
}
for(let k=1;k<=len;k++){
for(let i=0;i<=m;i++){
for(let j=0;j<=n;j++){
let [a,b] = count[k-1];
dp[i][j][k] = dp[i][j][k-1];
if(i - a >= 0 && j - b >= 0){
dp[i][j][k] = Math.max(dp[i][j][k],dp[i-a][j-b][k-1]+1);
}
}
}
}
return dp[m][n][len];
};
494. 目标和:给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。 返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
输入:nums: [1, 1, 1, 1, 1], S: 3
输出:5
解释:
-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3
一共有5种方法让最终目标和为3。
把这里的 + 看成背包问题中取某个物品,- 看成背包问题中不取某个物品,和背包问题唯一的区别就是,这里不取物品,是要减去物品重量的。因为引入了减去这个操作,因此最终的获得的价值可以是负数,最小为 - sum(nums),即物品和的负数,最大可以是 + sum(nums)。于是我们的状态定义为dp[i][j]表示,前i个数字,结果为j的种数。这里j可以是负数,在代码实现的时候,用字典来代替数组可以避免负数下标的问题。转移方程就是:
dp[i][j] += dp[i-1][j-nums[i-1]]
dp[i][j] += dp[i-1][j+nums[i-1]]
初始情况,dp[0][0] = 1,这表示,0个数,组成和为0的情况,有1种解法。完整的代码如下:
var buildDict = function (sum) {
let dict = {};
for (let i = -sum; i <= sum; i++) {
dict[i] = 0;
}
return dict;
};
// 注意到,这里是有负数的,dp[1][-a],这个是有可能为1的
var findTargetSumWays = function (nums, S) {
let sum = nums.reduce((s, n) => s + n, 0);
let n = nums.length;
let dp = Array(2).fill().map(() => buildDict(sum));
dp[0][0] = 1;
for (let i = 1; i <= n; i++) {
dp[i % 2] = buildDict(sum);
for (let j = -sum; j <= sum; j++) {
if (j - nums[i - 1] >= -sum) {
dp[i % 2][j] += dp[(i - 1) % 2][j - nums[i - 1]];
}
}
for (let j = sum; j >= -sum; j--) {
if (j + nums[i - 1] <= sum) {
dp[i % 2][j] += dp[(i - 1) % 2][j + nums[i - 1]];
}
}
}
return dp[n % 2][S] ? dp[n % 2][S] : 0
};
139. 单词拆分:给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。 说明: 拆分时可以重复使用字典中的单词。 你可以假设字典中没有重复的单词。
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。
这道题的解法有很多,BFS, DFS,都可以解,用动态规划的话,我们可以定义状态dp[i]表示前i个单词是否可以正好拆分成所有字典中的词,dp[i]如果是true的话,那么它一定是从前面某个dp[j]为true的状态转移过来的,且满足从 j -> i组成的字符串(前闭后开区间,跟substring方法一致)在字典中。于是代码如下
var wordBreak = function (s, wordDict) {
let set = new Set(wordDict);
let maxLen = Math.max(...wordDict.map(item => item.length));
let dp = Array(s.length + 1).fill(false);
dp[0] = true;
for (let i = 1; i <= s.length; i++) {
for (let j = i - 1; j >= i - maxLen; j--) {
if (set.has(s.substring(j, i))) {
dp[i] = dp[j] || dp[i];
}
}
}
return dp[s.length]
};
全部题目及链接
上篇
-
[799\. 背包问题VIII](https://www.lintcode.com/problem/backpack-viii/description)
-
[800\. 背包问题 IX](https://www.lintcode.com/problem/backpack-ix/description)
-
[801\. 背包问题X](https://www.lintcode.com/problem/backpack-x/description)
</div>
-
[971\. 剩余价值背包](https://www.lintcode.com/problem/surplus-value-backpack/description)
</div>
-
[1382\. 大容量背包](https://www.lintcode.com/problem/high-capacity-backpack/description)
下篇