两道经典 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),减少不必要的遍历。
相关推荐
IT大白鼠4 小时前
AIGC性能的关键瓶颈:算力、数据、算法三者如何互相制约?
算法·aigc
白雪茫茫5 小时前
监督学习、半监督学习、无监督学习算法详解
python·学习·算法·ai
FengyunSky5 小时前
浅析 空间频率响应 SFR 计算
算法
树下水月5 小时前
PHP 一种改良版的雪花算法
算法·php·dreamweaver
一只数据集6 小时前
全尺寸人形机器人灵巧手力觉触觉数据集-2908条ROSbag数据覆盖14大应用场景深度解析
大数据·人工智能·算法·机器人
罗西的思考7 小时前
【GUI-Agent】阿里通义MAI-UI 代码阅读(2)--- 实现
人工智能·算法·机器学习
刀法如飞8 小时前
TypeScript 数组去重的 20 种实现方式,哪一种你还不知道?
前端·javascript·算法
sali-tec9 小时前
C# 基于OpenCv的视觉工作流-章66-直线夹角
图像处理·人工智能·opencv·算法·计算机视觉
AC赳赳老秦9 小时前
接口测试自动化:用 OpenClaw 对接 Postman,实现批量回归测试、测试报告自动生成与推送
java·人工智能·python·算法·elasticsearch·deepseek·openclaw
_风满楼9 小时前
TDD实战-会议室冲突检测的红绿重构循环
前端·javascript·算法