背包问题
背包问题(Knapsack Problem)是组合优化中的经典问题。给定一组物品,每个物品具有重量和价值,要求在限定的总重量内,选择物品使得总价值最大。
0-1 背包问题
每个物品最多选一次。
状态定义
dp[i][j] 表示前 i 个物品,在容量为 j 时的最大价值。
状态转移方程
- 如果不选第
i个:dp[i][j] = dp[i-1][j] - 如果选第
i个:dp[i][j] = max(dp[i-1][j], dp[i-1][j - w[i]] + v[i])
空间优化 (O(V))
使用一维数组,并逆序遍历 j(避免重复选取)。
public int knapsack(int[] weights, int[] values, int capacity) {
int n = weights.length;
int[] dp = new int[capacity + 1];
for (int i = 0; i < n; i++) {
// 逆序遍历,确保第 i 个物品只选一次
for (int j = capacity; j >= weights[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - weights[i]] + values[i]);
}
}
return dp[capacity];
}
完全背包问题
每个物品可选无限次。
空间优化 (O(V))
使用一维数组,并正序遍历 j(由于可以无限选取,正序正好利用了已经选过的状态)。
public int completeKnapsack(int[] weights, int[] values, int capacity) {
int n = weights.length;
int[] dp = new int[capacity + 1];
for (int i = 0; i < n; i++) {
// 正序遍历,由于可以无限选取,可以直接利用已算好的状态
for (int j = weights[i]; j <= capacity; j++) {
dp[j] = Math.max(dp[j], dp[j - weights[i]] + values[i]);
}
}
return dp[capacity];
}
应用:零钱兑换 (Coin Change)
零钱兑换是背包问题的常见变体。
1. 最少硬币数 (LeetCode 322)
- 状态定义:
dp[i]表示凑齐金额i所需的最少硬币数。 - 状态转移:
dp[i] = min(dp[i], dp[i - coin] + 1)
2. 凑齐金额的方法数 (LeetCode 518)
- 状态定义:
dp[i]表示凑齐金额i的组合数。 - 状态转移:
dp[i] += dp[i - coin]
练习
- LeetCode 416:分割等和子集 (转化成 0-1 背包)
- LeetCode 322:零钱兑换
- LeetCode 518:零钱兑换 II (组合数)
- LeetCode 494:目标和
- LeetCode 377:组合总和 Ⅳ
- LeetCode 474:一和零 (二维背包)