[DFS BFS 动态规划] 279. 完全平方数(回溯法DFS → 动态规划、BFS、数学定理+排除法)

7 篇文章 0 订阅
7 篇文章 0 订阅

[DFS BFS 动态规划] 279. 完全平方数(回溯法DFS → 动态规划、BFS、数学定理+排除法)

279. 完全平方数

题目链接:https://leetcode-cn.com/problems/perfect-squares/


分类:

  • 回溯法:DFS思想,一趟DFS返回一个划分方案所需要的最少完全平方数个数(思路1);
  • 动态规划:辅助数组优化回溯法,dp[i] = 数字 i 可拆分的最少完全平方数个数(思路2);
  • BFS:用BFS树来理解,数字看做BFS树的节点,所有 <= 父节点的完全平方数看做边,父节点 - 边的结果作为父节点的孩子节点(思路3);
  • 数学:排除法、四平方定理、 n = (4^a)*(8b+7)则n至少可以拆分成4个数的平方和(思路4)。

在这里插入图片描述

思路1:回溯法DFS(暴力解)

对于数字n,我们枚举所有小于n的完全平方数,拿n与这些完全平方数依次相减。

一趟DFS就是选择一个完全平方数和n相减,然后拿得到的相减结果传入下一层递归,直到n==0,这一趟DFS就是对n的一种划分方案,每一趟DFS都会返回一个划分方案所使用的元素个数。

我们取所有划分方案所使用的完全平方数个数的最小值,返回即可。

设计递归函数:

  • 返回值:返回对n划分的最少完全平方数个数;

  • 递归出口:如果n == 0,返回0

  • 递归主体:如果n != 0,for循环枚举所有小于n的完全平方数,以每个完全平方数 i 为起点进行一趟DFS,把 n - i 传入下一层递归,递归函数返回的是划分 n - i 所需的最少完全平方数个数,再加上这一层所做的划分(n - i)也使用了一个完全平方数,就得到n的其中一个划分方案的最少完全平方数个数。

    取n的所有划分方案里完全平方数最少的数值返回。

class Solution {
    public int numSquares(int n) {
        return dfs(n);
    }
    //dfs回溯法递归实现
    public int dfs(int n){
        if(n == 0) return 0;
        int min = Integer.MAX_VALUE;
        for(int i = 1; i * i <= n; i++){
            //寻找所有划分方案里完全平方数个数的最小值
            min = Math.min(min, dfs(n - i * i)  + 1);
        }
        return min;
    }
}
  • 存在的问题:效率过低,用例[55]超时。

思路2:动态规划(推荐)

创建一个dp数组存放组成每个数字的最少完全平方数个数。

dp数组初始化:

置所有小于n的完全平方数对应的dp值=1,例如:dp[1]=1,dp[4]=1,dp[9]=1,…

dp数组的构造过程:

例如:n=12,要求dp[12]

其中,小于n的完全平方数有1,4,9,先拆分得:12=9+3,dp[9]+dp[3]=1+3=4,置min=4(计算dp[12]时dp[3]已经计算完毕);

选择下一个拆分方案:12=4+8,dp[4]+dp[8]=3,3<min,所以更新min=3;

选择下一个拆分方案:12=1+11,dp[1]+dp[11] > min,所以min保持不变;

所有小于12的完全平方数都处理完毕,取其中的最小值min作为dp[12]的值。

class Solution {
    public int numSquares(int n) {
        int[] dp = new int[n + 1];
        //dp数组初始化
        for(int i = 1; i * i <= n; i++){
            dp[i * i] = 1;
        }
        //构造dp数组
        for(int i = 1; i <= n; i++){
            //寻找<=i的所有完全平方数
            int min = Integer.MAX_VALUE;
            for(int j = 1; j * j <= i; j++){
                min = Math.min(dp[j * j] + dp[i - j * j], min);
            }
            dp[i] = min;
        }
        return dp[n];
    }
}
  • 时间复杂度:O(N√N),为求dp[n]需要处理dp[i=1~n]共O(N),对于每个dp[i],都遍历 <= i 的所有完全平方数,所以需要O(√N),所以整体时间复杂度为O(N√N);
  • 空间复杂度:O(N)

思路3:BFS(推荐)

思路1是DFS,每一趟DFS在当前递归层选择一个完全平方数与n相减,进入下一层递归又选择一个完全平方数与新的n相减,直到n==0才得到n的一种划分方案。

而BFS树如下图:(图片来源:详细通俗的思路分析,多解法 by windliang
在这里插入图片描述

BFS树中,把各个数字看做BFS树的节点,各个边就是拿<=n的所有完全平方数分别与n相减,相减的结果作为当前节点的孩子节点。

灰色节点表示该数字曾经出现过,但之前处理该数字时并没有得到最优解,所以再次遇到该数字时可以直接跳过。同一个数字只处理一次,达到剪枝的效果。

寻找完全平方数最少的划分方案就是做BFS,寻找最早出现n = 0的所在层数。

BFS的实现需要用到队列,且可能在BFS过程中得到重复的n,所以再使用一个set存放出现过的数字以便去重剪枝。

class Solution {
    public int numSquares(int n) {
        Queue<Integer> queue = new LinkedList<>();
        Set<Integer> set = new HashSet<>();
        queue.offer(n);
        set.add(n);
        int level = 0;//记录当前遍历节点所在的层数,即已使用的完全平方数个数
        //BFS流程
        while(!queue.isEmpty()){
            int size = queue.size();
            level++;
            //处理当前层的所有节点
            for(int i = 0; i < size; i++){
                int num = queue.poll();
                //每个数字都与所有<=它的完全平方数相减,相减结果作为孩子节点加入队列
                for(int j = 1; j * j <= n; j++){
                    int temp = num - j * j;
                    //如果num减为0,当前所在层数就是最少的完全平方数个数
                    if(temp == 0) return level;
                    //如果num曾经出现过,说明它一定不是最佳路径上的节点,直接跳过
                    if(set.contains(temp)) continue;
                    queue.offer(temp);
                    set.add(temp);
                }   
            }
        }
        return -1;
    }
}

思路4:数学(记下结论就行)

四平方定理:任何正整数都一定能拆分成不超过4个数的平方和,所以组成一个正整数的最少完全平方数个数只可能是1,2,3,4中的一个。

因此我们可以用排除法找出最终的答案:

  1. 判断num本身是不是某个数的平方,如果是,则答案就是1;如果不是,答案剩下2,3,4
  2. 还存在另一个定理:如果一个数 = (4^a)*(8b+7),那么它至少可以拆分4个数的平方和,所以如果num = (4^a)*(8b+7),那么答案就是4,如果不是,答案剩下2,3;
  3. 如果答案是2,则n=a2+b2,我们可以枚举所有小于n的完全平方数作为a,判断n-a^2是不是另一个数的平方,如果是则答案为2;
  4. 排除了其他三种可能,则答案只可能是3.

如何判断一个数num是不是某个数的平方?

使用sqrt计算num的平方根,并将平方根从float转成int,如果num不是平方数,则得到的平方根从float转int时就会存在精度丢失,我们再判断平方根*平方根是否==num,如果相等,说明num是某个数的平方,如果不是,说明num不是某个数的平方。

//数学法(其他思路的代码见笔记)
class Solution {
    public int numSquares(int n) {
        //判断答案是否为1
        if(isSquare(n)) return 1;
        //判断答案是否为4
        int temp = n;
        while(temp % 4 == 0){
            temp /= 4;
        }
        if(temp % 8 == 7) return 4;
        //判断答案是否为2
        for(int i = 1; i * i <= n; i++){
            if(isSquare(n - i * i)) return 2;
        }
        //最后只剩下3
        return 3;
    }
    //判断一个数是不是某个数的平方
    public boolean isSquare(int num){
        int sqrt = (int)Math.sqrt(num);
        return sqrt * sqrt == num;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值