【leetcode热题】 地下城游戏

恶魔们抓住了公主并将她关在了地下城 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 都是 1calculateMinimumHPHelper(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 加到 1if (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 就变为 1if (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 就是当前的 healthif (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   -3dp 数组
终点位置就是递归出口时候返回的值,边界扩展一下
用 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.mzph.cn/news/756974.shtml

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

相关文章

【pdf工具】pdf24tools

pdf24tools是一款功能强大的在线PDF工具,它为用户提供了多种实用的功能,方便用户处理和编辑PDF文件。 合并PDF文件、拆分PDF文件、压缩PDF文件和转换PDF文件格式…

分布式搜索引擎elasticsearch专栏三

1.数据聚合 聚合&#xff08;aggregations&#xff09;可以让我们极其方便的实现对数据的统计、分析、运算。例如&#xff1a; 什么品牌的手机最受欢迎&#xff1f; 这些手机的平均价格、最高价格、最低价格&#xff1f; 这些手机每月的销售情况如何&#xff1f; 实现这些…

[Qt项目实战]Qt实现美松标签打印机标签二维码打印(QR混排模式+页打印模式)

1、硬件信息、环境参数及配套资料 1.1 打印机信息及开发环境 打印机 美松标签打印机串口/USB通讯Qt5.9 64位程序 1.2 打印机配套开发资料 打印机主要配套测试工具、开发SDK及驱动等&#xff0c;均由厂家提供。 开发Demo及动态库&#xff1a;MsPrintSDK-DLL-V2.2.2.5 链接&…

Android Studio实现内容丰富的安卓社交论坛平台

获取源码请点击文章末尾QQ名片联系&#xff0c;源码不免费&#xff0c;尊重创作&#xff0c;尊重劳动 项目编号085 1.开发环境android stuido jdk1.8 eclipse mysql tomcat 2.功能介绍 安卓端&#xff1a; 1.注册登录 2.查看公告 3.视频列表 4.资源列表 5.帖子列表 6.个人中心 …

Design Script之案例练习:曲线组合

让我们来探索一下如何创建分组曲线函数。这个函数将取一个无序曲线列表(必须是连续的),并将它们组合成分组的循环。 在这个例子中,我们将采取一系列的9个爆炸矩形,并洗牌合成曲线。这给了我们36条单独的曲线,它们可以正确地组合回各自的矩形中。 一些分散的曲线显示在右边…

基于python在线图书馆信息管理系统flask-django-nodejs-php

科学技术日新月异的如今&#xff0c;计算机在生活各个领域都占有重要的作用&#xff0c;尤其在信息管理方面&#xff0c;在这样的大背景下&#xff0c;学习计算机知识不仅仅是为了掌握一种技能&#xff0c;更重要的是能够让它真正地使用到实践中去&#xff0c;以创新的视角去不…

Spring-Mybatis字段映射

MybatisComfig.xml文件设置 <settings><setting name"mapUnderscoreToCamelCase" value"true"/> </settings> 完成全局配置将数据库下划线映射为驼峰式命名

nodejs基于vue班级管理系统的设计与实现-flask-django-python-php

根据现实需要&#xff0c;此系统我们设计出一下功能&#xff0c;主要有以下功能模板。 1.学生功能&#xff1a;首页、个人中心、课程信息管理、学生成绩管理、班级事件管理、班费支出管理、班级相册管理、班级音乐角管理。 2.管理员功能&#xff1a;首页、个人中心、班级管理、…

【征稿进行时|见刊、检索快速稳定】2024年区块链、物联网与复合材料与国际学术会议 (ICBITC 2024)

【征稿进行时|见刊、检索快速稳定】2024年区块链、物联网与复合材料与国际学术会议 (ICBITC 2024) 大会主题: (主题包括但不限于, 更多主题请咨询会务组苏老师) 区块链&#xff1a; 区块链技术和系统 分布式一致性算法和协议 块链性能 信息储存系统 区块链可扩展性 区块…

[Uni-app] 微信小程序的圆环进度条

效果图&#xff1a; 组件完整代码如下&#xff1a; <template><view class"base-style":style"position: relative;width: diameter px;height: diameter px;display: flex;flex-direction: row;background-color: bgColor ;"><!…

《动手学深度学习》 第3章 线性神经网络 部分笔记

文章目录 前言一、线性回归1.知识点&#xff08;1&#xff09;解析解&#xff08;2&#xff09;泛化&#xff08;3&#xff09;随机梯度下降&#xff08;4&#xff09;python列表推导&#xff08;5&#xff09;全连接层 二、线性回归的从零开始实现1.知识点&#xff08;1&#…

HCIP作业

实验要求&#xff1a; 1、R6为ISP&#xff0c;接口IP地址均为公有地址&#xff0c;该设备只能配置IP地址&#xff0c;之后不能再对其进行任何配置&#xff1b; 2、R1-R5为局域网&#xff0c;私有IP地址192.168.1.0/24&#xff0c;请合理分配&#xff1b; 3、R1、R2、R4&#x…

硬件基础:带缓启动MOS管电源开关电路

电源开关电路&#xff0c;经常用在各“功能模块”电路的电源通断控制&#xff0c;是常用电路之一。 本文要讲解的电源开关电路&#xff0c;是用MOS管实现的&#xff0c;且带缓开启功能&#xff0c;非常经典。 一、电路说明 电源开关电路&#xff0c;尤其是MOS管电源开关电路…

webpack中常见的Loader?解决了什么问题?

一、是什么 loader 用于对模块的"源代码"进行转换&#xff0c;在 import 或"加载"模块时预处理文件 webpack做的事情&#xff0c;仅仅是分析出各种模块的依赖关系&#xff0c;然后形成资源列表&#xff0c;最终打包生成到指定的文件中。如下图所示&#…

微信小程序 ---- 慕尚花坊 结算支付

结算支付 01. 配置分包并跳转到结算页面 思路分析&#xff1a; 随着项目功能的增加&#xff0c;项目体积也随着增大&#xff0c;从而影响小程序的加载速度&#xff0c;影响用户的体验。 因此我们需要将 结算支付 功能配置成一个分包&#xff0c; 当用户在访问设置页面时&a…

使用 Redux 管理 React 应用状态

使用 Redux 管理 React 应用状态 在复杂的 React 应用中&#xff0c;管理组件状态变得越来越复杂&#xff0c;这时候引入 Redux 可以帮助我们更好地管理状态。Redux 是一个可预测状态容器&#xff0c;它可以帮助我们统一管理应用的状态&#xff0c;使得状态变化更加可控。本文…

MATLAB环境下基于K-SVD的一维时间序列信号瞬态特征提取

当旋转机械某一零部件发生故障时&#xff0c;其振动信号中就会产生周期性循环冲击响应—瞬态成分&#xff0c;这些瞬态成分往往包含着重要的故障信息。因此&#xff0c;利用合适的方法对振动信号中因局部故障引起的瞬态冲击响应成分进行提取是一种较为有效的旋转机械零部件故障…

【RabbitMQ | 第四篇】基于RabbitMQ实现延迟队列

文章目录 4.基于RabbitMQ实现延迟队列4.1延迟队列定义4.2基于DLX&#xff08;死信交换机&#xff09;实现延迟队列4.2.1实现思路4.2.2主要流程4.2.3实战&#xff08;1&#xff09;创建两个消息队列&#xff1a;原始消息队列、死信队列 and 为原始消息队列关联私信交换机&#x…

【Ubuntu 22.04 LTS】安装vmware提示没有兼容的gcc

在ubuntu 22.04 上运行wmware时显示找不到兼容的gcc 这里要求的是12.3.0版本&#xff0c;我查看了自己的gcc版本是上面的11.4.0 在ask ubuntu上找到了解决方法 尝试了这一条 三条命令执行完成之后&#xff0c;再次运行vm&#xff0c;没有提示gcc的问题 点击install下载相应模…

vue3+element Plus form 作为子组件,从父组件如何赋值?

刚开始接触vue3时&#xff0c;碰到一个很low的问题&#xff0c;将form作为子组件&#xff0c;在页面中给form表单项输入内容&#xff0c;输入框不显示值&#xff0c;知道问题出在哪&#xff0c;但因为vue3组合式api不熟悉&#xff0c;不知从哪下手... 效果图&#xff1a; 父组…