1. 程式人生 > >動態規劃經典教學題,上過《算導》的應該都會

動態規劃經典教學題,上過《算導》的應該都會

本文始發於個人公眾號:**TechFlow**,原創不易,求個關注

今天是LeetCode專題第41篇文章,我們一起來看一道經典的動態規劃問題Edit Distance,編輯距離。

今天這道題我本來是想跳過的,因為它實在是太經典了,屬於典型的老掉牙問題了。但是想了想,一方面因為之前立了flag要把所有Medium和Hard寫一遍,另一方面也是為了照顧萌新,所以還是把這題放上來了。相信上過演算法導論這門課的同學一定都見過它,如果你沒有上過屬於萌新,那也沒有關係,學習起來也不會很費勁的。

編輯距離

編輯距離非常經典,它指的是我們要花費多少力氣將一個字串A變成字串B。我們需要花費的力氣越多,那麼說明這兩個字串相差得越大,如果我們花費很少,說明兩個字串很接近。所以它可以用來作為衡量兩個字串相似程度的依據,今天這道題的題面就是要求兩個字串的編輯距離。

前面說了編輯距離就是我們將字串A通過編輯變成字串B花費的力氣,但是力氣是一個主觀的概念,我們需要將它量化。量化的方式也很簡單,我們規定刪除一個字元、新增一個字元和修改一個字元的花費都是1。整體編輯距離就是所有編輯操作的花費之和。

比如我們把horse變成rose,顯然只需要刪除ho並且插入一個o即可,所以編輯距離是3。

我們來看一個題目中的樣例:

Input: word1 = "intention", word2 = "execution"
Output: 5
Explanation: 
intention -> inention (remove 't')
inention -> enention (replace 'i' with 'e')
enention -> exention (replace 'n' with 'x')
exention -> exection (replace 'n' with 'c')
exection -> execution (insert 'u')

題解

我們拿到這道題本能地會想要從頭部開始依次比對來尋找答案,但是很快就能發現這樣是無法找到最優解的。原因也很簡單,因為我們在遍歷的過程當中沒辦法開天眼看到後面的情況。舉個簡單的例子,比如說我們現在有兩個字串,分別是aabbc和aabcc。

當我們發現b和c不匹配的時候,我們是不知道究竟是插入一個c還是刪除b還是把b轉變成c可以得到最優的結果的,因為我們不知道這兩個位置後面的位置是怎樣的。究竟當下如何決策對未來更好,我們只能得到當下儘可能好的決策,無法預測未來。所以貪心是不行的,直接進行運算也是不行的。

那要怎麼做呢?顯然到了這個時候,擺在我們面前的可行方案只剩下了動態規劃這一種了。

在這道問題當中,動態規劃的思想除了狀態轉移之外更加類似於遞推或者是記憶化搜尋。記憶化搜尋的意思是,雖然我們每一個狀態都有若干個決策,這些決策組合之後的樣本空間是非常龐大的,但是這些決策得到的結果數量是有限的,都可以歸納到某一個較小的範疇內。

舉個例子,比如A串的長度是5,B串的長度是3。我們對A串進行若干次編輯之後得到了B串,這當中編輯的種類是無窮的。因為我們可以隨意插入任意個字元,然後再將它們刪除,這都可以被視為一種解。但是這些操作得到的狀態是固定的,都是A串和B串相等了。那麼我們只需要記錄這個最終狀態,也就是A串長度5變成了B串長度3的這個狀態的最優解。

明確了這個之後,我們就可以愉快地遞推了。

因為對於A5B3的狀態來說,它並不是孤立的,它是可以從其他狀態推導得到的。比如A4B3,比如A5B2。如果我們儲存了這些所有的子狀態的最優解,那麼我們就可以從其中找到使得當下最優的決策。其實它就是搜尋的思想,我們要得到A5B3,需要從A4B3和A5B2當中找到一個最優的,那麼按道理我們又應該取搜尋A4B3和A5B2,但是這是沒有必要的,因為它們之間存在邏輯上的包含關係,A5B2和A4B3在計算A5B3的時候已經計算過了,所以我們把它儲存起來,直接查詢即可。

在這個問題當中,顯然我們用陣列儲存即可。如果從遞推的思路出發那麼這個就是一個遞推的問題,我們當前的狀態是從其他狀態推導得到的,我們尋找最優的遞推關係。如果從搜尋的思路出發就是搜尋每一個狀態的最佳答案,為了優化搜尋效率,我們記錄下中途得到每一個狀態的最優解。

無論怎麼理解,邏輯都是類似的,我們從區域性最優推導整體最優。只不過用狀態方程來表示更加清晰,我們用dp[i][j]表示A串前i個字元編輯成B串前j個字元的編輯距離。如果A[i] 和B[j]不等,那麼有三種方式可以得到相等。第一種是刪除i這個位置,第二種是插入B[j]這個字元,第三種是將A[i]編輯成B[j]。

如果刪除A[i]的話,上一個狀態是dp[i-1][j],如果插入B[j],那麼上一個狀態是dp[i][j-1]。如果是第三種,那麼上一個狀態是dp[i-1][j-1]。我們不知道這三種情況哪一種是最優的,所以都要考慮,通過min從其中選一個最小值。然後加上這次編輯帶來的消耗1。

如果i和j位置相等,那麼最優解一定是dp[i-1][j-1]。如果這些都理解了,程式碼就是幾行的事情。

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        n, m = len(word1), len(word2)
        if n == 0 or m == 0:
            return max(n, m)
        
        # 初始化為無窮大
        dp = [[0x3f3f3f3f for _ in range(m+2)] for _ in range(n+2)]
        
        # 由於下標從1開始,所以把字串增長一位
        word1 = ' ' + word1
        word2 = ' ' + word2
        
        # 初始化dp[0][i], dp[i][0]都等於i
        # 相當於填充i個字元
        dp[0][0] = 0
        for i in range(1, n+1):
            dp[i][0] = i
            
        for j in range(1, m+1):
            dp[0][j] = j
        
        for i in range(1, n+1):
            for j in range(1, m+1):
                # 狀態轉移方程
                if word1[i] == word2[j]:
                    dp[i][j] = dp[i-1][j-1]
                else:
                    dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
        
        return dp[n][m]

演算法導論在講解動態規劃的時候就是用這題做的例題,所以說真的非常的經典。沒有做過的同學一定不能錯過。

文章到這裡就結束了,如果喜歡本文,可以的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。

![](https://user-gold-cdn.xitu.io/2020/5/28/172592c17804dde8?w=258&h=258&f=png&