一天一大 leet(地下城游戏)难度:困难-Day20200712

时间:2022-07-25
本文章向大家介绍一天一大 leet(地下城游戏)难度:困难-Day20200712,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

题目:

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

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

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

为了尽快到达公主,骑士决定每次只向右或向下移动一步。

编写一个函数来计算确保骑士能够拯救到公主所需的最低初始健康点数。

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

-2(k)

-3

3

-5

-10

1

10

30

-5(p)

说明

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

抛砖引玉

看完题目想到了做过的机器人那题:不同路径

不同路径是障碍,而本题是记录每个路过节点的值


思路

  • 最低初始需要到终点前是没有多余的值即1
  • 现在问题变成了知道结束值1,推到起始值了
  • 逻辑反转,从结束值没到一个单元格减去本单元格的值直到推导到起点
  • 到达一个点只可能从左侧或者上面进入则公式为:cur[i - 1][j]-dungeon[i][j] cur[i][j-1]-dungeon[i][j]
  • 可能有不同的路径到达终点前值都为1,只取最小值

实现

  • 因为迭代过程中每一个[i][j]的变化都会生成一个新的路线那么默认矩阵中每个节点的值都为 1,代表一种可能
/**
 * @param {number[][]} dungeon
 * @return {number}
 */
var calculateMinimumHP = function (dungeon) {
  let m = dungeon.length,
      n = dungeon[0] ? dungeon[0].length : 0,
      cur = Array(m+1);
  // 初始化与dungeon对应数组,记录到达某个单元格是剩余数量
  for(let i = 0;i<m+1;i++){
      cur[i]= Array(n+1).fill(Number.MAX_VALUE)
  }
  // 终点前默认剩余1
  cur[m][n - 1] = cur[m - 1][n] = 1;
  for (let i = m-1; i >= 0; i--) {
      for (let j = n-1; j >= 0; j--) {
        // 本单元格入口剩余的最小值
        let itemMin = Math.min(cur[i + 1][j], cur[i][j + 1]);
        cur[i][j] = Math.max(itemMin - dungeon[i][j], 1);
      }
  }
  return cur[0][0]
}

存储对象cur降维

/**
 * @param {number[][]} dungeon
 * @return {number}
 */
var calculateMinimumHP = function (dungeon) {
  let m = dungeon.length,
      n = dungeon[0] ? dungeon[0].length : 0,
      cur = new Array(n + 1).fill(Number.MAX_VALUE);
  cur[n - 1] = 1;
  for (let i = m - 1; i >= 0; --i) {
    for (let j = n - 1; j >= 0; --j) {
      let itemMin = Math.min(cur[j], cur[j + 1]);
      cur[j] = Math.max(1, itemMin - dungeon[i][j]);
    }
  }
  return cur[0];
};

其他解法

  • 使用递归完成双层循环的路线选择
  • 上面通过Math.min之间选择了单元格入口方向
  • 下面通过更直观的方式去检查每个入口的剩余值
  • 使用递归每次都需要查询入口值,理论上就可以省略存储结果的对象cur 但是,每次都查询就会有效率问题,索引参考上面思路依旧使用cur记录已经查询到的结果
/**
 * @param {number[][]} dungeon
 * @return {number}
 */
var calculateMinimumHP = function (dungeon) {
  let m = dungeon.length,
      n = dungeon[0] ? dungeon[0].length : 0,
      cur = new Array(m);

  // 初始化,每一项都为0,代表还没记录
  for (let i = 0; i < m; i++) {
    cur[i] = Array(n).fill(0);
  }

  function getMin(dungeon, i, j){

     // 递归的出口
    if (i == m - 1 && j == n - 1) {
      return dungeon[i][j] > 0 ? 1 : 1 - dungeon[i][j];
    }

    // 如果备忘录中有,就直接返回它
    if (cur[i][j] > 0) return cur[i][j];

    let down = Number.MAX_VALUE,
        right = Number.MAX_VALUE;

    // 走下方的点,需要带着的最小安全血量
    if (i < m - 1) down = getMin(dungeon, i + 1, j);
    // 走右方的点,需要带着的最小安全血
    if (j < n - 1) right = getMin(dungeon, i, j + 1);

    if (down < right) {
      if (down - dungeon[i][j] <= 0) {
        cur[i][j] = 1;
      } else {
        cur[i][j] = down - dungeon[i][j];
      }
    } else {
      if (right - dungeon[i][j] <= 0) {
        cur[i][j] = 1;
      } else {
        cur[i][j] = right - dungeon[i][j];
      }
    }
    return cur[i][j];
  };

  return getMin(dungeon, 0, 0, cur);
};