本文以 LeetCode 第 174 题为例,介绍动态规划算法解决与字符串相关的问题。
LeetCode 174. Dungeon Game(地下城游戏)
其他动态规划的应用实例:
动态规划的应用(一):最短路问题
动态规划的应用(二):cutting stock 问题
动态规划的应用(三):字符串相关问题
动态规划的应用(四):LeetCode 1900. 最佳运动员的比拼回合
动态规划的应用(五):LeetCode 413, 446. 等差数列划分
LeetCode 174. Dungeon Game(地下城游戏)
问题描述
思路与代码
本题的思路很容易看出是动态规划,但仔细分析会发现,一条路径所需要的体力值并不是路径最终体力变化这么简单,具体分析可以参见官方题解:
因此,自然想到只能通过从终点向前反向递归的方式,实现动态规划算法。
于是,笔者提交了如下代码:
class Solution:
def calculateMinimumHP(self, dungeon: List[List[int]]) -> int:
def dp(mat_cost: List[List[int]], i: int, j: int) -> int:
m, n = len(mat_cost), len(mat_cost[0]) # 矩阵大小
if i == m - 1 and j == n - 1: # 终点
blood_need = 1 - mat_cost[-1][-1] if mat_cost[-1][-1] < 0 else 1
return blood_need
list_blood_need = []
if i < m - 1: # 向右移动
blood_need_route = dp(mat_cost=mat_cost, i=i + 1, j=j)
blood_need = blood_need_route - mat_cost[i][j] if blood_need_route - mat_cost[i][j] > 0 else 1
list_blood_need.append(blood_need)
if j < n - 1: # 向下移动
blood_need_route = dp(mat_cost=mat_cost, i=i, j=j + 1)
blood_need = blood_need_route - mat_cost[i][j] if blood_need_route - mat_cost[i][j] > 0 else 1
list_blood_need.append(blood_need)
blood_need = min(list_blood_need)
return blood_need
return dp(mat_cost=dungeon, i=0, j=0)
然而,在运行后面的算例时出现了超时,超时算例为一个
60
×
70
60 \times 70
60×70 的矩阵:
反思代码的实现方式,发现犯了一个低级错误,即动态规划的优势是避免重复搜索,然而前面的代码并没有存储到达每个点的中间结果。于是,笔者又将代码修改如下:
class Solution:
def calculateMinimumHP(self, dungeon: List[List[int]]) -> int:
def dp(mat_cost: List[List[int]], i: int, j: int, dict_res: Dict[Tuple[int, int], int]) -> int:
m, n = len(mat_cost), len(mat_cost[0]) # 矩阵大小
if i == m - 1 and j == n - 1: # 终点
blood_need = 1 - mat_cost[-1][-1] if mat_cost[-1][-1] < 0 else 1
dict_res[i, j] = blood_need # 存储中间结果
return blood_need
if (i, j) in dict_res.keys(): # 查询中间结果
return dict_res[i, j]
list_blood_need = []
if i < m - 1: # 向右移动
blood_need_route = dp(mat_cost=mat_cost, i=i + 1, j=j, dict_res=dict_res)
blood_need = blood_need_route - mat_cost[i][j] if blood_need_route - mat_cost[i][j] > 0 else 1
list_blood_need.append(blood_need)
if j < n - 1: # 向下移动
blood_need_route = dp(mat_cost=mat_cost, i=i, j=j + 1, dict_res=dict_res)
blood_need = blood_need_route - mat_cost[i][j] if blood_need_route - mat_cost[i][j] > 0 else 1
list_blood_need.append(blood_need)
blood_need = min(list_blood_need)
dict_res[i, j] = blood_need # 存储中间结果
return blood_need
return dp(mat_cost=dungeon, i=0, j=0, dict_res={})
重新提交运行,终于通过了,然而运行效果不太理想:
再次参考官方题解反思,发现并不需要通过递归的方式实现,只需要从终点(右下角)向起点(左上角)反向遍历即可。
代码如下:
class Solution:
def calculateMinimumHP(self, dungeon: List[List[int]]) -> int:
n, m = len(dungeon), len(dungeon[0])
big_m = 10 ** 8
dp = [[big_m] * (m + 1) for _ in range(n + 1)] # 增加一行一列,以便从终点开始
dp[n][m - 1] = dp[n - 1][m] = 1 # 边界外增加的一行一列置为 1
for i in range(n - 1, -1, -1):
for j in range(m - 1, -1, -1):
min_blood = min(dp[i + 1][j], dp[i][j + 1])
dp[i][j] = max(min_blood - dungeon[i][j], 1)
return dp[0][0]
运行效果: