預計閱讀時間:8 分鐘前幾天在網上看到一份鵝場的面試題,算法部分大半是動態(tài)規(guī)劃,最后一題就是寫一個計算編輯距離的函數,今天就專門寫一篇文章來探討一下這個經典問題。 我個人很喜歡編輯距離這個問題,因為它看起來十分困難,解法卻出奇得簡單漂亮,而且它是少有的比較實用的算法(是的,我承認很多算法問題都不太實用)。下面先來看下題目: 為什么說這個問題難呢,因為顯而易見,它就是難,讓人手足無措,望而生畏。 為什么說它實用呢,因為前幾天我就在日常生活中用到了這個算法。之前有一篇公眾號文章由于疏忽,寫錯位了一段內容,我決定修改這部分內容讓邏輯通順。但是公眾號文章最多只能修改 20 個字,且只支持增、刪、替換操作(跟編輯距離問題一模一樣),于是我就用算法求出了一個最優(yōu)方案,只用了 16 步就完成了修改。 再比如高大上一點的應用,DNA 序列是由 A,G,C,T 組成的序列,可以類比成字符串。編輯距離可以衡量兩個 DNA 序列的相似度,編輯距離越小,說明這兩段 DNA 越相似,說不定這倆 DNA 的主人是遠古近親啥的。 下面言歸正傳,詳細講解一下編輯距離該怎么算,相信本文會讓你有收獲。 一、思路編輯距離問題就是給我們兩個字符串 前文 最長公共子序列 說過,解決兩個字符串的動態(tài)規(guī)劃問題,一般都是用兩個指針 設兩個字符串分別為 'rad' 和 'apple',為了把 根據上面的 GIF,可以發(fā)現(xiàn)操作不只有三個,其實還有第四個操作,就是什么都不要做(skip)。比如這個情況: 因為這兩個字符本來就相同,為了使編輯距離最小,顯然不應該對它們有任何操作,直接往前移動 還有一個很容易處理的情況,就是 類似的,如果 下面詳解一下如何將這個思路轉化成代碼,坐穩(wěn),準備發(fā)車了。 二、代碼詳解先梳理一下之前的思路: base case 是 對于每對兒字符 if s1[i] == s2[j]: 有這個框架,問題就已經解決了。讀者也許會問,這個「三選一」到底該怎么選擇呢?很簡單,全試一遍,哪個操作最后得到的編輯距離最小,就選誰。這里需要遞歸技巧,理解需要點技巧,先看下代碼: 下面來詳細解釋一下這段遞歸代碼,base case 應該不用解釋了,主要解釋一下遞歸部分。 都說遞歸代碼的可解釋性很好,這是有道理的,只要理解函數的定義,就能很清楚地理解算法的邏輯。我們這里 dp(i, j) 函數的定義是這樣的:
記住這個定義之后,先來看這段代碼: if s1[i] == s2[j]: 如果
dp(i - 1, j) + 1, # 刪除
現(xiàn)在,你應該完全理解這段短小精悍的代碼了。還有點小問題就是,這個解法是暴力解法,存在重疊子問題,需要用動態(tài)規(guī)劃技巧來優(yōu)化。 怎么能一眼看出存在重疊子問題呢?前文 動態(tài)規(guī)劃之正則表達式 有提過,這里再簡單提一下,需要抽象出本文算法的遞歸框架: def dp(i, j): 對于子問題 三、動態(tài)規(guī)劃優(yōu)化對于重疊子問題呢,前文 動態(tài)規(guī)劃詳解 介紹過,優(yōu)化方法無非是備忘錄或者 DP table。 備忘錄很好加,原來的代碼稍加修改即可:
主要說下 DP table 的解法: 首先明確 dp 數組的含義,dp 數組是一個二維數組,長這樣:
def dp(i, j) -> int 有了之前遞歸解法的鋪墊,應該很容易理解。dp 函數的 base case 是 既然 dp 數組和遞歸 dp 函數含義一樣,也就可以直接套用之前的思路寫代碼,唯一不同的是,DP table 是自底向上求解,遞歸解法是自頂向下求解: 三、擴展延伸一般來說,處理兩個字符串的動態(tài)規(guī)劃問題,都是按本文的思路處理,建立 DP table。為什么呢,因為易于找出狀態(tài)轉移的關系,比如編輯距離的 DP table: 還有一個細節(jié),既然每個 你可能還會問,這里只求出了最小的編輯距離,那具體的操作是什么?之前舉的修改公眾號文章的例子,只有一個最小編輯距離肯定不夠,還得知道具體怎么修改才行。 這個其實很簡單,代碼稍加修改,給 dp 數組增加額外的信息即可:
我們的最終結果不是 重復此過程,可以一步步回到起點 這就是編輯距離算法的全部內容,希望本文對你有幫助。 |
|