第九章?動態規劃part16
- ?583.?兩個字符串的刪除操作?
// dp數組中存儲word1和word2最長相同子序列的長度 class Solution {public int minDistance(String word1, String word2) {int len1 = word1.length();int len2 = word2.length();int[][] dp = new int[len1 + 1][len2 + 1];for (int i = 1; i <= len1; i++) {for (int j = 1; j <= len2; j++) {if (word1.charAt(i - 1) == word2.charAt(j - 1)) {dp[i][j] = dp[i - 1][j - 1] + 1;} else {dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);}}}return len1 + len2 - dp[len1][len2] * 2;} }// dp數組中存儲需要刪除的字符個數 class Solution {public int minDistance(String word1, String word2) {int[][] dp = new int[word1.length() + 1][word2.length() + 1];for (int i = 0; i < word1.length() + 1; i++) dp[i][0] = i;for (int j = 0; j < word2.length() + 1; j++) dp[0][j] = j;for (int i = 1; i < word1.length() + 1; i++) {for (int j = 1; j < word2.length() + 1; j++) {if (word1.charAt(i - 1) == word2.charAt(j - 1)) {dp[i][j] = dp[i - 1][j - 1];}else{dp[i][j] = Math.min(dp[i - 1][j - 1] + 2,Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1));}}}return dp[word1.length()][word2.length()];} }
思路:dp數組表示需要刪除的字符的最小個數,遞推公式如果word1[i-1]和word2[j-1]相等的話,直接將dp[i-1][j-1]賦值給dp[i][j],如果不相等的話,就比較取最小值。還有一個關鍵的地方在于初始化dp數組,當i=0時表示word1為null需要將word2里面的字符全部刪掉。j=0時同理。
- ?72.?編輯距離?
public int minDistance(String word1, String word2) {int m = word1.length();int n = word2.length();int[][] dp = new int[m + 1][n + 1];// 初始化for (int i = 1; i <= m; i++) {dp[i][0] = i;}for (int j = 1; j <= n; j++) {dp[0][j] = j;}for (int i = 1; i <= m; i++) {for (int j = 1; j <= n; j++) {// 因為dp數組有效位從1開始// 所以當前遍歷到的字符串的位置為i-1 | j-1if (word1.charAt(i - 1) == word2.charAt(j - 1)) {dp[i][j] = dp[i - 1][j - 1];} else {dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1;}}}return dp[m][n]; }
思路:關鍵就是推導遞推公式,dp數組表示i-1變為j-1所需的最小操作數。
2. 確定遞推公式
在確定遞推公式的時候,首先要考慮清楚編輯的幾種操作,整理如下:
if (word1[i - 1] == word2[j - 1])不操作 if (word1[i - 1] != word2[j - 1])增刪換
也就是如上4種情況。
if (word1[i - 1] == word2[j - 1])
?那么說明不用任何編輯,dp[i][j]
?就應該是?dp[i - 1][j - 1]
,即dp[i][j] = dp[i - 1][j - 1];
此時可能有同學有點不明白,為啥要即
dp[i][j] = dp[i - 1][j - 1]
呢?那么就在回顧上面講過的
dp[i][j]
的定義,word1[i - 1]
?與?word2[j - 1]
相等了,那么就不用編輯了,以下標i-2為結尾的字符串word1和以下標j-2為結尾的字符串word2
的最近編輯距離dp[i - 1][j - 1]
就是?dp[i][j]
了。在下面的講解中,如果哪里看不懂,就回想一下
dp[i][j]
的定義,就明白了。在整個動規的過程中,最為關鍵就是正確理解
dp[i][j]
的定義!if (word1[i - 1] != word2[j - 1])
,此時就需要編輯了,如何編輯呢? - 操作一:word1刪除一個元素,那么就是以下標i - 2為結尾的word1 與 j-1為結尾的word2的最近編輯距離 再加上一個操作。
-
即?
dp[i][j] = dp[i - 1][j] + 1;
- 操作二:word2刪除一個元素,那么就是以下標i - 1為結尾的word1 與 j-2為結尾的word2的最近編輯距離 再加上一個操作。
-
即?
dp[i][j] = dp[i][j - 1] + 1;
這里有同學發現了,怎么都是刪除元素,添加元素去哪了。
word2添加一個元素,相當于word1刪除一個元素,例如?
word1 = "ad" ,word2 = "a"
,word1
刪除元素'd'
?和?word2
添加一個元素'd'
,變成word1="a", word2="ad"
, 最終的操作數是一樣! dp數組如下圖所示意的:a a d+-----+-----+ +-----+-----+-----+| 0 | 1 | | 0 | 1 | 2 |+-----+-----+ ===> +-----+-----+-----+a | 1 | 0 | a | 1 | 0 | 1 |+-----+-----+ +-----+-----+-----+d | 2 | 1 |+-----+-----+
操作三:替換元素,
word1
替換word1[i - 1]
,使其與word2[j - 1]
相同,此時不用增刪加元素。可以回顧一下,
if (word1[i - 1] == word2[j - 1])
的時候我們的操作 是?dp[i][j] = dp[i - 1][j - 1]
?對吧。那么只需要一次替換的操作,就可以讓 word1[i - 1] 和 word2[j - 1] 相同。
所以?
dp[i][j] = dp[i - 1][j - 1] + 1;
綜上,當?
if (word1[i - 1] != word2[j - 1])
?時取最小的,即:dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
遞歸公式代碼如下:
if (word1[i - 1] == word2[j - 1]) {dp[i][j] = dp[i - 1][j - 1]; } else {dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1; }
- ?編輯距離總結篇???
動態規劃之編輯距離總結篇
本周我們講了動態規劃之終極絕殺:編輯距離,為什么叫做終極絕殺呢?
細心的錄友應該知道,我們在前三篇動態規劃的文章就一直為 編輯距離 這道題目做鋪墊。
#判斷子序列
動態規劃:392.判斷子序列?(opens new window)給定字符串 s 和 t ,判斷 s 是否為 t 的子序列。
這道題目 其實是可以用雙指針或者貪心的的,但是我在開篇的時候就說了這是編輯距離的入門題目,因為從題意中我們也可以發現,只需要計算刪除的情況,不用考慮增加和替換的情況。
- if (s[i - 1] == t[j - 1])
- t中找到了一個字符在s中也出現了
- if (s[i - 1] != t[j - 1])
- 相當于t要刪除元素,繼續匹配
-
狀態轉移方程:
if (s[i - 1] == t[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1; else dp[i][j] = dp[i][j - 1];
#不同的子序列
動態規劃:115.不同的子序列?(opens new window)給定一個字符串 s 和一個字符串 t ,計算在 s 的子序列中 t 出現的個數。
本題雖然也只有刪除操作,不用考慮替換增加之類的,但相對于動態規劃:392.判斷子序列?(opens new window)就有難度了,這道題目雙指針法可就做不了。
當s[i - 1] 與 t[j - 1]相等時,dp[i][j]可以有兩部分組成。
一部分是用s[i - 1]來匹配,那么個數為dp[i - 1][j - 1]。
一部分是不用s[i - 1]來匹配,個數為dp[i - 1][j]。
這里可能有同學不明白了,為什么還要考慮 不用s[i - 1]來匹配,都相同了指定要匹配啊。
例如: s:bagg 和 t:bag ,s[3] 和 t[2]是相同的,但是字符串s也可以不用s[3]來匹配,即用s[0]s[1]s[2]組成的bag。
當然也可以用s[3]來匹配,即:s[0]s[1]s[3]組成的bag。
所以當s[i - 1] 與 t[j - 1]相等時,dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
當s[i - 1] 與 t[j - 1]不相等時,dp[i][j]只有一部分組成,不用s[i - 1]來匹配,即:dp[i - 1][j]
所以遞推公式為:dp[i][j] = dp[i - 1][j];
狀態轉移方程:
if (s[i - 1] == t[j - 1]) {dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; } else {dp[i][j] = dp[i - 1][j]; }
#兩個字符串的刪除操作
動態規劃:583.兩個字符串的刪除操作?(opens new window)給定兩個單詞 word1 和 word2,找到使得 word1 和 word2 相同所需的最少步數,每步可以刪除任意一個字符串中的一個字符。
本題和動態規劃:115.不同的子序列?(opens new window)相比,其實就是兩個字符串可以都可以刪除了,情況雖說復雜一些,但整體思路是不變的。
- 當word1[i - 1] 與 word2[j - 1]相同的時候
- 當word1[i - 1] 與 word2[j - 1]不相同的時候
-
當word1[i - 1] 與 word2[j - 1]相同的時候,dp[i][j] = dp[i - 1][j - 1];
當word1[i - 1] 與 word2[j - 1]不相同的時候,有三種情況:
情況一:刪word1[i - 1],最少操作次數為dp[i - 1][j] + 1
情況二:刪word2[j - 1],最少操作次數為dp[i][j - 1] + 1
情況三:同時刪word1[i - 1]和word2[j - 1],操作的最少次數為dp[i - 1][j - 1] + 2
那最后當然是取最小值,所以當word1[i - 1] 與 word2[j - 1]不相同的時候,遞推公式:dp[i][j] = min({dp[i - 1][j - 1] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1});
狀態轉移方程:
if (word1[i - 1] == word2[j - 1]) {dp[i][j] = dp[i - 1][j - 1]; } else {dp[i][j] = min({dp[i - 1][j - 1] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1}); }
#編輯距離
動態規劃:72.編輯距離?(opens new window)給你兩個單詞 word1 和 word2,請你計算出將 word1 轉換成 word2 所使用的最少操作數 。
編輯距離終于來了,有了前面三道題目的鋪墊,應該有思路了,本題是兩個字符串可以增刪改,比?動態規劃:判斷子序列?(opens new window),動態規劃:不同的子序列?(opens new window),動態規劃:兩個字符串的刪除操作?(opens new window)都要復雜的多。
在確定遞推公式的時候,首先要考慮清楚編輯的幾種操作,整理如下:
- if (word1[i - 1] == word2[j - 1])
- 不操作
- if (word1[i - 1] != word2[j - 1])
- 增
- 刪
- 換
-
也就是如上四種情況。
if (word1[i - 1] == word2[j - 1]) 那么說明不用任何編輯,dp[i][j] 就應該是 dp[i - 1][j - 1],即dp[i][j] = dp[i - 1][j - 1];
此時可能有同學有點不明白,為啥要即dp[i][j] = dp[i - 1][j - 1]呢?
那么就在回顧上面講過的dp[i][j]的定義,word1[i - 1] 與 word2[j - 1]相等了,那么就不用編輯了,以下標i-2為結尾的字符串word1和以下標j-2為結尾的字符串word2的最近編輯距離dp[i - 1][j - 1] 就是 dp[i][j]了。
在下面的講解中,如果哪里看不懂,就回想一下dp[i][j]的定義,就明白了。
在整個動規的過程中,最為關鍵就是正確理解dp[i][j]的定義!
if (word1[i - 1] != word2[j - 1]),此時就需要編輯了,如何編輯呢?
操作一:word1增加一個元素,使其word1[i - 1]與word2[j - 1]相同,那么就是以下標i-2為結尾的word1 與 i-1為結尾的word2的最近編輯距離 加上一個增加元素的操作。
即 dp[i][j] = dp[i - 1][j] + 1;
操作二:word2添加一個元素,使其word1[i - 1]與word2[j - 1]相同,那么就是以下標i-1為結尾的word1 與 j-2為結尾的word2的最近編輯距離 加上一個增加元素的操作。
即 dp[i][j] = dp[i][j - 1] + 1;
這里有同學發現了,怎么都是添加元素,刪除元素去哪了。
word2添加一個元素,相當于word1刪除一個元素,例如 word1 = "ad" ,word2 = "a",word2添加一個元素d,也就是相當于word1刪除一個元素d,操作數是一樣!
操作三:替換元素,word1替換word1[i - 1],使其與word2[j - 1]相同,此時不用增加元素,那么以下標i-2為結尾的word1 與 j-2為結尾的word2的最近編輯距離 加上一個替換元素的操作。
即 dp[i][j] = dp[i - 1][j - 1] + 1;
綜上,當 if (word1[i - 1] != word2[j - 1]) 時取最小的,即:dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
遞歸公式代碼如下:
if (word1[i - 1] == word2[j - 1]) {dp[i][j] = dp[i - 1][j - 1]; } else {dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1; }
#總結
心思的錄友應該會發現我用了三道題做鋪墊,才最后引出了動態規劃:72.編輯距離?(opens new window),Carl的良苦用心呀,你們體會到了嘛!