前言
在动态规划的进阶阶段,有两道题是绕不开的:《零钱兑换》和《单词拆分》。它们都用到了「完全背包」的思想,一个是典型的数值类 DP,一个是字符串类 DP,掌握了它们,你就能打通 DP 的一大半任督二脉。
一、零钱兑换(LeetCode 322)
题目描述
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1。你可以认为每种硬币的数量是无限的。
核心思路:完全背包 DP
这是一道典型的完全背包问题:
- 背包容量:
amount - 物品:硬币面额(每个可以无限次使用)
- 目标:用最少的硬币数量装满背包
状态定义
dp[i] 表示凑成金额 i 所需的最少硬币数。
转移方程
对于每个金额 i,遍历所有硬币面额 coin:dp[i] = min(dp[i], dp[i - coin] + 1)
边界条件
dp[0] = 0(金额为 0 时,需要 0 个硬币)- 其余
dp[i]初始化为无穷大(表示暂时无法凑出)
代码实现(Java 版)
java
运行
public class CoinChange {
public int coinChange(int[] coins, int amount) {
// dp[i] = 凑成金额i的最少硬币数
int[] dp = new int[amount + 1];
// 初始化,设置为无穷大
Arrays.fill(dp, amount + 1);
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int coin : coins) {
if (i >= coin) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
// 如果dp[amount]还是无穷大,说明无法凑出
return dp[amount] > amount ? -1 : dp[amount];
}
public static void main(String[] args) {
CoinChange solution = new CoinChange();
int[] coins = {1, 2, 5};
int amount = 11;
System.out.println(solution.coinChange(coins, amount)); // 输出:3(5+5+1)
}
}
关键知识点
- 时间复杂度:O (amount × n),n 为硬币种类数
- 空间复杂度:O (amount)
- 优化技巧:硬币先排序,遇到
coin > i可以提前 break;BFS 解法也可以实现,第一次到达amount的层数就是最少硬币数。
二、单词拆分(LeetCode 139)
题目描述
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
核心思路:字符串 DP + 完全背包
这道题是字符串类的完全背包问题:
- 背包容量:字符串
s的长度 - 物品:字典中的单词(每个可以无限次使用)
- 目标:判断能否用单词拼接出整个字符串
状态定义
dp[i] 表示字符串s的前i个字符能否被字典中的单词拼接而成。
转移方程
对于每个位置i,遍历所有j < i:如果dp[j] == true,且子串s[j:i]在字典中,则dp[i] = true。
边界条件
dp[0] = true(空字符串可以被拼接)
代码实现(Java 版)
java
运行
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class WordBreak {
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> wordSet = new HashSet<>(wordDict);
int n = s.length();
boolean[] dp = new boolean[n + 1];
dp[0] = true; // 空字符串可被拼接
for (int i = 1; i <= n; i++) {
for (int j = 0; j < i; j++) {
if (dp[j] && wordSet.contains(s.substring(j, i))) {
dp[i] = true;
break; // 找到即可提前退出
}
}
}
return dp[n];
}
public static void main(String[] args) {
WordBreak solution = new WordBreak();
String s = "leetcode";
List<String> wordDict = List.of("leet", "code");
System.out.println(solution.wordBreak(s, wordDict)); // 输出:true
}
}
关键知识点
- 时间复杂度:O (n²),n 为字符串长度(内层遍历 j,外层遍历 i)
- 空间复杂度:O (n + m),m 为字典单词数(存储 Set)
- 优化技巧:可以提前计算字典中单词的最大长度,j 的遍历范围缩小到
[i - maxLen, i),减少不必要的遍历。