LeetCode Notes_#279 完全平方數
阿新 • • 發佈:2020-09-06
LeetCode Notes_#279 完全平方數
LeetCodeContents
題目
給定正整數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~(int)sqrt(n)
,每個位置的元素是下標的平方 - 根據狀態轉移方程,從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
,每一條邊代表的就是一個平方數。
程式碼和二叉樹的層序遍歷很相似,存在以下幾個不同點:
- 這裡的下一層節點不是通過左右指標來尋找,而是通過當前層節點減去平方數計算得到。
- 重複節點只需入隊一次。
- 如果重複節點位於同一層,那麼兩個節點的子樹結構完全相同,計算出來的最短路徑一樣,如果保留則造成多餘的計算。
- 如果重複節點位於不同層,那麼後訪問到的節點必然離根節點更遠,又因為兩個節點的子樹結構完全相同(即到0節點的距離相同),所以第二次之後訪問的重複節點可以被忽略。
- 需要新增一個計數器
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()