你好!我是Johngo!
LeetCode專題「字符串」現在准備到了 5 期內容來啦。
本來想要把「最長公共子序列」和「最長上升子序列」一起和大家把思路分享一下,都屬於可以使用動態規划的思想進行解決。但貌似還是兩塊內容。
所以,今天先把「最長公共子序列」分享出來和大家聊聊。
后面再出一期把「最長上升子序列」詳細的分享,后面這一期內容估計會比較多。
題外話,上一期的抽書活動還沒有結束,感興趣的可以繼續參與哈!【 https://mp.weixin.qq.com/s/V9srFVVrDxVRW8XYNK8pLg 】
說在前面
言歸正傳,這一期來說說字符串的第五塊內容 「字符串 - 最長公共子序列」
github:https://leetcode-cn.com/problems/longest-common-subsequence/
文檔地址:https://github.com/xiaozhutec/share_leetcode/tree/master/docs
整體架構:
字符串 - 最長公共子序列
今天這期內容是字符串的第 5 期。
之前談到過子串和子序列的區別,子串指的是向字符串截取固定長度的子字符串。
而子序列在LeetCode有過解釋:
一個字符串的子序列是指這樣一個新的字符串:它是由原字符串在不改變字符的相對順序的情況下刪除某些字符(也可以不刪除任何字符)后組成的新字符串。
既然是公共子序列,涉及到的肯定至少是兩個字符串的公共子序列比較
比如:
text1 = "abcde", text2 = "ace"
它的公共元素就是"ace"。
如何在兩個字符串中找到其公共的公共的部分,首先想到的肯定是暴力求解,逐項對比。最先想到,然而也是最先放棄的,因為時間復雜度最高。這塊也不做討論。
其次,最先想到的是動態規划來解決,記錄遍歷時的每一個狀態。
關於動態規划的部分,之前已經完整的寫過一篇,超過萬字,非常全面【 https://mp.weixin.qq.com/s/ZqOWomyra90BRzNukHr3-Q 】
案例
整體關於字符串「最長公共子序列」方面的問題一般來說都會用動態規划的思想去解決!
下面會通過一個典型案例具體來看是怎么解決的,使用 LeetCode 的 1143 題 進行舉例。
1143.最長公共子序列【中等】
1143.最長公共子序列【中等】
給定兩個字符串 text1 和 text2,返回這兩個字符串的最長 公共子序列的長度。如果不存在公共子序列 ,返回 0 。
一個字符串的子序列是指這樣一個新的字符串:它是由原字符串在不改變字符的相對順序的情況下刪除某些字符(也可以不刪除任何字符)后組成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。
兩個字符串的 公共子序列 是這兩個字符串所共同擁有的子序列。輸入:text1 = "abcde", text2 = "ace" 輸出:3 解釋:最長公共子序列是 "ace" ,它的長度為 3 。
這里用動態規划的思想進行解決。對於兩個字符串的比較,一定會涉及到利用二維數組來進行解決。
按照之前說的四步驟,動態數組定義、初始化、狀態轉移方程、優化、、。
以下利用字符串 “abcde" 和 ”ace” 為例,計算最長公共子序列。
令 s1="abcde", s2="ace"
一、動態數組定義
根據s1
的長度為5,s2
的長度為3。
初始化一個 3 行 5 列的二維數組dp
,賦初值全為 0。用來存儲動態規划過程中記錄的值。
$dp[i][j]$ 代表位置 $(i, j)$ 最長公共子序列的值!
二、初始化
針對動態規划一般的初始化方法,一定是邊界的初始化。
這里是二維數組,咱們這里要初始化的地方是第 0 行和第 0 列。
① 針對 0 行
s1=“abcde”, s2=“ace”,s2[0] 與 s1 的每一個字符進行比較,只有第 0 位置字符是相同的,第 0 位置為 1。由於公共子序列的規則,那第 0 行初始化全為 1。
① 針對 0 列
s1=“abcde”, s2=“ace”,s1[0] 與 s2 的每一個字符進行比較,同樣只有第 0 位置字符是相同的,第 0 位置為 1。依然是由於公共子序列的規則那第 0 列初始化全為 1。
根據初始化的情況,下面用代碼描述:
if text1[0] == text2[0]:
dp[0][0] = 1
for i in range(1, size1):
dp[0][i] = 1 if dp[0][i-1] == 1 else int(text1[i] == text2[0])
for j in range(1, size2):
dp[j][0] = 1 if dp[j-1][0] == 1 else int(text2[j] == text1[0])
三、狀態轉移方程
很明顯可以分為兩種情況:
- 當前位置的字符相同
- 當前位置的字符不相同
① 當 text1[i] == text2[j] 的時候,說明當前字符相同,只要將上一個字符對應的 dp 的值加 1 就可以了。即 dp[i][j] = dp[i-1][j-1] + 1。
注意,這里一定是 dp[i-1][j-1],因為此處的字符相同。那么,要想計算此處 dp[i][j] 的值,一定是與位置 i-1 與 j-1 的位置相關的。
② 當 text1[i] != text2[j] 的時候,此時當前字符不相同,那么此處 dp[i][j] 的數值一定沿用上一個 dp 的數值。所以,取得一定是其中的最大值 max(dp[i-1][j], dp[i][j-1])。
綜上所述,可以得到該問題的狀態轉移方程:
$$
dp[i][j]= \begin{cases}
dp[i-1][j-1] + 1, & text1[i] = text2[j] \[2ex]
max(dp[i-1][j], dp[i][j-1]), & text1[i] \neq text2[j]
\end{cases}
$$
從位置 (1, 1) 位置開始計算,判斷兩個字符串在當前字符串是否相等:
-
如果相等,則取dp[i-1][j-1]+1
-
如果不相等,則取max(dp[i][j-1], dp[i-1][j])
根據上述的狀態轉移方程,以及s1="abcde", s2="ace",下面把二維數組填滿,得到最后的答案!
① 位置(1,1):$s1[1] \neq s2[1] => dp[1][1]=max(dp[0][1], dp[1][0])=1$
② 位置(1,2):$s1[2] == s2[1] => dp[1][2]=dp[0][0]+1$
③ 位置(1,3):$s1[3] \neq s2[1] => dp[1][3]=max(dp[0][3], dp[1][2])=2$
④ 位置(1,4):$s1[4] \neq s2[1] => dp[1][4]=max(dp[0][4], dp[1][3])=2$
⑤ 位置(2,1):$s1[1] \neq s2[2] => dp[2][1]=max(dp[2][0], dp[1][1])=1$
⑥ 位置(2,2):$s1[2] != s2[2] => dp[2][2]=max(dp[2][1], dp[1][2])=2$
⑦ 位置(2,3):$s1[3] != s2[2] => dp[2][3]=max(dp[2][2], dp[1][3])=2$
⑧ 位置(2,4):$s1[4] == s2[2] => dp[2][4]=dp[1][3]+1=3$
這個就是二維數據中,每一格的填充方式。
根據上述的一個清晰的思路,狀態轉移填充二位數組的代碼描述:
for i in range(1, size2):
for j in range(1, size1):
if text2[i] == text1[j]:
# 注意這里是dp[i-1][j-1]+1
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i][j-1], dp[i-1][j])
return dp[-1][-1]
好了,這個是一個全面的答案:
def longestCommonSubsequence(self, text1, text2):
size1 = len(text1)
size2 = len(text2)
# 1 先定義 dp 數組
dp = [[0 for _ in range(size1)] for _ in range(size2)]
# 2 初始化 dp 數組的第 0 行和第 0 列
if text1[0] == text2[0]:
dp[0][0] = 1
for i in range(1, size1):
dp[0][i] = 1 if dp[0][i-1] == 1 else int(text1[i] == text2[0])
for j in range(1, size2):
dp[j][0] = 1 if dp[j-1][0] == 1 else int(text2[j] == text1[0])
# 3 動態方程進行求解
for i in range(1, size2):
for j in range(1, size1):
if text2[i] == text1[j]:
# 注意這里是dp[i-1][j-1]+1
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i][j-1], dp[i-1][j])
return dp[-1][-1]
最后,$dp[-1][-1]$ 代表的就是最后一個位置的值,也是截止到最后最長公共子序列的最大值!
以上就是該類型題目使用動態規划的常規思路了。
為什么說常規思路?因為一般情況下動態規划問題可以有空間方面的優化,而且該類型題目上述解決方案中是有優化空間的。
四、優化
在很多時候,咱們遇到使用動態規划利用二維數據解決問題的時候,通常可以進行空間方面的優化。
在之前的文章有很詳細的說明,有興趣大家可以看看【 https://mp.weixin.qq.com/s/ZqOWomyra90BRzNukHr3-Q 】
因為通常,在二維的情況下,當前$(i, j)$的取值,最多只與上一個位置或者左面位置的值有關系,而與跨行或者跨列是沒有關系的。
比如說在計算位置(2,3)的時候:$s1[3] != s2[2] => dp[2][3]=max(dp[2][2], dp[1][3])=2$
發現是與第 0 行以及之前和第 1 列以及之前的所有數據都是沒有關系的,所以可以從這方面進行空間的優化。
空間上除了定義變量外,一個2行2列的二維數組就可以解決!
有興趣的同學可以在評論區寫出優化后的代碼,或者如果有需要,在評論區留言,我在后面文章中進行這類題目關於優化方式的分享!
好了,今天就關於字符串「最長公共子序列」進行了分享。
另外,方便的話也在我的github
👇 加顆星,它是我持續輸出最大最大的動力,感謝大家!
github:https://github.com/xiaozhutec/share_leetcode
對了,「一周送書」上周的抽書活動還沒有結束,感興趣的可以繼續參與哈!
如果感覺內容對你有些許的幫助,求點贊,求在看!
下期想看哪方面的,評論區告訴我!
咱們下期見!bye~~