【完虐算法】「字符串-最長公共子序列」全面總結


你好!我是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~~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM