一、背包类型问题
1.0-1背包问题
(动态规划通用套路)
1.明确 状态 和 选择
状态有两个,背包的容量 和 可选择的物品,选择,对于每件物品选择放入背包 或 不装入背包
2.明确dp数组的定义
dp[i] [w] 的定义; 对于前i个物品,当前背包的容量为W,这种情况下可以装的最大价值
比如 dp[3] [5] = 6 表示若只对前三个物品进行选择,当背包容量为5时,最多可以装下的价值是6
base case 是 dp[0] […] = dp[…] [0] = 0,没有物品或者背包没有空间的时候,能装的最大价值就是0
3.选择
思考状态转移的逻辑
考虑dp[i] [w]:
- 如果没有把第i个物品装入,那么最大价值dp[i] [w] = dp[i-1] [w]
- 如果把第i个物品装入,那么最大价值 dp[i] [w] = val[i-1] + dp[i-1] [w - wt[i-1]]
- 注意val[i-1]就是第i个物品
- 拿了第i个物品,那么i-1之前的能装的容量只剩 w - wt[i-1](第i个物品的容量)
int knapsack(int W, int N, int[] wt, int[] val) {
assert N == wt.length;
// base case 已初始化,默认创建所有的值都是0
int[][] dp = new int[N + 1][W + 1];
for (int i = 1; i <= N; i++) {
for (int w = 1; w <= W; w++) {
if (w - wt[i - 1] < 0) {
// 这种情况下只能选择不装入背包
dp[i][w] = dp[i - 1][w];
} else {
// 装入或者不装入背包,择优
dp[i][w] = Math.max(
dp[i - 1][w - wt[i-1]] + val[i-1],
dp[i - 1][w]
);
}
}
}
return dp[N][W];
}
2.子集背包问题
分割等和子集
给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
根据1题,先计算出sum/2,之后定义二维数组,dp[i] [sum/2] 表示前i个数总和是否等于sum/2,等于为true,否则为false
boolean canPartition(int[] nums) {
int sum = 0;
for (int num : nums) sum += num;
// 和为奇数时,不可能划分成两个和相等的集合
if (sum % 2 != 0) return false;
int n = nums.length;
sum = sum / 2;
boolean[][] dp = new boolean[n + 1][sum + 1];
// base case
for (int i = 0; i <= n; i++)
dp[i][0] = true;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= sum; j++) {
if (j - nums[i - 1] < 0) {
// 背包容量不足,不能装入第 i 个物品
dp[i][j] = dp[i - 1][j];
} else {
// 装入或不装入背包
//注意 || 的使用非常巧妙,只要有一种情况为true dp[i][j]就为true
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]];
}
}
}
return dp[n][sum];
}
3.完全背包问题
零钱兑换2
int change(int amount, int[] coins) {
int n = coins.length;
int[][] dp = new int[n + 1][amount + 1];
// base case, dp[0][j]不用初始化,创建时所有的值都为0
for (int i = 0; i <= n; i++)
dp[i][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= amount; j++)
if (j - coins[i-1] >= 0) // >= 0 表示还有使用硬币的机会
dp[i][j] = dp[i - 1][j]
+ dp[i][j - coins[i-1]];
else
dp[i][j] = dp[i - 1][j]; // < 0 已选择硬币的总面值已经大于j,无法选第i个硬币
}
return dp[n][amount];
}