动态规划经典三题_完全平方数

279. 完全平方数

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

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

示例 1:

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

示例 2:

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

方法一、记忆化搜索

@cache  # 缓存装饰器,避免重复计算 dfs 的结果(记忆化)
def dfs(i: int, j: int) -> int:
    if i == 0:
        return inf if j else 0
    if j < i * i:
        return dfs(i - 1, j)  # 只能不选
    return min(dfs(i - 1, j), dfs(i, j - i * i) + 1)  # 不选 vs 选

class Solution:
    def numSquares(self, n: int) -> int:
        return dfs(isqrt(n), n)

以下是漫长的步骤详解,不理解具体算法如何递归的可以看,理解的大佬可以直接跳过~~

示例解析:n = 12

我们重点分析每一步递归调用中参数的变化过程,并理解为什么最终返回的是 3

第一步:初始调用

dfs(3, 12)

因为 isqrt(12) = 3,所以最大的平方数是 3*3=9


Step 1: dfs(3, 12)

判断:

  • i == 0 ❌
  • j < i*i → 12 < 9 ❌

进入分支:

min(dfs(2, 12), dfs(3, 12 - 9) + 1) = min(dfs(2,12), dfs(3,3)+1)

Step 2: dfs(2, 12)

继续递归:

  • i == 0 ❌
  • 12 < 4 ❌
min(dfs(1,12), dfs(2, 12 - 4) + 1) = min(dfs(1,12), dfs(2,8)+1)

Step 3: dfs(1,12)

继续递归:

  • i == 0 ❌
  • 12 < 1 ❌
min(dfs(0,12), dfs(1,12 - 1)+1) = min(inf, dfs(1,11)+1)

由于 dfs(0,12) = inf,所以只考虑右边。


Step 4: dfs(1,11)

继续递归:

  • i == 0 ❌
  • 11 < 1 ❌
min(dfs(0,11), dfs(1,10)+1)

继续下去会一直递归到 dfs(1, 0)


Step N: dfs(1, 0)

  • i == 0 ✔️
  • j == 0 ✔️
  • 返回 0

所以:

  • dfs(1, 1) = dfs(1, 0) + 1 = 0 + 1 = 1
  • dfs(1, 2) = dfs(1, 1) + 1 = 1 + 1 = 2
  • ...
  • dfs(1, 11) = 11(需要 11 个 1)

所以回到:

  • dfs(1,12) = dfs(1,11) + 1 = 12

回到 Step 3: dfs(2,8)

尝试用 2² = 4

dfs(2, 8) = min(dfs(1,8), dfs(2,4)+1)

继续分析 dfs(2,4)

  • i = 2j = 4
  • 4 >= 4,可以减去 4
dfs(2,4) = min(dfs(1,4), dfs(2,0)+1)
  • dfs(2,0) = 0
  • dfs(1,4) = 4(4个1)
  • 所以 dfs(2,4) = min(4, 0+1) = 1

因此:

  • dfs(2,8) = min(dfs(1,8)=8, dfs(2,4)+1=2) = 2

回到 Step 2: dfs(2,12)

  • dfs(2,12) = min(dfs(1,12)=12, dfs(2,8)+1=3) = 3

回到 Step 1: dfs(3,12)

  • dfs(3,12) = min(dfs(2,12)=3, dfs(3,3)+1)
  • 现在分析 dfs(3,3)

Step X: dfs(3,3)

  • i=3j=3
  • i*i = 9 > 3 → 进入 dfs(2,3)

继续:

  • i=2j=3
  • 4 > 3 → dfs(1,3)

继续:

  • i=1j=3
  • 1 <= 3 → min(dfs(0,3), dfs(1,2)+1)
  • dfs(0,3) = inf
  • dfs(1,2) = min(dfs(0,2), dfs(1,1)+1) = min(inf, 1+1) = 2
  • 所以 dfs(1,3) = 3

最终:

  • dfs(3,3) = 3
  • dfs(3,12) = min(3, 3+1=4) = 3

✅ 最终结果:

dfs(3, 12) = 3

也就是:

12 = 4 + 4 + 4,用了 3 个完全平方数。


总结:每一步数值变化如下

调用 参数 (i, j) 返回值
dfs(3,12) i=3,j=12 min(dfs(2,12), dfs(3,3)+1) = min(3, 3+1) = 3
dfs(2,12) i=2,j=12 min(dfs(1,12), dfs(2,8)+1) = min(12, 2+1) = 3
dfs(1,12) i=1,j=12 dfs(1,11)+1 = 11+1 = 12
dfs(2,8) i=2,j=8 min(dfs(1,8), dfs(2,4)+1) = min(8, 1+1) = 2
dfs(2,4) i=2,j=4 min(dfs(1,4), dfs(2,0)+1) = min(4, 0+1) = 1
dfs(3,3) i=3,j=3 dfs(2,3) = dfs(1,3) = 3

补充说明:

  • @cache 是 Python 的装饰器,用于缓存函数调用结果,避免重复计算。
  • inf 是浮点型无穷大,在 Python 中用于表示不可能的情况。
  • isqrt(n) 是 Python 内置函数,返回不大于 √n 的最大整数(比 int(math.sqrt(n)) 更精确)。

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

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        n = len(coins)
        f = [[inf] * (amount + 1) for _ in range(2)]
        f[0][0] = 0
        for i, x in enumerate(coins):
            for c in range(amount + 1):
                if c < x:
                    f[(i + 1) % 2][c] = f[i % 2][c]
                else:
                    f[(i + 1) % 2][c] = min(f[i % 2][c], f[(i + 1) % 2][c - x] + 1)
        ans = f[n % 2][amount]
        return ans if ans < inf else -1

2787. 将一个数字表示成幂的和的方案数

给你两个整数 n 和 x 。

请你返回将 n 表示成一些 互不相同 正整数的 x 次幂之和的方案数。换句话说,你需要返回互不相同整数 [n1, n2, ..., nk] 的集合数目,满足 n = n1x + n2x + ... + nkx 。

由于答案可能非常大,请你将它对 109 + 7 取余后返回。

比方说,n = 160 且 x = 3 ,一个表示 n 的方法是 n = 23 + 33 + 53 。

示例 1:

输入:n = 10, x = 2
输出:1
解释:我们可以将 n 表示为:n = 32 + 12 = 10 。
这是唯一将 10 表达成不同整数 2 次方之和的方案。

示例 2:

输入:n = 4, x = 1
输出:2
解释:我们可以将 n 按以下方案表示:
- n = 41 = 4 。
- n = 31 + 11 = 4 。
class Solution:
    def numberOfWays(self, n: int, x: int) -> int:
        f = [1] + [0] * n  # 初始化DP数组:f[s] 表示组成和为 s 的方案数
        # 初始状态:f[0] = 1,表示“和为0”的方案就是啥也不选
        
        for i in range(1, n + 1):  # 枚举所有底数 i(1 到 n)
            v = i ** x  # 当前考虑的数是 i 的 x 次幂
            if v > n:   # 如果这个数太大,跳出循环
                break
                
            # 倒序遍历(0/1背包)从 n 到 v
            for s in range(n, v - 1, -1):
                f[s] += f[s - v]  # 转移方程:将 v 加入组合中,组成 s
                
        return f[n] % 1_000_000_007  # 返回最终和为 n 的方案数

总结思路:

步骤 内容
初始化 f[0] = 1 和为 0 有 1 种方式(空集)
枚举 i = 1...n 将 i 的 x 次幂作为候选数
倒序更新 DP 数组 保证每个数最多用一次(0/1背包)
更新 f[s] += f[s - v] 表示:新方案 = 旧方案 + 使用 v 的方案
取模 避免大数溢出

你可能感兴趣的:(动态规划篇_刷题笔记,我的学习记录,动态规划,算法)