力扣:地下城游戏,手把手教你做困难题
174. 地下城游戏 【困难题】【动态规划】
一些恶魔抓住了公主(P)并将她关在了地下城的右下角。地下城是由 M x N 个房间组成的二维网格。我们英勇的骑士(K)最初被安置在左上角的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。
骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。
有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。
为了尽快到达公主,骑士决定每次只向右或向下移动一步。
编写一个函数来计算确保骑士能够拯救到公主所需的最低初始健康点数。
例如,考虑到如下布局的地下城,如果骑士遵循最佳路径右 -> 右 -> 下 -> 下
,则骑士的初始健康点数至少为 7。
题目讲解
【核心思想】
这道题,第一眼看过去就是一道动态规划题。但为什么这是困难题呢?因为这与普通的动态规划不一样,注意这一句如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。也就是说,它不是简单的求最小值的问题,其中还有一定的约束。
举个例子:
1(K) |
-3 |
3 |
---|---|---|
0 |
-2 |
0 |
-3 |
-3 |
-3(P) |
这个例子中如果只看走到格子(1, 2)
的结果的话,肯定是 下 -> 右 -> 右
最好,因为这样初始生命只需要 2 就够了。而另一条路 右 -> 右 -> 下
则需要初始生命 3 。
但是如果继续走到格子(2, 2)
,那么最优方向一定是从 (1, 2)
过来(另一个方向负数太多)。但是到 (1, 2)
的最优路线保存的是 下 -> 右 -> 右
这一条,走到终点总和是 -4 ,初始所需最小生命增大为 5 。而另一条原本不怎么好的路线 右 -> 右 -> 下
总和是 -2 ,初始所需最小生命 3 ,所以仍然保持不变。
这样看来原本不好的路线在最后的结果里是可能会变好的,因此不好保存下来直接递推。那怎么办呢?
还记得以前我们公众号分享过一个动态规划的通解(公众号后台返回“动态规划”可得),其中构建dp数组的目标,就是让这个dp数组能直接返回答案。所以,本题我们构建的dp数组,存放的就是还未经过当前格子时,保证骑士经过这个格子而不死的最低健康数,那么我们就应该从后往前来填充这个dp数组了。
【举例】
拿此图举例,从终点开始,dp数组应该初始化为下图,因为(3,3)
要扣5点血,所以还未经过当前格子时,保证骑士经过这个格子而不死的最低健康数为6
0(K) |
0 |
0 |
---|---|---|
0 |
0 |
0 |
0 |
0 |
6(P) |
第二步就是初始化边界,如下图。这里要注意的是,骑士的血最少必须保证为1,不然就死了。
0(K) |
0 |
2 |
---|---|---|
0 |
0 |
5 |
1 |
1 |
6(P) |
最后,我们的状态转移方程为:
int minNum = Math.min(dp[i+1][j],dp[i][j+1]);
dp[i][j] = Math.max(minNum-dungeon[i][j],1);
填满整个格子如下,最终返回dp[0][0]
7(K) |
5 |
2 |
---|---|---|
6 |
11 |
5 |
1 |
1 |
6(P) |
【代码】
class Solution {
public int calculateMinimumHP(int[][] dungeon) {
int n = dungeon.length, m = dungeon[0].length;
int[][] dp = new int[n][m];
dp[n-1][m-1] = (-1)*Math.min(0, dungeon[n-1][m-1])+1;
if(n==1 && m==1)
return dp[n-1][m-1];
for(int i=n-2;i>=0;i--)
dp[i][m-1] = Math.max(dp[i+1][m-1]-dungeon[i][m-1],1);
for(int i=m-2;i>=0;i--)
dp[n-1][i] = Math.max(dp[n-1][i+1]-dungeon[n-1][i],1);
for(int i=n-2;i>=0;i--){
for(int j=m-2;j>=0;j--){
int minNum = Math.min(dp[i+1][j],dp[i][j+1]);
dp[i][j] = Math.max(minNum-dungeon[i][j],1);
}
}
return dp[0][0];
}
}
- 【重磅】微软Facebook联手发布AI生态系统,CNTK+Caffe2+PyTorch挑战TensorFlow
- hduoj-----(1068)Girls and Boys(二分匹配)
- 使用Django suit或Bootstrap美化admin模板
- hdu---------(1026)Ignatius and the Princess I(bfs+dfs)
- hdu-----(1113)Word Amalgamation(字符串排序)
- HDUoj-------(1128)Self Numbers
- cf------(round 2)A. Winner
- cf------(round)#1 C. Ancient Berland Circus(几何)
- MySQL配置TokuDB的简单总结
- cf------(round)#1 B. Spreadsheets(模拟)
- sysbench压测MyCAT的shell脚本
- qemu-kvm中vcpu虚拟化到底是咋整的?
- 【给 iOS 开发者】人工智能在 iOS 开发上的应用和机会
- 【Python】Selenium辅助海量基金数据获取
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- R语言使用链梯法Chain Ladder和泊松定律模拟和预测未来赔款数据
- Android ViewPager实现左右滑动的实例
- R语言通过伽玛与对数正态分布假设下的广义线性模型对大额索赔进行评估预测
- R语言中回归模型预测的不同类型置信区间应用比较分析
- 第06期:Prometheus 存储
- 新特性解读 | 数组范围遍历功能
- 技术分享 | MySQL 内存管理初探
- 新特性解读 | 窗口函数的适用场景
- Android自定义View 仿QQ侧滑菜单的实现代码
- Android view随触碰滑动效果
- TextView使用SpannableString设置复合文本 SpannableString实现TextView的链接效果
- FragmentTabHost使用方法详解
- Android编程实现仿优酷圆盘旋转菜单效果的方法详解【附demo源码下载】
- Android绘制圆形百分比加载圈效果
- Android自定义view实现动态柱状图