力扣174. 地下城游戏

算法 专栏收录该内容
130 篇文章 0 订阅

力扣174. 地下城游戏

一、题目描述

在这里插入图片描述
在这里插入图片描述

二、分析

  • 这个题一看就可以用动态规划,就需要确定动态规划的状态和选择以及状态转移方程

  • 如果按照从左上往右下的顺序进行动态规划,对于每一条路径,我们需要同时记录两个状态。第一个是「从出发点到当前点的路径和」,第二个是「从出发点到当前点所需的最小初始值。而这两个状态的重要程度相同

  • 例如:
    在这里插入图片描述

  • 从 (0,0)到 (1,2) 有多条路径(即动态规划的选择),我们取其中最有代表性的两条:
    在这里插入图片描述

  • 绿色路径「从出发点到当前点的路径和」为 1,「从出发点到当前点所需的最小初始值」为 3

  • 蓝色路径「从出发点到当前点的路径和」为 -1,「从出发点到当前点所需的最小初始值」为 2

我们希望「从出发点到当前点的路径和」尽可能大,而「从出发点到当前点所需的最小初始值」尽可能小。这两条路径各有优劣。

  • 在上图中,我们知道应该选取绿色路径,因为蓝色路径的路径和太小,使得蓝色路径需要增大初始值到 4 才能走到终点,而绿色路径只要 3 点初始值就可以直接走到终点。

  • 但是如果把终点的 -2 换为 0,蓝色路径只需要初始值 2,绿色路径仍然需要初始值 3,最优决策就变成蓝色路径了。

  • 因此,如果按照从左上往右下的顺序进行动态规划,我们无法直接确定到达 (1,2) 的方案,因为动态规划的两个状态无论怎么选择都在相互影响。也就是说,这样的动态规划是不满足其自身的特性「无后效性」的。

  • 所以此时我们需要从后往前思考:我:公主,你自杀吧,我走不过去。公主:傻屌,起点等着,我去找你!

  • 定义dp函数:

int dp(vector<vector<int>>& dungeon, int i, int j);
  • dp函数的定义:

  • 从dungeon[i][j]到达终点(右下角)所需的最少生命值是dp(dungeon, i, j)。

  • 那么可以这样写代码

int calculateMinimumHP(vector<vector<int>>& dungeon)  {
    // 我们想计算左上角到右下角所需的最小生命值
    return dp(dungeon, 0, 0);
}

int dp(vector<vector<int>>& dungeon, int i, int j) {
    int m = dungeon.size();
    int n = dungeon[0].size();
    // base case
    if (i == m - 1 && j == n - 1) {
        return dungeon[i][j] >= 0 ? 1 : 1 - dungeon[i][j];
    }
    ...
}
  • 根据新的dp函数定义和 base case,我们想求dp(0, 0),那就应该试图通过dp(i, j+1)和dp(i+1, j)推导出dp(i, j),这样才能不断逼近 base case,正确进行状态转移

  • 如图:
    在这里插入图片描述

  • 具体来说,「从A到达右下角的最少生命值」应该由「从B到达右下角的最少生命值」和「从C到达右下角的最少生命值」推导出来:

  • 假设dp(0, 1) = 5, dp(1, 0) = 4,那么可以肯定要从A走向C,因为 4 小于 5 嘛

  • 那么怎么推出dp(0, 0)是多少呢?分两种情况:

  • 第一种情况:假设A坐标的值为 1,既然知道下一步要往C走,且dp(1, 0) = 4,意味着走到dungeon[1][0]的时候至少要有 4 点生命值,那么就可以确定骑士出现在A点时需要 4 - 1 = 3 点初始生命值,对吧。

  • 第二种概况:那如果A坐标的值为 10,落地就能捡到一个大血瓶,超出了后续需求,4 - 10 = -6 意味着骑士的初始生命值为负数,这显然不可以,骑士的生命值小于 1 就挂了,所以这种情况下骑士的满足最低的初始生命值应该是 1。

  • 综上,状态转移方程已经推出来了:

int res = min(
    dp(i + 1, j),
    dp(i, j + 1)
) - dungeon[i][j];

dp(i, j) = ret <= 0 ? 1 : ret;

三、完整代码

  • dp函数
class Solution {
public:
    int dp(vector<vector<int>>& dungeon, int i, int j, vector<vector<int>>& dict) {
        //代表走到终点,当前只需判断终点坐标值的大小。如果为正,骑士走到这里至少需要
        //1滴血(小于1滴血骑士就挂了),如果为负,那么骑士走到这里至少需要(abs(负) + 1)
        //滴血,abs(负)是为了补回来, + 1是为了让骑士不死
        //
        if (i == dungeon.size() - 1 && j == dungeon[0].size() - 1) {
            return dungeon[i][j] > 0 ? 1 : 1 - dungeon[i][j];
        }

        //代表越界情况:因为每次求的是下方和右方的最小值,所以返回max,避免影响
        if (i == dungeon.size() || j == dungeon[0].size()) {
            return INT_MAX;
        }
        
        //备忘录
        if (dict[i][j] != -1) {
            return dict[i][j];
        }
        
        //知道dp(dungeon, i + 1, j, dict)和dp(dungeon, i, j + 1, dict),那么
        //肯定选着需要血最少的
        int ret = min(dp(dungeon, i + 1, j, dict), dp(dungeon, i, j + 1, dict)) - dungeon[i][j];
        //判断结果,原理见博客
        dict[i][j] = (ret <= 0 ? 1 : ret);
        
        return dict[i][j];
    }

    int calculateMinimumHP(vector<vector<int>>& dungeon) {
        if(dungeon.empty() || dungeon[0].empty()) {
            return 0;
        } 

        vector<vector<int>> dict(dungeon.size(), vector<int>(dungeon[0].size(), -1));
        return dp(dungeon, 0, 0, dict);
    }
};

  • dp数组
class Solution {
public:
    int calculateMinimumHP(vector<vector<int>>& dungeon) {
        if(dungeon.empty() || dungeon[0].empty()) {
            return 0;
        } 
        int n = dungeon.size(), m = dungeon[0].size();
        vector<vector<int>> dp(n + 1, vector<int>(m + 1, INT_MAX));

        dp[n][m - 1] = dp[n - 1][m] = 1;
        for(int i = n - 1; i >= 0; --i) {
            for(int j = m - 1; j >= 0; --j) {
                int ret = min(dp[i + 1][j], dp[i][j + 1]) - dungeon[i][j];
                dp[i][j] = ret > 0 ? ret : 1;
            }
        }
        return dp[0][0];
    }
};

  • 2
    点赞
  • 2
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值