两道经典 DP 题:零钱兑换 & 单词拆分(完全背包 + 字符串 DP)

前言

在动态规划的进阶阶段,有两道题是绕不开的:《零钱兑换》和《单词拆分》。它们都用到了「完全背包」的思想,一个是典型的数值类 DP,一个是字符串类 DP,掌握了它们,你就能打通 DP 的一大半任督二脉。


一、零钱兑换(LeetCode 322)

题目描述

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1。你可以认为每种硬币的数量是无限的。

核心思路:完全背包 DP

这是一道典型的完全背包问题

  • 背包容量:amount
  • 物品:硬币面额(每个可以无限次使用)
  • 目标:用最少的硬币数量装满背包
状态定义

dp[i] 表示凑成金额 i 所需的最少硬币数。

转移方程

对于每个金额 i,遍历所有硬币面额 coindp[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),减少不必要的遍历。
相关推荐
疯狂打码的少年1 小时前
有序线性表删除一个元素:顺序存储 vs 单链表,平均要移动多少个元素?
数据结构·算法·链表
y = xⁿ2 小时前
20天速通LeetCode day07:前缀和
数据结构·算法·leetcode
载数而行5202 小时前
算法集训1:模拟,枚举,错误分析,前缀和,差分
算法
hehelm2 小时前
vector模拟实现
前端·javascript·算法
Tina学编程3 小时前
[HOT 100]今日一练------划分字母区间
算法·hot 100
RTC老炮3 小时前
RaptorQ前向纠错算法架构分析
网络·算法·架构·webrtc
故事和你913 小时前
洛谷-数据结构1-1-线性表2
开发语言·数据结构·算法·动态规划·图论
m0_555762903 小时前
从原始信号到IQ图的数学公式推导
算法
靠沿3 小时前
【递归、搜索与回溯算法】专题四——综合练习
算法·深度优先