力扣hot100之279:完全平方数
力扣hot100之279:完全平方数

LeetCode 279. 完全平方数

题目描述

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

完全平方数指的是一个整数,其值等于另一个整数的平方。 比如:

  • 1 = 1 * 1

  • 4 = 2 * 2

  • 9 = 3 * 3

  • 16 = 4 * 4

这些都属于完全平方数。


示例分析

示例 1

输入: n = 12
输出: 3

解释:

12 = 4 + 4 + 4

所以最少需要 3 个完全平方数。


示例 2

输入: n = 13
输出: 2

解释:

13 = 4 + 9

所以最少需要 2 个完全平方数。


解题思路

这道题很多同学第一次看到,会先想到动态规划。 但你给出的这份代码其实是用 BFS(广度优先搜索) 来做的,而且思路非常巧妙。

核心想法是:

把“一个数减去某个完全平方数”看成一次状态转移。

例如:

n = 12

我们可以从 12 出发:

  • 减去 1,得到 11

  • 减去 4,得到 8

  • 减去 9,得到 3

这就相当于从状态 12 可以走到多个新状态。

于是整道题就变成了:

n 出发,每次减去一个完全平方数,最少走几步能到达 0

这其实就是一个最短路径问题,而 BFS 最擅长解决的正是“无权图中的最短路径”。


为什么 BFS 可以做这题

我们把每一个数字看成图中的一个节点。

如果当前数字是 cur,那么我们可以尝试减去所有不超过它的完全平方数:

cur - 1^2
cur - 2^2
cur - 3^2
...

每减去一次完全平方数,就相当于走了一步。

于是:

  • 第 1 层表示:用了 1 个完全平方数后可以得到的所有结果

  • 第 2 层表示:用了 2 个完全平方数后可以得到的所有结果

  • 第 3 层表示:用了 3 个完全平方数后可以得到的所有结果

当某一层第一次出现 0 时,就说明:

当前层数就是最少需要的完全平方数个数

这就是为什么 BFS 中的 level 恰好可以表示答案。


你的代码

class Solution {
    public int numSquares(int n) {
        Queue<Integer> queue = new LinkedList<>();
        HashSet<Integer> visited = new HashSet<>();
        int level = 0;
        queue.add(n);
        while (!queue.isEmpty()) {
            int size = queue.size();
            level++; 
            for (int i = 0; i < size; i++) {
                int cur = queue.poll();
                for (int j = 1; j * j <= cur; j++) {
                    int next = cur - j * j;
                    if (next == 0) {
                        return level;
                    }
                    if (!visited.contains(next)) {
                        queue.offer(next);
                        visited.add(next);
                    }
                }
            }
        }
        return -1;
    }
}

这份代码的思路非常标准,整体流程就是:

  1. n 开始做 BFS

  2. 每一层尝试减去所有可能的完全平方数

  3. 如果减到 0,说明当前层数就是最优答案

  4. visited 去重,避免重复搜索


BFS 中的 level 为什么是答案

这是这道题最关键的一点。

在 BFS 中:

  • 第 1 层:表示从 n 经过 1 次减法能到达的状态

  • 第 2 层:表示经过 2 次减法能到达的状态

  • 第 3 层:表示经过 3 次减法能到达的状态

而每次减法本质上就是“使用了一个完全平方数”。

所以:

第几层第一次到达 0,就表示最少用了几个完全平方数。

这就是 BFS 的天然优势: 第一次到达目标时,一定是步数最少的时候。


代码解析

1. 定义队列和去重集合

Queue<Integer> queue = new LinkedList<>();
HashSet<Integer> visited = new HashSet<>();
  • queue:用来做 BFS,保存当前待处理的数字状态

  • visited:记录已经访问过的状态,避免重复入队


2. 定义层数

int level = 0;

level 表示当前 BFS 走到了第几层。 也可以理解为:当前已经使用了多少个完全平方数。


3. 起点入队

queue.add(n);

我们从 n 这个状态开始出发。


4. BFS 主循环

while (!queue.isEmpty()) {

只要队列不为空,就继续做广度优先搜索。


5. 记录当前层节点数,并进入下一层

int size = queue.size();
level++;

这里 size 表示当前层有多少个状态。 每处理完这一层,说明又多用了一个完全平方数,所以 level++


6. 依次处理当前层的每个状态

for (int i = 0; i < size; i++) {
    int cur = queue.poll();

从队列中取出当前数字 cur


7. 枚举所有可能减去的完全平方数

for (int j = 1; j * j <= cur; j++) {
    int next = cur - j * j;

这里枚举:

  • 1^2

  • 2^2

  • 3^2

直到 j * j <= cur 为止。

每次减去一个完全平方数,得到下一个状态 next


8. 如果减到 0,直接返回当前层数

if (next == 0) {
    return level;
}

这说明:

  • 当前用了 level 个完全平方数

  • 并且第一次成功凑到了 n

由于 BFS 是按层扩展的,所以这一定是最少个数。


9. 没访问过的状态才入队

if (!visited.contains(next)) {
    queue.offer(next);
    visited.add(next);
}

如果这个状态之前没有处理过,就加入队列。

为什么一定要去重?

因为同一个数字可能会通过不同路径重复到达。 如果不去重,搜索树会急剧膨胀,效率会差很多。


10. 理论上的兜底返回

return -1;

正常情况下不会走到这里,因为根据题意总能拆成若干个平方数。 这里只是一个形式上的兜底。


示例推演

我们以:

n = 12

为例。


第 1 层

初始:

queue = [12]
level = 1

处理 12

  • 12 - 1 = 11

  • 12 - 4 = 8

  • 12 - 9 = 3

所以第 1 层结束后:

queue = [11, 8, 3]

说明用了 1 个完全平方数后,可以得到这些状态。


第 2 层

现在:

level = 2

处理 11

  • 11 - 1 = 10

  • 11 - 4 = 7

  • 11 - 9 = 2

处理 8

  • 8 - 1 = 7

  • 8 - 4 = 4

处理 3

  • 3 - 1 = 2

去重后,队列中会有若干新状态。


第 3 层

继续往下扩展时,会遇到:

4 - 4 = 0

于是返回:

3

说明:

12 = 4 + 4 + 4

最少需要 3 个完全平方数。


再看 n = 13

初始:

queue = [13]

第 1 层

  • 13 - 1 = 12

  • 13 - 4 = 9

  • 13 - 9 = 4

得到:

[12, 9, 4]

第 2 层

处理 9 时:

9 - 9 = 0

于是返回:

2

说明:

13 = 4 + 9

最少需要 2 个完全平方数。


为什么 visited 很重要

比如某个数字 7,可能既能由:

11 - 4

得到,也能由:

8 - 1

得到。

如果不加 visited,那么 7 会被重复入队、重复搜索,造成大量重复计算。

所以:

HashSet<Integer> visited = new HashSet<>();

是 BFS 能高效运行的关键之一。


这题和动态规划的关系

这道题其实也可以用动态规划来做。

比如定义:

dp[i] = 和为 i 的完全平方数的最少数量

转移方程是:

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

动态规划和 BFS 都能解决这题。


BFS 的优点

  • 思路很像“最短路”,比较直观

  • 一旦搜到 0 就能提前结束

动态规划的优点

  • 写法更稳定

  • 更像标准的完全背包 / 最值 DP 模型

你这份 BFS 写法非常适合拿来理解“最少步数 = BFS 分层”的思想。


复杂度分析

假设每个状态最多会尝试 sqrt(n) 个平方数。

时间复杂度

一般可以写成:

O(n * sqrt(n))

因为每个小于等于 n 的状态最多访问一次,而每次会枚举若干个平方数。


空间复杂度

O(n)

因为队列和 visited 最坏情况下都可能存储不少状态。


总结

这道题的核心在于换一个角度理解它:

不是在凑平方数,而是在图上找从 n 到 0 的最短路径。

每次减去一个完全平方数,就相当于走一步。 因此:

  1. n 当作起点

  2. 每次减去一个平方数扩展到下一层

  3. 第一次到达 0 时,层数就是答案

你可以把这题总结成一句话:

完全平方数最少个数,本质就是从 n 走到 0 的最短步数,BFS 分层刚好解决。

这是一道非常经典的“数论外壳 + 图论思维”的题,值得彻底掌握。


如果对您有帮助的话,能否支持一下博主?
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇