动态规划之没有条件创造条件

动态规划之没有条件创造条件


  一个问题需要使用动态规划,则需要满足几个条件,这些条件在先前的文章中也都列举过。其中 有些条件是可以适当放缩的,有的条件是绝对需要满足的,那就是无后效性,通俗来说就是当前状态的计算,之前的状态已经是确定的。
  有些时候,明明知道一个题需要用动态规划了,但是怎么就好像哪里出了问题,可以静下心来想想,是否是哪个条件略微不满足了。如果不满足了怎么办,那就是改变策略,让算法满足这个条件。
  看下面一道题目,题目来源于leetcode174.

一些恶魔抓住了公主(P)并将她关在了地下城的右下角。地下城是由 M x N 个房间组成的二维网格。我们英勇的骑士(K)最初被安置在左上角的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。
骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。

有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。
为了尽快到达公主,骑士决定每次只向右或向下移动一步。 编写一个函数来计算确保骑士能够拯救到公主所需的最低初始健康点数。

例如,考虑到如下布局的地下城,如果骑士遵循最佳路径 右 -> 右 -> 下 -> 下,则骑士的初始健康点数至少为 7。
-2(K)   -3   3
-5    -10   1
10    30   -5(P )

说明: 骑士的健康点数没有上限。
任何房间都可能对骑士的健康点数造成威胁,也可能增加骑士的健康点数,包括骑士进入的左上角房间以及公主被监禁的右下角房间。

  看到这种网格,而且只能往下走或者往右走,一看就知道这种问题必然要用动态规划。这种问题算是比较基础的动态规划问题了。
  但是贸然去做就容易出问题,以往这种问题都是整条路径,然后求个最终到目的地的代价。而这个问题,会出现正负代价的抵消。后面的收益不能抵消前面路径的代价。说的通俗点就是任何一个点都不能死。已经死了,哪怕后面就是血也吃不到。
  举个例子,5->-7->10,这种情况,尽管整条路径的收益是正的,但是如果在-7处死了,10根本就没有意义。假如我们需要确保当前剩余的血量是最多的,但是就可能走入死胡同,后面的所有路径都会耗血很多。如果我们确保后面消耗的血量少,那么还要看当前剩余的血量。前后状态交织,就不满足动态规划的要素。
  但是这个问题,似乎又那么熟悉,直觉告诉自己,肯定是动态规划。这个时候就要转换问题,创造条件。正常的算法人一定要有这个直觉,至于能否成功转换,这就看能力了。
  假设有这样一条路径,5->-7->10->-11->2->-4->20。那么最少需要带多少血呢,显然答案和最后的20肯定没有关系。倒数第二个数是-4,那么显然在经过-4之前,至少有5滴血。在往前,依次类推。可以看到,从后往前的过程中,过去的状态就彻底确定了。这就满足了动态规划的要素。
  这个问题的转变策略在于,将问题反过来考虑,从后往前,每一步的状态是确定的。这个时候回到二维的网格上。
  定义dp[i][j]为如果经过(i,j)的位置到达终点,到该位置之前,至少还需要的血量,S[i][j]为经过位置(i,j)获得的血量,正值表示获得。根据dp[i][j+1]和dp[i+1][j]就可以确定dp[i][j]。dp[i][j] = min(dp[i][j+1],dp[i+1][j]) + S[i][j]滴血。如果算出来这个是负值,但是至少允许的血量不能是负值,也不能死掉,所以到这个位置至少还需要一滴血。也就是要对最终的结果和1取一个max。
  这个问题的关键转换就在于,从左上往右下走,前面的状态不能确定,不满足无后效性。而转换问题为出终点溯源,这个时候已经走过的状态就确定了,明白了这个思想,代码就很好写了。

class Solution:
    def calculateMinimumHP(self, dungeon: List[List[int]]) -> int:
        dungeon[-1][-1] = max(1, 1- dungeon[-1][-1])
        # 表示计算要到达终点至少的剩余的血量
        for j in range(len(dungeon[0])-1, 0, -1):
            dungeon[-1][j-1] = max(1, dungeon[-1][j] - dungeon[-1][j-1])
            # 计算最后一行的情况,表示经过该元素到达终点,则到达这个元素之前最少剩余血量。
        for i in range(len(dungeon)-1, 0, -1):
            dungeon[i-1][-1] = max(1, dungeon[i][-1] - dungeon[i-1][-1])
            # 把每一行的最后一个元素单独处理
            for j in range(len(dungeon[0]) - 1, 0, -1):
                dungeon[i-1][j - 1] = max(1, min(dungeon[i-1][j], dungeon[i][j-1]) - dungeon[i-1][j - 1])
                # dp的核心代码
        return dungeon[0][0]

  上面的代码是在原来矩阵上直接操作的,没有开辟空间。如果开辟空间的话建议多开辟一行一列,这样久不用把边界情况单独处理。
  阅读完本文之后,希望大家能够结合自己的思考,掌握一类问题,而不是单单这个问题。

猜你喜欢

转载自blog.csdn.net/m0_38065572/article/details/106820331