1. 程式人生 > >經典DP:測試雞蛋承受力

經典DP:測試雞蛋承受力

現有雞蛋N個,M層樓,要測試這批雞蛋的承受力, 即測試這批雞蛋最多從幾樓扔下去後沒事,最少測試幾次能保證測出蛋的承受力。  

這題要“最少”“保證”測出這批蛋的承受力。因此,我們要找到所有方式的最壞情況中次數最小的。

程式碼思維上可以這麼理解

for 方式i in 所有方式:
    for 情況j in 所有情況:
        通過找到所有情況中最大次數,找到最壞情況
    通過比較最壞情況的最小值,找到最好的方式

比如:我可以每次一層層扔,也可以每次兩層層扔。但是,如果現在在第二層,那麼扔一次就到了,如果在100層,那要扔100次。因此要考慮最壞情況,就像考慮時間複雜度最小的演算法一樣。

很明顯,只要扔一個雞蛋下去,就能判斷承受力>或< 此樓層,從此排除另一半樓層,這跟二分不是很像麼! 可能很多人上來就要用二分法。但是,這就是這道題的經典之處之一了——這不像經典的二分法可以進行,因為雞蛋個數有限。如果雞蛋只有1個,你從m/2樓丟下去碎了,沒雞蛋了,那還測什麼。。。所以二分法只是雞蛋個數足夠時的左臨界值。

因此很容易想到搜尋,如果用搜索,第一個蛋從第k層扔下去,有100種可能性,然後往剩餘樓層扔。時間複雜度接近O(m^n),指數級的複雜度顯然打擾了。

還是先看看狀態,如果從第k層丟一個雞蛋下去,會有兩種情況

1. 蛋碎了: 那麼下一次需要測[原最底層, k-1], 若每次把最底層看成1,那麼,需要測k-1層樓。蛋數-1

2. 蛋不碎:那麼下一次需要測[k +1, 原最頂層],若設最頂層為m,需要測m- k層樓。蛋數不變

而且,每次丟一個蛋後,都會有新的最底層跟最高層,即新的第1層跟第m層,這種子結構容易考慮到動態規劃了

由於倒著考慮反而難,因為最後一個蛋比較特殊,必須保證它碎的一瞬間是找到樓層,也就是說,一層層丟,所以我們從倒數第二個蛋考慮,發現前面的狀態也可以用,就是碎不碎的問題。

由於題目給出變數有兩個,一般來說,狀態也至少有兩個變數。

設剩餘需要測的層數為m,此時可以用層數來代表狀態,設f(m, n)為剩餘m層,n個雞蛋下,保證測出承受力的最小次數

狀態為f(m, n) = min( 之前最小的方式也即f(m, n),max(f(k-1, n-1), f(m-k, n)))  其中n > 1

注意邊界: 

這式子也很容易看出邊界之一,即每個f(m, n)必須賦最大的初值(可以是無限大),保證能找到最小的f(m, n)

另外,還有一個邊界,就是,只有一個雞蛋時,只能一層層扔

技巧:對所有f(m, n)賦值層數m,可以同時解決兩個問題,因為此初始化的值是一層層丟的值,也是最大值。

下面給出Java版的程式碼:

import java.util.Scanner;

public class eggsLimit {
	public int eggsPrint(int M, int N){
		if(M <= 1 || N < 1)
			return 0;
		
		int[][] array = new int[M + 1][N + 1];
		//必須先初始化,因為會用到邊界的資料,同時,能把其他值初始化,陣列預設為0,Min肯定是0
		//此初始化的值剛好是此層的最大值, 你願意分為邊界n = 1時然後所有初始值為INF也行
		for(int m = 1; m <= M; m++){
			for(int n = 1;  n <= N; n++){
				//把每層最大次數初始化為層數
					array[m][n] = m;
			}
		}
		

			for(int m = 1; m <= M; m++){
				for(int n = 2;  n <= N; n++){
					//列舉從第幾層丟
					for(int k = 1; k <= m; k++){
						//每層找到最大的a[m][n],代表最壞的可能性,所有a[m][n]中找到最小的。代表最好的選k方式 
						array[m][n] = Math.min(array[m][n], 1 + Math.max(array[k - 1][n - 1], array[m - k][n]));
					}
			}
		}
		return array[M][N];
	}

	public static void main(String arg[]){
		eggsLimit e = new eggsLimit();
		Scanner scan = new Scanner(System.in);
		int M = scan.nextInt();
		int N = scan.nextInt();
		int result = e.eggsPrint(M, N);
		System.out.print(result);
	}

}

時間複雜度為O(M*M*N),空間複雜度為O(M*N)。由於每次只需要每種情況的最壞情況比較,若使用滾動陣列,可以使空間複雜度降到O(M),具體程式碼就不再給出