题目描述:
一些恶魔抓住了公主(P)并将她关在了地下城的右下角。地下城是由 M x N 个房间组成的二维网格。我们英勇的骑士(K)最初被安置在左上角的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。
骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。
有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。
为了尽快到达公主,骑士决定每次只向右或向下移动一步。
编写一个函数来计算确保骑士能够拯救到公主所需的最低初始健康点数。
例如,考虑到如下布局的地下城,如果骑士遵循最佳路径 右 -> 右 -> 下 -> 下,则骑士的初始健康点数至少为 7。
分析1:
刚开始拿到这个题的时候,我第一时间想到的就是用动态数组的方法,定义一个dp[i][j],表示骑士到i,j位置时的血量,我发现这个题最终求的是从(0,0)位置到(m,n)位置的最小初始值,也就是说我们要求从(0,0)到(m,n)经历过的最小生命值,那么这样定义就得不到我们想要的数据,然后我想定义两个dp1[i][j]表示骑士到i,j位置时的血量,dp2[i][j]表示从(0,0)到(i,j)经历过的最小生命值,我们希望的是dp1尽可能大,dp2尽可能小。我编程了一会儿发现dp1与dp2并不能形成一定的关系—dp1[i][j]走的路径不一定是dp2[i][j]走的路径,所以无法推导出递推公式,也有可能是我能力问题无法推导出来公式。
因此,如果按照从左上往右下的顺序进行动态规划,我们无法直接确定到达 (m,n) 的方案,因为有两个重要程度相同的参数同时影响后续的决策。也就是说,这样的动态规划是不满足「无后效性」的。
所以我们要从右下往左上的顺序进行动态规划,令 dp[i][j]dp[i][j] 表示从坐标 (i,j)到终点所需的最小初始值。换句话说,当我们到达坐标 (i,j)时,如果此时我们的路径和不小于 dp[i][j],我们就能到达终点。
dp数组定义
dp[i][j]表示从(i,j)到(m,n)的最小初始值
递推公式
dp[i][j] = max(1,min(dp[i+1][j]-nums[i][j],dp[i][j+1]-nums[i][j])) //初始值一定要大于等于1
临界值初始化
当 i=n-1或者 j=m-1 时,dp[i][j] 转移需要用到的 dp[i][j+1] 和 dp[i+1][j] 中有无效值,因此代码实现中给无效值赋值为极大值。特别地,dp[n−1][m−1] 转移需要用到的 dp[n-1][m]和 dp[n][m-1] 均为无效值,因此我们给这两个值赋值为 1。
代码1:
int calculateMinimumHP(vector<vector<int>>& dungeon)
{
int m = dungeon.size(), n = dungeon[0].size();
vector<vector<int>> dp(m, vector<int>(n));
if(dungeon[m-1][n-1] >= 0)
{
dp[m-1][n-1] = 1;
}
else
{
dp[m-1][n-1] = 1-dungeon[m-1][n-1];
}
for (int i = m-1 ; i >= 0; --i)
{
for (int j = n-1 ; j >= 0; --j)
{
if(i == m-1 && j == n-1) continue;
if(i+1 < m && j+1 < n)
{
int minn = min(dp[i+1][j],dp[i][j+1]);
if(dungeon[i][j] - minn >= 0 )
{
dp[i][j] = 1;
}
else
{
dp[i][j] = -(dungeon[i][j] - minn);
}
}
else if(i+1 < m)
{
int minn = dp[i+1][j];
if((dungeon[i][j] - minn) >= 0 )
{
dp[i][j] = 1;
}
else
{
dp[i][j] = dungeon[i][j] - minn;
dp[i][j] *=-1;
}
}
else if(j+1 < n)
{
int minn = dp[i][j+1];
if(dungeon[i][j] - minn >= 0 )
{
dp[i][j] = 1;
}
else
{
dp[i][j] = dungeon[i][j] - minn;
dp[i][j] *=-1;
}
}
}
}
return dp[0][0];
}
因为 if(i == m-1 && j == n-1) continue; 写成了break,改了一个多小时的BUG…
分析2:
因为是反过来逆推,所以我们应该要想到递归算法,此题还可以用DFS的方法–求(0,0)到(m,n)的最小生命初始值可以转换成求min((1,0)到(m,n)与(0,1)到(m,n)的最小生命初始值),因此这是一个显而易见的递归题
代码2:
vector<vector<int>> memo;// 定义记忆化数组
int calculateMinimumHP(vector<vector<int>>& dungeon)
{
memo.resize(dungeon.size());
for(int i=0;i<memo.size();i++)
{
memo[i].resize(dungeon[0].size());
}
return dfs(dungeon, dungeon.size(), dungeon[0].size(), 0, 0);//从(0,0)坐标开始
}
int dfs(vector<vector<int>>& dungeon, int m, int n, int i, int j)
{
// 到达终点,递归终止。
if (i == m - 1 && j == n - 1)
{
if(dungeon[m-1][n-1] >= 0) return 1;
else
{
return 1-dungeon[m-1][n-1];
}
}
// 如果memo数组中有值,直接取出并返回,不进行后续的搜索。
if (memo[i][j] > 0)
{
return memo[i][j];
}
//向右搜+向下搜
int minRes = 0;
if (i == m - 1) ///只能向右搜
{
minRes = max(dfs(dungeon, m, n, i, j + 1) - dungeon[i][j], 1);
}
else if (j == n - 1) //只能向下搜
{
minRes = max(dfs(dungeon, m, n, i + 1, j) - dungeon[i][j], 1);
}
else //向下与向右搜
{
int minn = min(dfs(dungeon, m, n, i + 1, j), dfs(dungeon, m, n, i, j + 1));
minRes = max(minn - dungeon[i][j], 1);
}
// 将结果存入memo数组
return memo[i][j] = minRes;
}
注意事项:
注意要用一个memo记忆数组来存放计算过的值,否则时间复杂度不能通过此题