题目
1049. 最后一块石头的重量Ⅱ
题解:
java
class Solution {
public int lastStoneWeightII(int[] stones) {
int sum = 0;
for (int stone : stones) {
sum += stone;
}
// 背包的最大容量就是总重量的一半
int target = sum / 2;
int n = stones.length;
// 1. 定义二维 dp 数组
// dp[i][j] 表示从前 i 块石头中任意选,装进容量为 j 的背包,能装的最大重量
int[][] dp = new int[n][target + 1];
// 2. 初始化第一行 (i = 0,只考虑第 0 块石头)
// 只要容量 j 大于等于第 0 块石头的重量,就可以装进去
for (int j = stones[0]; j <= target; j++) {
dp[0][j] = stones[0];
}
// 3. 双重循环遍历推导
for (int i = 1; i < n; i++) { // 外层遍历物品 (石头)
for (int j = 0; j <= target; j++) { // 内层遍历背包容量
if (j < stones[i]) {
// 当前背包容量装不下第 i 块石头,只能不装
dp[i][j] = dp[i - 1][j];
} else {
// 装得下,在"不装"和"装"之间取最大值
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - stones[i]] + stones[i]);
}
}
}
// 4. 计算最终结果
// dp[n-1][target] 是背包能装下的最大重量(即上面推导的 B)
// 另一堆石头的重量就是 sum - dp[n-1][target] (即上面推导的 A)
// 最后碰撞剩下的重量就是 A - B,即 (sum - dp[n-1][target]) - dp[n-1][target]
return sum - 2 * dp[n - 1][target];
}
}
滚动优化:
java
class Solution {
public int lastStoneWeightII(int[] stones) {
// 1. 算出全部石头的总重量
int sum = 0;
for (int stone : stones) {
sum += stone;
}
// 2. 确定背包的最大容量(向下取整)
int target = sum / 2;
// 3. 定义一维 dp 数组:dp[j] 表示容量为 j 的背包最多能装的重量
// Java 默认初始化全为 0,符合背包容量为 0 时装入重量为 0 的事实
int[] dp = new int[target + 1];
// 4. 开始一维 01 背包推导
// 外层循环:遍历每一块石头 (物品)
for (int i = 0; i < stones.length; i++) {
// 内层循环:遍历背包容量
// 【核心铁律】:一维 01 背包必须从大到小【倒序】遍历!
// 遍历下限是当前石头的重量 stones[i],因为容量小于它时根本装不下,dp[j] 原封不动即可
for (int j = target; j >= stones[i]; j--) {
// 状态转移方程:比较【不拿当前石头】和【拿当前石头】哪个凑出的重量更大
dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
// 5. 算出最终结果
// 此时 dp[target] 里面装的就是背包(小堆石头)能达到的最大重量 B
// 大堆石头的重量 A = sum - dp[target]
// 它们碰撞后剩下的重量就是 A - B = (sum - dp[target]) - dp[target]
return sum - 2 * dp[target];
}
}
494. 目标和
题解:
java
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for (int num : nums) {
sum += num;
}
// 1. 过滤绝对无法凑出的情况
// 如果 target 的绝对值大于 sum,或者 (target + sum) 除不尽 2
if (Math.abs(target) > sum || (target + sum) % 2 != 0) {
return 0;
}
// 2. 确定背包容量
int bagSize = (target + sum) / 2;
int n = nums.length;
// 3. 定义二维 dp 数组
// dp[i][j] 表示使用前 i 个物品,刚好凑出和为 j 的方法数
int[][] dp = new int[n][bagSize + 1];
// 4. 初始化第一行 (i = 0,只考虑第 0 个数字)
// 坑点:如果 nums[0] 本身就是 0,那么装满容量为 0 的背包有 2 种方法 (装或者不装)
if (nums[0] == 0) {
dp[0][0] = 2;
} else {
dp[0][0] = 1; // 容量为 0,不装 nums[0] 也是一种方法
if (nums[0] <= bagSize) {
dp[0][nums[0]] = 1; // 容量刚好等于 nums[0],装进去也是一种方法
}
}
// 5. 双重循环遍历推导
for (int i = 1; i < n; i++) { // 遍历物品
for (int j = 0; j <= bagSize; j++) { // 遍历背包容量 (组合问题,容量要从 0 开始推导)
if (j < nums[i]) {
// 容量不够,只能不选当前数字
dp[i][j] = dp[i - 1][j];
} else {
// 容量够,方案数 = (不选的方案数) + (选的方案数)
// 注意这里是求和 (+),不是求最大值 (Math.max)
dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];
}
}
}
// 6. 返回结果:考虑所有物品,凑满容量为 bagSize 的方法数
return dp[n - 1][bagSize];
}
}
滚动数组优化:
java
class Solution {
public int findTargetSumWays(int[] nums, int target) {
// --- 步骤 1:数学转化与边界检查 ---
int sum = 0;
for (int num : nums) {
sum += num;
}
// 边界检查 1:如果目标和的绝对值大于所有数字的总和,无论怎么加减都不可能凑出
// 边界检查 2:根据推导 left - right = target, left + right = sum
// 得出 left = (sum + target) / 2。如果和为奇数,说明无法凑出整数的 left,直接返回 0
if (Math.abs(target) > sum || (sum + target) % 2 != 0) {
return 0;
}
int n = nums.length;
// 巧妙复用 target 变量:此时的 target 已经变成了我们需要用加法的数字凑出的"背包容量" (即 left)
target = (sum + target) / 2;
// --- 步骤 2:定义一维 dp 数组 ---
// dp[j] 的物理含义:从数组中挑选若干个数字,凑成和为 j 的【方案总数】
int[] dp = new int[target + 1];
// --- 步骤 3:极其严谨的手动初始化 (针对第 0 个物品 nums[0]) ---
// 这一步完美地将二维 dp[0][j] 的逻辑拍扁到了一维
if (nums[0] == 0) {
// 特判:如果第一个数字就是 0
// 凑成和为 0 有两种方案:【加上这个 0】或者【不加这个 0】
dp[0] = 2;
} else {
// 如果第一个数字不是 0
// 凑成和为 0 只有一种方案:【什么都不选】
dp[0] = 1;
// 如果这个数字刚好能放进背包,那么凑成 nums[0] 也有一种方案:【选它】
if (nums[0] <= target) {
dp[nums[0]] = 1;
}
}
// --- 步骤 4:一维滚动数组的核心推导 ---
// 外层循环:遍历剩下的物品(因为第 0 个物品在上面已经初始化过了,所以从 i = 1 开始)
for (int i = 1; i < n; i++) {
// 内层循环:遍历背包容量 j
// 【铁律】:01 背包的一维优化,内层必须从大到小【倒序遍历】!
// 倒序是为了保证在计算 dp[j] 时,等号右边用到的 dp[j - nums[i]] 是上一轮(i-1)的旧数据,
// 从而保证当前物品 nums[i] 最多只会被使用一次。
// 下限是 nums[i],因为容量 j 如果小于 nums[i],根本装不下当前物品,方案数依然是上一轮的 dp[j],无需更新。
for (int j = target; j >= nums[i]; j--) {
// 状态转移方程(组合计数问题专属的加法原理):
// 凑出容量 j 的总方案数 = 【不选当前物品 nums[i] 的方案数 (旧的 dp[j])】
// + 【选了当前物品 nums[i] 的方案数 (旧的 dp[j - nums[i]])】
dp[j] = dp[j] + dp[j - nums[i]];
}
}
// --- 步骤 5:返回结果 ---
// 遍历完所有物品后,背包容量为 target 时的方案数,就是最终答案
return dp[target];
}
}
474. 一和零
题解:
java
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
int len = strs.length;
if (len == 0) return 0;
// 1. 定义三维 dp 数组 (完全按照标准模板的物理下标定义)
// dp[i][j][k] 表示:从下标为 [0, i] 的字符串中任意取
// 最多有 j 个 0 和 k 个 1 的前提下,能包含的最大字符串数量
int[][][] dp = new int[len][m + 1][n + 1];
// --- 核心差异 1:手动初始化第 0 个物品 (strs[0]) ---
int[] firstCount = countZerosOnes(strs[0]);
int zeros0 = firstCount[0];
int ones0 = firstCount[1];
// 只要背包的 0 和 1 的容量能装下第一个字符串,就把这一层的价值设为 1
for (int j = zeros0; j <= m; j++) {
for (int k = ones0; k <= n; k++) {
dp[0][j][k] = 1;
}
}
// 2. 开始推导状态 (因为 i=0 已经处理,所以从 i=1 开始)
for (int i = 1; i < len; i++) {
// 统计当前第 i 个物品(字符串)的"重量"
int[] count = countZerosOnes(strs[i]);
int zeros = count[0];
int ones = count[1];
for (int j = 0; j <= m; j++) {
for (int k = 0; k <= n; k++) {
if (j < zeros || k < ones) {
// 动作 A:容量装不下当前字符串,只能继承上一层的结果
dp[i][j][k] = dp[i - 1][j][k];
} else {
// 动作 B:容量足够,在"不选"和"选"中取最大值
dp[i][j][k] = Math.max(
dp[i - 1][j][k],
dp[i - 1][j - zeros][k - ones] + 1
);
}
}
}
}
// --- 核心差异 2:返回值的下标变化 ---
// 考虑了下标从 0 到 len-1 的所有字符串,所以第一维下标是 len - 1
return dp[len - 1][m][n];
}
// 辅助方法:统计字符串中 0 和 1 的数量
private int[] countZerosOnes(String s) {
int[] count = new int[2];
for (char c : s.toCharArray()) {
count[c - '0']++;
}
return count;
}
}
滚动优化:
java
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
// 1. 定义二维滚动 dp 数组
// dp[j][k] 表示最多有 j 个 0 和 k 个 1 的情况下,最多能选的字符串数量
int[][] dp = new int[m + 1][n + 1];
// 2. 初始化
// 容量为 0 时,一个字符串都装不下,最大价值为 0。
// Java 中 int 数组默认初始化为 0,完美契合,无需额外代码。
// 3. 开始遍历推导
// 外层循环:遍历每一个物品(字符串)
for (String str : strs) {
// 统计当前物品(字符串)的两个"重量"维度:0 的个数和 1 的个数
int zeros = 0, ones = 0;
for (char c : str.toCharArray()) {
if (c == '0') zeros++;
else ones++;
}
// 内层双重循环:遍历背包的两个容量维度
// 【核心铁律】:因为是 01 背包的空间优化,容量维度必须【倒序遍历】!
// 无论是先遍历 j 还是先遍历 k 都可以,但方向必须都是从大到小。
// 下限分别是 zeros 和 ones,因为容量比当前物品重量还小时,根本装不下,dp 保持旧值即可。
for (int j = m; j >= zeros; j--) {
for (int k = n; k >= ones; k--) {
// 状态转移方程:比较【不装当前字符串】和【装当前字符串】哪个得到的数量更多
dp[j][k] = Math.max(dp[j][k], dp[j - zeros][k - ones] + 1);
}
}
}
// 4. 最终返回容量拉满到 m 和 n 时的最大字符串数量
return dp[m][n];
}
}