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

前言

在动态规划的进阶阶段,有两道题是绕不开的:《零钱兑换》和《单词拆分》。它们都用到了「完全背包」的思想,一个是典型的数值类 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),减少不必要的遍历。
相关推荐
朝朝辞暮i20 分钟前
C++ 第 21 课:struct —— 把一组相关数据打包在一起
开发语言·c++·算法
青梅橘子皮26 分钟前
Blue---二分算法(二分查找与二分答案)
算法
长河38 分钟前
Java的GC(垃圾回收)中,标记无用对象的核心方法是可达性分析算法
java·jvm·算法
月光船幽幽1 小时前
加性偏移外推提升参数识别可靠性
python·算法
Ivanqhz2 小时前
层归一化、残差、前馈网络与激活函数简述
服务器·数据库·人工智能·深度学习·算法
不会就选b2 小时前
算法日常・每日刷题--<动态规划>3
数据结构·算法
-dzk-3 小时前
【堆】LC 347.前 K 个高频元素
算法·堆
H.莓飛3 小时前
【数据结构】栈_OJ题
linux·数据结构·算法·centos
Zootopia6263 小时前
多架 eVTOL集群排班调度方案思考
人工智能·算法·数学建模·matlab·动态规划·无人机·evtol
和裕4 小时前
年度框架直供 vs 零散按需采购:定制纸箱采购成本、交付与服务核心区别全对比
大数据·运维·网络·人工智能·算法