動態規劃之沒有條件創造條件

動態規劃之沒有條件創造條件


  一個問題需要使用動態規劃,則需要滿足幾個條件,這些條件在先前的文章中也都列舉過。其中 有些條件是可以適當放縮的,有的條件是絕對需要滿足的,那就是無後效性,通俗來說就是當前狀態的計算,之前的狀態已經是確定的。
  有些時候,明明知道一個題需要用動態規劃了,但是怎麼就好像哪裏出了問題,可以靜下心來想想,是否是哪個條件略微不滿足了。如果不滿足了怎麼辦,那就是改變策略,讓算法滿足這個條件。
  看下面一道題目,題目來源於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]

  上面的代碼是在原來矩陣上直接操作的,沒有開闢空間。如果開闢空間的話建議多開闢一行一列,這樣久不用把邊界情況單獨處理。
  閱讀完本文之後,希望大家能夠結合自己的思考,掌握一類問題,而不是單單這個問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章