切换主题
背包问题篇
0-1背包
- 什么是
0-1背包呢?
我们有
n件物品和一个容量为C的背包,记第i件物品的重量为w[i],价值为v[i],求将哪些物品装入背包可使价值总和最大?
如果限定每件物品最多只能选取1次(即0或1次),则问题称为0-1背包问题。
通用情况:
- 状态定义:
dp[i][j] = x,表示前i件物品放入容量为j的背包时,可以获取的最大价值为x; base case:根据情况分析;- 状态转移方程:
dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j - w[i]] + v[i]),其中j - w[i]必须大于等于0。
- 状态定义:
遍历技巧:
JavaScript// 1. 非组合问题(包括不需考虑元素之间的顺序的组合问题) for (const num of nums) { // 外层循环遍历 物品 for (let j = cap; j >= num; j--) { // 内层循环遍历 背包(0-1背包倒序遍历) // 状态转移方程 } } // 2. 组合问题(需考虑元素之间的顺序) for (let j = num; j <= cap; j++) { // 外层循环遍历 背包 for (const num of nums) { // 内层循环遍历 物品 // 状态转移方程 } }0-1背包常见的几类问题:- 组合问题 ==> 状态转移方程:
dp[j] += dp[j-num]; - 存在问题 ==> 状态转移方程:
dp[j] ||= dp[j-num]; - 最值问题 ==> 状态转移方程:
dp[j] = Math.max(dp[j], dp[j-num] + 1) or Math.min(dp[j], dp[j-num] + 1)。
- 组合问题 ==> 状态转移方程:
分割等和子集
- 状态定义:
dp[i][j] = true/false,表示从[0, i]这个子区间内挑选一些正整数(每个数只能用一次),使得这些数的和恰好等于/不等于j; base case:dp[0][0] = true;- 状态转移方程:
dp[i][j] = dp[i-1][j] || dp[i-1][j - nums[i]]。
JavaScript
const canPartition = nums => {
const sum = nums.reduce((acc, cur) => acc + cur, 0)
if (sum & 1) return false
const cap = sum >> 1
const dp = Array(cap + 1).fill(0)
dp[0] = true
for (const num of nums) {
for (let j = cap; j >= num; j--) {
dp[j] ||= dp[j - num]
}
}
return dp[cap]
}时间复杂度:O(n*cap);空间复杂度:O(cap)。
目标和
- 状态定义:
dp[i][j] = x,表示前i个数能凑出和为j的方法有x种; base case:dp[0][0] = 1;- 状态转移方程:
dp[i][j] = dp[i-1][j] + dp[i-1][j - num[i-1]]。
JavaScript
const findTargetSumWays = (nums, target) => {
const sum = nums.reduce((acc, cur) => acc + cur, target)
if (sum & 1) return false
const cap = sum >> 1
const dp = Array(cap + 1).fill(0)
dp[0] = 1
for (const num of nums) {
for (let j = cap; j >= num; j--) {
dp[j] += dp[j - num]
}
}
return dp[cap]
}时间复杂度:O(n*cap);空间复杂度:O(cap)。
最后一块石头的重量 II
- 状态定义:
dp[i][j] = x,表示在前i块石头中选取石头且总重量不超过j的情况下的最大总重量为x; base case:dp[0][0] = 0;- 状态转移方程:
dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j - stones[i-1]] + stones[i-1])。
JavaScript
const lastStoneWeightII = stones => {
let sum = stones.reduce((acc, cur) => acc + cur, 0)
const cap = sum >> 1
const dp = Array(cap + 1).fill(0)
for (const stone of stones) {
for (let j = cap; j >= stone; j--) {
dp[j] = Math.max(dp[j], dp[j - stone] + stone)
}
}
return sum - 2 * dp[cap]
}时间复杂度:O(n*cap);空间复杂度:O(cap)。
一和零
- 状态定义:
dp[i][j][k] = x,表示前i个字符串中选出若干个,在使得其中0的数目不超过j、1的数目不超过k的情况下能够选出的最多的字符串数目为x; base case:dp[0][0][0] = 0;- 状态转移方程:
dp[i][j][k] = Math.max(dp[i-1][j][k], dp[i-1][j - cntZero][k - cntOne] + 1)。
JavaScript
const findMaxForm = (strs, m, n) => {
const cnt = (str, num = 0) => {
for (const char of str) {
if (char === '0') num++
}
return num
}
const dp = Array(m + 1).fill(0).map(_ => Array(n + 1).fill(0))
for (const str of strs) {
const cntZero = cnt(str)
const cntOne = str.length - cntZero
for (let j = m; j >= cntZero; j--) {
for (let k = n; k >= cntOne; k--) {
dp[j][k] = Math.max(dp[j][k], dp[j - cntZero][k - cntOne] + 1)
}
}
}
return dp[m][n]
}时间复杂度:O(len*mn);空间复杂度:O(mn)。
不同的子序列
问题转化为用str提供的石头填满容量为cap的背包,每个石头需要按顺序选择且只能选择一次,因此是一个0-1背包问题。
- 状态定义:
dp[i][j] = x,表示str中的前i个字符能够构成tar中前j个字符的种类数为x; base case:dp[i][0] = 1;- 状态转移方程:
dp[i][j] = dp[i-1][j] + dp[i-1][j - 1]。
JavaScript
const numDistinct = (str, tar) => {
const cap = tar.length
const dp = Array(cap + 1).fill(0)
dp[0] = 1
for (const char of str) {
for (let j = cap; j > 0; j--) {
if (char === tar.charAt(j - 1)) {
dp[j] += dp[j - 1]
}
}
}
return dp[cap]
}时间复杂度:O(n*cap);空间复杂度:O(cap)。
完全背包
什么又是完全背包呢?
如果每件物品最多可以选取
无限次,则问题称为完全背包问题。
- 状态定义:
dp[i][j] = x,表示前i件物品放入容量为j的背包时,可以获取的最大价值为x;base case:根据情况分析;- 状态转移方程:
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-w[i]] + v[i])。
零钱兑换
- 状态定义:
dp[i][j] = x,表示从前i种硬币中组成金额j所需最少的硬币数量为x; base case:dp[0][0] = 0;- 状态转移方程:
dp[i][j] = Math.min(dp[i-1][j], dp[i][j - coins[i-1]] + 1)。
JavaScript
const coinChange = (coins, cap) => {
const dp = Array(cap + 1).fill(Infinity)
dp[0] = 0
for (const coin of coins) {
for (let j = coin; j <= cap; j++) {
dp[j] = Math.min(dp[j], dp[j - coin] + 1)
}
}
return dp[cap] === Infinity ? -1 : dp[cap]
}时间复杂度:O(n*cap);空间复杂度:O(cap)。
零钱兑换 II
- 状态定义:
dp[i][j] = x,表示从前i种硬币中凑出金额j的硬币组合数为x; base case:dp[0][0] = 1,即空集合;- 状态转移方程:
dp[i][j] = dp[i-1][j] + dp[i][j - coins[i-1]]。
JavaScript
const change = (cap, coins) => {
const dp = Array(cap + 1).fill(0)
dp[0] = 1
for (const coin of coins) {
for (let j = coin; j <= cap; j++) {
dp[j] += dp[j - coin]
}
}
return dp[cap]
}时间复杂度:O(n*cap);空间复杂度:O(cap)。
组合总和 Ⅳ
- 状态定义:
dp[i][j] = x,表示从前i个数能凑出和为j的组合数为x; base case:dp[0][0] = 1;- 状态转移方程:
dp[i][j] = dp[i-1][j] + dp[i][j - nums[i-1]]。
JavaScript
const combinationSum4 = (nums, cap) => {
const dp = Array(cap + 1).fill(0)
dp[0] = 1
for (let j = 1; j <= cap; j++) {
for (const num of nums) {
if (j >= num) {
dp[j] += dp[j - num]
}
}
}
return dp[cap]
}时间复杂度:O(n*cap);空间复杂度:O(cap)。
完全平方数
- 状态定义:
dp[i][j] = x,从前i个完全平方数中凑出整数j所需最少的完全平方数的个数为x; base case:dp[0][0] = 0,即不选任何完全平方数即可得到数值0;- 状态转移方程:
dp[i][j] = Math.min(dp[i-1][j], dp[i][j - num*num] + 1)。
JavaScript
const numSquares = n => {
const dp = Array(n + 1).fill(Infinity)
dp[0] = 0
const N = n * n
for (let num = 1; num <= N; num++) {
const I = num * num
for (let j = I; j <= n; j++) {
dp[j] = Math.min(dp[j], dp[j - I] + 1)
}
}
return dp[n]
}时间复杂度:O(n*Math.sqrt(n));空间复杂度:O(n)。