從 LeetCode 279 實踐動態規劃

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];
}





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章