【LeetCode】279.完全平方数(四种方法,不怕不会!开拓思维)

这篇博客介绍了如何解决LeetCode中的完全平方数问题,探讨了四种方法:动态规划、贪心枚举、贪心+BFS以及数学运算。动态规划策略通过递归减少计算量,贪心枚举结合了递归和贪心思想,贪心+BFS通过广度优先搜索优化空间复杂度,而数学运算部分则利用拉格朗日和Legendre的平方和定理解析问题。
摘要由CSDN通过智能技术生成

题目

链接

image-20200710145351219

题解

方法一:动态规划

思路

1,对于正整数N, 所有的解都是 N = 一个整数的平方 + 另一个整数; 直白点, N = AxA + B
2, 而B又是由 “一个整数的平方 + 另一个整数” 组成的; 那么, B = CxC + D
3,总结下就是:N = IxI + N’ 而 N’ = IxI + N’’

4, 本题要解的问题:正整数N最少由多个平方数相加;
5, 那么,N的最优解 = 1 + (N’的最优解)。而N’肯定小于N。
6, 所以本题的思路就是,对每一个N,观察1到N-1中,谁的解最小,那么N的解就是它+1.

7, 但是我们没必要1到N-1中的每一个数都去观察,因为有些组合不满足N = IxI + N’,譬如12 = 2+N’是不需要的,因为2不是某个数的平方。所以我们观察的范围要大大减小。

拿12举例,我们只能观察:
12 = 1 + 11
12 = 4 + 8
12 = 9 + 3
我们要得出3,8,11中谁的解最优,那么12的解就是它+1。

8, 我们从1到N计算, 2的解从1里找,3的解从[2,1]里找,4的解从[3,2,1]里找,依次类推,最后算到N的解即可。

数学理解

//假设最小公式值m = ƒ(n) 
//那么n的值满足下列公式 ∑(A[i] * A[i]) = n 
//令 k 为满足最小值 m 的时候,最大的平方数  。 令  d + k * k = n ;  d >= 0; 
   // 注意:一定要是满足m最小的时候的k值,一味的取最大平方数,就是贪心算法了
//得出 f(d) + f(k*k) = f(n);
//显然 f(k*k) = 1; 则  f(d) + 1 = f(n); 因为 d = n - k*k;
//则可以推出ƒ(n - k * k) + 1 = ƒ(n) ;  且 k * k <= n;

我们来理解一下动态规划方程dp[i] = Math.min(dp[i], dp[i - j * j] + 1)

    #动态方程的全写版本应该是:
    for(int j = 1;j*j<=i;j++){
   
		dp[i] = Math.main(dp[i],dp[i-j*j]+dp[j*j];
}
    # dp[i]:表示完全平方数和为i的 最小个数
    # 初始状态dp[i]均取最大值i,即 1+1+...+1,i个1; dp[0] = 0
    # dp[i] = min(dp[i], dp[i-j*j]+1),其中, j是平方数, j=1~k,其中k*k要保证 <= i
    # 意思就是:(完全平方数和为i的最小个数) 等于 (当前完全平方数和为i的最大个数)dp[i] 与 (完全平方数和为 i - j * j 的最小个数 + 完全平方数和为 j * j的 最小个数)的最小个数
    #   可以看到 dp[j*j] 是等于1
class Solution {
   
    public int numSquares(int n) {
   
        int[] dp = new int[n + 1]; // 默认初始化值都为0
        for (int i = 1; i <= n; i++) {
   
            dp[i] = i; // 最坏的情况就是每次+1
            for (int j = 1; i - j * j >= 0; j++) {
    
                dp[i] = Math.min(dp[i], dp[i - j * j] + 1); // 动态转移方程
            }
        }
        return dp[n];
    }
}

方法二:贪心枚举

递归解决方法为我们理解问题提供了简洁直观的方法。我们仍然可以用递归解决这个问题。为了改进上述暴力枚举解决方案,我们可以在递归中加入贪心。我们可以将枚举重新格式化如下:

从一个数字到多个数字的组合开始,一旦我们找到一个可以组合成给定数字 n 的组合,那么我们可以说我们找到了最小的组合,因为我们贪心的从小到大的枚举组合。

为了更好的解释,我们首先定义一个名为 is_divided_by(n, count) 的函数,该函数返回一个布尔值,表示数字 n 是否可以被一个数字 count 组合,而不是像前面函数 numSquares(n) 返回组合的确切大小。

image-20200710173515393

与递归函数 numSquare(n) 不同,is_divided_by(n, count) 的递归过程可以归结为底部情况(即 count==1)更快。

下面是一个关于函数 is_divided_by(n, count) 的例子,它对 输入 n=5count=2 进行了分解。

在这里插入图片描述
通过这种重新构造的技巧,我们可以显著降低堆栈溢出的风险。

算法:

  • 首先,我们准备一个小于给定数字 n 的完全平方数列表(称为 square_nums)。
  • 在主循环中,将组合的大小(称为 count)从 1 迭代到 n,我们检查数字 n 是否可以除以组合的和,即 is_divided_by(n, count)
  • 函数 is_divided_by(n, count) 可以用递归的形式实现,汝上面所说。
  • 在最下面的例子中,我们有 count==1,我们只需检查数字 n 是否本身是一个完全平方数。可以在 square_nums 中检查,即 n \in \text{square_nums}n∈square_nums。如果 square_nums 使用的是集合数据结构,我们可以获得比 n == int(sqrt(n)) ^ 2 更快的运行时间。

关于算法的正确性,通常情况下,我们可以用反证法来证明贪心算法。这也不例外。假设我们发现 count=m 可以除以 n,并且假设在以后的迭代中存在另一个 count=p 也可以除以 n,并且这个数的组合小于找到的数,即 p<m。如果给定迭代的顺序,count = p 会在 count=m 之前被发现,因此,该算法总是能够找到组合的最小大小。

下面是一些示例实现。Python 解决方案需要大约 70ms,这比当时大约 90% 的提交要快。

class Solution {
   
  Set<Integer> square_nums = new HashSet<Integer>();

  protected boolean is_divided_by(int n, int count) {
   
    if (count == 1) {
   
      return square_nums.contains(n);
    }

    for (Integer square : square_nums) {
   
      if (is_divided_by(n - square, count - 1)) {
   
        return true;
      }
    }
    return false;
  }

  public int numSquares(int n) {
   
    this.square_nums.clear();

    for (int i = 1; i * i <= n; ++i) {
   
      
  • 6
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值