LeetCode174. 地下城游戏 | 刷题打卡

522 阅读4分钟

一、题目描述

难度 困难

一些恶魔抓住了公主(P)并将她关在了地下城的右下角。地下城是由 M x N 个房间组成的二维网格。我们英勇的骑士(K)最初被安置在左上角的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。

骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。

有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。

为了尽快到达公主,骑士决定每次只向右或向下移动一步。   编写一个函数来计算确保骑士能够拯救到公主所需的最低初始健康点数。

例如,考虑到如下布局的地下城,如果骑士遵循最佳路径 右 -> 右 -> 下 -> 下,则骑士的初始健康点数至少为 7

image.png

说明:

骑士的健康点数没有上限。

任何房间都可能对骑士的健康点数造成威胁,也可能增加骑士的健康点数,包括骑士进入的左上角房间以及公主被监禁的右下角房间。

二、思路分析

关键信息:

  • 如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡;即骑士在每一个房间至少得有一点健康点数

  • 骑士决定每次只向右或向下移动一步;即方向有两个,向右或向下

  • 确保骑士能够拯救到公主所需的最低初始健康点数;即找一个可行的最小值

如果从前往后推导的话,因为不知道后面的情况,所以每往前走一步都有可能需要更新前面的值,这样太费力了,还是从后往前推导好一点。从后往前,每个房间都确立一个最优解,到起点的时候自然也是最优解,这个就是动态规划吧。那么,动手干吧!

动态规划逆推过程:

image.png

要确保进入公主房间(2, 2)的时候可以剩1点健康点数,那得有1 - (-5) = 6点;

如果骑士从上方房间(1, 2)进入公主房间,则进入(1, 2)时需要的点数为 6 - 1 = 5点;

如果骑士从左边房间(2, 1)进入,则进入(2, 1)需要点数为6 - 30 = -24,骑士需要的健康点数是大于等于1的,所以进入(2, 1)需要的点数是Math.max(1, -24) = 1点;

如果骑士进入了(1, 1)房间,选择往右走的需要6 - (-10) = 16点;选择往下走需要1 - (-10) = 11点;那当然是取小的那个往下走了,即计算公式是Math.min(6 - (-10), 1 - (-10)),优化一下就是Math.min(6, 1) - (-10);再结合需要大于等于1的条件,即公式是Math.max(1, Math.min(6, 1) - (-10));得出进入(1, 1)房间的最小点数是11点;

公式从具体到抽象就是Math.max(1, Math.min(dp[i][j + 1], dp[i + 1][j]) - dungeon[i][j])

如果骑士进入了(0, 2)房间,那么根据上面得出来的公式,代入就是Math.max(1, Math.min(6, x) - (3)),咦,到(0, 2)的时候不能往右走了,那我这个公式还有用吗?Math.min(6, x)是找一个最小值,只能往下走,它得出的值得是6,那我给x一个大于6的值,这个公式不就可以继续用了!那怎么确保x是最大的呢?Number里面有一个最大值,我也是才知道的:Number.MAX_VALUE

然后为了让这个公式可以用,我们可以在右边多加一列数据,在下边多加一行数据;里面的值都是Number.MAX_VALUE

到这里可以试着用一下公式推导每个房间需要的最小点数了:

咦,怎么得出的值都是最大值呢,哦哦,原来是没明确开始可以确定的值,那个值可以最初明确呢?自然是公主所在房间,即公主所在房间向右和向下我们虚拟加的房间,设置房间点数为1好了。

一顿操作猛如虎,可以说是很用心了

image.png

三、AC代码

var calculateMinimumHP = function (dungeon) {
  let m = dungeon.length;
  if (m == 0) return 0;
  let n = dungeon[0].length;
  let max = Number.MAX_VALUE;
  // 先建一个二维数组,填充每一个值都是最大值(求最小值填充最大值,求最大值填充0或最小值)
  let dp = Array.from(m + 1); // 要多一行出来
  for (let i = 0; i <= m; i++) {
    dp[i] = [];
    for (let j = 0; j <= n; j++) {
      dp[i].push(max);
    }
  }
  // 最后一个dp[i][j]向右和向下的那两个要有1点
  dp[m][n - 1] = 1;
  dp[m - 1][n] = 1;
  // 再从公主哪里跑回起点
  for (let i = m - 1; i >= 0; i--) {
    for (let j = n - 1; j >= 0; j--) {
      let temp = Math.min(dp[i][j + 1], dp[i + 1][j]) - dungeon[i][j];
      dp[i][j] = Math.max(1, temp);
    }
  }
  // console.log(dp)
  return dp[0][0];
};

四、总结

写代码不难,上手试一下就好了;不过思路分析的过程挺磨人的,希望可以让你看懂!本文正在参加活动刷题打卡任务,来一起交流一下吧!