丟棋子問題 ——(動態規划)


題目描述

  一座大樓一共有0~N層,地面算第0層,最高一層為第N層。已知棋子從第0層掉落肯定不會摔碎,從第i層掉落可能回摔碎,也可能不會摔碎(1<=i<=N)。給定整數N作為樓層數,再給定整數K作為棋子數,返回如果想找到棋子不會摔碎的最高層數,即使在最差的情況下仍的最少次數。一次只能仍一個棋子。

例子

N=10, K=1.
返回10。因為只有1顆棋子,所以不得不從第一層開始一直試到第十層,最差情況要扔10次。

N=3, K=2.
返回2。現在第2層仍1顆棋子,如果碎了,試第1層;如果沒碎,試第3層。

N=105, K=2.
返回14。

第一顆棋子嘗試層數 若第一顆碎了第二顆棋子嘗試的層數
第一次嘗試 14 1~13
第一次沒碎 27 15~26
第二次沒碎 39 28~38
第三次沒碎 50 40~49
第四次沒碎 60 51~59
第五次沒碎 69 61~68
第六次沒碎 77 70~76
第七次沒碎 84 78~83
第八次沒碎 90 85~89
第九次沒碎 95 91~94
第十次沒碎 99 96~98
第十一次沒碎 102 100,101
第十二次沒碎 104 103
第十三次沒碎 105

方法一:暴力算法

  設P(N,K)的返回值時N層樓時有K個棋子在最差的情況下仍的最少次數。

  1. 如果N==0,棋子在第0層肯定不會碎,所以P(0, K) = 0;
  2. 如果K==1,樓層有N層,只有1個棋子,故只能從第一次開始嘗試,P(N,1)=N;
  3. 對於N>0且K>1, 我們需考慮第1個棋子是從那層開始仍的。如果第1個棋子從第i層開始仍,那么有以下兩種情況:
      (1) 碎了。沒必要嘗試第i層以上的樓層了,接下來的問題就變成了剩下i-1層樓和K-1個棋子,所以總步數為 1+P(i-1, K-1);
      (2)沒碎。 那么可以知道沒必要嘗試第i層及其以下的樓層了,接下來的問題就變成了還剩下N-i層和K個棋子,所以總步數為 1+P(N-i, K).
  4. 根據題意應該選取(1)和(2)中較差的那種情況,即 max{ P(i-1, K-1), P(N-i, K)}+1。 由於i的取值范圍是 1到N, 那么步數最少的情況為, P(N, K)=min{ max{P(i-1, K-1), P(N-i, K)}(1<=i<=N) }+1。
** Solution one **
** 時間復雜度 O(N!) **
    public int solutionOne(int N, int K){
        if ( N<1 || K<1 ) 
            return 0;
        return helper1(N, K);       
    }
    private int helper1(int N, int K){
        if ( N==0 ) return 0;
        if ( K==1 ) return N;

        int min = Integer.MAX_VALUE;
        for(int i=1; i<=N; ++i){
            min = Math.min(min, Math.max( helper1(i-1, K-1), helper1(N-i, K)));
        }
        return min+1;
    }

方法二:動態規划

  通過研究以上遞歸函數發現, P(N, K)過程依賴於P(0...N-1, K-1) 和 P(0...N-1, K)。所以,若把所有的遞歸的返回值看作是一個二維數組,可以用動態規划的方法優化遞歸過程。從而減少計算量。
  dp[0][K] = 0, dp[N][1] = N, dp[N][K] = min( max(dp[i-1][K-1], dp[N-i][K])) + 1。

時間復雜度 O(N^2 * K)

** Solution two **
    public int solutionTwo(int N, int K){
        if ( N<1 || K<1 ) 
            return 0;
        if ( K == 1 ) return N;
        int[][] dp = new int[N+1][K+1];
        for(int i=1; i<dp.length; ++i) {
        	dp[i][1] = i;
        }
        for(int i=1; i<dp.length; ++i) {
        	for(int j=2; j<=K; ++j) {
        		int min = Integer.MAX_VALUE;
        		for(int k=1; k<i+1; ++k) {
        			min = Math.min(min, Math.max(dp[k-1][j-1], dp[i-k][j]));
        		}
        		dp[i][j] = min + 1;
        	}
        }
        return dp[N][K];
    }

方法三:優化的動態規划

  分析動態規划的過程發現,dp[N][K]只需要它左邊的數據dp[0...N-1][K-1] 和它上面一排的數據dp[0...N-1][K]。那么在動態規划計算時,就可以用兩個數組不停復用的方式實現,而不需要申請整個二維數組的空間。

** Solution Three **
    public int solutionThree(int N, int K){
        if ( N<1 || K<1 ) 
            return 0;
        if ( K == 1 ) return N;
        int[] preArr = new int[N+1];
        int[] curArr = new int[N+1];
        for(int i=1; i<curArr.length; ++i) {
        	curArr[i] = i;
        }
        for(int i=1; i<K; ++i) {
        	int[] tmp = preArr;
        	preArr = curArr;
        	curArr = tmp;
        	for(int j=1; j<curArr.length; ++j){
        		int min = Integer.MAX_VALUE;
        		for(int k=1; k<j+1; ++k) {
        			min = Math.min(min,  Math.max(preArr[k-1], curArr[j-k]));
        		}
        		curArr[j] = min + 1;
        	}
        }
        return curArr[curArr.length-1];
    }

方法四:四邊形不等式優化動態規划

  方法二和三的時間復雜度還是很高。但我們注意到,求解動態規划表中的值時,有枚舉的過程,此時往往可以用“四邊形不等式”及其相關猜想來進行優化。
  **優化方式:四邊形不等式及其猜想: **
  1. 如果已經求出了 k+1 個棋子在解決 n 層樓時的最少步驟 (dp[n][k+1]), 那么如果在這個嘗試的過程中發現,第 1 個棋子仍在 m 層樓的這種嘗試導致最終的最優解。 則在求 k 個棋子在解決 n 層樓時 (dp[n][k]), 第 1 個棋子不需要去嘗試 m 層以上的樓。
  舉個例子,3個棋子在解決100層樓時,第1個棋子仍在37層樓時最終導致了最優解,那么2個棋子在解決100層樓時,第1個棋子不需要去嘗試37層樓以上的樓層。
  2. 如果已經求出了 k 個棋子在解決 n 層樓時的最少步驟 (dp[n][k]), 那么如果在這個嘗試的過程中發現,第 1 個棋子仍在 m 層樓的這種嘗試最終導致了最優解。則在求 k 個棋子在解決 n+1 層樓時 (dp[n+1][k]), 不需要嘗試 m 層以下的樓。
  舉個例子,2個棋子在解決10層樓時,第1個棋子仍在4層樓時最終導致了最優解。那么2個棋子在解決11層樓或者更多的樓層時,第1個棋子也不需要去嘗試1,2和3層樓,只需才4層及其以上樓層開始嘗試。
  也就是說,動態規划表中的兩個參數分別為棋子數和樓層數,樓數變多以后,第1個棋子的嘗試樓層的下限是可以確定的。棋子變少之后,第1個棋子的嘗試樓層的上限也是確定的。這樣就省去了還多無效的枚舉過程。
  **一般通過“四邊形不等式”的優化可以把時間復雜度降低一個維度,可以從 O(N^2 * k) 或 O(N^3) 降低到 O(N^2)。

** Solution Four **
    public int solutionFour(int N, int K){
        if ( N<1 || K<1 ) 
            return 0;
        if ( K == 1 ) return N;
        int[][] dp = new int[N+1][K+1];
        for(int i=1; i<dp.length; ++i) {
        	dp[i][1] = i;
        }
        int[] cands = new int[K+1];
        for(int i=1; i<K+1; ++i) {
        	dp[1][i] = 1;
        	cands[i] = 1;
        }
        for(int i=2; i<N+1; ++i) {
        	for(int j=K; j>1; --j) {
        		int min = Integer.MAX_VALUE;
        		int minEnum = cands[j];
        		int maxEnum = j==K? i/2+1 : cands[j+1];
        		for(int k=minEnum; k<maxEnum+1; ++k ) {
        			int cur = Math.max(dp[k-1][j-1], dp[i-k][j]);
        			if (cur<=min) {
        				min = cur;
        				cands[j] = k;
        			}
        		}
        		dp[i][j] = min+1;
        	}
        }
        return dp[N][K];
    }

方法五: 最優解

  最優解比一上各種方法都快。首先我們換個角度來看這個問題,以上各種方法解決問題是N層樓有K個棋子最少仍多少次。現在反過來看K個棋子如果可以仍M次,最多可以解決多少樓層這個問題。根據上文實現的函數可以生成下表。在這個表中記作map, map[i][j]的意義為i個棋子仍j次最多搞定的樓層數。

棋子數\次數 0 1 2 3 4 5 6 7 8 9 10
1 0 1 2 3 4 5 6 7 8 9 10
2 0 1 3 6 10 15 21 28 36 45 55
3 0 1 3 7 14 25 41 63 92 129 175
4 0 1 3 7 15 30 56 98 162 255 385
5 0 1 3 7 15 31 62 119 218 381 637

  通過研究map表發現,第一排的值從左到有一次為1,2,3...,第一縱列都為0, 初次之外的其他位置(i, j),都有 map[i][j] == map[i][j-1] + map[i-1][j-1] + 1.
  將設i個棋子仍j次最多搞定m層樓,“搞定最多”說明每次仍的位置都是最優的且在棋子肯定夠用的情況下,若第1個棋子仍在a層樓是最優的。
  1. 如果第1個棋子以碎,那么就向下,看i-1個棋子扔j-1次最多搞定多少層樓;
  2. 如果第1個棋子沒碎,那么就向上,看i個棋子扔j-1次最多搞定多少層樓;
  3. a層樓本身也是被搞定的1層;
  1、2、3的總樓數就是i個棋子扔j次最多搞定的樓數,map表的生成過程極為簡單,同時數值增長的極快。原始問題可以通過map表得到很好的解決。
  例如,想求5個棋子搞定200層樓最少扔多少次的問題,注意到第5行第8列對應的值為218,是第5行的所有值中第一次超過200的值,則可以知道5個棋子搞定200層樓最少扔8次。同時在map表中其實9列10列的值也完全可以不需要計算,因為算到第8列就已經搞定,那么時間復雜度得到進一步的優化。另外還有一個特別重要的優化,我們知道N層樓完全用二分的方式扔logN+1次就直接可以確定哪層樓是會碎的最低層樓,所以當棋子數大於logN+1時,我們就可以返回logN+1.
  如果棋子數位K,樓數位N, 最終的結果位M次,那么最優解的時間復雜度為O(KxM), 在棋子數大於logN+1時,時間復雜度為O(logN). 在只要1個棋子的時候, KxM等於N, 在其他情況下 KxM要比N得多。

** Solution Five **
    public int solutionFive(int N, int K){
        if ( N<1 || K<1 ) 
            return 0;
        int bsTimes = log2N(N) + 1;
        if (K >= bsTimes) {
        	return bsTimes;
        }
        int[] dp = new int[K];
        int res = 0;
        while (true) {
        	++res;
        	int previous = 0;
        	for(int i = 0; i < dp.length; ++i) {
        		int tmp = dp[i];
        		dp[i] = dp[i] + previous + 1;
        		previous = tmp;
        		if (dp[i] >= N) {
        			return res;
        		}
        	}
        }
    }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM