動態規劃求最長公共子串和子序列

想了解更多數據結構以及算法題,可以關注微信公衆號“數據結構和算法”,每天一題爲你精彩解答。也可以掃描下面的二維碼關注
在這裏插入圖片描述

1,最長公共子串

假如有兩個字符串,s1=“people"和s2=“eplm”,我們要求他倆最長的公共子串。我們一眼就能看出他們的最長公共子串是"pl”,長度是2。但如果字符串特別長的話就不容易那麼觀察了。

1,暴力求解:暴力求解對於字符串比較短的我們還可以接受,如果字符串太長實在是效率太低,所以這種我們就不再考慮

2,動態規劃:我們用一個二維數組dp[i][j]表示第一個字符串前i個字符和第二個字符串前j個字符組成的最長公共字符串的長度。那麼我們在計算dp[i][j]的時候,我們首先要判斷s1.charAt(i)是否等於s2.charAt(j),如果不相等,說明當前字符無法構成公共子串,所以dp[i][j]=0。如果相等,說明可以構成公共子串,我們還要加上他們前一個字符構成的最長公共子串長度,也就是dp[i-1][j-1]。所以我們很容易找到遞推公式

    if(s1.charAt(i) == s2.charAr(j))
        dp[i][j] = dp[i-1][j-1] + 1;
    else
        dp[i][j] = 0;
畫圖分析

在這裏插入圖片描述
我們看到在動態規劃中,最大值不一定是在最後一個空格內,所以我們要使用一個臨時變量在遍歷的時候記錄下最大值。代碼如下

 public static int maxLong(String str1, String str2) {
     if (str1 == null || str2 == null || str1.length() == 0 || str2.length() == 0)
         return 0;
     int max = 0;
     int[][] dp = new int[str1.length() + 1][str2.length() + 1];
     for (int i = 1; i <= str1.length(); i++) {
         for (int j = 1; j <= str2.length(); j++) {
             if (str1.charAt(i - 1) == str2.charAt(j - 1))
                 dp[i][j] = dp[i - 1][j - 1] + 1;
            else
                dp[i][j] = 0;
            max = Math.max(max, dp[i][j]);
        }
    }
    Util.printTwoIntArrays(dp);//這一行是打印測試數據的,也可以去掉
    return max;
}

2-3行是一些邊界的判斷。

重點是在8-11行,就是我們上面提到的遞推公式。

第12行是記錄最大值,因爲這裏最大值不一定出現在數組的最後一個位置,所以要用一個臨時變量記錄下來。

第15行主要用於數據的測試打印,也可以去掉。

我們還用上面的數據來測試一下,看一下結果

public static void main(String[] args) {
    System.out.println(maxLong("eplm", "people"));
}

運行結果

在這裏插入圖片描述

結果和我們上面圖中分析的完全一致。

我們發現上面的代碼有個規律,就是在遍歷的時候只使用了dp數組的上面一行,其他的都用不到,所以我們可以考慮把二維數組轉化爲一位數組,來看下代碼

 public static int maxLong(String str1, String str2) {
     if (str1 == null || str2 == null || str1.length() == 0 || str2.length() == 0)
         return 0;
     int max = 0;
     int[] dp = new int[str2.length() + 1];
     for (int i = 1; i <= str1.length(); i++) {
         for (int j = str2.length(); j >= 1; j--) {
             if (str1.charAt(i - 1) == str2.charAt(j - 1))
                 dp[j] = dp[j - 1] + 1;
            else
                dp[j] = 0;
            max = Math.max(max, dp[j]);
        }
        Util.printIntArrays(dp);//這一行和下面一行是打印測試數據的,也可以去掉
        System.out.println();
    }
    return max;
}

上面第7行的for循環我們使用的倒序的方式,這是因爲dp數組後面的值會依賴前面的值,而前面的值不依賴後面的值,所以後面的值先修改對前面的沒影響,但前面的值修改會對後面的值有影響,所以這裏要使用倒序的方式。

我們還用上面的兩個字符串來測試打印一下

在這裏插入圖片描述

我們看到結果和之前的完全一樣。

2,最長公共子序列

上面我們講了最長公共子串,子串是連續的。下面我們來講一下最長公共子序列,而子序列不是連續的。我們還來看上面的兩個字符串s1=“people”,s2=“eplm”,我們可以很明顯看到他們的最長公共子序列是"epl",我們先來畫個圖再來找一下他的遞推公式。

最長公共子序列畫圖分析

在這裏插入圖片描述

我們通過上面的圖分析發現,子序列不一定都是連續的,只要前面有相同的子序列,哪怕當前比較的字符不一樣,那麼當前字符串之前的子序列也不會爲0。換句話說,如果當前字符不一樣,我們只需要把第一個字符串往前退一個字符或者第二個字符串往前退一個字符然後記錄最大值即可。

舉個例子,比如圖中第4行第4列(就是圖中灰色部分),p和m不相等,如果字符串"eplm"退一步是"epl"再和"epop"對比我們發現有2個相同的子序列(也就是上面表格中數組(2,3)的位置)。如果字符串"peop"退一步是"peo"再和"eplm"對比我們發現只有1個相同的子序列(這裏的pe和ep只能有一個相同,要麼p相同,要麼e相同,因爲子序列的順序不能變)(也就是上面表格中數組(3,2)的位置)。所以我們很容易找出遞推公式

    if(s1.charAt(i) == s2.charAr(j))
        dp[i][j] = dp[i-1][j-1] + 1;
    else
        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);

有了上面的遞推公式,代碼就很容易寫出來了,我們來看下

 public static int maxLong(String str1, String str2) {
     if (str1 == null || str2 == null || str1.length() == 0 || str2.length() == 0)
         return 0;
     int[][] dp = new int[str1.length() + 1][str2.length() + 1];
     for (int i = 1; i <= str1.length(); i++) {
         for (int j = 1; j <= str2.length(); j++) {
             if (str1.charAt(i - 1) == str2.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]);
        }
    }
    Util.printTwoIntArrays(dp);//這一行是打印測試數據的,也可以去掉
    return dp[str1.length()][str2.length()];
}

我們發現他和最長公共子串的唯一區別就在第10行,我們還用圖中分析的兩個字符串測試一下,看一下結果
在這裏插入圖片描述
我們看到打印的結果和上面圖中分析的完全一致。上面在講到最長公共子串的時候我們可以把二維數組變爲一維數組來實現對代碼性能的優化,這裏我們也可以參照上面的代碼來優化一下,但這裏和上面稍微有點不同,如果當前字符相同的時候,他會依賴左上角的值,但這個值有可能會被上一步計算的時候就被替換掉了,所以我們必須要先保存下來,我們來看下代碼

 public static int maxLong(String str1, String str2) {
     if (str1 == null || str2 == null || str1.length() == 0 || str2.length() == 0)
         return 0;
     int[] dp = new int[str2.length() + 1];
     int last = 0;
     for (int i = 1; i <= str1.length(); i++) {
         for (int j = 1; j <= str2.length(); j++) {
             int temp = dp[j];//dp[j]這個值會被替換,所以替換之前要把他保存下來
             if (str1.charAt(i - 1) == str2.charAt(j - 1))
                dp[j] = last + 1;
            else
                dp[j] = Math.max(dp[j], dp[j - 1]);
            last = temp;
        }
        Util.printIntArrays(dp);//這一行和下面一行是打印測試數據的,也可以去掉
        System.out.println();
    }
    return dp[str2.length()];
}

代碼在第8行的時候先把要被替換的值保存下來,我們還是用上面的數據來測試一下,看一下打印結果
在這裏插入圖片描述
我們看到結果和我們之前分析的完全一致。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章