切换主题
背包问题篇
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)
。