【LeetCode Hot100 刷题日记 (84/100)】279. 完全平方数 —— 动态规划、数学、完全平方数、四平方和定理🧠

4 阅读6分钟

📌 题目链接:279. 完全平方数 - 力扣(LeetCode)

🔍 难度:中等 | 🏷️ 标签:动态规划、数学、完全平方数、四平方和定理

⏱️ 目标时间复杂度:O(n√n)(DP)或 O(√n)(数学法)

💾 空间复杂度:O(n)(DP)或 O(1)(数学法)


在 LeetCode 的经典题目 279. 完全平方数 中,我们被要求找出“和为 n 的完全平方数的最少数量”。这道题看似简单,却蕴含了两种截然不同的解题哲学:动态规划的通用性数学定理的优雅性。无论你是准备面试还是夯实算法基础,这道题都值得你深入掌握。


🔗 题目链接

leetcode.cn/problems/pe…


🔍 题目分析

给定一个正整数 n,我们需要将其表示为若干个完全平方数(如 1, 4, 9, 16...)之和,并使得所用的完全平方数数量最少

例如:

  • n = 124 + 4 + 4 → 最少 3 个
  • n = 134 + 9 → 最少 2 个

关键点:

  • 不要求平方数互不相同;
  • 要求最少数量,而非所有组合;
  • n 的范围是 1 ≤ n ≤ 10⁴,适合使用 DP 或数学优化。

⚙️ 核心算法及代码讲解

本题有两种主流解法:

✅ 方法一:动态规划(通用、易理解、面试高频)

思路核心

定义 f[i] 表示组成数字 i 所需的最少完全平方数个数。

状态转移方程:

f[i] = min{ f[i - j²] } + 1   ,其中 j² ≤ i

边界条件:f[0] = 0(虽然 0 不能由正平方数组成,但作为递推起点必须设为 0)

代码详解(C++)

vector<int> f(n + 1);               // f[i] 表示和为 i 的最少平方数个数
for (int i = 1; i <= n; i++) {
    int minn = INT_MAX;             // 初始化当前最小值为无穷大
    for (int j = 1; j * j <= i; j++) {  // 枚举所有可能的平方数 j²
        minn = min(minn, f[i - j * j]); // 从子问题 f[i - j²] 转移
    }
    f[i] = minn + 1;                // 加上当前使用的 j²,总数 +1
}
return f[n];

优点:逻辑清晰,适用于任意“硬币找零”类问题变种。
⚠️ 缺点:时间复杂度较高(O(n√n)),但在 n ≤ 10⁴ 下完全可接受。


✅ 方法二:数学法(基于四平方和定理,极致优化)

📜 四平方和定理(Lagrange's Four-Square Theorem)

任意正整数都可以表示为至多 4 个完全平方数之和。

更进一步,Legendre 补充了判定条件:

当且仅当 n = 4ᵏ × (8m + 7) 时,n 必须用 4 个平方数表示;否则最多用 3 个。

因此,答案只能是 1、2、3 或 4。

判定流程:

  1. 是否为完全平方数? → 是则返回 1。
  2. 是否满足 n = 4ᵏ(8m+7) → 是则返回 4。
  3. 能否拆成两个平方数之和? → 枚举 a,检查 n - a² 是否为平方数 → 是则返回 2。
  4. 否则 → 必为 3。

代码详解(C++)

// 判断 x 是否为完全平方数
bool isPerfectSquare(int x) {
    int y = sqrt(x);                // 取整数平方根
    return y * y == x;              // 验证平方是否等于原数
}

// 判断是否满足 n = 4^k * (8m + 7)
bool checkAnswer4(int x) {
    while (x % 4 == 0) {            // 不断除以 4,直到无法整除
        x /= 4;
    }
    return x % 8 == 7;              // 检查剩余部分是否 ≡7 (mod 8)
}

int numSquares(int n) {
    if (isPerfectSquare(n)) return 1;       // 情况1:本身就是平方数
    if (checkAnswer4(n)) return 4;          // 情况2:必须用4个
    for (int i = 1; i * i <= n; i++) {      // 情况3:尝试拆成两个
        int j = n - i * i;
        if (isPerfectSquare(j)) return 2;
    }
    return 3;                               // 情况4:只能是3
}

优点:时间复杂度 O(√n),空间 O(1),极其高效。
💡 面试加分项:能说出四平方和定理,展现数学素养!


🧩 解题思路(分步拆解)

动态规划法步骤:

  1. 初始化:创建长度为 n+1 的数组 ff[0] = 0
  2. 外层循环:遍历 i 从 1 到 n,计算每个 f[i]
  3. 内层循环:枚举所有 j 满足 j² ≤ i
  4. 状态转移f[i] = min(f[i - j²]) + 1
  5. 返回结果f[n] 即为答案。

数学法步骤:

  1. 检查 1n 是否为完全平方数?
  2. 检查 4:不断除以 4,看是否最终模 8 余 7。
  3. 检查 2:枚举 a ∈ [1, √n],看 n - a² 是否为平方数。
  4. 默认 3:若以上都不满足,则答案为 3。

📊 算法分析

方法时间复杂度空间复杂度适用场景
动态规划O(n√n)O(n)通用解法,适合变形题(如限制平方数种类)
数学法O(√n)O(1)本题最优解,体现数学洞察力

💡 面试建议:先写 DP 解法(稳妥),再提数学优化(惊艳)!


💻 代码

C++

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

// 方法一:动态规划
class SolutionDP {
public:
    int numSquares(int n) {
        vector<int> f(n + 1);
        for (int i = 1; i <= n; i++) {
            int minn = INT_MAX;
            for (int j = 1; j * j <= i; j++) {
                minn = min(minn, f[i - j * j]);
            }
            f[i] = minn + 1;
        }
        return f[n];
    }
};

// 方法二:数学法
class SolutionMath {
public:
    bool isPerfectSquare(int x) {
        int y = sqrt(x);
        return y * y == x;
    }

    bool checkAnswer4(int x) {
        while (x % 4 == 0) {
            x /= 4;
        }
        return x % 8 == 7;
    }

    int numSquares(int n) {
        if (isPerfectSquare(n)) {
            return 1;
        }
        if (checkAnswer4(n)) {
            return 4;
        }
        for (int i = 1; i * i <= n; i++) {
            int j = n - i * i;
            if (isPerfectSquare(j)) {
                return 2;
            }
        }
        return 3;
    }
};

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    SolutionMath sol;
    // 测试用例
    cout << sol.numSquares(12) << "\n"; // 输出: 3
    cout << sol.numSquares(13) << "\n"; // 输出: 2
    cout << sol.numSquares(1) << "\n";  // 输出: 1
    cout << sol.numSquares(7) << "\n";  // 输出: 4 (7 = 4^0 * (8*0+7))
    cout << sol.numSquares(6) << "\n";  // 输出: 3 (4+1+1)

    return 0;
}

JavaScript

// 方法一:动态规划
var numSquares = function(n) {
    const f = new Array(n + 1).fill(0);
    for (let i = 1; i <= n; i++) {
        let minn = Number.MAX_VALUE;
        for (let j = 1; j * j <= i; j++) {
            minn = Math.min(minn, f[i - j * j]);
        }
        f[i] = minn + 1;
    }
    return f[n];
};

// 方法二:数学法
const isPerfectSquare = (x) => {
    const y = Math.floor(Math.sqrt(x));
    return y * y === x;
};

const checkAnswer4 = (x) => {
    while (x % 4 === 0) {
        x /= 4;
    }
    return x % 8 === 7;
};

var numSquares = function(n) {
    if (isPerfectSquare(n)) {
        return 1;
    }
    if (checkAnswer4(n)) {
        return 4;
    }
    for (let i = 1; i * i <= n; i++) {
        let j = n - i * i;
        if (isPerfectSquare(j)) {
            return 2;
        }
    }
    return 3;
};

🎯 面试高频考点总结

  • 动态规划建模能力:能否将“最少数量”转化为状态转移?
  • 数学定理应用:是否知道四平方和定理?能否快速判断 4 的情况?
  • 边界处理f[0] = 0 的合理性?sqrt 取整误差如何避免?
  • 优化意识:在 DP 基础上,能否想到数学剪枝?

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!