徹底理解動態(tài)規(guī)劃:最長公共超序列
大家好,我是小風哥,今天這篇文章會開啟動態(tài)規(guī)劃這個主題,動態(tài)規(guī)劃是算法中非常重要的思想之一。
今天的題目是最短公共超序列,如果一個字符串s在刪除某些字符后形成t,那么我們說s是t的超序列,現(xiàn)在給定兩個字符串str1與str2,返回str1與str2的最長公共超序列,如果有多個的話返回任意一個即可。
假設str1為"abac",str2為“cab”,那么這兩個字符串的最短公共超序列是“cabac”;而如果str1為“bc”,str2為“cab”,那么最短公共超序列是“cabc”或者“bcab”。
想一想該怎樣解決問題。
子問題與選擇
動態(tài)規(guī)劃類問題的關鍵在于找出子問題以及子問題與原始問題的關聯(lián),要想找出子問題就需要知道每一步的選擇是什么。
在這個問題中如果str1=“abec”、str2=“aecd”,因為st1與str2的第一個字符相同,那么我們知道字符‘a(chǎn)’一定是最短公共超序列中的一個,這樣str1就變?yōu)榱恕癰ec”,str2就變?yōu)榱恕癳cd”,而這個問題本質上和原始問題沒有區(qū)別,就這樣我們找到了子問題。
現(xiàn)在,str1=“bec”和str2=“ecd”的第一個字符不再相等該怎么辦呢?很簡單:
超序列中包含str1的第一個字符,這樣str1就變?yōu)榱恕癳c”,str2依然是“ecd”,假設此時str1與str2的最短公共超序列為supers1
超序列中包含str2的第一個字符,這樣str1依然是“bec”,str2就變?yōu)榱恕癱d”,假設此時str1與str2的最短公共超序列為supers2
原始問題的最長公共超序列一定是supers1與supers2中最短的那一個。
現(xiàn)在我們找到了原始問題與子問題的關聯(lián)。
狀態(tài)空間樹
基于上述分析,我們可以很容易的畫出這樣的狀態(tài)空間樹:
上圖中每一個方框都代碼一個子問題,如果某個子問題中的兩個字符串的首字符不相等那么會衍生出兩個新的的子問題,超序列要么使用str1的第一個字符,要么使用str2的第一個字符;而如果str1與str2的首字符相同,那么超序列中只需要新增該字符即可。
該狀態(tài)空間樹的葉子節(jié)點為所有str1與str2都為空,此時經(jīng)過從根節(jié)點到葉子結點一路的選擇我們就得到了其對應的超序列,從上圖看有兩種最短公共超序列“bcab”與“cabc”,長度都是4。
從這棵狀態(tài)空間樹中你可以輕易的看到原始問題是如何分解為子問題的以及如果利用子問題的解來構建原始問題的解。
圖中每個方框代表一個子問題,決定子問題的只有兩個元素,str1與str2首字符的在各自對應字符串的起始位置i和j,因此我們定義遞歸函數(shù)scs(shortest common supersequence的縮寫):
該遞歸函數(shù)的含義是字符串str1[i, str1.length()-1]與字符串str2[j, str2.length()-1]的最短公共超序列的長度是多少。
基于上述分析與畫出的狀態(tài)空間樹你可以很容易的寫出其遞歸實現(xiàn):
可以看到實際上我們只需要四行代碼就可以搞定問題,(注意看該遞歸實現(xiàn),和最長回文子串這個示例的實現(xiàn)在形式上幾乎完全相同,是不是很有趣)。
在這這里將str1與str2作為全局變量,這樣你可以清楚的看到遞歸函數(shù)scs的返回值只依賴于參數(shù)i和j,而參數(shù)i的取值屬于[0, str1.length()],j的取值屬于[0, str2.length()],因此參數(shù)i和j的組合最多只有(str1.length() + 1) * (str2.length() + 1) 個。
重復子問題
再來觀察下上述遞歸代碼的形成的狀態(tài)空間樹:
這里存在著一些完全相同的子問題,這些子問題會被重復計算,因此我們可以將子問題解記錄下來,當再次遇到該子問題時直接返回結果即可:
增加cache后每個子問題只被計算一次,這實際上就是遞歸版的動態(tài)規(guī)劃代碼了。
動態(tài)規(guī)劃實現(xiàn)
接下來我們著手將自頂向下的遞歸代碼轉為自底向上的動態(tài)規(guī)劃代碼。
既然子問題的個數(shù)就只有這么多,因此可以使用數(shù)組dp來保存子問題解,注意看上述遞歸函數(shù)只依賴兩個參數(shù),因此數(shù)組dp是二維的,即(str1.length() + 1) * (str2.length() + 1)的二維數(shù)組:
接下來就是最小子問題是什么,注意觀察上述兩個遞歸出口,可以看到最小子問題分別是str1與str2為空的情況,基于這兩種情況我們可以很容易的構建出最小子問題解,將遞歸代碼中的:
轉為:
最后我們手動利用兩個for循環(huán)構造出所有i和j的組合,將遞歸函數(shù)中除去遞歸出口的這一部分:
直接放到兩個for循環(huán)之中,并且將遞歸調用轉為對數(shù)組dp的讀寫:
這樣完整的實現(xiàn)代碼為: