背包问题
背包问题是一类经典的动态规划问题,包括 0-1 背包、完全背包、多重背包等变体。本文将详细介绍各种背包问题的解法和优化技巧。
1. 0-1 背包问题
1.1 基本问题
问题描述:有 n 个物品,每个物品有重量 w[i] 和价值 v[i]。现在有一个容量为 W 的背包,求解如何选择物品放入背包,使得物品的总价值最大。每个物品只能选择一次。
javascript
function knapsack01(weights, values, capacity) {
const n = weights.length;
// dp[i][j] 表示前 i 个物品,容量为 j 时的最大价值
const dp = Array(n + 1).fill().map(() => Array(capacity + 1).fill(0));
for (let i = 1; i <= n; i++) {
for (let j = 0; j <= capacity; j++) {
if (weights[i-1] <= j) {
// 可以放入第 i 个物品
dp[i][j] = Math.max(
dp[i-1][j], // 不放入
dp[i-1][j-weights[i-1]] + values[i-1] // 放入
);
} else {
// 不能放入第 i 个物品
dp[i][j] = dp[i-1][j];
}
}
}
return dp[n][capacity];
}
1.2 空间优化
由于每个状态只依赖于上一行的状态,我们可以使用一维数组进行空间优化:
javascript
function knapsack01Optimized(weights, values, capacity) {
const n = weights.length;
const dp = Array(capacity + 1).fill(0);
for (let i = 0; i < n; i++) {
for (let j = capacity; j >= weights[i]; j--) {
dp[j] = Math.max(
dp[j],
dp[j - weights[i]] + values[i]
);
}
}
return dp[capacity];
}
1.3 输出选择的物品
javascript
function knapsack01WithItems(weights, values, capacity) {
const n = weights.length;
const dp = Array(n + 1).fill().map(() => Array(capacity + 1).fill(0));
const selected = Array(n).fill(false);
// 填充 dp 数组
for (let i = 1; i <= n; i++) {
for (let j = 0; j <= capacity; j++) {
if (weights[i-1] <= j) {
dp[i][j] = Math.max(
dp[i-1][j],
dp[i-1][j-weights[i-1]] + values[i-1]
);
} else {
dp[i][j] = dp[i-1][j];
}
}
}
// 回溯找出选择的物品
let i = n, j = capacity;
while (i > 0 && j > 0) {
if (dp[i][j] !== dp[i-1][j]) {
selected[i-1] = true;
j -= weights[i-1];
}
i--;
}
return {
maxValue: dp[n][capacity],
selected
};
}
2. 完全背包问题
2.1 基本问题
问题描述:有 n 个物品,每个物品有重量 w[i] 和价值 v[i]。现在有一个容量为 W 的背包,每个物品可以选择无限次,求解如何选择物品放入背包,使得物品的总价值最大。
javascript
function completePack(weights, values, capacity) {
const n = weights.length;
const dp = Array(capacity + 1).fill(0);
for (let i = 0; i < n; i++) {
for (let j = weights[i]; j <= capacity; j++) {
dp[j] = Math.max(
dp[j],
dp[j - weights[i]] + values[i]
);
}
}
return dp[capacity];
}
2.2 完全背包的变体
零钱兑换
问题描述:给定不同面额的硬币 coins 和一个总金额 amount,计算可以凑成总金额所需的最少的硬币个数。
javascript
function coinChange(coins, amount) {
const dp = Array(amount + 1).fill(Infinity);
dp[0] = 0;
for (const coin of coins) {
for (let i = coin; i <= amount; i++) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
return dp[amount] === Infinity ? -1 : dp[amount];
}
3. 多重背包问题
问题描述:有 n 个物品,每个物品有重量 w[i]、价值 v[i] 和数量 k[i]。现在有一个容量为 W 的背包,求解如何选择物品放入背包,使得物品的总价值最大。
javascript
function multiplePack(weights, values, counts, capacity) {
const n = weights.length;
const dp = Array(capacity + 1).fill(0);
for (let i = 0; i < n; i++) {
// 转换为 0-1 背包问题
let num = counts[i];
let weight = weights[i];
let value = values[i];
// 二进制优化
for (let k = 1; num > 0; k <<= 1) {
const amount = Math.min(k, num);
const newWeight = weight * amount;
const newValue = value * amount;
// 0-1 背包
for (let j = capacity; j >= newWeight; j--) {
dp[j] = Math.max(
dp[j],
dp[j - newWeight] + newValue
);
}
num -= amount;
}
}
return dp[capacity];
}
4. 优化技巧
状态压缩:
- 使用一维数组代替二维数组
- 注意遍历顺序的区别
二进制优化:
- 用于多重背包问题
- 将 k 件物品转换为 log k 件物品
初始化优化:
- 根据问题要求选择合适的初始值
- 考虑边界情况的处理
5. 面试要点
基本概念:
- 理解动态规划的状态定义
- 掌握状态转移方程的推导
- 理解不同类型背包问题的区别
优化方法:
- 空间优化的思路和实现
- 二进制优化的应用场景
- 初始化和边界条件的处理
变体问题:
- 零钱兑换
- 物品组合
- 最值问题
6. 常见面试题
6.1 分割等和子集
问题描述:给定一个只包含正整数的非空数组,判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
javascript
function canPartition(nums) {
const sum = nums.reduce((a, b) => a + b, 0);
if (sum % 2 !== 0) return false;
const target = sum / 2;
const dp = Array(target + 1).fill(false);
dp[0] = true;
for (const num of nums) {
for (let i = target; i >= num; i--) {
dp[i] = dp[i] || dp[i - num];
}
}
return dp[target];
}
6.2 目标和
问题描述:给定一个非负整数数组和一个目标值 S,现在可以在每个数字前加上 + 或 -,求所有使得和为 S 的添加符号的方法数。
javascript
function findTargetSumWays(nums, target) {
const sum = nums.reduce((a, b) => a + b, 0);
if (Math.abs(target) > sum) return 0;
const dp = Array(2 * sum + 1).fill(0);
dp[nums[0] + sum] = 1;
dp[-nums[0] + sum] += 1;
for (let i = 1; i < nums.length; i++) {
const next = Array(2 * sum + 1).fill(0);
for (let j = -sum; j <= sum; j++) {
if (dp[j + sum] > 0) {
next[j + nums[i] + sum] += dp[j + sum];
next[j - nums[i] + sum] += dp[j + sum];
}
}
dp.splice(0, dp.length, ...next);
}
return dp[target + sum];
}
总结
背包问题是动态规划中的经典问题,掌握其解题思路和优化技巧对于理解动态规划有很大帮助:
问题分类:
- 0-1 背包:每个物品最多选择一次
- 完全背包:每个物品可以选择无限次
- 多重背包:每个物品有特定的数量限制
核心思想:
- 状态定义
- 状态转移
- 边界处理
- 空间优化
实战技巧:
- 理解问题转化
- 掌握优化方法
- 灵活处理变体