完全平方数

题目要求:

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

这是一道非常经典的算法题(对应 LeetCode 279. 完全平方数)。解决这道题最常见且最易于理解的方法是使用动态规划(Dynamic Programming)。

方法一:动态规划(首选)

思路
我们可以定义一个数组dp,其中dp[i]表示和为i的完全平放数的最少数量
对于任意一个数字i,它可以由一个较小的数字i-jj加上一个完全平方数j j组成。
因此,状态转移方程为:dp[i] = Min(dp[i],dp[i-j* j]+1),其中j* j <= i。

Java代码实现如下

class Solution {
    public int numSquares(int n) {
        // dp[i] 表示数字 i 最少可以由几个完全平方数相加构成
        int[] dp = new int[n + 1];
        
        // 初始状态,0 由 0 个完全平方数组成
        dp[0] = 0;
        
        // 遍历 1 到 n,计算每个数的 dp 值
        for (int i = 1; i <= n; i++) {
            // 最坏的情况就是每次都由 1 的平方组成,即 i 个 1
            dp[i] = i; 
            
            // 尝试减去所有的完全平方数 j*j,找到最小值
            for (int j = 1; j * j <= i; j++) {
                dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
            }
        }
        
        return dp[n];
    }
}

复杂度分析:

  • 时间复杂度: o(n*根号n)。外层循环执行n次,内层循环执行根号n次
  • 空间复杂度:O(n)。需要长度为n+1的数组dp来保存中间状态

方法二:暴力搜索

暴力搜索通常是我们思考算法题的起点,对于这道题,最直接的暴力解法就是递归(回溯/深度优先1搜索DFS)

对于当前的数字n:

我们尝试减去所有小于等于n的完全平方数()
每减去一个完全平方数i^2, 剩下了 n-i^2。
我们递归地去求n-i^2需要地最少完全平方数数量
将所有尝试地结果进行比较,取最小值再加1(加上刚才减去的那个完全平方数),就是答案。

Java代码实现如下:

class Solution{
  public int numSquares(int n){
    //递归终止条件:如果n是0,说明刚好被完全平方数扣完了,需要0个数
    if(n==0){
      return 0;
    }

    //初始化为一个比较大的数。因为最坏情况是由n个1组成,所以n就是理论最大值
    int minCount = n;
    
    //尝试当前可以选择的所有完全平方数(1,4,9,16)
    for(int i = 1;i*i<=n;i++){
      //递归调用:当前消耗了1个完全平方数(i*i),剩下的数字n-i*i仍给下一层递归
      int count  = numSquares(n-i*i) +1;
       minCount =  Math.min(minCount,count); 
    }
    
    return minCount;
}

暴力解法为什么在实际提交中会失败(超时 TLE)?

暴力解法的逻辑是绝对正确且严密的,但它有一个致命的缺陷:包含海量的重复计算(重叠子问题)
我们以 numSquares(12) 为例,看看递归的展开过程:

  • 当尝试减去1时,进入递归求numSquares(11)。

    • 在算numSquares(11) 时,尝试减去 4,会去求 numSquares(7)。
  • 当尝试减去 4 时,进入递归求 numSquares(8)。

    • 在算 numSquares(8) 时,尝试减去 1,也会去求 numSquares(7)。
      你会发现,数字 7 被计算了两次!随着 n 的增大,这种重复计算会呈指数级爆炸式增长。
  • 时间复杂度:极高

  • 空间复杂度:O(n),主要是递归调用栈的最大深度(最坏情况下每次减1,递归n层)

从暴力到动态规划(顿悟时刻)

看到这里,你可能发现了:动态规划其实就是带了备忘录的暴力解法

既然暴力解法慢是因为“重复计算了相同的 numSquares(x)”,那我们是不是可以在第一次算出 numSquares(7) 时,就把它的答案记在一个小本本(数组或哈希表)上?下次再遇到 7 时,直接翻本子查答案,不往下递归了。

  • 暴力递归 + 记忆化(备忘录) = 自顶向下的动态规划
  • 直接从小到大填表 = 自底向上的动态规划

这就是算法优化的美妙之处:暴力解法指明了问题的拆解方向,而动态规划修补了暴力解法效率低下的漏洞。

posted @ 2026-05-21 22:08  ZealousMclaren  阅读(13)  评论(0)    收藏  举报