【leetcode热题】 地下城游戏

news2024/11/28 4:38:18

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

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

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

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

返回确保骑士能够拯救到公主所需的最低初始健康点数。

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

示例 1:

输入:dungeon = [[-2,-3,3],[-5,-10,1],[10,30,-5]]
输出:7
解释:如果骑士遵循最佳路径:右 -> 右 -> 下 -> 下 ,则骑士的初始健康点数至少为 7 。

示例 2:

输入:dungeon = [[0]]
输出:1

解法一 回溯法

最直接暴力的方法就是做搜索了,在每个位置无非就是向右向下两种可能,然后去尝试所有的解,然后找到最小的即可,也就是做一个 DFS 或者说是回溯法。

//全局变量去保存最小值
int minHealth = Integer.MAX_VALUE;

public int calculateMinimumHP(int[][] dungeon) {
    //calculateMinimumHPHelper 四个参数
    //int x, int y, int health, int addHealth, int[][] dungeon
    //x, y 代表要准备到的位置,x 代表是哪一列,y 代表是哪一行
    //health 代表当前的生命值
    //addHealth 代表当前已经增加的生命值
    //初始的时候给加 1 点血,addHealth 和 health 都是 1
    calculateMinimumHPHelper(0, 0, 1, 1, dungeon);
    return minHealth;
}

private void calculateMinimumHPHelper(int x, int y, int health, int addHealth, int[][] dungeon) {
    //加上当前位置的奖励或惩罚
    health = health + dungeon[y][x];
    //此时是否需要加血,加血的话就将 health 加到 1
    if (health <= 0) {
        addHealth = addHealth + Math.abs(health) + 1;
    }

    //是否到了终点
    if (x == dungeon[0].length - 1 && y == dungeon.length - 1) {
        minHealth = Math.min(addHealth, minHealth);
        return;
    }

    //是否加过血
    if (health <= 0) {
        //加过血的话,health 就变为 1
        if (x < dungeon[0].length - 1) {
            calculateMinimumHPHelper(x + 1, y, 1, addHealth, dungeon);
        }
        if (y < dungeon.length - 1) {
            calculateMinimumHPHelper(x, y + 1, 1, addHealth, dungeon);
        }
    } else {
        //没加过血的话,health 就是当前的 health
        if (x < dungeon[0].length - 1) {
            calculateMinimumHPHelper(x + 1, y, health, addHealth, dungeon);
        }
        if (y < dungeon.length - 1) {
            calculateMinimumHPHelper(x, y + 1, health, addHealth, dungeon);
        }
    }

}

然后结果是意料之中的,会超时。

然后我们就需要剪枝,将一些情况提前结束掉,最容易想到的就是,如果当前加的血已经超过了全局最小值,那就可以直接结束,不用进后边的递归。

if (addHealth > minHealth) {
    return;
}
Copy

然后发现对于给定的 test case 并没有什么影响。

之所以超时,就是因为我们会经过很多重复的位置,比如

0 1 2
3 4 5
6 7 8
如果按 DFS,第一条路径就是 0 -> 1 -> 2 -> 5 -> 8
然后通过回溯,第二次判断的路径就会是 0 -> 1 -> 4 -> 5 -> ...
我们会发现它又会来到 5 这个位置
其他的也类似,如果表格很大的话,不停的回溯,一些位置会经过很多次

接下来,就会想到用 map 去缓冲我们过程中求出的解,key 话当然是 x 和 y 了,value 呢?存当前的 health 和 addhealth?那第二次来到这个位置的时候,我们并不能做什么,比如举个例子。

第一次来到 (3,5) 的时候,health 是 5addhealth 是 6

第二次来到 (3,5) 的时候,health 是 4addhealth 是 7,我们什么也做不了,我们并不知道未来它会走什么路。

因为走的路是由 health 和 addhealth 共同决定的,此时来到相同的位置,由于 health 和 addhealth 都不一样,所以未来的路也很有可能变化,所以我们并不能通过缓冲结果来剪枝。

我们最多能判断当 xyhealth 和 addhealth 全部相同的时候提前结束,但这种情况也很少,所以并不能有效的加快搜索速度。

这条路看起来到死路了,我们换个思路,去用动态规划。

动态规划的关键就是去定义我们的状态了,这里直接将要解决的问题定义为我们的状态。

用 dp[i][j] 存储从起点 (0, 0) 到达 (i, j) 时候所需要的最小初始生命值。

到达 (i,j) 有两个点,(i-1, j) 和 (i, j-1)

接下来就需要去推导状态转移方程了。

* * 8 * 
* 7 ! ?
? ? ? ?

假如我们要求上图中 ! 位置的 dp,假设之前的 dp 已经都求出来了。

那么 dp 是等于感叹号上边的 dp 还是等于它左边的 dp 呢?选较小的吗?

但如果 8 对应的当时的 health 是 100,而 7 对应的是 5,此时更好的选择应该是 8

那就选 health 大的呗,那 dp 不管了吗?极端的例子,假如此时的位置已经是终点了,很明显我们应该选择从左边过来,也就是 7 的位置过来,之前的 health 并不重要了。

所以推到这里会发现,因为我们有两个不确定的变量,一个是 dp ,也就是从起点 (0, 0) 到达 (i, j) 时候所需要的最小初始生命值,还有一个就是当时剩下的生命值。

当更新 dp 的时候我们并不知道它应该是从上边下来,还是从左边过来有利于到达终点的时候所需的初始生命值最小。

换句话讲,依赖过去的状态,并不能指导我们当前的选择,因为还需要未来的信息。

所以到这里,我再次走到了死胡同,就去看 Discuss 了,这里分享下别人的做法。

解法二 递归

看到 这里 评论区的一个解法。

所需要做的就是将上边动态规划的思路逆转一下。

  ↓
→ *

之前我们考虑的是当前这个位置,它应该是从上边下来还是左边过来会更好些,然后发现并不能确定。

现在的话,看下边的图。

* → x  
↓
y

我们现在考虑从当前位置,应该是向右走还是向下走,这样我们是可以确定的。

如果我们知道右边的位置到终点的需要的最小生命值是 x,下边位置到终点需要的最小生命值是 y

很明显我们应该选择所需生命值较小的方向。

如果 x < y,我们就向右走。

如果 x > y,我们就向下走。

知道方向以后,当前位置到终点的最小生命值 need 就等于 x 和 y 中较小的值减去当前位置上边的值。

如果算出来 need 大于 0,那就说明我们需要 need 的生命值到达终点。

如果算出来 need 小于等于 0,那就说明当前位置增加的生命值很大,所以当前位置我们只需要给一个最小值 1,就足以走到终点。

举个具体的例子就明白了。

如果右边的位置到终点的需要的最小生命值是 5,下边位置到终点需要的最小生命值是 8

所以我们选择向右走。

如果当前位置的值是 2,然后 need = 5 - 2 = 3,所以当前位置的初始值应该是 3

如果当前位置的值是 -3,然后 need = 5 - (-3) = 8,所以当前位置的初始值应该是 8

如果当前位置的值是 10,说明增加的生命值很多,need = 5 - 10 = -5,此时我们只需要将当前位置的生命值初始为 1 即可。

然后每个位置都这样考虑,递归也就出来了。

递归出口也很好考虑, 那就是最后求终点到终点需要的最小生命值。

如果终点位置的值是正的,那么所需要的最小生命值就是 1

如果终点位置的值是负的,那么所需要的最小生命值就是负值的绝对值加 1

public int calculateMinimumHP(int[][] dungeon) {
    return calculateMinimumHPHelper(0, 0, dungeon);
}

private int calculateMinimumHPHelper(int i, int j, int[][] dungeon) {
    //是否到达终点
    if (i == dungeon.length - 1 && j == dungeon[0].length - 1) {
        if (dungeon[i][j] > 0) {
            return 1;
        } else {
            return -dungeon[i][j] + 1;
        }
    }
    //右边位置到达终点所需要的最小值,如果已经在右边界,不能往右走了,赋值为最大值
    int right = j < dungeon[0].length - 1 ? calculateMinimumHPHelper(i, j + 1, dungeon) : Integer.MAX_VALUE;
    //下边位置到达终点需要的最小值,如果已经在下边界,不能往下走了,赋值为最大值
    int down = i < dungeon.length - 1 ? calculateMinimumHPHelper(i + 1, j, dungeon) : Integer.MAX_VALUE;
    //当前位置到终点还需要的生命值
    int need = right < down ? right - dungeon[i][j] : down - dungeon[i][j];
    if (need <= 0) {
        return 1;
    } else {
        return need;
    }
}

当然还是意料之中的超时了。

不过不要慌,还是之前的思想,我们利用 map 去缓冲中间过程的值,也就是 memoization 技术。

这个 map 的 key 和 value 就显而易见了,key 是坐标 i,jvalue 的话就存当最后求出来的当前位置到终点所需的最小生命值,也就是 return 前同时存进 map 中。

public int calculateMinimumHP(int[][] dungeon) {
    return calculateMinimumHPHelper(0, 0, dungeon, new HashMap<String, Integer>());
}

private int calculateMinimumHPHelper(int i, int j, int[][] dungeon, HashMap<String, Integer> map) {
    if (i == dungeon.length - 1 && j == dungeon[0].length - 1) {
        if (dungeon[i][j] > 0) {
            return 1;
        } else {
            return -dungeon[i][j] + 1;
        }
    }
    String key = i + "@" + j;
    if (map.containsKey(key)) {
        return map.get(key);
    }
    int right = j < dungeon[0].length - 1 ? calculateMinimumHPHelper(i, j + 1, dungeon, map) : Integer.MAX_VALUE;
    int down = i < dungeon.length - 1 ? calculateMinimumHPHelper(i + 1, j, dungeon, map) : Integer.MAX_VALUE;
    int need = right < down ? right - dungeon[i][j] : down - dungeon[i][j];
    if (need <= 0) {
        map.put(key, 1);
        return 1;
    } else {
        map.put(key, need);
        return need;
    }
}

解法三 动态规划

其实解法二递归写完以后,很快就能想到动态规划怎么去解了。虽然它俩本质是一样的,但用动态规划可以节省递归压栈的时间,直接从底部往上走。

我们的状态就定义成解法二递归中返回的值,用 dp[i][j] 表示从 (i, j) 到达终点所需要的最小生命值。

状态转移方程的话和递归也一模一样,只需要把函数调用改成取直接取数组的值。

因为对于边界的情况,我们需要赋值为最大值,所以数组的话我们也扩充一行一列将其初始化为最大值,比如

奖惩数组
1   -3   3
0   -2   0
-3  -3   -3

dp 数组
终点位置就是递归出口时候返回的值,边界扩展一下
用 M 表示 Integer.MAXVALUE
0 0 0 M
0 0 0 M
0 0 4 M
M M M M

然后就可以一行一行或者一列一列的去更新 dp 数组,当然要倒着更新
因为更新 dp[i][j] 的时候我们需要 dp[i+1][j] 和 dp[i][j+1] 的值

然后代码就出来了,可以和递归代码做个对比。

public int calculateMinimumHP(int[][] dungeon) {
    int row = dungeon.length;
    int col = dungeon[0].length;
    int[][] dp = new int[row + 1][col + 1];
    //终点所需要的值
    dp[row - 1][col - 1] = dungeon[row - 1][col - 1] > 0 ? 1 : -dungeon[row - 1][col - 1] + 1;
    //扩充的边界更新为最大值
    for (int i = 0; i <= col; i++) {
        dp[row][i] = Integer.MAX_VALUE;
    }
    for (int i = 0; i <= row; i++) {
        dp[i][col] = Integer.MAX_VALUE;
    }

    //逆过来更新
    for (int i = row - 1; i >= 0; i--) {
        for (int j = col - 1; j >= 0; j--) {
            if (i == row - 1 && j == col - 1) {
                continue;
            }
            //选择向右走还是向下走
            dp[i][j] = Math.min(dp[i + 1][j], dp[i][j + 1]) - dungeon[i][j];
            if (dp[i][j] <= 0) {
                dp[i][j] = 1;
            }
        }
    }
    return dp[0][0];
}

如果动态规划做的多的话,必不可少的一步就是空间复杂度可以进行优化,比如 5题,10题,53题,72题 ,115 题 等等都已经用过了。

因为我们的 dp 数组在更新第 i 行的时候,我们只需要第 i+1 行的信息,而 i+2i+3 行的信息我们就不再需要了,我们我们其实不需要二维数组,只需要一个一维数组就足够了。

public int calculateMinimumHP(int[][] dungeon) {
    int row = dungeon.length;
    int col = dungeon[0].length;
    int[] dp = new int[col + 1];

    for (int i = 0; i <= col; i++) {
        dp[i] = Integer.MAX_VALUE;
    }
    dp[col - 1] = dungeon[row - 1][col - 1] > 0 ? 1 : -dungeon[row - 1][col - 1] + 1;
    for (int i = row - 1; i >= 0; i--) {
        for (int j = col - 1; j >= 0; j--) {
            if (i == row - 1 && j == col - 1) {
                continue;
            }
            dp[j] = Math.min(dp[j], dp[j + 1]) - dungeon[i][j];
            if (dp[j] <= 0) {
                dp[j] = 1;
            }
        }
    }
    return dp[0];
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1530306.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

TSINGSEE青犀数字化、智能化视频技术推动森林防火智慧监管

一、背景分析 中央网络安全和信息化委员会印发《“十四五”国家信息化规划》&#xff0c;明确指出“提升林草生态网络感知能力&#xff0c;完善生态系统保护成效数字化监测评估体系”。这为数字化系统建设引领了方向&#xff0c;中国林业信息化建设迈入了新的阶段&#xff0c;全…

六、C#快速排序算法

简介 快速排序是一种常用的排序算法&#xff0c;它基于分治的思想&#xff0c;通过将一个无序的序列分割成两个子序列&#xff0c;并递归地对子序列进行排序&#xff0c;最终完成整个序列的排序。 其基本思路如下&#xff1a; 选择数组中的一个元素作为基准&#xff08;pivot…

绝地求生:现在购买通行证还能兑换成长型武器吗?

大家好&#xff0c;我闲游盒&#xff0c;这几天收到几位盒友的私信咨询我现在购买通行证还能获得一把成长型武器吗&#xff1f;我相信还有许多盒友也有此困惑&#xff0c;那我就在这统一回复了&#xff0c;目前距通行证和商城物资箱礼包下架还有最后16天时间&#xff0c;众所周…

fs方法举例

fs.readFile() 读取文件 const fs require(node:fs) const path require(node:path) const s path.resolve(__dirname, ./hello.txt) const buf fs.readFileSync(s) console.log(buf.toString())输出的Buffer对象 用toString()方法转字符串之后 fs.appendFile() 创建新…

Vue项目使用process.env关键字及Vue.config.js配置解决前端跨域问题

1.process.env 是Node.js 中的一个环境 1.打开命令行查看环境: 2.process.env与Vue CLI 项目 Vue Cli 有以下三种运行模式 development 模式用于 vue-cli-service serve test 模式用于 vue-cli-service test:unit production 模式用于 vue-cli-service build 和 vue-cli-se…

Java设计模式 | 简单工厂模式

概述 需求 设计一个咖啡店点餐系统设计一个咖啡类&#xff08;Coffee&#xff09;&#xff1b;并定义其两个子类&#xff08;美式咖啡AmericanCoffee和拿铁咖啡LatteCoffee&#xff09;&#xff1b;再设计一个咖啡店类&#xff08;CoffeeStore&#xff09;&#xff0c;其具备…

【开源】SpringBoot框架开发不良邮件过滤系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 系统用户模块2.2 收件箱模块2.3 发件箱模块2.4 垃圾箱模块2.5 回收站模块2.6 邮箱过滤设置模块 三、实体类设计3.1 系统用户3.2 邮件3.3 其他实体 四、系统展示五、核心代码5.1 查询收件箱档案5.2 查询回收站档案5.3 新…

玩转C语言——深入理解指针

一、指针概念 1.1 内存和地址 在开始学习指针前&#xff0c;我们先来讲一个例子&#xff0c;假如你身处一栋楼中&#xff0c;你点了一份外卖&#xff0c;那么&#xff0c;外卖员如何能找到你&#xff1f;有两种方法。法一&#xff1a;直接一间一间找&#xff0c;这样做不仅消耗…

数据结构(三)复杂度的深层次剖析

之前发布了数据结构&#xff08;一&#xff09;&#xff0c;很多同学反响不够清晰&#xff0c;那今天就发一篇对复杂度专题的博客&#xff0c;希望对大家理解复杂度提供一些帮助。 时间复杂度 我们先来一个理解一个复杂度&#xff0c;二分查找的复杂度&#xff08;之前写过二…

JSONP 实现跨域请求案例

后端使用 express 搭建&#xff0c;案例代码如下&#xff1a; const express require(express)const app express() const PORT 3000app.get(/data, (req, res) > {const jsonData {name: Alan,age: 666,city: GD}const callback req.query.callback // 获取前端中的回…

野火ESP8266模块开发-基于Arduino IDE

一、野火ESP8266模块介绍 ESP8266 拥有高性能无线 SOC&#xff0c;给移动平台设计师带来福音&#xff0c;它以最低成本提供最大实用性&#xff0c;为 WiFi 功能嵌入其他系统提供无限可能。ESP8266 是一个完整且自成体系的 WiFi 网络解决方案&#xff0c;能够独立运行&#xff0…

网络工程师之路由交换技术篇

网络工程师之路由交换技术篇 路由交换之技术篇ARPICMPBPDUIPv6IP编址MAC其他技术点参考 以下均为个人笔记&#xff0c;摘录到csdn做备份 路由交换之技术篇 ARP Operation Code指定了ARP报文的类型&#xff0c; 包括ARP request 和ARP reply&#xff1b;取值为1或者2 &#x…

计算机毕业设计-基于深度学习的验证码识别方法设计与实现

概要 验证码是一个系统区分人类与非人类行为的有效方式。验证码识别技术能够使计算机程序输入正确的验证码&#xff0c;伪装成人类用户进入目标系统。另一方面&#xff0c;系统方面需要考虑破解验证码识别技术&#xff0c;修补验证方式的可能漏洞&#xff0c;使之能够更有效地区…

返回a除以b的余数 operator.mod(a, b) 返回a的b次幂 operator.pow(a, b)

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 返回a除以b的余数 operator.mod(a, b) 返回a的b次幂 operator.pow(a, b) [太阳]选择题 请问以下程序中operator.pow(2, 3)的执行结果是&#xff1a; import operator print("【执行】ope…

springboot284基于HTML5的问卷调查系统的设计与实现

问卷调查系统的设计与实现 摘 要 传统信息的管理大部分依赖于管理人员的手工登记与管理&#xff0c;然而&#xff0c;随着近些年信息技术的迅猛发展&#xff0c;让许多比较老套的信息管理模式进行了更新迭代&#xff0c;问卷信息因为其管理内容繁杂&#xff0c;管理数量繁多导…

【vue核心技术实战精讲】1.1 Vue开篇介绍 + 1.2 Vue的起步 和 插值表达式

文章目录 准备开始适应人群vue 框架学习路线一、vue 基础1、历史介绍2、前端框架与库的区别? 二、vue的起步 和 插值表达式Stage 1&#xff1a;下载包&#xff0c;并放入项目中Stage 2&#xff1a;编码 准备开始 适应人群 有一定的HTML/CSS/JavaScriptES6基础开发人员 vue …

深入解析MySQL数据库锁机制

✨✨谢谢大家捧场&#xff0c;祝屏幕前的小伙伴们每天都有好运相伴左右&#xff0c;一定要天天开心哦&#xff01;✨✨ &#x1f388;&#x1f388;作者主页&#xff1a; 喔的嘛呀&#x1f388;&#x1f388; ✨✨ 帅哥美女们&#xff0c;我们共同加油&#xff01;一起进步&am…

基于springboot+vue的光影视频网站

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

【机器学习智能硬件开发全解】(九)—— 政安晨:通过ARM-Linux掌握基本技能【C语言程序的预处理过程】

政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论⭐收藏 收录专栏: 机器学习智能硬件开发全解 希望政安晨的博客能够对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff01; C语言程序的预处理过程是在编译阶段之前进行的&#x…

基础-笔试题6

1、tcp/udp是属于哪一层&#xff1f;tcp/udp有何优缺点&#xff1f; tcp /udp属于运输层 TCP 服务提供了数据流传输、可靠性、有效流控制、全双工操作和多路复用技术等。 与 TCP 不同&#xff0c; UDP 并不提供对 IP 协议的可靠机制、流控制以及错误恢复功能等。由于 UDP 比较…