算法训练营 Day37 - 动态规划part06

322. 零钱兑换

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

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

示例 1:

  • 输入:coins = [1, 2, 5], amount = 11
  • 输出:3
  • 解释:11 = 5 + 5 + 1

示例 2:

  • 输入:coins = [2], amount = 3
  • 输出:-1

示例 3:

  • 输入:coins = [1], amount = 0
  • 输出:0

示例 4:

  • 输入:coins = [1], amount = 1
  • 输出:1

示例 5:

  • 输入:coins = [1], amount = 2
  • 输出:2

提示:

  • 1 <= coins.length <= 12
  • 1 <= coins[i] <= 2^31 - 1
  • 0 <= amount <= 10^4

如果求组合数就是外层for循环遍历物品,内层for遍历背包。

如果求排列数就是外层for遍历背包,内层for循环遍历物品。

每种硬币的数量是无限的,可以看出是典型的完全背包问题。

dp[j]:凑足总额为j所需钱币的最少个数为dp[j]

凑足总额为j - coins[i]的最少个数为dp[j - coins[i]],那么只需要加上一个钱币coins[i]即dp[j - coins[i]] + 1就是dp[j](考虑coins[i])

所以dp[j] 要取所有 dp[j - coins[i]] + 1 中最小的。

递推公式:dp[j] = min(dp[j - coins[i]] + 1, dp[j]);

首先凑足总金额为0所需钱币的个数一定是0,那么dp[0] = 0;

其他下标对应的数值呢?

考虑到递推公式的特性,dp[j]必须初始化为一个最大的数,否则就会在min(dp[j - coins[i]] + 1, dp[j])比较的过程中被初始值覆盖。

所以下标非0的元素都是应该是最大值。

我们要凑齐 amount,就像是要爬上第 amount 级台阶。你手里有几种不同长度的"砖块"(硬币面额 coins)。

  • 状态定义dp[j] 代表到达第 j 级台阶所需的最少砖块数。

  • 初始状态dp[0] = 0(站在地面不需要砖块)。其他位置先设为一个"不可能的高度"(如 amount + 1 或无穷大),表示还没找到路径。

当你站在第 j 级台阶时,你怎么来的?

你可能是从 j - coins[0] 跳上来的,也可能是从 j - coins[1] 跳上来的......

为了步数最少,你会看一眼这几个前置位,谁用的步数最少,你就选谁,然后再加 1(加上当前这一枚硬币)。

dp[j] = min(dp[j], dp[j - coins[i]] + 1)

python 复制代码
def coinChange(coins, amount):
    # 初始化 dp 数组,初始值为 amount + 1(比任何可能的硬币数都大)
    dp = [amount + 1] * (amount + 1)
    dp[0] = 0
    
    # 遍历每一种面额的硬币
    for coin in coins:
        # 遍历背包:由于每种硬币无限,所以从 coin 开始正向遍历
        for j in range(coin, amount + 1):
            # 如果能通过当前硬币减少步数,则更新
            dp[j] = min(dp[j], dp[j - coin] + 1)
            
    # 如果 dp[amount] 还是初始值,说明凑不出来
    return dp[amount] if dp[amount] <= amount else -1

279.完全平方数

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

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

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。

示例 1:

  • 输入:n = 12
  • 输出:3
  • 解释:12 = 4 + 4 + 4

示例 2:

  • 输入:n = 13
  • 输出:2
  • 解释:13 = 4 + 9

提示:

  • 1 <= n <= 10^4

Gemini said

这道题(LeetCode 279. 完全平方数)其实和你刚才问的"零钱兑换"本质上是完全一样的问题

如果你能理解零钱兑换,这道题只需做个简单的"脑筋急转弯":

  • 金额 amount :变成了这里的整数 n

  • 硬币面额 coins :变成了所有小于等于 n 的完全平方数(1,4,9,16,...)。

  • 目标:凑齐 n 所需的最少"完全平方数"个数。


1. 核心思路:动态规划 (DP)

我们可以沿用完全背包的思路,构建一个 dp 数组。

  • 定义状态dp[i] 表示凑齐整数 i 所需的最少完全平方数个数。

  • 状态转移方程 : 假设我们正在计算 dp[i],我们可以尝试减去一个完全平方数 j2。 那么有:dp[i]=min(dp[i],dp[i−j2]+1) 其中 j2≤i。

  • 边界条件dp[0] = 0(凑齐 0 需要 0 个数)。

  • 初始化 :由于要求最小值,我们将 dp 数组初始化为无穷大(或者 n + 1)。


2. 代码实现 (Python)

python 复制代码
import math

def numSquares(n):
    # 1. 初始化 dp 数组,最大值设为 n (全是1的情况)
    dp = [i for i in range(n + 1)] 
    
    # 2. 预先计算出所有可能的"硬币":完全平方数
    # 比如 n=12,squares = [1, 4, 9]
    max_square_index = int(math.sqrt(n)) + 1
    squares = [i**2 for i in range(1, max_square_index)]
    
    # 3. 开始动态规划
    for square in squares:
        for i in range(square, n + 1):
            # 这里的逻辑和零钱兑换一模一样
            dp[i] = min(dp[i], dp[i - square] + 1)
            
    return dp[n]

139.单词拆分

给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。

说明:

拆分时可以重复使用字典中的单词。

你可以假设字典中没有重复的单词。

示例 1:

  • 输入: s = "leetcode", wordDict = ["leet", "code"]
  • 输出: true
  • 解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。

示例 2:

  • 输入: s = "applepenapple", wordDict = ["apple", "pen"]
  • 输出: true
  • 解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。
  • 注意你可以重复使用字典中的单词。

示例 3:

  • 输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
  • 输出: false

想象你面前有一条长长的面包(字符串 s),你想知道能不能把它切成几段,且每一段都在菜单(wordDict)里。

  • 状态定义dp[i] 表示字符串的前 i 个字符能否被拆分成功。

    • dp[0] = True:空字符串,默认可以被拆分。
  • 递推公式 : 如果要判断 dp[i](前 i 个字符)是否为 True,我们需要在 0i 之间找一个切割点 j

    1. 如果前 j 个字符是可以拆分的(即 dp[j] == True);

    2. 且剩余的部分 s[j:i](从 ji 的子串)在字典 wordDict 中;

    3. 那么,前 i 个字符就是可以拆分的:dp[i] = True

你可以把 dp[i] 看作是一个接力赛

"如果接力棒能传到第 j 个位置(dp[j] 是真),并且从第 j 到第 i 这一段路有人跑(子串在字典里),那么接力棒就能传到第 i 个位置。"

公式表示:

dp[i] = dp[j] && check(s[j:i]) (其中 0 \\le j \< i

python 复制代码
def wordBreak(s, wordDict):
    # 为了查找更快,把 list 转成 set
    word_set = set(wordDict)
    n = len(s)
    
    # dp[i] 表示 s 的前 i 个字符是否可以拆分
    dp = [False] * (n + 1)
    dp[0] = True # 初始状态
    
    # 遍历背包(字符串的长度)
    for i in range(1, n + 1):
        # 遍历物品(分割点 j)
        for j in range(i):
            # 如果 前 j 个能拼成,且 [j, i] 这段也在字典里
            if dp[j] and s[j:i] in word_set:
                dp[i] = True
                break # 只要找到一种拆分方式,dp[i] 就是 True,跳出当前循环
                
    return dp[n]

关于多重背包,你该了解这些!

https://programmercarl.com/%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80%E5%A4%9A%E9%87%8D%E8%83%8C%E5%8C%85.html

背包问题总结篇!

https://programmercarl.com/%E8%83%8C%E5%8C%85%E6%80%BB%E7%BB%93%E7%AF%87.html

相关推荐
星空露珠1 小时前
迷你世界UGC3.0脚本Wiki角色模块管理接口 Actor
开发语言·数据库·算法·游戏·lua
我星期八休息1 小时前
深入理解哈希表
开发语言·数据结构·c++·算法·哈希算法·散列表
一叶落4382 小时前
LeetCode 54. 螺旋矩阵(C语言详解)——模拟 + 四边界收缩
java·c语言·数据结构·算法·leetcode·矩阵
寻寻觅觅☆2 小时前
东华OJ-进阶题-19-排队打水问题(C++)
开发语言·c++·算法
Storynone2 小时前
【Day27】LeetCode:56. 合并区间,738. 单调递增的数字
python·算法·leetcode
Boop_wu2 小时前
[Java 算法] 模拟
算法
khddvbe2 小时前
C++中的代理模式实战
开发语言·c++·算法
计算机安禾3 小时前
【C语言程序设计】第31篇:指针与函数
c语言·开发语言·数据结构·c++·算法·leetcode·visual studio
Frostnova丶3 小时前
LeetCode 3070. 元素和小于等于 k 的子矩阵数目
算法·leetcode·矩阵