動態規划經典——最長公共子序列問題 (LCS)和最長公共子串問題


一.最長公共子序列問題(LCS問題)

給定兩個字符串A和B,長度分別為m和n,要求找出它們最長的公共子序列,並返回其長度。例如:

  A = "HelloWorld"

    B = "loop"

則A與B的最長公共子序列為 "loo",返回的長度為3。此處只給出動態規划的解法:定義子問題dp[i][j]為字符串A的第一個字符到第 i 個字符串和字符串B的第一個字符到第 j 個字符的最長公共子序列,如A“app”,B“apple”dp[2][3]表示 “ap” 和 “app” 的最長公共字串。注意到代碼中 dp 的大小為 (n + 1) x (m + 1) ,這多出來的一行和一列是第 行和第 列,初始化為 0,表示空字符串和另一字符串的子串的最長公共子序列,例如dp[0][3]表示  "" 和 “app” 的最長公共子串。

當我們要求dp[i][j],我們要先判斷A的第i個元素B的第j個元素是否相同即判斷A[i - 1]B[j -1]是否相同,如果相同它就是dp[i-1][j-1]+ 1,相當於在兩個字符串都去掉一個字符時的最長公共子序列再加 1;否則最長公共子序列dp[i][j - 1] dp[i - 1][j]中大者。所以整個問題的初始狀態為:
 $$ dp[i][0] =0 , dp[0][j] = 0$$
相應的狀態轉移方程為:
$$  dp[i][j] = \begin{cases} \max\{dp[i - 1][j],dp[i][j - 1]\} ,& {A[i - 1]  != B[j - 1]} \\ dp[i - 1][j - 1] + 1 , & {A[i - 1]  == B[j - 1]} \end{cases}  $$
代碼的實現如下:
class LCS
{
public:
    int findLCS(string A, int n, string B, int m)
    {
        if(n == 0 || m == 0)//特殊輸入
            return 0;
        int dp[n + 1][m + 1];//定義狀態數組
        for(int i = 0 ; i <= n; i++)//初始狀態
            dp[i][0] = 0;
        for(int i = 0; i <= m; i++)
            dp[0][i] = 0;
        for(int i = 1; i <= n; i++)
            for(int j = 1; j<= m; j++)
            {
                if(A[i - 1] == B[j - 1])//判斷A的第i個字符和B的第j個字符是否相同
                    dp[i][j] = dp[i -1][j - 1] + 1;
                else
                    dp[i][j] = max(dp[i - 1][j],dp[i][j - 1]);
            }
            return dp[n][m];//最終的返回結果就是dp[n][m]
    }
};

該算法的時間復雜度為O(n*m),空間復雜度為O(n*m)。此外,由於遍歷時是從下標1開始的,因為下標為0表示空字符串;所以第A的第i個字符實際上為A[i -1],B的第j個字符為B[j-1]。

二.最長公共子串問題

給定兩個字符串A和B,長度分別為m和n,要求找出它們最長的公共子串,並返回其長度。例如:

  A = "HelloWorld"

    B = "loop"

則A與B的最長公共子串為 "lo",返回的長度為2。我們可以看到子序列和子串的區別:子序列和子串都是字符集合的子集,但是子序列不一定連續,但是子串一定是連續的。同樣地,這里只給出動態規划的解法:定義dp[i][j]表示以A中第i個字符結尾的子串和B中第j個字符結尾的子串的的最大公共子串(公共子串實際上指的是這兩個子串的所有部分)的長度(要注意這里和LCS的不同,LCS中的dp[i+1][j+1]一定是大於等於dp[i][j]的;但最長公共子串問題就不一定了,它的dp[i][j]表示的子串不一定是以A[0]開頭B[0]開頭的,但是一定是以A[i-1]、B[j-1]結尾的),同樣地, dp 的大小也為 (n + 1) x (m + 1) ,這多出來的一行和一列是第 行和第 列,初始化為 0,表示空字符串和另一字符串的子串的最長公共子串。

當我們要求dp[i][j],我們要先判斷A的第i個元素B的第j個元素是否相同即判斷A[i - 1]和 B[j -1]是否相同,如果相同它就是dp[i - 1][j- 1] + 1,相當於在兩個字符串都去掉一個字符時的最長公共子串再加 1;否則最長公共子串取0。所以整個問題的初始狀態為:

$$ dp[i][0] =0 , dp[0][j] = 0$$

相應的狀態轉移方程為:
$$  dp[i][j] = \begin{cases} 0 ,& {A[i - 1]  != B[j - 1]} \\ dp[i - 1][j - 1] + 1 , & {A[i - 1]  == B[j - 1]} \end{cases}  $$
代碼的實現如下:
class LongestSubstring {
public:
    int findLongest(string A, int n, string B, int m) {
         if(n == 0 || m == 0)
            return 0;
        int rs = 0;
        int dp[n + 1][m + 1];
        for(int i = 0 ; i <= n; i++)//初始狀態
            dp[i][0] = 0;
        for(int i = 0; i <= m; i++)
            dp[0][i] = 0;
        for(int i = 1; i <= n; i++)
            for(int j = 1; j<= m; j++)
            {
                if(A[i - 1] == B[j - 1])
                {
                    dp[i][j] = dp[i -1][j - 1] + 1;
                    rs = max(rs,dp[i][j]);//每次更新記錄最大值
                }

                else//不相等的情況
                    dp[i][j] = 0;
            }
            return rs;//返回的結果為rs
    }
};

該算法的時間復雜度為O(n*m),空間復雜度為O(n*m)。同樣地,遍歷下標也是從1開始的。不過關於最長公共子串問題,有幾點需要注意下:

1.由於dp[i][j]不像LCS是個遞增的數組,所以它在每次更新時需要同時更新最大值rs,且最后返回的結果是rs。而LCS中返回的直接就是dp[n][m]。

2.從代碼上來看,兩者的結構其實差不多,只不過狀態轉移方程有些小許的不同,分析過程也類似。

3.另外,關於這量兩種問題還有更優的解法,不過本文主要是DP的思想去解決,當然其中還有對DP的優化,不過此處不再詳述。

參考:https://www.nowcoder.com/questionTerminal/c996bbb77dd447d681ec6907ccfb488a

   https://blog.csdn.net/u012102306/article/details/53184446


免責聲明!

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



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