1. 程式人生 > 實用技巧 >LeetCode Notes_#279 完全平方數

LeetCode Notes_#279 完全平方數

LeetCode Notes_#279 完全平方數

LeetCode

Contents

題目

給定正整數n,找到若干個完全平方數(比如1, 4, 9, 16, ...)使得它們的和等於 n。你需要讓組成和的完全平方數的個數最少。
示例1:

輸入: n = 12
輸出: 3 
解釋: 12 = 4 + 4 + 4.

示例 2:

輸入: n = 13
輸出: 2
解釋: 13 = 4 + 9.

思路分析

本題有點像剪繩子問題,要把一個數字拆分,且拆分出來的部分,要滿足一個最優化的條件。
方法比較多,包括暴力遞迴,記憶化遞迴,動態規劃,BFS,數學法。
比較常見的就是中間的3種,暴力遞迴效能差,數學法則沒有普適性,需要記下來。記憶化遞迴和動態規劃則思路非常相似。
所以記錄了比較常見的,具有普適性的兩種寫法,動態規劃法和BFS法。

解答

方法1:動態規劃

  1. 計算平方數的陣列,下標範圍是1~(int)sqrt(n),每個位置的元素是下標的平方
  2. 根據狀態轉移方程,從dp[0]開始計算,迭代地計算出dp[n],就是結果
class Solution {
    public int numSquares(int n) {
        //[0,n]一共是n + 1個數字
        int dp[] = new int[n + 1];
        //因為下面用到Math.min取最小值,所以初始化為最大,否則可能不被更新
        Arrays.fill(dp, Integer.MAX_VALUE);
        //沒有實際意義,因為輸入都是正數
//設定為0可以正確計算dp[1],同時使得陣列下標與numSquares(n)的結果對應起來 dp[0] = 0; //max_square_index的數字的平方是小於n的最大數字,更大的平方數不可能組成n int max_square_index = (int)Math.sqrt(n); //儲存所有可能組成n的平方數,下標範圍是[1, max_square_index] int[] square_nums = new int[max_square_index + 1]; for(int i = 1
;i <= max_square_index;i++){ //square_nums[0]是預設值0 square_nums[i] = i * i; } //從dp[1]開始,迭代地計算出dp[n],就是最終結果 for(int i = 1;i <= n;i++){ for(int j = 1;j <= max_square_index;j++){ if(i >= square_nums[j]) dp[i] = Math.min(dp[i], dp[i - square_nums[j]] + 1); } } return dp[n]; } }

複雜度分析

時間複雜度:
空間複雜度:

方法2:BFS

將原問題轉化為一個n叉樹的BFS遍歷問題。
如下圖,將輸入的n作為根節點。
每個節點的孩子節點的值是當前節點減去一個平方數(依次減去1,4,9...)。
則值為0的節點距離根節點的最短路徑就是結果。
例如下圖中的橙色節點,距離根節點距離是3,可以寫出分解的表示式12 = 4 + 4 + 4,每一條邊代表的就是一個平方數。
程式碼和二叉樹的層序遍歷很相似,存在以下幾個不同點:

  1. 這裡的下一層節點不是通過左右指標來尋找,而是通過當前層節點減去平方數計算得到。
  2. 重複節點只需入隊一次。
    • 如果重複節點位於同一層,那麼兩個節點的子樹結構完全相同,計算出來的最短路徑一樣,如果保留則造成多餘的計算。
    • 如果重複節點位於不同層,那麼後訪問到的節點必然離根節點更遠,又因為兩個節點的子樹結構完全相同(即到0節點的距離相同),所以第二次之後訪問的重複節點可以被忽略。
  3. 需要新增一個計數器level,記錄BFS的層數。
class Solution {
    public int numSquares(int n) {
        Queue<Integer> queue = new LinkedList<>();
        //用於判斷重複節點
        HashSet<Integer> visited = new HashSet<>();
        //level從0開始算起
        int level = 0;
        queue.offer(n);
        visited.add(n);
        while(!queue.isEmpty()){
            //size是一層節點的數量
            int size = queue.size();
            for(int i = 0;i < size;i++){
                int cur = queue.poll();
                //遇到0節點,返回當前的level
                if(cur == 0) return level;
                for(int j = 1;j * j <= cur;j++){
                    int next = cur - j * j;//當前數字減去一個平方數,就是下一層的數字
                    if(!visited.contains(next)){
                        queue.offer(next);
                        visited.add(next);
                    }
                }
            }
            level++;
        }
        //除非輸入負數,不然任何數字都可以由1組成
        return -1;
    }
}

官方題解中使用了HashSet資料結構,將其當作佇列使用,還可以起到避免重複節點的作用。不過看知乎上的討論,HashSet並不保證輸出是有序的,是否有序跟jdk版本有關,所以最好別這樣寫。
Java遍歷HashSet為什麼輸出是有序的? - 知乎
相比於官方題解,這個題解更加清晰,供參考。
詳細通俗的思路分析,多解法

複雜度分析

不會分析...待學習
時間複雜度:O()
空間複雜度:O()