動態規劃——Dungeon Game

這又是個題幹很搞笑的題目:惡魔把公主囚禁在魔宮的右下角,騎士從魔宮的左上角開始穿越整個魔宮到右下角拯救公主,爲了以最快速度拯救公主,騎士每次只能向下或者向右移動一個房間,算法

每一個房間內都有一個整數值,負數表示騎士到當前房間要減小這個生命值,非負數表示騎士到當前房間能夠增長這個生命值。騎士的初始生命值是一個正整數,請給出騎士須要的最少的初始生命值。數組

很明顯這是個動態規劃的題目,並且這個題目的大致框架很常見,就是對一個二維數組進行遍歷同時維護另外一個dp二維數組,每一個dp的值都是與其左側和上側的dp值有關。框架

因爲我習慣於使用動態規劃的正序解法,一開始的時候我仍是使用的正序解法:測試

維護兩個二維數組opt和dp,opt[i][j]表示騎士從左上角到 ( i , j ) 的最優路徑的整個過程當中耗血量的最小值(這是個負數),dp[i][j]表示騎士從左上角到 ( i , j )最少的耗血量。優化

此時的狀態轉移方程爲:spa

i!=0 且 j!=0 時:若是dp[i][j-1]>dp[i-1][j]時,opt[i][j] = dungeon[i][j] + opt[i][j-1],dp[i][j] = min(opt[i][j],dp[i][j-1])code

                         若是dp[i][j-1]<dp[i-1][j]時,opt[i][j] = dungeon[i][j] + opt[i-1][j],dp[i][j] = min(opt[i][j],dp[i-1][j])blog

                         若是dp[i][j-1]==dp[i-1][j]時,opt[i][j] = dungeon[i][j] + opt[i-1][j]>=opt[i-1][j]?opt[i-1][j]:opt[i-1][j],dp[i][j] = min(opt[i][j],dp[i-1][j])io

若是 i==0或者j==0處於邊界,opt和dp的更新只與其上側或左側那個不越界的位置有關,這種狀況很簡單再也不詳述class

若是採用這種正序解法,最後的輸出結果是 dp[i][j]>=0?1:(1-dp[i][j])。

這種正序解法看上去沒什麼問題,可是在實際測試的時候,LeetCode給出的下面的這個測試用例是沒法經過的:

若是使用我上面給出的正序解法,結果爲5,路線是down -> right -> right -> down。但實際上,LeetCode上給定的結果是3,路線是right -> right -> down -> down

當時我爲此很苦惱,並且花費了很長時間去修改這個解法可是最終失敗了。

若是回顧一下動態規劃的相關定義:

有很看似非多項式的問題常常可使用動態規劃來實現P的解法。好比比較有名的斐波那契數列、揹包問題、集合劃分問題等。那這個題目可否也符合使用動態規劃的條件呢,咱們來分析一下。
首先,能採用動態規劃求解的問題通常具備3個條件:

  1. 最優化原理:若是問題的最優解所包含的子問題的解也是最優的,就稱該問題具備最優子結構,即知足最優化原理。
  2. 無後效性:即某階段狀態一旦肯定,就不受這個狀態之後決策的影響。也就是說,某狀態之後的過程不會影響之前的狀態,只與當前狀態有關。
  3. 有重疊子問題:即子問題之間是不獨立的,一個子問題在下一階段決策中可能被使用到。(該性質並非動態規劃適用的必要條件,可是若是沒有這條性質,動態規劃算法同其餘算法相比就不具有優點)

雖然我總結出來了狀態轉移方程,也就是那個遞推公式,可是顯然大前提第2條已經不知足了,前面的dp已經影響了後面的dp值。到此爲止,正序解法我沒有找到正解,乾脆倒序求解。

倒序解法:

dp[i][j]表示以( i ,  j )爲騎士出發的左上角到達右下角所需的最小初始生命值。

除去邊界,dp[i][j]顯然仍與dp[i][j+1]和dp[i+1][j]有關:一個來自下側,一個來自右側

按道理講,若是能從(i,j)到(i,j+1)或(i+1,j),應該有  dp[i][j+1] = dungeon[i][j]+dp[i][j] 或 dp[i+1][j] = dungeon[i][j]+dp[i][j],

也就是說 dp[i][j] = dp[i][j+1]-dungeon[i][j] 或 dp[i][j] = dp[i+1][j]-dungeon[i][j],不過在實際推算時是由dp[i][j+1]和dp[i+1][j]計算dp[i][j],並且dungeon[i][j]有多是很大的正值(就是補血補不少的那種),

dp[i][j]到dp[i][j+1]或dp[i+1][j]可能會計算出負值,也就是說騎士在(i,j)處即便生命值是負的可能通過dungeon[i][j]這個補血補的不少的房間後都能到達dp[i][j+1]或dp[i+1][j],根據題目的規定此時dp[i][j]應該等於1(最小的正整數),用方程表示即爲dp[i][j] = max(1,dp[i][j+1]-dungeon[i][j]) 或 dp[i][j] = max(1,dp[i+1][j]-dungeon[i][j]),這兩個方程表明兩種選擇,那選擇的標準是什麼呢?由題意只騎士的初始生命值要儘量小,如此一來就能將這兩個方程合併了:dp[i][j] = min( max(1,dp[i][j+1]-dungeon[i][j]),max(1,dp[i+1][j]-dungeon[i][j]) )。這樣狀態轉移方程就求解出來了。

這道題若是換做是一個習慣使用倒序解法的解答者來解答可能會很快就能求解出來,像我這樣習慣於正序解法的這個題上吃了很大的虧,這個題我作了很長時間,中途去睡了一覺(中午1點到下午4點半。。。),結果到了五點半纔想到換解法。

下面上代碼:

 1 class Solution {
 2     public int calculateMinimumHP(int[][] dungeon) {
 3         int mlen = dungeon.length;
 4         if(mlen==0)return 1;
 5         int nlen = dungeon[0].length;
 6         int[][]opt = new int[mlen][nlen];
 7         int[][]dp = new int[mlen][nlen];
 8         for(int i = mlen-1;i>=0;i--) {
 9             for(int j = nlen-1;j>=0;j--) {
10                 if(i==mlen-1&&j==nlen-1)dp[i][j] = dungeon[i][j]>=0?1:(1-dungeon[i][j]);
11                 else if(i==mlen-1) {
12                     dp[i][j] = Math.max(dp[i][j+1]-dungeon[i][j],1);
13                 }
14                 else if(j==nlen-1) {
15                     dp[i][j] = Math.max(dp[i+1][j]-dungeon[i][j],1);
16                 }
17                 else {
18                     
19                     dp[i][j] = Math.min(Math.max(dp[i+1][j]-dungeon[i][j],1),Math.max(1,dp[i][j+1]-dungeon[i][j]));
20                 }
21             }
22         }
23         /*
24         for(int i = 0;i<mlen;i++) {
25             for(int j = 0;j<nlen;j++)
26                 System.out.print(dp[i][j]+" ");
27             System.out.println();
28         }
29         System.out.println();
30         for(int i = 0;i<mlen;i++) {
31             for(int j = 0;j<nlen;j++)
32                 System.out.print(opt[i][j]+" ");
33             System.out.println();
34         }
35         */
36         return dp[0][0];
37     }
38 }

此次題目的求解的確是個很艱辛的過程,吃一塹長一智吧!

相關文章
相關標籤/搜索