在问 · 算法题解

174. 地下城游戏

困难动态规划从右下到左上的动态规划力扣原题 ↗

题目描述

地牢是一个 m x n 网格 dungeon。骑士从左上角出发,公主在右下角。骑士每一步只能向右或向下走一格。

每进入一个格子,骑士的生命值就加上该格的数值:负数表示被恶魔扣血,0 表示空房间,正数表示补血。起点和终点格子也照样生效。

骑士的初始生命值是正整数。只要某一时刻生命值不大于 0,骑士就立刻死亡。返回骑士能活着到达公主所在格子所需的最小初始生命值。

示例 1
输入:dungeon = [[-2,-3,3],[-5,-10,1],[10,30,-5]]
输出:7

解释:沿 右、右、下、下 走,经过 -2、-3、3、1、-5。初始为 7 时生命值依次为 5、2、5、6、1,始终为正。

示例 2
输入:dungeon = [[0]]
输出:1

解释:格子值为 0,但初始生命值至少要是 1。

数据范围

  • m == dungeon.length
  • n == dungeon[i].length
  • 1 <= m, n <= 200
  • -1000 <= dungeon[i][j] <= 1000

解题思路:从右下到左上的动态规划

从起点往后推不行:前面剩多少血和后面需要多少血互相牵制。反过来从终点往回推:dp[i][j] 表示走进 (i,j) 之前至少要有的血量。从这里出发只能去右边或下边,选所需血量较小的那个,再减去本格的增减值;结果至少为 1,因为血量任何时候都要为正。

为什么正确

dp[i][j] = max(1, min(dp[i+1][j], dp[i][j+1]) - dungeon[i][j]),它只依赖右边和下边已经算好的格子。

复杂度分析

时间 O(mn),空间 O(mn)(可压缩到 O(n))。

易错点

从左上角正推并记录“最大剩余血量”是错的;另外别忘了 max(1, ...),补血再多也不能让所需血量低于 1。

参考代码

Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。

class Solution:
    def calculateMinimumHP(self, dungeon: List[List[int]]) -> int:
        m, n = len(dungeon), len(dungeon[0])
        dp = [[math.inf] * (n + 1) for _ in range(m + 1)]
        dp[m][n - 1] = dp[m - 1][n] = 1
        for i in range(m - 1, -1, -1):
            for j in range(n - 1, -1, -1):
                need = min(dp[i + 1][j], dp[i][j + 1]) - dungeon[i][j]
                dp[i][j] = max(1, need)
        return dp[0][0]

载入编辑器,自己跑一遍 →

其他解法

正向时路径上有两个互相影响的量(初始生命值和当前生命值),二分答案先固定其中一个;反向动态规划直接定义“从这里出发至少需要多少生命”,只用一次 O(mn) 就得到答案。

解法 2:二分答案 + 正向动态规划 时间 O(mn·log C),空间 O(mn)

初始生命值越大越容易通关,具有单调性,可以二分初始生命值 h。给定 h 后,从左上角正向做动态规划:best[i][j] 表示走到 (i,j) 时能保留的最大生命值,由上方和左方中仍大于 0 的较大者加上当前格子得到,结果不大于 0 则不可达。右下角可达就说明 h 足够。C 是答案的取值上界。

固定 h 后“保留最大生命值”具有最优子结构,正向动态规划变得可行,帮助理解为什么不固定 h 时正向做不通。代价是多一个 log C 因子。

from typing import List

class Solution:
    def calculateMinimumHP(self, dungeon: List[List[int]]) -> int:
        m, n = len(dungeon), len(dungeon[0])

        def ok(h):
            best = [[0] * n for _ in range(m)]
            for i in range(m):
                for j in range(n):
                    if i == 0 and j == 0:
                        prev = h
                    else:
                        prev = max(best[i - 1][j] if i > 0 else 0,
                                   best[i][j - 1] if j > 0 else 0)
                    if prev > 0 and prev + dungeon[i][j] > 0:
                        best[i][j] = prev + dungeon[i][j]
            return best[m - 1][n - 1] > 0

        lo, hi = 1, 1
        while not ok(hi):
            hi *= 2
        while lo < hi:
            mid = (lo + hi) // 2
            if ok(mid):
                hi = mid
            else:
                lo = mid + 1
        return lo

动态规划 专题的其他题目

查看 动态规划 全部题目 →