动态规划——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 }

此次题目的求解的确是个很艰辛的过程,吃一堑长一智吧!

相关文章
相关标签/搜索