代码随想录算法训练营 Day33 | 动态规划 part06

322. 零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1

你可以认为每种硬币的数量是无限的。

cpp 复制代码
class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        int n = coins.size();
        // 1. 定义 DP 数组
        // dp[j] 表示:凑成金额 j 所需的 **最少** 硬币个数
        vector<int> dp(amount + 1, INT_MAX); 
        // 2. 初始化
        // 凑成金额 0 需要 0 个硬币
        dp[0] = 0;
        // 3. 状态转移(完全背包:求组合的最小值)
        // 外层物品,内层背包正序遍历
        for(int i = 0; i < n; i++){
            for(int j = coins[i]; j <= amount; j++){
                // 递推公式:求最小值
                if(dp[j-coins[i]]==INT_MAX) continue;
                // dp[j] = min(不用当前硬币, 使用当前硬币)
                dp[j] = min(dp[j], dp[j - coins[i]] + 1);
            }
        }
        // 4. 判断结果
        // 如果 dp[amount] 没被更新过,说明无法凑出,返回 -1
        return dp[amount] == INT_MAX ? -1 : dp[amount];
    }
};

总结

1. 为什么是求组合,不是求排列?

因为我们只关心"最少几张纸币",[1, 1, 5][5, 1, 1] 凑出 7 块钱都是 3 张硬币。所以外层物品、内层背包是绝对正确的。

2. 为什么 dp[0] = 0

这是所有"求个数/步数"题的通用初始化。凑出 0 元需要 0 个硬币。


279. 完全平方数

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,14916 都是完全平方数,而 311 不是。

cpp 复制代码
class Solution {
public:
    int numSquares(int n) {
        // 1. 定义 DP 数组
        // dp[j] 表示:凑成和为 j 的完全平方数所需的最少数量
        vector<int> dp(n + 1, INT_MAX);
        // 2. 初始化
        dp[0] = 0; // 凑成和为 0 需要 0 个平方数
        // 3. 遍历物品与背包
        // 物品是什么?是所有的完全平方数 (1, 4, 9, 16...)
        // 这里直接用 i*i 动态生成了物品,极其巧妙,省去了提前打表的步骤
        for(int i = 1; i * i <= n; i++){
            // 背包容量是 n,从当前物品重量 i*i 开始正序遍历(完全背包)
            for(int j = i * i; j <= n; j++){
                // 【高光细节】防止 INT_MAX + 1 导致负数溢出
                if(dp[j - i * i] == INT_MAX) continue;
                // 递推公式:求最少数量
                // dp[j] = min(不用当前的平方数 i*i, 用当前的平方数 i*i)
                dp[j] = min(dp[j], dp[j - i * i] + 1);
            }
        }
        // 4. 返回结果
        return dp[n];
    }
};

总结

1. 完美的物品抽象

在传统的完全背包题中,题目会直接给你一个数组(比如硬币面额)。

而这道题只给了一个数字 n:

  • 物品:所有的完全平方数 1^2,2^2,3^2...k^2(其中 k^2≤n)。
  • 背包容量:目标和 n
  • 物品重量:i^2。
  • 物品价值:数量 1
2. 为什么外层是 i,内层是 j

这里求的是"最少数量",12 = 4 + 4 + 412 = 4 + 4 + 4,顺序不重要,所以这是一个 求组合的最小值 问题。

按照我们总结的铁律,求组合必须外层物品,内层背包。


139. 单词拆分

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

cpp 复制代码
class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        // 1. 优化查询:将字典转化为哈希集合,将查找时间从 O(N) 降为 O(1)
        unordered_set<string> st(wordDict.begin(), wordDict.end());
        int n = s.size();
        // 2. 定义 DP 数组
        // dp[j] 表示:字符串 s 的前 j 个字符(即 s[0...j-1])能否被成功拆分
        vector<bool> dp(n + 1, false);
        // 3. 初始化
        // 空字符串永远可以被拆分(什么都不拆),这是递推的基础
        dp[0] = true;
        // 4. 状态转移
        // 外层遍历背包容量(字符串的结束位置 j)
        for(int j = 1; j <= n; j++){
            // 内层遍历物品(寻找分割点 i,i 的范围是 [0, j-1])
            for(int i = 0; i < j; i++){
                // 截取子串 s[i...j-1]
                string str = s.substr(i, j - i);
                // 递推逻辑:
                // 如果截取的子串在字典中,并且它前面的部分 s[0...i-1] 也能被成功拆分
                // 那么当前的前 j 个字符就能被成功拆分!
                if(st.find(str) != st.end() && dp[i]){
                    dp[j] = true;
                    // 找到一种拆分方法即可,不需要继续找其他的 i 了,直接 break 提速
                    break; 
                }
            }
        }
        return dp[n];
    }
};

总结

1. 思路

遇到字符串能不能被分割的问题,最直觉的想法就是:在哪下刀?

假设我们要判断前 j 个字符是否合法,我们在 0j-1 之间找一刀切下去:

  • 左边:s[0...i-1],它能不能拆开?看 dp[i]
  • 右边:s[i...j-1],它是不是个单词?去哈希表里查一下。
    两边都成立,dp[j] 就成立。这就是纯粹的 区间 DP 思想,远比强行套用"外层物品内层背包"的完全背包公式要清晰得多。
2. 两个细节
  • unordered_set 的引入:在动态规划内部做字符串查找是非常致命的性能消耗。直接用哈希集合预处理。
  • dp[0] = true 的哨兵作用:如果没有这个初始化,当整个字符串就是一个单词时(比如 i=0),dp[0] 就会是 false,导致整体判断失败。

背包问题总结

类别一:0-1 背包(物品只能用一次)

这是所有背包问题的基础。

核心特征是:为了防止同一个物品在同一轮被反复累加,内层遍历背包容量时必须是倒序(从大到小)。

  • 经典应用:分割等和子集、最后一块石头的重量 II、一和零(二维费用 0-1 背包)。
  • 通用代码框架:
cpp 复制代码
    for(int i = 0; i < 物品数量; i++) {           // 外层:物品
        for(int j = 背包最大容量; j >= weight[i]; j--) { // 内层:容量【倒序】
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }

类别二:完全背包(物品可以无限次使用)

与 0-1 背包的唯一区别在于:既然可以重复拿,那计算当前状态时就需要用到本层已经更新过的状态。因此,内层遍历背包容量必须是正序(从小到大)。

  • 经典应用:零钱兑换、完全平方数、组合总和 Ⅳ。
  • 通用代码框架:
cpp 复制代码
    for(int i = 0; i < 物品数量; i++) {            // 外层:物品
        for(int j = weight[i]; j <= 背包最大容量; j++) { // 内层:容量【正序】
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }

类别三:两大灵魂追问 ------ 求最值 vs 求方案数

确定了正倒序之后,递推公式和初始化方式完全取决于题目在求什么。

1. 求"最值"(最大价值 / 最少硬币数)
  • 递推公式:dp[j] = max(dp[j], dp[j-w] + v)min(dp[j],dp[j-w] + 1)
  • 初始化逻辑:
    • 求最大值:全数组初始化为 0
    • 求最小值:dp[0] = 0,其余全初始化为一个极大的数(如 0x3f3f3f3fINT_MAX)。
  • 易错点:求最小值时,执行 dp[j-w] + 1 之前,必须加 if 判断(如 if(dp[j-w] != INT_MAX)),否则必定发生 INT_MAX + 1 导致整数溢出变成负数!
2. 求"方案数"(有多少种凑法)
  • 递推公式:累加操作 dp[j] += dp[j-w]
  • 初始化逻辑:死记硬背 dp[0] = 1,其余为 0。 (凑成总和 0 的方法数是 1,即"什么都不选",这是所有方案数累加的基石)。
  • 易错点:如果硬币面额多、金额大,方案数会呈指数级爆炸,普通的 int 必定溢出。数组类型果断换成 uint64_tlong long

类别四:求方案数的终极细分 ------ 组合 vs 排列

这是完全背包中最喜欢考的弯弯绕绕。当题目要求求"方案数"时,外层和内层循环到底谁先谁后?

  • 求组合数(不强调顺序,[1,5][5,1] 算同一种)
    • 铁律:外层物品,内层背包(正序)
    • 原理:先固定放 1 块钱的,再固定放 5 块钱的,物品放入的相对顺序被锁死,不会产生重复。
    • 代表题:零钱兑换 II
  • 求排列数(强调顺序,[1,5][5,1] 算不同的两种)
    • 铁律:外层背包,内层物品(正序)
    • 原理:对于每一个背包容量,都把所有硬币从头到尾试一遍,这就保留了先拿 5 还是先拿 1 的顺序差异。
    • 代表题:组合总和 Ⅳ

附加:如何识别"伪装的背包题"?

有时候题目根本没提"背包"、"容量",需要抽象转化:

  1. 二维费用:题目出现两个并列的限制条件(如:最多有 m 个 0 和 n 个 1),立刻想到二维 0-1 背包,dp 变成二维数组,内层套两个倒序循环。
  2. 隐藏物品数组:题目只给了一个目标值 n,没给数组。自己用循环生成物品(如:完全平方数中,物品是 i*i;爬楼梯中,物品是 1~m)。
  3. 字符串拆分(如单词拆分):虽然常被归类为完全背包,但最清晰的解法是脱离背包公式,用区间 DP 切割法:外层右边界,内层左边界一刀切,判断左半边 dp[i] 是否合法且右半边是否在字典中。
相关推荐
_日拱一卒3 小时前
LeetCode:240搜索二维矩阵Ⅱ
数据结构·线性代数·leetcode·矩阵
aini_lovee3 小时前
半定规划(SDP)求解的 MATLAB 实现
算法
米粒13 小时前
力扣算法刷题 Day 41(买卖股票)
算法·leetcode·职场和发展
幻风_huanfeng3 小时前
人工智能之数学基础:内点法和外点法的区别和缺点
人工智能·算法·机器学习·内点法·外点法
MIngYaaa5203 小时前
The 6th Liaoning Provincial Collegiate Programming Contest - External 复盘
算法
CylMK3 小时前
题解:P11625 [迷宫寻路 Round 3] 迷宫寻路大赛
c++·数学·算法
计算机安禾3 小时前
【数据结构与算法】第44篇:堆(Heap)的实现
c语言·开发语言·数据结构·c++·算法·排序算法·图论
kaikaile19953 小时前
能量算子的MATLAB实现与详细算法
人工智能·算法·matlab
tankeven4 小时前
HJ175 小红的整数配对
c++·算法