8-今日一扣-leetcode-376-递归/动态规划/递归贪心/BFS-M*

完全平方数

题目

Leetcode
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

示例 1:
输入: n = 12
输出: 3
解释: 12 = 4 + 4 + 4.

示例 2:
输入: n = 13
输出: 2
解释: 13 = 4 + 9.

解决方案

递归暴力破解法

这个解法会超出时间限制。但是后续的算法是对该算法的一个提升。
原理:
n u m S q u a r e s ( n ) = min ⁡ ( n u m S q u a r e ( n − s q u a r e ) + 1 ) numSquares(n) = \min(numSquare(n-square)+1) numSquares(n)=min(numSquare(nsquare)+1)

square是所有的平方数。即1,4,9,16····

这个公式很好理解,例如我们想要求13的最小完全平方的个数,那么
13 = 12 + 1,即我们只需要知道12的最少完全平方个数在加上1
13 = 9 + 4, 即我们需要求9的最少完全平方个数在加1
13 = 4 + 9 …略

13可以改成上述三个组合(第二个加数必须是完全平方数k,第一个加数是13-k算出来的),然后比较上面3个结果,选择最小的
显然这是个递归的问题。

class Solution {

   	private static int[] squareNums ;
	private boolean isCalcuSquare = false;
	public int numSquares(int n) {
		// 暴力破解递归法
		
		if(!isCalcuSquare) {
			// +1是为了和index对应,好理解。
			squareNums = new int[(int)Math.sqrt(n) + 1];
			for(int i = 0; i<squareNums.length; i++) {
				squareNums[i]  = i*i;
			}
			isCalcuSquare = true;
		}
		
		//递归出口
		for(int square : squareNums) {
			if(square == n) {
				return 1;
			}
		}
		
		int ans = Integer.MAX_VALUE;
        // 不要遍历0,因为里面存的是0,造成死循环
		for(int i=1; i<squareNums.length; i++) {
			if( n < squareNums[i]) {
				// 如果当前计算的数小于square,就退出
				continue;
			}
			int newAns = numSquares(n-squareNums[i]) + 1;
			ans = Math.min(ans, newAns);
		}
		return ans;
	}
}

动态规划

解决递归运行速度慢的一个思路就是使用DP(动态规划)。基本思想是将中间的结果存储起来,下次使用的时候就不需要重新进行计算。是利用空间来换取时间的思想。
递归做了很多的重复计算。
我们把算过的存在一个数组中,那么下次在使用时就可以直接读取,避免了再次递归。
利用的公式仍然为
n u m S q u a r e s ( n ) = min ⁡ ( n u m S q u a r e ( n − s q u a r e ) + 1 ) numSquares(n) = \min(numSquare(n-square)+1) numSquares(n)=min(numSquare(nsquare)+1)

square是所有的平方数。即1,4,9,16····

/**
参考:https://leetcode-cn.com/problems/perfect-squares/solution/wan-quan-ping-fang-shu-by-leetcode/
*/
class Solution {
	public int numSquares(int n) {
		// 用来存储结果,+1为了让index和结果对应
		int [] ansArray = new int[n+1];
		// 初始值设置为最大
		Arrays.fill(ansArray,Integer.MAX_VALUE);
		ansArray[0] = 0;
		// 初始化平方数组
		// +1 为了对齐
		int[] squareNums = new int[(int)Math.sqrt(n) + 1];
		for(int i = 0; i<squareNums.length; i++){
			squareNums[i] = i*i;
		}
		
		// 从头开始计算 结果数组
		for(int i = 1; i<=n; i++) {
			
			for(int j=1; j<squareNums.length;j++) {
				// 要计算的结果比平方数小,则结束平方数
				int square = squareNums[j];
				if(i<square ) {
					break;
				}
				/* 关键步骤 */
				// 目标值:i
				// square一定小于等于i,所以 i-square的值,在先前的迭代中一定已经计算过了,直接使用就可以
				int newNums = ansArray[i - square] + 1; //在i,j都等1时,这句话会取到ansArray[0],所以置零
				// 将最小的存到结果中。
				ansArray[i] = Math.min(ansArray[i], newNums);
			}
		}
		return ansArray[n];
	}
}

注意:

  • 计算n时,实际上是从1开始计算,一直计算到n,把n之前的所有结果都算了一遍,在算i时,会用到i之前的结果。并保存到了ansArray中。
  • 时间复杂度:外循环n次,内循环需要次数最多为i = n时,是 n \sqrt n n 次,所以最终时间复杂度 O ( n n ) O(n\sqrt n) O(nn )
  • 空间复杂度:n+ n \sqrt n n 长度数组,故空间复杂度为 O ( n ) O(n) O(n)

递归贪心

主要思想是构造一个函数isDivied(n, count)。这个函数用来判定n是否能够通过count个平方数来构造。如果有这个函数,我们就从count=1开始,最先返回true的count值就是我们的结果。

isDivied通过递归设计。如果count == 1,那么n只有可能是平方数,否则不可分。这也是递归的出口。 当count != 1时,我们就开始尝试减去1个平方数,同时count-1来继续调用isDivied。

简而言之,就先看1个行不行,如果不行,在这个基础上两个行不行,所有的两个都不行,那么再尝试三个。
如图,count=1直接返回,count=2时,递归两层,count=3时,递归三层
在这里插入图片描述

class Solution {
    // 存储平方数。这里使用集合,查找比较快
    Set<Integer> squareNums = new HashSet<>();

	public int numSquares(int n) {
        //清空
        this.squareNums.clear();    
        //初始化平方数
        for(int i = 1; i*i <= n; i++){
            this.squareNums.add(i*i);
        }

        // 从1开始遍历。1代表本身就是平方数,2表示可由两个平方数组成。以此类推
        int count = 1;
        for(; count <=n; count++){
            if(isDivided(n,count)){
                //如果可以被整分,则就是结果。
                return count;
            }
        }
        return count;
	}

    private boolean isDivided(int n, int count){
        //递归出口
        if (count==1){
            return squareNums.contains(n);
        }
        // 递归调用,确定count-1是否可分。
        for(Integer square: squareNums){
        	if(square > n){
                continue;
            }
            if(isDivided(n-square, count-1)){
                return true;
            }
            //不能这么写,因为往下还有多种情况,这样只是在返回第一个square的情况。
            //return isDivided(n-square, count-1);
        }
        return false;
    }
}

BFS

在这里插入图片描述
上述的递归构建了递归树,实际上我们想要寻找层数最少的目标。可以使用广度优先遍历。

class Solution {
    // 存储平方数。这里使用集合,查找比较快

	public int numSquares(int n) {
        List<Integer> squareNums = new ArrayList<>();
        Queue<Integer> queue = new LinkedList<>();
        queue.add(n);
        //初始化平方数
        for(int i = 1; i*i<=n; i++){
            squareNums.add(i*i);
        }
        int level = 0;
    
        while(!queue.isEmpty()){
            int size = queue.size();
            level++;
            // 遍历层
            while(size>0){
                size --;
                int tar = queue.poll();
                
                for(int square : squareNums){    
                    if(square > tar){
                        break;
                    }
                    if(square == tar){
                        return level;
                    }
                    queue.add(tar-square);
                }
            }   
        }
        return level;
  }
}

queue使用链表运行时间很长,在leetcode上提交:

通过	261 ms	262.2 MB	Java

而按照官方的思路,使用HashSet来存储queue,时间差了很多:

通过	47 ms	39.9 MB	Java
// HashSet作为队列
class Solution {
    // 存储平方数。这里使用集合,查找比较快

	public int numSquares(int n) {
        List<Integer> squareNums = new ArrayList<>();
        Set<Integer> queue = new HashSet<>();
        queue.add(n);
        //初始化平方数
        for(int i = 1; i*i<=n; i++){
            squareNums.add(i*i);
        }
        int level = 0;
    
        while(!queue.isEmpty()){
            Set<Integer> nextQ = new HashSet<>();
            level++;
            // 遍历层
            for(int tar : queue){           
                for(int square : squareNums){    
                    if(square > tar){
                        break;
                    }
                    if(square == tar){
                        return level;
                    }
                    nextQ.add(tar-square);
                }
            }
            queue = nextQ;   
        }
        return level;
  }
}

之所以时间和内存差距这么大,是因为HashSet保证了节点唯一不重复,而LinkedList的队列中会有重复的节点,如图上的第三层,里面的节点7就出现了两次,也就会被重复计算。该现象在数字越大越明显。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值