|
5 | 5 |
|
6 | 6 | ## 题目大意
|
7 | 7 |
|
8 | | -**描述**:给定一个正整数 `n`。从中找到若干个完全平方数(比如 `1`、`4`、`1`、`16` ...),使得它们的和等于 `n`。 |
| 8 | +**描述**:给定一个正整数 $n$。从中找到若干个完全平方数(比如 1ドル、4、9、16...$),使得它们的和等于 $n$。 |
9 | 9 |
|
10 | | -**要求**:返回和为 `n` 的完全平方数的最小数量。 |
| 10 | +**要求**:返回和为 $n$ 的完全平方数的最小数量。 |
11 | 11 |
|
12 | 12 | **说明**:
|
13 | 13 |
|
|
33 | 33 |
|
34 | 34 | ## 解题思路
|
35 | 35 |
|
36 | | -暴力枚举思路:对于小于 `n` 的完全平方数,直接暴力枚举所有可能的组合,并且找到平方数个数最小的一个。 |
| 36 | +暴力枚举思路:对于小于 $n$ 的完全平方数,直接暴力枚举所有可能的组合,并且找到平方数个数最小的一个。 |
37 | 37 |
|
38 | 38 | 并且对于所有小于 $n$ 的完全平方数($k = 1, 4, 9, 16, ...$),存在公式:$ans(n) = min(ans(n - k) + 1),k = 1,4,9,16,...$
|
39 | 39 |
|
|
45 | 45 |
|
46 | 46 | 我们可以转换一下思维。
|
47 | 47 |
|
48 | | -1. 将 `n` 作为根节点,构建一棵多叉数。 |
49 | | -2. 从 `n` 节点出发,如果一个小于 `n` 的数刚好与 `n` 相差一个平方数,则以该数为值构造一个节点,与 `n` 相连。 |
| 48 | +1. 将 $n$ 作为根节点,构建一棵多叉数。 |
| 49 | +2. 从 $n$ 节点出发,如果一个小于 $n$ 的数刚好与 $n$ 相差一个平方数,则以该数为值构造一个节点,与 $n$ 相连。 |
50 | 50 |
|
51 | | -那么求解和为 `n` 的完全平方数的最小数量就变成了求解这棵树从根节点 `n` 到节点 `0` 的最短路径,或者说树的最小深度。 |
| 51 | +那么求解和为 $n$ 的完全平方数的最小数量就变成了求解这棵树从根节点 $n$ 到节点 0ドル$ 的最短路径,或者说树的最小深度。 |
52 | 52 |
|
53 | 53 | 这个过程可以通过广度优先搜索来做。
|
54 | 54 |
|
55 | 55 | ### 思路 1:广度优先搜索
|
56 | 56 |
|
57 | | -1. 定义 `visited` 为标记访问节点的 set 集合变量,避免重复计算。定义 `queue` 为存放节点的队列。使用 `count` 表示为树的最小深度,也就是和为 `n` 的完全平方数的最小数量。 |
58 | | -2. 首先,我们将 `n` 标记为已访问,即 `visited.add(n)`。并将其加入队列 `queue` 中,即 `queue.append(n)`。 |
59 | | -3. 令 `count` 加 `1`,表示最小深度加 `1`。然后依次将队列中的节点值取出。 |
60 | | -4. 对于取出的节点值 `value`,遍历可能出现的平方数(即遍历 $[1, \sqrt{value} + 1]$ 中的数)。 |
| 57 | +1. 定义 $visited$ 为标记访问节点的 set 集合变量,避免重复计算。定义 $queue$ 为存放节点的队列。使用 $count$ 表示为树的最小深度,也就是和为 $n$ 的完全平方数的最小数量。 |
| 58 | +2. 首先,我们将 $n$ 标记为已访问,即 `visited.add(n)`。并将其加入队列 $queue$ 中,即 `queue.append(n)`。 |
| 59 | +3. 令 $count$ 加 1ドル$,表示最小深度加 1ドル$。然后依次将队列中的节点值取出。 |
| 60 | +4. 对于取出的节点值 $value$,遍历可能出现的平方数(即遍历 $[1, \sqrt{value} + 1]$ 中的数)。 |
61 | 61 | 5. 每次从当前节点值减去一个平方数,并将减完的数加入队列。
|
62 | | - 1. 如果此时的数等于 `0`,则满足题意,返回当前树的最小深度。 |
63 | | - 2. 如果此时的数不等于 `0`,则将其加入队列,继续查找。 |
| 62 | + 1. 如果此时的数等于 0ドル$,则满足题意,返回当前树的最小深度。 |
| 63 | + 2. 如果此时的数不等于 0ドル$,则将其加入队列,继续查找。 |
64 | 64 |
|
65 | 65 | ### 思路 1:代码
|
66 | 66 |
|
@@ -97,3 +97,57 @@ class Solution:
|
97 | 97 | - **时间复杂度**:$O(n \times \sqrt{n})$。
|
98 | 98 | - **空间复杂度**:$O(n)$。
|
99 | 99 |
|
| 100 | +### 思路 2:动态规划 |
| 101 | + |
| 102 | +我们可以将这道题转换为「完全背包问题」中恰好装满背包的方案数问题。 |
| 103 | + |
| 104 | +1. 将 $k = 1, 4, 9, 16, ...$ 看做是 $k$ 种物品,每种物品都可以无限次使用。 |
| 105 | +2. 将 $n$ 看做是背包的装载上限。 |
| 106 | +3. 这道题就变成了,从 $k$ 种物品中选择一些物品,装入装载上限为 $n$ 的背包中,恰好装满背包最少需要多少件物品。 |
| 107 | + |
| 108 | +###### 1. 划分阶段 |
| 109 | + |
| 110 | +按照当前背包的载重上限进行阶段划分。 |
| 111 | + |
| 112 | +###### 2. 定义状态 |
| 113 | + |
| 114 | +定义状态 $dp[w]$ 表示为:从完全平方数中挑选一些数,使其和恰好凑成 $w$ ,最少需要多少个完全平方数。 |
| 115 | + |
| 116 | +###### 3. 状态转移方程 |
| 117 | + |
| 118 | +$dp[w] = min \lbrace dp[w], dp[w - num] + 1$ |
| 119 | + |
| 120 | +###### 4. 初始条件 |
| 121 | + |
| 122 | +- 恰好凑成和为 0ドル,ドル最少需要 0ドル$ 个完全平方数。 |
| 123 | +- 默认情况下,在不使用完全平方数时,都不能恰好凑成和为 $w$ ,此时将状态值设置为一个极大值(比如 $n + 1$),表示无法凑成。 |
| 124 | + |
| 125 | +###### 5. 最终结果 |
| 126 | + |
| 127 | +根据我们之前定义的状态,$dp[w]$ 表示为:将物品装入装载上限为 $w$ 的背包中,恰好装满背包,最少需要多少件物品。 所以最终结果为 $dp[n]$。 |
| 128 | + |
| 129 | +1. 如果 $dp[n] \ne n + 1,ドル则说明:$dp[n]$ 为装入装载上限为 $n$ 的背包,恰好装满背包,最少需要的物品数量,则返回 $dp[n]$。 |
| 130 | +2. 如果 $dp[n] = n + 1,ドル则说明:无法恰好装满背包,则返回 $-1$。因为 $n$ 肯定能由 $n$ 个 1ドル$ 组成,所以这种情况并不会出现。 |
| 131 | + |
| 132 | +### 思路 2:代码 |
| 133 | + |
| 134 | +```Python |
| 135 | +class Solution: |
| 136 | + def numSquares(self, n: int) -> int: |
| 137 | + dp = [n + 1 for _ in range(n + 1)] |
| 138 | + dp[0] = 0 |
| 139 | + |
| 140 | + for i in range(1, int(sqrt(n)) + 1): |
| 141 | + num = i * i |
| 142 | + for w in range(num, n + 1): |
| 143 | + dp[w] = min(dp[w], dp[w - num] + 1) |
| 144 | + |
| 145 | + if dp[n] != n + 1: |
| 146 | + return dp[n] |
| 147 | + return -1 |
| 148 | +``` |
| 149 | + |
| 150 | +### 思路 2:复杂度分析 |
| 151 | + |
| 152 | +- **时间复杂度**:$O(n \times \sqrt{n})$。 |
| 153 | +- **空间复杂度**:$O(n)$。 |
0 commit comments