目录
[1. 背包问题定义](#1. 背包问题定义)
[2. 通用解题框架](#2. 通用解题框架)
[3. 遍历顺序底层逻辑](#3. 遍历顺序底层逻辑)
[二、01 背包典型题目:](#二、01 背包典型题目:)
[(一)目标和(LeetCode 494)](#(一)目标和(LeetCode 494))
[1. 题目描述](#1. 题目描述)
[2. 问题转化(核心!)](#2. 问题转化(核心!))
[3. 详细分析](#3. 详细分析)
[4. 易错点(踩过的坑!)](#4. 易错点(踩过的坑!))
[5. 难点](#5. 难点)
[6. Java 实现](#6. Java 实现)
[二维 DP 解法---好理解一点,新手友好~](#二维 DP 解法---好理解一点,新手友好~)
[一维 DP 解法(空间优化)](#一维 DP 解法(空间优化))
[7. 过程展示](#7. 过程展示)
[二维 DP 表填充(关键行)](#二维 DP 表填充(关键行))
[一维 DP 更新过程](#一维 DP 更新过程)
[(二)分割等和子集(LeetCode 416)](#(二)分割等和子集(LeetCode 416))
[2、问题转化(01 背包核心)](#2、问题转化(01 背包核心))
[2.1 核心推导](#2.1 核心推导)
[2.2 对应背包模型](#2.2 对应背包模型)
[3.1 状态定义](#3.1 状态定义)
[3.2 初始化(基准状态)](#3.2 初始化(基准状态))
[3.3 状态转移](#3.3 状态转移)
[一维转移(倒序!01 背包核心)](#一维转移(倒序!01 背包核心))
[6、Java 实现](#6、Java 实现)
[1. 二维 DP 解法(易理解,新手首选)](#1. 二维 DP 解法(易理解,新手首选))
[2. 一维 DP 解法(空间优化,刷题常用)](#2. 一维 DP 解法(空间优化,刷题常用))
[7、过程展示(示例 1:nums=[1,5,11,5],s=22,target=11)](#7、过程展示(示例 1:nums=[1,5,11,5],s=22,target=11))
[7.1 二维 DP 表填充(关键行)](#7.1 二维 DP 表填充(关键行))
[7.2 一维 DP 更新过程](#7.2 一维 DP 更新过程)
[8、与 "目标和" 的对比(01 背包两类目标)](#8、与 “目标和” 的对比(01 背包两类目标))
[(一)最少硬币数(LeetCode 322)](#(一)最少硬币数(LeetCode 322))
[1. 题目描述](#1. 题目描述)
[2. 问题转化](#2. 问题转化)
[3. 详细分析](#3. 详细分析)
[4. 易错点](#4. 易错点)
[5. 难点](#5. 难点)
[6. Java 实现](#6. Java 实现)
[二维 DP 解法](#二维 DP 解法)
[一维 DP 解法](#一维 DP 解法)
[7. 过程展示](#7. 过程展示)
[(二)完全平方数的最少数量(LeetCode 279)](#(二)完全平方数的最少数量(LeetCode 279))
[1. 题目描述](#1. 题目描述)
[2. 问题转化](#2. 问题转化)
[3. 详细分析](#3. 详细分析)
[4. 易错点](#4. 易错点)
[5. 难点](#5. 难点)
[6. Java 实现](#6. Java 实现)
[二维 DP 解法](#二维 DP 解法)
[一维 DP 解法](#一维 DP 解法)
[7. 过程展示](#7. 过程展示)
[(三)单词拆分(LeetCode 139)](#(三)单词拆分(LeetCode 139))
[1. 题目描述](#1. 题目描述)
[2. 问题转化](#2. 问题转化)
[3. 详细分析](#3. 详细分析)
[4. 易错点](#4. 易错点)
[5. 难点](#5. 难点)
[6. Java 实现](#6. Java 实现)
[二维 DP 解法](#二维 DP 解法)
[一维 DP 解法(优化)](#一维 DP 解法(优化))
[7. 过程展示](#7. 过程展示)
[1. 01 背包 vs 完全背包核心对比](#1. 01 背包 vs 完全背包核心对比)
[2. 解题通用步骤(背下来!)](#2. 解题通用步骤(背下来!))
[3. 关键思维](#3. 关键思维)
一、背包问题核心理论
1. 背包问题定义
背包问题是动态规划的经典分支,核心是「给定物品集合 + 目标容量,通过选择物品满足容量约束,求最优解(数量 / 最值 / 布尔值)」,主要分为两类:
| 背包类型 | 核心规则 | 适用场景 | 核心遍历逻辑 |
|---|---|---|---|
| 01 背包 | 每个物品只能选 1 次 | 目标和、分割等和子集 | 先物品 → 倒序遍历容量(防重复) |
| 完全背包 | 每个物品可重复选 | 最少硬币数、完全平方数 | 先物品 → 正序遍历容量(允重复) |
2. 通用解题框架
无论 01 / 完全背包,核心步骤一致,仅在「遍历顺序 / 转移细节」有差异:
- 问题转化:识别「物品」「容量」「优化目标」(方案数 / 最值 / 布尔值);
- 状态定义 :
dp[i][j](二维)= 考虑前i个物品,容量为j的最优解;dp[j](一维)= 容量为j的最优解(空间优化);- 初始化 :确定基准状态(如
dp[0][0]=1/dp[0]=0);- 状态转移:基于「选 / 不选当前物品」推导;
- 遍历顺序:按背包类型确定(01 倒序 / 完全正序)。
3. 遍历顺序底层逻辑
- 01 背包倒序:避免同一物品被重复选(更新大索引时,小索引仍为「上一轮状态」);
- 完全背包正序:允许同一物品被重复选(更新大索引时,小索引已为「本轮状态」);
- 完全背包可互换「物品 / 容量」遍历顺序,01 背包不可(必须先物品后倒序容量)。
二、01 背包典型题目:
(一)目标和(LeetCode 494)
1. 题目描述
给定整数数组nums和目标值target,给每个数加+或-,求最终和为target的方案数。示例:nums=[1,1,1,1,1], target=3 → 输出 5。
2. 问题转化(核心!)
设数组总和为s,选部分数加+(和为p),另一部分加-(和为s-p):p - (s-p) = target → 化简得 p = (target + s) / 2。问题转化为:从nums中选若干数(每个数只能选 1 次),和为p的方案数(01 背包)。
3. 详细分析
状态定义
- 二维:
dp[i][j]= 考虑前i个数,凑和为j的方案数; - 一维:
dp[j]= 凑和为j的方案数(空间优化)。
初始化
- 二维:
dp[0][0] = 1(无物品时,凑 0 的方案数 = 1),dp[0][j>0] = 0; - 一维:
dp[0] = 1(基准),其余默认 0。
状态转移
- 二维:
java
if (j >= nums[i-1]) {
// 选:dp[i-1][j-nums[i-1]] + 不选:dp[i-1][j]
dp[i][j] = dp[i-1][j] + dp[i-1][j - nums[i-1]];
} else {
dp[i][j] = dp[i-1][j]; // 不够选,只能不选
}
-
一维(倒序):
javafor (int num : nums) { for (int j = p; j >= num; j--) { dp[j] += dp[j - num]; // 选+不选的叠加 } }
4. 易错点(踩过的坑!)
- 变量名混淆:用
target存总和,导致逻辑混乱(需单独用s存总和,p存目标和); - 遍历顺序错误:正序遍历容量→变成完全背包(重复选数);
- 合法性判断缺失:未判断
(target+s)是否为偶数、p是否非负; - 初始化遗漏:未设
dp[0]=1(基准丢失,结果全 0)。
5. 难点
- 问题转化:从「加减符号」抽象出「01 背包」的思维跳跃;
- 一维遍历逻辑:倒序的底层原因(避免重复选数);
- 边界处理:
p为负 / 总和小于|target|的特殊情况。
6. Java 实现
二维 DP 解法---好理解一点,新手友好~
java
class Solution {
public int findTargetSumWays(int[] nums, int target) {
// 1. 计算总和+合法性判断
int s = 0;
for (int num : nums) s += num;
if (Math.abs(target) > s || (target + s) % 2 != 0) return 0;
int p = (target + s) / 2;
if (p < 0) return 0;
// 2. 二维DP初始化
int n = nums.length;
int[][] dp = new int[n + 1][p + 1];
dp[0][0] = 1; // 基准
// 3. 状态转移
for (int i = 1; i <= n; i++) {
int num = nums[i - 1];
for (int j = 0; j <= p; j++) {
if (j >= num) {
dp[i][j] = dp[i-1][j] + dp[i-1][j - num];
} else {
dp[i][j] = dp[i-1][j];
}
}
}
return dp[n][p];
}
}
一维 DP 解法(空间优化)
java
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int s = 0;
for (int num : nums) s += num;
if (Math.abs(target) > s || (target + s) % 2 != 0) return 0;
int p = (target + s) / 2;
if (p < 0) return 0;
int[] dp = new int[p + 1];
dp[0] = 1; // 基准
// 01背包:先物品,后倒序容量
for (int num : nums) {
for (int j = p; j >= num; j--) {
dp[j] += dp[j - num];
}
}
return dp[p];
}
}
7. 过程展示
以nums=[1,1,1,1,1], target=3(s=5, p=4)为例:
二维 DP 表填充(关键行)
| i\j | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 0 | 1 | 0 | 0 | 0 | 0 |
| 1 | 1 | 1 | 0 | 0 | 0 |
| 2 | 1 | 2 | 1 | 0 | 0 |
| 3 | 1 | 3 | 3 | 1 | 0 |
| 4 | 1 | 4 | 6 | 4 | 1 |
| 5 | 1 | 5 | 10 | 10 | 5 |
最终dp[5][4]=5(正确答案)。 |
一维 DP 更新过程
初始dp=[1,0,0,0,0]:
- 遍历第 1 个 1:
dp=[1,1,0,0,0]; - 遍历第 2 个 1:
dp=[1,2,1,0,0]; - 遍历第 3 个 1:
dp=[1,3,3,1,0]; - 遍历第 4 个 1:
dp=[1,4,6,4,1]; - 遍历第 5 个 1:
dp=[1,5,10,10,5]。
(二)分割等和子集(LeetCode 416)
这道题是 01 背包的基础布尔型应用(目标是 "能否凑满容量"),比 "目标和" 更贴近 01 背包的原始模型,是理解 "01 背包判断可行性" 的关键题目,补充后能完整覆盖 01 背包的 "方案数" 和 "可行性" 两类目标。
1、题目描述
给定一个只包含正整数 的非空数组 nums,判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
- 示例 1:输入
nums=[1,5,11,5]→ 输出true(子集 1:[1,5,5],子集 2:[11],和均为 11); - 示例 2:输入
nums=[1,2,3,5]→ 输出false(总和 11 为奇数,无法分割)。
2、问题转化(01 背包核心)
2.1 核心推导
要分割成两个和相等的子集 → 数组总和s必须是偶数 (否则直接返回false);设目标和 target = s / 2 → 问题转化为:从nums中选若干数(每个数只能选 1 次),能否凑出和为target(01 背包的 "可行性判断")。
2.2 对应背包模型
- 物品:数组中的每个数字(每个只能选 1 次);
- 容量:
target(总和的一半); - 目标:判断 "能否用物品装满容量"(布尔值)。
3、详细分析
3.1 状态定义
- 二维 DP :
dp[i][j]= 考虑前i个数字,能否凑出和为j(true/false);i:前i个数字(范围 0~nums.length,i=0表示无数字);j:目标和(范围 0~target)。
- 一维 DP :
dp[j]= 能否凑出和为j(空间优化,压缩i维度)。
3.2 初始化(基准状态)
- 二维:
dp[0][0] = true(无数字时,凑出和为 0 的方案存在:选 0 个数);dp[0][j>0] = false(无数字时,无法凑出大于 0 的和)。
- 一维:
dp[0] = true(基准),其余默认false(Java 布尔数组默认值)。
3.3 状态转移
核心逻辑:对第i个数字(对应nums[i-1]),分 "选" 和 "不选" 两种情况:
二维转移
java
if (j >= nums[i-1]) {
// 选:dp[i-1][j-nums[i-1]](前i-1个数凑j-nums[i-1]可行) OR 不选:dp[i-1][j]
dp[i][j] = dp[i-1][j] || dp[i-1][j - nums[i-1]];
} else {
// 数字比j大,无法选,继承"不选"的结果
dp[i][j] = dp[i-1][j];
}
一维转移(倒序!01 背包核心)
java
for (int num : nums) {
// 倒序遍历:避免同一数字被重复选
for (int j = target; j >= num; j--) {
// 只要"不选num可行"或"选num可行",dp[j]就为true
dp[j] = dp[j] || dp[j - num];
}
}
4、易错点(高频踩坑)
- 总和奇偶性判断缺失 :直接开始 DP,忽略
s为奇数的情况(必返回false); - 遍历顺序错误 :正序遍历容量→变成完全背包(数字被重复选,比如
nums=[2,2],正序会错误返回true,但实际 01 背包也正确,需注意:布尔型 01 背包正序可能结果对,但逻辑错); - 元素剪枝遗漏 :数组中存在
num > target的元素(直接返回false,因为单个元素超过目标和,无法凑出); - 初始化错误 :未设
dp[0]=true(基准丢失,所有状态都为false)。
5、难点
- 问题抽象能力:从 "分割子集" 到 "01 背包可行性" 的转化(新手易卡在这一步);
- 提前剪枝优化 :
- 总和为奇数 → 直接返回
false; - 单个元素 > target → 直接返回
false; - 遍历中若
dp[target]提前变为true,可直接返回(剪枝);
- 总和为奇数 → 直接返回
- 一维遍历的逻辑理解:倒序的本质是 "保证每个数字只选一次"(和目标和的一维逻辑完全一致)。
6、Java 实现
1. 二维 DP 解法(易理解,新手首选)
class Solution {
public boolean canPartition(int[] nums) {
// 1. 计算总和+合法性判断
int s = 0;
for (int num : nums) s += num;
// 总和为奇数,直接返回false
if (s % 2 != 0) return false;
int target = s / 2;
// 2. 二维DP初始化
int n = nums.length;
boolean[][] dp = new boolean[n + 1][target + 1];
dp[0][0] = true; // 基准:无数字凑0可行
// 3. 状态转移
for (int i = 1; i <= n; i++) {
int num = nums[i - 1];
// 剪枝:当前数字超过target,直接继承上一行结果
if (num > target) return false;
for (int j = 0; j <= target; j++) {
if (j >= num) {
dp[i][j] = dp[i-1][j] || dp[i-1][j - num];
} else {
dp[i][j] = dp[i-1][j];
}
}
// 提前剪枝:已找到可行方案
if (dp[i][target]) return true;
}
return dp[n][target];
}
}
2. 一维 DP 解法(空间优化,刷题常用)
class Solution {
public boolean canPartition(int[] nums) {
int s = 0;
for (int num : nums) s += num;
if (s % 2 != 0) return false;
int target = s / 2;
// 一维DP初始化:dp[0]=true,其余false
boolean[] dp = new boolean[target + 1];
dp[0] = true;
// 01背包:先物品,后倒序容量
for (int num : nums) {
// 剪枝:当前数字超过target,无法凑出
if (num > target) return false;
for (int j = target; j >= num; j--) {
dp[j] = dp[j] || dp[j - num];
}
// 提前剪枝
if (dp[target]) return true;
}
return dp[target];
}
}
7、过程展示(示例 1:nums=[1,5,11,5],s=22,target=11)
7.1 二维 DP 表填充(关键行)
| i\j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | T | F | F | F | F | F | F | F | F | F | F | F |
| 1(num=1) | T | T | F | F | F | F | F | F | F | F | F | F |
| 2(num=5) | T | T | F | F | F | T | T | F | F | F | F | F |
| 3(num=11) | T | T | F | F | F | T | T | F | F | F | F | T |
| 4(num=5) | T | T | F | F | F | T | T | F | F | F | T | T |
✅ 遍历到i=3(num=11)时,dp[3][11]=true,提前返回true。 |
7.2 一维 DP 更新过程
初始dp=[T,F,F,F,F,F,F,F,F,F,F,F](索引 0~11):
- 遍历 num=1:
dp=[T,T,F,F,F,F,F,F,F,F,F,F]; - 遍历 num=5:
dp=[T,T,F,F,F,T,T,F,F,F,F,F]; - 遍历 num=11:
dp=[T,T,F,F,F,T,T,F,F,F,F,T](dp [11]=true,提前返回)。
8、与 "目标和" 的对比(01 背包两类目标)
| 题目 | 01 背包目标 | 状态转移核心 | 初始化基准 | ||
|---|---|---|---|---|---|
| 分割等和子集 | 可行性(布尔值) | `dp[j] = dp[j] | dp[j-num]` | dp[0]=true |
|
| 目标和 | 方案数(数值) | dp[j] += dp[j-num] |
dp[0]=1 |
||
| 共性 | 均需倒序遍历容量 | 依赖 "选 / 不选" 的状态叠加 | 以dp[0]为基准 |
三、完全背包典型题目
(一)最少硬币数(LeetCode 322)
1. 题目描述
给定硬币数组coins(可重复用)和金额amount,求凑成amount的最少硬币数,凑不出返回 - 1。示例:coins=[1,2,5], amount=11 → 输出 3(5+5+1)。
2. 问题转化
物品 = 硬币,容量 = 金额,每个物品可重复选,求「凑满容量的最少物品数」(完全背包求 min)。
3. 详细分析
状态定义
- 二维:
dp[i][j]= 考虑前i种硬币,凑金额j的最少硬币数; - 一维:
dp[j]= 凑金额j的最少硬币数。
初始化
- 二维:
dp[0][0] = 0,dp[0][j>0] = amount+1(标记 "不可达"); - 一维:
dp[0] = 0,其余初始为amount+1(避免用Integer.MAX_VALUE溢出)。
状态转移
-
二维:
javaif (j >= coins[i-1]) { // 选:dp[i][j-coins[i-1]]+1(可重复选,用dp[i]而非dp[i-1]) dp[i][j] = Math.min(dp[i-1][j], dp[i][j - coins[i-1]] + 1); } else { dp[i][j] = dp[i-1][j]; } -
一维(正序):
javafor (int coin : coins) { for (int j = coin; j <= amount; j++) { dp[j] = Math.min(dp[j], dp[j - coin] + 1); } }
4. 易错点
- 初始化错误:用
i+1而非amount+1(小金额初始值不合理); - 剪枝依赖缺失:
break需先排序硬币,否则漏解; - 结果判断缺失:未检查
dp[amount]是否仍为amount+1(需返回 - 1)。
5. 难点
- 不可达标记选择:
amount+1是最优(避免溢出); - 完全背包的 "重复选" 逻辑:二维用
dp[i]而非dp[i-1]; - 排序 + 剪枝的优化逻辑(硬币大于金额时跳过)。
6. Java 实现
二维 DP 解法
java
class Solution {
public int coinChange(int[] coins, int amount) {
int n = coins.length;
int[][] dp = new int[n + 1][amount + 1];
// 初始化:不可达标记为amount+1
for (int j = 1; j <= amount; j++) {
dp[0][j] = amount + 1;
}
dp[0][0] = 0;
for (int i = 1; i <= n; i++) {
int coin = coins[i-1];
for (int j = 0; j <= amount; j++) {
if (j >= coin) {
dp[i][j] = Math.min(dp[i-1][j], dp[i][j - coin] + 1);
} else {
dp[i][j] = dp[i-1][j];
}
}
}
return dp[n][amount] == amount + 1 ? -1 : dp[n][amount];
}
}
一维 DP 解法
java
class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1);
dp[0] = 0;
// 完全背包:先物品,后正序容量
for (int coin : coins) {
for (int j = coin; j <= amount; j++) {
dp[j] = Math.min(dp[j], dp[j - coin] + 1);
}
}
return dp[amount] == amount + 1 ? -1 : dp[amount];
}
}
7. 过程展示
以coins=[1,2,5], amount=11为例:
- 一维初始
dp=[0,12,12,12,12,12,12,12,12,12,12,12]; - 遍历 coin=1:
dp=[0,1,2,3,4,5,6,7,8,9,10,11]; - 遍历 coin=2:
dp=[0,1,1,2,2,3,3,4,4,5,5,6]; - 遍历 coin=5:
dp=[0,1,1,2,2,1,2,2,3,3,2,3];最终dp[11]=3(正确)。
(二)完全平方数的最少数量(LeetCode 279)
1. 题目描述
给定整数n,求组成n的最少完全平方数数量。示例:n=12 → 输出 3(4+4+4)。
2. 问题转化
物品 = 完全平方数(1,4,9,...),容量 =n,物品可重复选,求「凑满容量的最少物品数」(完全背包求 min)。
3. 详细分析
状态定义
- 二维:
dp[i][j]= 考虑前i个平方数,凑j的最少数量; - 一维:
dp[j]= 凑j的最少数量。
初始化
- 二维:
dp[0][0]=0,dp[0][j>0]=j(最坏全用 1); - 一维:
dp[0]=0,其余初始为j。
状态转移
-
二维:
javaint square = i * i; // 第i个平方数为i² if (j >= square) { dp[i][j] = Math.min(dp[i-1][j], dp[i][j - square] + 1); } else { dp[i][j] = dp[i-1][j]; } -
一维(正序):
javafor (int i = 1; i * i <= n; i++) { int square = i * i; for (int j = square; j <= n; j++) { dp[j] = Math.min(dp[j], dp[j - square] + 1); } }
4. 易错点
- 贪心陷阱:优先选最大平方数(如 n=12 贪心得 4,实际 3);
- 平方数遍历边界:未限制
i*i <=n(导致越界); - 状态转移遗漏:未取 min(直接覆盖值)。
5. 难点
- 贪心 vsDP 的取舍:局部最优≠全局最优;
- 数学定理优化:拉格朗日四平方和定理(O (√n) 时间);
- 平方数的生成逻辑(避免重复计算)。
6. Java 实现
二维 DP 解法
java
class Solution {
public int numSquares(int n) {
int maxSquare = (int) Math.sqrt(n);
int[][] dp = new int[maxSquare + 1][n + 1];
// 初始化
for (int j = 1; j <= n; j++) {
dp[0][j] = j;
}
dp[0][0] = 0;
for (int i = 1; i <= maxSquare; i++) {
int square = i * i;
for (int j = 0; j <= n; j++) {
if (j >= square) {
dp[i][j] = Math.min(dp[i-1][j], dp[i][j - square] + 1);
} else {
dp[i][j] = dp[i-1][j];
}
}
}
return dp[maxSquare][n];
}
}
一维 DP 解法
java
class Solution {
public int numSquares(int n) {
int[] dp = new int[n + 1];
for (int j = 1; j <= n; j++) {
dp[j] = j; // 初始化最坏情况
}
dp[0] = 0;
for (int i = 1; i * i <= n; i++) {
int square = i * i;
for (int j = square; j <= n; j++) {
dp[j] = Math.min(dp[j], dp[j - square] + 1);
}
}
return dp[n];
}
}
7. 过程展示
以n=12为例:
- 一维初始
dp=[0,1,2,3,4,5,6,7,8,9,10,11,12]; - 遍历 square=1:无变化(已最优);
- 遍历 square=4:
dp[4]=1, dp[8]=2, dp[12]=3; - 遍历 square=9:
dp[9]=1, dp[12]=min(3, dp[3]+1=4) → 3;最终dp[12]=3(正确)。
(三)单词拆分(LeetCode 139)
1. 题目描述
给定字符串s和单词列表wordDict,判断s能否拆分为字典中的单词(单词可重复用)。示例:s="leetcode", wordDict=["leet","code"] → 输出 true。
2. 问题转化
物品 = 字典单词,容量 = 字符串长度,物品可重复选,求「能否用单词拼接出前i个字符」(完全背包求布尔值)。
3. 详细分析
状态定义
- 二维:
dp[i][j]= 考虑前i个单词,能否拼接出s的前j个字符; - 一维:
dp[j]= 能否拼接出s的前j个字符。
初始化
- 二维:
dp[0][0] = true(无单词时,空串可拼接); - 一维:
dp[0] = true(基准),其余为 false。
状态转移
-
二维:
javaString word = wordDict.get(i-1); int len = word.length(); if (j >= len && s.substring(j-len, j).equals(word)) { dp[i][j] = dp[i-1][j] || dp[i][j - len]; } else { dp[i][j] = dp[i-1][j]; } -
一维(正序):
javafor (int j = 1; j <= s.length(); j++) { for (String word : wordDict) { int len = word.length(); if (j >= len && dp[j - len] && s.substring(j-len, j).equals(word)) { dp[j] = true; break; } } }
4. 易错点
- 贪心陷阱:用 flag 锁定拆分位置(错过正确路径);
- 索引错误:
substring的左闭右开规则(如j-len到j); - 初始化错误:未设
dp[0]=true(基准丢失); - 查询效率低:未用
HashSet优化字典查询。
5. 难点
- 字符串与背包的转化:将 "拼接" 抽象为 "凑容量";
- 子串截取的边界处理:避免越界;
- 布尔值的状态转移(只要有一条路径可行即为 true)。
6. Java 实现
二维 DP 解法
java
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
int n = wordDict.size();
int m = s.length();
boolean[][] dp = new boolean[n + 1][m + 1];
dp[0][0] = true; // 基准
for (int i = 1; i <= n; i++) {
String word = wordDict.get(i-1);
int len = word.length();
for (int j = 0; j <= m; j++) {
dp[i][j] = dp[i-1][j]; // 先继承不选当前单词的结果
if (j >= len && s.substring(j-len, j).equals(word)) {
dp[i][j] = dp[i][j] || dp[i][j - len];
}
}
}
return dp[n][m];
}
}
一维 DP 解法(优化)
java
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
int m = s.length();
boolean[] dp = new boolean[m + 1];
dp[0] = true;
Set<String> wordSet = new HashSet<>(wordDict); // 优化查询
// 完全背包:先容量,后物品(等价)
for (int j = 1; j <= m; j++) {
for (String word : wordSet) {
int len = word.length();
if (j >= len && dp[j - len] && s.substring(j-len, j).equals(word)) {
dp[j] = true;
break;
}
}
}
return dp[m];
}
}
7. 过程展示
以s="leetcode", wordDict=["leet","code"]为例:
- 一维初始
dp=[true,false,false,false,false,false,false,false,false]; - j=4:
substring(0,4)="leet",dp[0]=true→dp[4]=true; - j=8:
substring(4,8)="code",dp[4]=true→dp[8]=true;最终dp[8]=true(正确)。
四、整体总结
1. 01 背包 vs 完全背包核心对比
| 维度 | 01 背包 | 完全背包 |
|---|---|---|
| 选取规则 | 物品选 1 次 | 物品可重复选 |
| 遍历顺序 | 先物品→倒序容量 | 先物品→正序容量(可互换) |
| 二维转移 | 依赖dp[i-1][j-num] |
依赖dp[i][j-num] |
| 核心应用 | 目标和、分割等和子集 | 最少硬币数、完全平方数 |
| 易错点 | 正序遍历导致重复选 | 贪心陷阱、初始化溢出 |
2. 解题通用步骤(背下来!)
- 识别背包类型:判断物品能否重复选(01 / 完全);
- 抽象物品 & 容量 :
- 01 背包:物品 = 数字,容量 = 目标和;
- 完全背包:物品 = 硬币 / 平方数 / 单词,容量 = 金额 / 数值 / 字符串长度;
- 定义状态:二维(易理解)→ 一维(空间优化);
- 初始化基准 :
- 方案数:
dp[0]=1; - 最值:
dp[0]=0,其余为极大 / 极小值; - 布尔值:
dp[0]=true,其余为 false;
- 方案数:
- 状态转移:基于「选 / 不选」推导,取 min/max/ 叠加 / 或;
- 遍历顺序:按背包类型确定,验证边界。
3. 关键思维
- DP 的核心是「覆盖所有路径」,而非贪心的「局部最优」;
- 二维 DP 是理解本质的基础,一维 DP 是刷题的最优选择;
- 背包问题的本质是「状态的复用」,遍历顺序决定复用规则。
