1. 程式人生 > 遊戲 >《靈魂駭客2》“日刊・林檎與惡魔的未來預測”開始!

《靈魂駭客2》“日刊・林檎與惡魔的未來預測”開始!

最長公共子序列問題

題目:最長公共子序列問題

《程式設計師程式碼面試指南》第65題 P220 難度:尉★★☆☆

本題是非常經典的動態規劃問題,先來介紹求解動態規劃表的過程。如果str1長度Mstr2長度N,生成大小為M×N的矩陣dpdp[i][j]的含義是str1[0..i]與str2[0..j]的最長公共子序列的長度從左到右再從上到下計算矩陣dp

  1. 矩陣dp第一列dp[0..M-1][0]dp[i][0]的含義是str1[0..i]與str2[0]的最長公共子序列長度。顯然dp[i][0]最大為1一旦dp[i][0]被設定為1,之後的dp[i+1..M-1][0]也都為1
  2. 矩陣dp第一行
    dp[0][0..N-1]與步驟1同理
  3. 對於其他位置(i,j)dp[i][j]的值可能來自這三種情況:①可能是dp[i-1][j] ②可能是dp[i][j-1] ③如果str1[i]==str2[j],還可能是dp[i-1][j-1]+1。這三個可能的值中,選最大的作為dp[i][j]的值

具體過程參看如下的getdp方法:

public int[][] getdp(char[] str1, char[] str2) {
    int[][] dp = new int[str1.length][str2.length];
    dp[0][0] = str1[0] == str2[0] ? 1 : 0;
    for (int i = 1; i < str1.length; i++) {
        dp[i][0] = Math.max(dp[i - 1][0], str1[i] == str2[0] ? 1 : 0);
    }
    for (int j = 1; j < str2.length; j++) {
        dp[0][j] = Math.max(dp[0][j - 1], str1[0] == str2[j] ? 1 : 0);
    }
    for (int i = 1; i < str1.length; i++) {
        for (int j = 1; j < str2.length; j++) {
            dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
            if (str1[i] == str2[j]) {
                dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - 1] + 1);
            }
        }
    }
    return dp;
}

dp矩陣中最右下角的值代表str1整體和str2整體的最長公共子序列的長度。通過整個dp矩陣的狀態,可以得到最長公共子序列。具體方法如下:

  1. 從矩陣的右下角開始,有三種移動方式:向上向左向左上。假設移動的過程中,i表示此時的行數,j表示此時的列數,同時用一個變數res來表示最長公共子序列。
  2. 如果dp[i][j]大於dp[i-1][j]和dp[i][j-1],說明之前在計算dp[i][j]的時候一定是選擇了決策dp[i-1][j-1]+1,可以確定str1[i]等於str2[j],並且這個字元一定屬於最長公共子序列,把這個字元放進res,然後向左上方移動
  3. 如果dp[i][j]等於dp[i-1][j]
    ,說明之前在計算dp[i][j]的時候,dp[i-1][j-1]+1這個決策不是必須選擇的決策,向上方移動即可。
  4. 如果dp[i][j]等於dp[i][j-1],與步驟3同理,向左方移動
  5. 如果dp[i][j]同時等於dp[i-1][j]和dp[i][j-1]向上還是向下無所謂,選擇其中一個即可,反正不會錯過必須選擇的字元。

也就是說,通過dp求解最長公共子序列的過程就是還原出當時如何求解dp的過程,來自哪個策略,就朝哪個方向移動。全部過程請參看如下程式碼中的Icse方法。

public String lcse(String str1, String str2) {
    if (str1 == null || str2 == null || str1.equals("") || str2.equals("")) {
        return "";
    }
    char[] chs1 = str1.toCharArray();
    char[] chs2 = str2.toCharArray();
    int[][] dp = getdp(chs1, chs2);
    int m = chs1.length - 1;
    int n = chs2.length - 1;
    char[] res = new char[dp[m][n]];
    int index = res.length - 1;
    while (index >= 0) {
        if (n > 0 && dp[m][n] == dp[m][n - 1]) {
            n--;
        } else if (m > 0 && dp[m][n] == dp[m - 1][n]) {
            m--;
        } else {
            res[index--] = chs1[m];
            m--;
            n--;
        }
    }
    return String.valueOf(res);
}

總的時間複雜度為O(M×N)。如果本題只想求最長公共子序列的長度,則可以使用空間壓縮的方法。