本周做了一个动态规划的题目:Dungeon Game
题目描述
The demons had captured the princess (P) and imprisoned her in the bottom-right corner of a dungeon. The dungeon consists of M x N rooms laid out in a 2D grid. Our valiant knight (K) was initially positioned in the top-left room and must fight his way through the dungeon to rescue the princess.
The knight has an initial health point represented by a positive integer. If at any point his health point drops to 0 or below, he dies immediately.
Some of the rooms are guarded by demons, so the knight loses health (negative integers) upon entering these rooms; other rooms are either empty (0's) or contain magic orbs that increase the knight's health (positive integers).
In order to reach the princess as quickly as possible, the knight decides to move only rightward or downward in each step.
Write a function to determine the knight's minimum initial health so that he is able to rescue the princess.
给出一个二维矩阵,武士在左上角的位置,公主在右下角,武士要从左上角出发去救公主。矩阵每个位置有一个数字,正数表示可以为武士补的血点数,负数表示武士减少的血点数。武士的血为零或小于零时,立刻就死掉。求武士至少需要多少血才能去救到公主。
举个例子,如以下图,如果武士走的路径是右,右,下,下的话,经过-2,-3,3,1,-5,他需要的初始血量至少是7,保证到达公主时血量是大于0就可以了。
如果走的是下下右右呢,他必须是先经过-2,-5,此时就需要8滴血了,要不然到10的地方就会死掉。然后经过10,30就增加了40个血,再到-5时剩余的血量很充足。
思路
假设给定的矩阵是m行n列的,dungeon[m][n]。按照动态规划的思想,子问题可以是记录到达每个点需要的血量,用dp[i][j]表示。我们先按照上面的例子来分析一下。每次只能往下或者往右走,所以第一行和第一列是初始情况。
第一行:
dp[0][0]是3;
dp[0][1]是dp[0][0]加上3,就是6;
dp[0][2]该位置对应的是正数,血量不会减,还会增加,需要的血量跟dp[0][1]是一样的;
第一列:
dp[0][0]是3;
dp[1][0]是dp[0][0]加上5,就是8;
dp[2][0]该位置对应的是正数,需要的血量跟dp[1][0]是一样的,即8;
第二行第二列:
dp[1][1]可以来自dp[0][1],也可以来自dp[1][0],所以,选择小的一个,即dp[0][1]+10=16;
第二行第三列:
dp[1][2]该位置是正数,从dp[1][1]和dp[0][2]中选择小的一个,即dp[0][2] = 6;
第三行第二列:
dp[2][1]该位置是正数,从dp[2][0]和dp[1][1]选择小的一个,即8;
第三行第二列:
dp[2][2]该位置是负数,首先从dp[2][1]和dp[1][2]中选择小的一个,加上5,得到11.
发现问题:走到位置(2, 2)最少需要的血点应该是7,问题是在哪里呢?因为在经过中间的正数时,没有算上可以加的血点,所以最后结果不对。那该怎么解决?
想法一:
在计算每一个位置的dp值时,如果它的上一位置是正数,说明到这里需要的血点数要先减去上一位置的数字。比如说计算dp[1][2],也就是血量为+1的位置时,可以用以下方法:
Temp1 = (dungeon[1][1] > 0 ? dp[1][1]-dungeon[1][1] : dp[1][1]);
Temp2 = (dungeon[0][2] > 0 ? dp[0][2]-dungeon[0][2] : dp[0][2]);
按照这样的想法去实现,然而,结果是错的。
仔细想想,走到血量为+1这个位置,是否说明需要的血量就会变少呢?其实并不是啊,因为前面还是需要6个血量支持武士走到这里啊。这条路错的不明显,看另一条路,就是-2到-5,10,30到-5这条路,当走到30时,前面的10是正数,按照上面的计算,需要的血量从原来的8减到1(支持他活着就好),结果算到后面就只需要6个血量,这明显是有问题的,6个血量在-5的时候就死掉了。所以,用这种想法不行。
想法二:
反着思考这个问题,只要到达目的地时候血量是1就够了。那可以从右下角开始返回去算啊!在公主所在的位置,血量是-5,也就是说,到达这个位置之前,武士的血量必须至少是6啊。如果公主所在位置血量是个正数,如4,那就说明武士在到达这个位置之前,血量是1就可以了。当往回走一个位置时,用上一个位置的需血量减去当前位置的需血量(负数减去相当去加上)就可以得到当前位置的需血量啦。特别要注意的是,如果减了之后变成负数,说明当前位置是可以补血的,而且补完血之后就可以走到终点了,所以,减到负数的话,该位置的dp值就为1.
按照这个思路,首先子问题dp[i][j]还是表示在第i行第j列的需要的血量。初始化情况是第一行和第一列。先计算dp[m-1][n-1],然后:
- i从m-2减到0,dp[i][n-1]是等于dp[i + 1][n - 1] - dungeon[i][n - 1]或者1;
- j从n-1减到0,dp[m-1][j]是等于dp[m - 1][j + 1] - dungeon[m - 1][j])或者1;
接着是计算其他子问题,i从m-2开始,j从n-1开始:
- dp[i][j]从dp[i][j + 1] - dungeon[i][j]和 dp[i + 1][j] - dungeon[i][j]选取小的一个或者1;
什么情况下要取1?
就是当算出来的结果小于或等于0的时候,所以,用max(1, ...)来得到结果。
代码展示
int calculateMinimumHP(vector<vector<int>>& dungeon) {
int m = dungeon.size();
if (m == 0)
return 0;
int n = dungeon[0].size();
if (m == 1 && n == 1) {
if (dungeon[0][0] >= 0)
return 1;
else
return -dungeon[0][0] + 1;
}
int dp[m][n];
dp[m - 1][n - 1] = (dungeon[m - 1][n - 1] < 0) ? (-dungeon[m - 1][n - 1] + 1) : 1;
for (int j = n - 2; j >= 0; j--)
dp[m - 1][j] = max((dp[m - 1][j + 1] - dungeon[m - 1][j]), 1);
for (int i = m - 2; i >= 0; i--) {
dp[i][n - 1] = max(dp[i + 1][n - 1] - dungeon[i][n - 1], 1);
}
for (int i = m - 2; i >= 0; i--) {
for (int j = n - 2; j >= 0; j--) {
dp[i][j] = max(min(dp[i][j + 1] - dungeon[i][j], dp[i + 1][j] - dungeon[i][j]), 1);
}
}
return dp[0][0];
}