• Medium | LeetCode 279. 完全平方数 | 动态规划 | BFS


    279. 完全平方数

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

    给你一个整数 n ,返回和为 n 的完全平方数的 最少数量

    完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,14916 都是完全平方数,而 311 不是。

    示例 1:

    输入:n = 12
    输出:3 
    解释:12 = 4 + 4 + 4
    

    示例 2:

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

    提示:

    • 1 <= n <= 104

    解题思路

    方法一:暴力枚举法

    先转化问题:给定一个完全平方数列表和正整数N, 求出完全平方数组合成N的组合, 要求组合中的解拥有完全平方数的最小个数。

    采用动态规划的方法求解。这种方法本质上是暴力枚举所有的组合。

    状态转移方程如下:

    [ ext { numSquares }(n)=min ( ext { numSquares }(mathbf{n}-mathbf{k})+1) quad forall k in square space numbers ]

    这里采用递归的方式实现动态规划。

    class Solution1 {
     
        private int[] squareList;
    
        public int numSquares(int n) {
            double sqrt = Math.sqrt(n);
            // 先生成平方数列表 最大的完全平方数刚好不大于N
            squareList = new int[Double.valueOf(sqrt).intValue()];
            for (int i = 1; i <= squareList.length; i++) {
                squareList[i-1] = i * i;
            }
            return findSquare(n);
        }
    
        public int findSquare(int n) {
            if (isSquare(n)) {
                // 递归出口, N是一个包含在完全平方数列表中的数字
                return 1;
            }
            int minNumber = Integer.MAX_VALUE;
            // 遍历完全平方数列表的所有数
            for (int square : squareList) {
                if (square < n) {
                    // 计算 n - square 需由多少个完全平方数组成
                    minNumber = Math.min(findSquare(n - square) + 1, minNumber);
                }
            }
            return minNumber;
        }
    
        public boolean isSquare(int n) {
            int left = 0, right = squareList.length - 1;
            while (left <= right) {
                int mid = (left + right) >> 1;
                if (n < squareList[mid]) {
                    left = mid + 1;
                } else if (n > squareList[mid]) {
                    right = mid - 1;
                } else {
                    return true;
                }
            }
            return false;
        }
    }
    

    方法二:动态规划

    方法一中实际上有很多的结果重复计算。一种解决的办法是使用int[] dp = new int[n+1]的数组来保留中间的计算结果。下面的代码的思想基本和第一种办法相同。一个不同点时, 这里采用自底向上的迭代的方法实现。

    public int numSquares(int n) {
        // 动态规划数组, dp[i]表示组成数字i的完全平方数的最小的个数
        int dp[] = new int[n + 1];
        // 初始化为无穷大数组
        Arrays.fill(dp, Integer.MAX_VALUE);
        // bottom case
        dp[0] = 0;
    
        // 预先建立平方数数组
        int max_square_index = (int) Math.sqrt(n) + 1;
        int square_nums[] = new int[max_square_index];
        for (int i = 1; i < max_square_index; ++i) {
            square_nums[i] = i * i;
        }
    
        for (int i = 1; i <= n; ++i) {
            // 遍历完全平方数列表, 尝试使用每一个不大于 i 的完全平方数
            for (int s = 1; s < max_square_index; ++s) {
                if (i < square_nums[s])
                    // 剪枝, 当完全平方数列表的某个数大于数字i时,后面的数字也就不用计算了。
                    break;
                dp[i] = Math.min(dp[i], dp[i - square_nums[s]] + 1);
            }
        }
        return dp[n];
    }
    

    方法三: 贪心枚举

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

    class Solution3 {
    
        private Set<Integer> square_nums = new HashSet<Integer>();
    
        public int numSquares(int n) {
            this.square_nums.clear();
    
            for (int i = 1; i * i <= n; ++i) {
                this.square_nums.add(i * i);
            }
    
            int count = 1;
            for (; count <= n; ++count) {
                // 依次提高完全平方数的个数, 先看这个数能不能被1个完全数组合, 再看2个, 3个
                if (is_divided_by(n, count))
                    return count;
            }
            return count;
        }
        
        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;
        }    
    }
    

    方法四:贪心 + BFS

    把方法三的思路, 描绘成 一棵树, 然后通过遍历的方式, 找到叶节点是完全平方数的最小深度。

    1. 首先,我们准备小于给定数字 n 的完全平方数列表(即 square_nums)。
    2. 然后创建 queue 遍历,该变量将保存所有剩余项在每个级别的枚举。
    3. 在主循环中,我们迭代 queue 变量。在每次迭代中,我们检查余数是否是一个完全平方数。
      3.1 如果余数不是一个完全平方数,就用其中一个完全平方数减去它,得到一个新余数,
      3.2 然后将新余数添加到 next_queue 中,以进行下一级的迭代。
      一旦遇到一个完全平方数的余数,我们就会跳出循环,这也意味着我们找到了解。

    时间复杂度和空间复杂度和方法三相当

    public int numSquares(int n) {
    
        ArrayList<Integer> square_nums = new ArrayList<Integer>();
        for (int i = 1; i * i <= n; ++i) {
            square_nums.add(i * i);
        }
    
        Set<Integer> queue = new HashSet<Integer>();
        // 初始时, queue里的数字, 
        queue.add(n);
    
        int level = 0;
        while (queue.size() > 0) {
            // 记录当前遍历的层次
            level += 1;
            Set<Integer> next_queue = new HashSet<Integer>();
    
            for (Integer remainder : queue) {
                // 对于队列中的每个数
                for (Integer square : square_nums) {
                    // 用完全平方数列表的每个数字
                    if (remainder.equals(square)) {
                        // 如果发现了当前层次的数字是一个完全平方数, 则返回当前的层次
                        return level;
                    } else if (remainder < square) {
                        break;
                    } else {
                        // 如果当前数字不是完全平方数, 将剩余项添加到下一层的队列
                        next_queue.add(remainder - square);
                    }
                }
            }
            queue = next_queue;
        }
        return level;
    }
    

    方法五:数学运算

    1770 年,Joseph Louis Lagrange证明了一个定理,称为四平方和定理,也称为 Bachet 猜想,它指出每个自然数都可以表示为四个整数平方和:
    在 1797 年,Adrien Marie Legendre用他的三平方定理完成了四平方定理,证明了正整数可以表示为三个平方和的一个特殊条件:
    n != 4^k * (8*m + 7) 等价于 n 为三数平方和
    至今没有任何定理能解决一个数是否是两个数的平方和的问题, 只能使用枚举的办法
    所以算法思路为: 首先检查 数字 n 的形式是否为 n = 4^k * (8m+7) 如果是, 则直接返回4
    进一步检查这个数本身是否是一个完全平方数,或者这个数是否可以分解为两个完全平方数和。
    在底部的情况下,这个数可以分解为 3 个平方和。
    时间复杂度: O(N ^ 1/2)
    空间复杂度: O(1)

    protected boolean isSquare(int n) {
        int sq = (int) Math.sqrt(n);
        return n == sq * sq;
    }
    
    public int numSquares(int n) {
        // four-square and three-square theorems.
        while (n % 4 == 0)
            n /= 4;
        if (n % 8 == 7)
            return 4;
    
        if (this.isSquare(n))
            return 1;
        // enumeration to check if the number can be decomposed into sum of two squares.
        for (int i = 1; i * i <= n; ++i) {
            if (this.isSquare(n - i * i))
                return 2;
        }
        // bottom case of three-square theorem.
        return 3;
    }
    
  • 相关阅读:
    oracle单行函数 之 转换函数
    oracle单行函数 之 时间函数
    oracle单行函数 之 数字函数
    oracle单行函数 之 字符函数
    oracle 之 如何链接别人电脑的oracle
    轻应用介绍
    Web 目录枚举与遍历漏洞解决
    接口测试工具(Postman)
    Tomcat 编码不一致导致乱码
    持久化配置管理 diamond 使用简介
  • 原文地址:https://www.cnblogs.com/chenrj97/p/14351847.html
Copyright © 2020-2023  润新知