Given a positive integer n, find the least number of perfect square numbers (for example, 1, 4, 9, 16, ...) which sum to n.
For example, given n = 12, return 3 because 12 = 4 + 4 + 4; given n = 13, return 2 because 13 = 4 + 9.
原題如上,乍一看覺得很簡單,但是仔細一想,自己舉了幾個例子,比如 n = 147, 則 n = 7² + 7² + 7²,或者 n = 11² + 5² + 1 ,而不是 12²+1+1+1。所以這個問題還是需要對平方值小於 n 的每一個值 i 進行考慮,即遍歷。而剩下的對於每一個 n-i² 又是一個不確定的數,於是很自然地就想到遞歸,對於每一個 n-i² 不就是和 n 一樣的問題嗎,只不過是數字不同罷了。因此將求n的 least number of perfect square numbers 問題轉化爲多個 n-i² 子問題。
對於將母問題轉化爲多個子問題的算法,目前我學到的主要有分治算法和動態規劃算法。這兩種算法都是由數學上的遞推公式而來。那麼區別在哪裏呢?
一般來說,分治算法會使用遞歸,將母問題轉化爲幾個(2個以上)子問題解的和,也就是說子問題的解是組成母問題解的一部分。而動態規劃算法一般將遞歸算法寫成非遞歸算法(將子問題的解記錄在一個表中來實現),通過對子問題解的篩選,來決定母問題的解。本題顯然是後者,因此採用動態規劃算法更爲合適。
寫這篇文章之前看了一個博主關於動態規劃算法的介紹,感覺非常有趣,很適合新手入門,鏈接如下:
http://blog.csdn.net/woshioosm/article/details/7438834
確定一個問題能否用動態規劃算法,需要確定如下幾個關鍵點:
- 最優子結構
對於 i² 小於 n 的值,必然有一個是最優解(不排除多個最優解,但我們只需要找到一個即可)。
- 子問題重疊
求解每一個子問題 n-i² 的 least number of perfect square numbers 和求解母問題 n 的 least number of perfect square numbers ,是一樣的問題,只不過數值大小不同罷了。
- 子問題獨立
求解一個子問題 n-i² 並不會影響另一個子問題的解。
- 邊界
最後一個子問題一定是所求的數值可以由一個平方數直接表示。
- 備忘錄
大家應該都知道遞歸之所以慢,很大程度上是因爲進行了很多不必要的重複計算。爲了保證對每一個子問題只
進行一次求解,需要使用一個表來記錄所有計算過的子問題的解。
以下是使用了遞推的動態規劃代碼實現。雖然保證每個子問題只計算了一遍,但由於使用了遞歸等原因,因此
效率上還是有待改善。
public class Solution { public int numSquares(int n) { if(n<=0) return 0; int record[] = new int[n+1]; //⑤備忘錄,保證計算過的n的結果result再次需要時可以直接使用 for(int i=0;i<n+1;i++) record[i] = 0; return result(n,record); } public int result(int n,int record[]){ //遞歸,從結果向起始推 非遞歸,從起始向結果推 int k = Squre(n); if(n == k*k) //③邊界 return 1; int time = Integer.MAX_VALUE; for(int i = 1;i<=k;i++){ //④子問題獨立 if(record[n-i*i] == 0) record[n-i*i] = result(n-i*i,record); //②子問題重疊,子問題和母問題是同樣的問題 time = Math.min(time,record[n-i*i]+1); //①最優子問題 } return time; } public int Squre(int n){ if(n<=0) return 0; int i = 1; while(i*i<=n) i++; return i-1; } }
上述使用遞歸的動態規劃算法是從結果向起始推進的。有一種效率更高的方案,不使用遞歸,從起始向結果推進。
二者的思考方向相反,可以借鑑一下。
dp[n] indicates that the perfect squares count of the given n, and we have:
dp[0] = 0 dp[1] = dp[0]+1 = 1 dp[2] = dp[1]+1 = 2 dp[3] = dp[2]+1 = 3 dp[4] = Min{ dp[4-1*1]+1, dp[4-2*2]+1 } = Min{ dp[3]+1, dp[0]+1 } = 1 dp[5] = Min{ dp[5-1*1]+1, dp[5-2*2]+1 } = Min{ dp[4]+1, dp[1]+1 } = 2 . . . dp[13] = Min{ dp[13-1*1]+1, dp[13-2*2]+1, dp[13-3*3]+1 } = Min{ dp[12]+1, dp[9]+1, dp[4]+1 } = 2 . . . dp[n] = Min{ dp[n - i*i] + 1 }, n - i*i >=0 && i >= 1
and the sample code is like below:
public int numSquares(int n) { int[] dp = new int[n + 1]; Arrays.fill(dp, Integer.MAX_VALUE); dp[0] = 0; for(int i = 1; i <= n; ++i) { int min = Integer.MAX_VALUE; int j = 1; while(i - j*j >= 0) { min = Math.min(min, dp[i - j*j] + 1); //從起始向結果考慮,計算從dp[1]開始到dp[n] ++j; } dp[i] = min; } return dp[n]; }