算法設計 - LCS 最長公共子序列&&最長公共子串 &&LIS 最長遞增子序列

雖是讀書筆記,可是如轉載請註明出處 http://segmentfault.com/blog/exploring/
.. 拒絕伸手複製黨java

本章講解:
1. LCS(最長公共子序列)O(n^2)的時間複雜度,O(n^2)的空間複雜度;
2. 與之相似但不一樣的最長公共子串方法。
最長公共子串用動態規劃可實現O(n^2)的時間複雜度,O(n^2)的空間複雜度;還能夠進一步優化,用後綴數組的方法優化成線性時間O(nlogn);空間也能夠用其餘方法優化成線性。
3.LIS(最長遞增序列)DP方法可實現O(n^2)的時間複雜度,進一步優化最佳可達到O(nlogn)算法


一些定義:
字符串 XY 長度 分別mnsegmentfault

子串:字符串S的子串r[i,...,j],i<=j,表示r串從i到j這一段,也就是順次排列r[i],r[i+1],...,r[j]造成的字符串數組

前綴:Xi =﹤x1,⋯,xi﹥X 序列的前 i 個字符 (1≤i≤m)
Yj=﹤y1,⋯,yj﹥Y 序列的前 j 個字符 (1≤j≤n)
假定 Z=﹤z1,⋯,zk﹥∈LCS(X , Y)緩存

有關後綴數組的定義dom

LCS

問題描述

定義:
一個數列 S,若是分別是兩個或多個已知數列的子序列,且是全部符合此條件序列中最長的,則 S 稱爲已知序列的最長公共子序列。
例如:輸入兩個字符串 BDCABA 和 ABCBDAB,字符串 BCBA 和 BDAB 都是是它們的最長公共子序列,則輸出它們的長度 4,並打印任意一個子序列. (Note: 不要求連續)ide

判斷字符串類似度的方法之一 - LCS 最長公共子序列越長,越類似。優化

複雜度

對於通常性的 LCS 問題(即任意數量的序列)是屬於 NP-hard。但當序列的數量肯定時,問題可使用動態規劃(Dynamic Programming)在多項式時間解決。可達時間複雜度:O(m*n)
July 10分鐘講LCS視頻spa

暴力方法

clipboard.png

動態規劃方法

最優子結構性質:
設序列 X=<x1, x2, …, xm>Y=<y1, y2, …, yn> 的一個最長公共子序列 Z=<z1, z2, …, zk>,則:code

  1. xm = yn,則 zk = xm = ynZk-1Xm-1Yn-1 的最長公共子序列;
    clipboard.png
  2. xm ≠ yn, 要麼ZXm-1Y 的最長公共子序列,要麼 ZXYn-1 的最長公共子序列。
    2.1 若 xm ≠ ynzk≠xm ,則 ZXm-1Y 的最長公共子序列;
    2.2 若 xm ≠ yn 且 zk ≠yn ,則 ZXYn-1 的最長公共子序列。
    綜合一下2 就是求兩者的大者

遞歸結構:
圖片描述
遞歸結構容易看到最長公共子序列問題具備子問題重疊性質。例如,在計算 XY 的最長公共子序列時,可能要計算出 XYn-1Xm-1Y 的最長公共子序列。而這兩個子問題都包含一個公共子問題,即計算 Xm-1Yn-1 的最長公共子序列。
圖片描述

遞歸結構容易看到最長公共子序列問題具備子問題重疊性質。例如,在計算 XY 的最長公共子序列時,可能要計算出 XYn-1Xm-1Y 的最長公共子序列。而這兩個子問題都包含一個公共子問題,即計算Xm-1Yn-1 的最長公共子序列。

計算最優值:
子問題空間中,總共只有O(m*n) 個不一樣的子問題,所以,用動態規劃算法自底向上地計算最優值能提升算法的效率。

長度表C 和 方向變量B:
圖片描述
java實現:

/* 動態規劃
 * 求最長公共子序列
 * @ author by gsm
 * @ 2015.4.1
 */
import java.util.Random;
public class LCS {

    public static int[][] lengthofLCS(char[] X, char[] Y){
        /* 構造二維數組c[][]記錄X[i]和Y[j]的LCS長度 (i,j)是前綴
         * c[i][j]=0; 當 i = j = 0;
         * c[i][j]=c[i-1][j-1]+1; 當 i = j > 0; Xi == Y[i]
         * c[i][j]=max(c[i-1][j],c[i][j+1]); 當 i = j > 0; Xi != Y[i]
         * 須要計算 m*n 個子問題的長度 即 任意c[i][j]的長度
         * -- 填表過程
         */
        int[][]c = new int[X.length+1][Y.length+1];

        // 動態規劃計算全部子問題
        for(int i=1;i<=X.length;i++){
            for (int j=1;j<=Y.length;j++){
                if(X[i-1]==Y[j-1]){
                    c[i][j] = c[i-1][j-1]+1;
                }
                else if(c[i-1][j] >= c[i][j-1]){
                    c[i][j] = c[i-1][j];
                }
                else{
                    c[i][j] = c[i][j-1];
                }
            }
        }

        // 打印C數組
        for(int i=0;i<=X.length;i++){
            for (int j=0;j<=Y.length;j++){
                System.out.print(c[i][j]+" ");
            }
            System.out.println();
        }
        return c;
    }
    // 輸出LCS序列
    public static void print(int[][] arr, char[] X, char[] Y, int i, int j) {
        if(i == 0 || j == 0)
            return;
        if(X[i-1] == Y[j-1]) {
            System.out.print("element " + X[i-1] + " ");
            // 尋找的
            print(arr, X, Y, i-1, j-1);
        }else if(arr[i-1][j] >= arr[i][j-1]) {
            print(arr, X, Y, i-1, j);
        }else{
            print(arr, X, Y, i, j-1);
        }
    }
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        char[] x ={'A','B','C','B','D','A','B'}; 
        char[] y ={'B','D','C','A','B','A'}; 
        int[][] c = lengthofLCS(x,y);
        print(c, x, y, x.length, y.length);
    }
}

最長公共子串

一個問題

定義 2 個字符串 query 和 text, 若是 query 裏最大連續字符子串在 text 中存在,則返回子串長度. 例如: query="acbac",text="acaccbabb", 則最大連續子串爲 "cba", 則返回長度 3.

方法

時間複雜度:O(m*n)的DP

這個 LCS 跟前面說的最長公共子序列的 LCS 不同,不過也算是 LCS 的一個變體,在 LCS 中,子序列是沒必要要求連續的,而子串則是 「連續」 的

咱們仍是像以前同樣 「從後向前」 考慮是否能分解這個問題,相似最長公共子序列的分析,這裏,咱們使用c[i,j] 表示 以 XiYj 結尾的最長公共子串的長度,由於要求子串連續,因此對於 XiYj 來說,它們要麼與以前的公共子串構成新的公共子串;要麼就是不構成公共子串。故狀態轉移方程

X[i-1] == Y[j-1],c[i,j] = c[i-1,j-1] + 1;

X[i-1] != Y[j-1],c[i,j] = 0;

對於初始化,i==0 或者 j==0,c[i,j] = 0
代碼:

public class LCString {
    public  static int lengthofLCString(String X, String Y){
        /* 構造二維數組c[][]記錄X[i]和Y[j]的LCS長度 (i,j)是前綴
         * c[i][j]=0; 當 i = j = 0;
         * c[i][j]=c[i-1][j-1]+1; 當 i = j > 0; Xi == Y[i]
         * c[i][j]=0; 當 i = j > 0; Xi != Y[i]
         * 須要計算 m*n 個子問題的長度 即 任意c[i][j]的長度
         * -- 填表過程
         */
        int[][]c = new int[X.length()+1][Y.length()+1];
        int maxlen = 0;
        int maxindex = 0;
        for(int i =1;i<=X.length();i++){
            for(int j=1;j<=Y.length();j++){
                if(X.charAt(i-1) == Y.charAt(j-1)){
                    c[i][j] = c[i-1][j-1]+1;
                    if(c[i][j] > maxlen)
                    {
                        maxlen = c[i][j];
                        maxindex = i + 1 - maxlen;
                    }
                }
            }
        }
        return maxlen;
    }

    public static void main(String[] args) {
        String X = "acbac";
        String Y = "acaccbabb";
        System.out.println(lengthofLCString(X,Y)); 
    }
}

時間複雜度O(nlogn)的後綴數組的方法

有關後綴數組以及求最長重複子串
前面提事後綴數組的基本定義,與子串有關,能夠嘗試這方面思路。因爲後綴數組最典型的是尋找一個字符串的重複子串,因此,對於兩個字符串,咱們能夠將其鏈接到一塊兒,若是某一個子串 s 是它們的公共子串,則 s 必定會在鏈接後字符串後綴數組中出現兩次,這樣就將最長公共子串轉成最長重複子串的問題了,這裏的後綴數組咱們使用基本的實現方式。

值得一提的是,在找到兩個重複子串時,不必定就是 X 與 Y 的公共子串,也多是 X 或 Y 的自身重複子串,故在鏈接時候咱們在 X 後面插入一個特殊字符‘#’,即鏈接後爲 X#Y。這樣一來,只有找到的兩個重複子串剛好有一個在 #的前面,這兩個重複子串纔是 X 與 Y 的公共子串

各方案複雜度對比

設字符串 X 的長度爲 m,Y 的長度爲 n,最長公共子串長度爲 l。

對於基本算法(brute force),X 的子串(m 個)和 Y 的子串(n 個)一一對比,最壞狀況下,複雜度爲 O(m*n*l),空間複雜度爲 O(1)。

對於 DP 算法,因爲自底向上構建最優子問題的解,時間複雜度爲 O(m*n);空間複雜度爲 O(m*n),固然這裏是可使用滾動數組來優化空間的,滾動數組在動態規劃基礎回顧中屢次提到。

對於後綴數組方法,鏈接到一塊兒並初始化後綴數組的時間複雜度爲 O(m+n),對後綴數組的字符串排序,因爲後綴數組有 m+n 個後綴子串,子串間比較,故複雜度爲 O((m+n)*l*lg(m+n)),求得最長子串遍歷後綴數組,複雜度爲 O(m+n),因此總的時間複雜度爲 O((m+n)*l*lg(m+n)),空間複雜度爲 O(m+n)。

總的來講使用後綴數組對數據作一些 「預處理」,在效率上仍是能提高很多的。

LIS 最長遞增子序列

問題描述:找出一個n個數的序列的最長單調遞增子序列: 好比A = {5,6,7,1,2,8} 的LIS是5,6,7,8

1. O(n^2)的複雜度:

1.1 最優子結構:
LIS[i] 是以arr[i]爲末尾的LIS序列的長度。則:
LIS[i] = {1+Max(LIS(j))}; j<i, arr[j]<arr[i];
LIS[i] = 1, j<i, 可是不存在arr[j]<arr[i];
因此問題轉化爲計算Max(LIS(j)) 0<i<n

1.2 重疊的子問題:
arr[i] (1<= i <= n)每一個元素結尾的LIS序列的值是 重疊的子問題。
因此填表時候就是創建一個數組DP[i], 記錄以arr[i]爲序列末尾的LIS長度。

1.3 DP[i]怎麼計算?
遍歷全部j<i的元素,檢查是否DP[j]+1>DP[i] && arr[j]<arry[i] 如果,則能夠更新DP[i]

int maxLength = 1, bestEnd = 0;
DP[0] = 1;
prev[0] = -1;

for (int i = 1; i < N; i++)
{
   DP[i] = 1;
   prev[i] = -1;

   for (int j = i - 1; j >= 0; j--)
      if (DP[j] + 1 > DP[i] && array[j] < array[i])
      {
         DP[i] = DP[j] + 1;
         prev[i] = j;
      }

   if (DP[i] > maxLength)
   {
      bestEnd = i;
      maxLength = DP[i];
   }

2. O(nlog)的複雜度

基本思想:

首先經過一個數組MaxV[nMaxLength]來緩存遞增子序列LIS的末尾元素最小值;經過nMaxLength 記錄到當前遍歷爲止的最長子序列的長度;

而後咱們從第2元素開始,遍歷給定的數組arr
1. arr[i] > MaxV[nMaxLength], 將arr[i]插入到MaxV[++nMaxLength]的末尾 -- 意味着咱們找到了一個新的最大LIS
2. arr[i] <= MaxV[nMaxLength], 找到MaxV[]中剛剛大於arr[i]的元素,arr[j].arr[i]替換arr[j]
由於MaxV是一個有序數組,查找過程可使用log(N)的折半查找。
這樣運行時間: n個整數和每一個都須要折半查找 -- n*logn = O(nlogn)

  • if > 說明j可以放在最長子序列的末尾造成一個新的最長子序列.
  • if< 說明j須要替換前面一個剛剛大array[j]的元素

最後,輸出LIS時候,咱們會用一個LIS[]數組,這邊LIS[i]記錄的是以元素arr[i]爲結尾的最長序列的長度


初始化準備工做:

MaxV[1]首先會被設置成序列第一個元素 即 MaxV[1] = arr[0],在遍歷數組的過程當中會不斷的更新。
nMaxLength = 1


舉個栗子:
arr = {2 1 5 3 6 4 8 9 7}

  • 首先i=1, 遍歷到1, 1 經過跟MaxV[nMaxLength]比較: 1<MaxV[nMaxLength],
    發現1更有潛力(更小的有潛力,更小的替換之)
    1 更有潛力, 那麼1替換MaxV[nMaxLength]MaxV[nMaxLength] =1 ;
    這個時候 MaxV={1}, nMaxlength = 1,LIS[1] = 1;

  • 而後 i =2, 遍歷到5, 5經過跟MaxV[nMaxLength]比較, 5>MaxV[nMaxLength],
    發現5 更大; 連接到目前獲得的LIS尾部;
    這個時候 MaxV={1,5}nMaxlength++ = 2MaxV[nMaxLength]=5LIS[i] = 1+1 = 2;

  • 而後 i =3,遍歷到3, 3 經過跟MaxV[nMaxLength]比較, 3<MaxV[nMaxLength],
    發現3更有 潛力,而後從 nMaxLength往前比較,找到第一個剛剛比3大元素替換之。(稍後解釋什麼叫剛剛大)
    這個時候 MaxV={1,3}, nMaxlength = 2; 3只是替換, LIS[i]不變 = LIS[3]= 2;

  • 而後 i =4,遍歷到6, 6 經過跟 MaxV[nMaxLength]比較, 6>MaxV[nMaxLength],
    發現6更大; 6就應該連接到目前獲得的LIS尾部;
    這個時候,MaxV={1,3,6} ,nMaxlength = 3MaxV[nMaxLength+1]=6 , LIS[4] = 3

  • 而後i =5,遍歷到4, 4 經過跟MaxV[nMaxLength] = 6比較, 4<MaxV[nMaxLength],
    發現4更有潛力,而後從nMaxLength往前比較,找到剛剛比4大元素 也就是 6替換之。
    這個時候 MaxV={1,3,4}, nMaxlength = 3,4只是替換, LIS[i]不變 = LIS[5]= 3;

  • 而後i=6, 遍歷到8, 8經過跟MaxV[nMaxLength]比較, 8>MaxV[nMaxLength],
    發現8更大; 8就應該連接到目前獲得的LIS尾部;
    這個時候 MaxV={1,3,4,8}, nMaxlength = 4, Maxv[nMaxlength]=8 LIS[6]=4,

  • 而後i=7, 遍歷到9, 9經過跟MaxV[nMaxLength]比較, 9>MaxV[nMaxLength],
    發現9更大; 9就應該連接到目前獲得的LIS尾部;
    這個時候 MaxV={1,3,4,8,9}, nMaxlength = 5, Maxv[nmaxlength]=9, LIS[7] = 5;

  • 而後i=8, 遍歷到7, 7 經過跟MaxV[nMaxLength] = 9比較, 7<MaxV[nMaxLength],
    發現7更有潛力,而後從nMaxLength往前比較,找到第一個比7大元素 也就是 8替換之。
    這個時候 MaxV={1,3,4,7,9}, nMaxLength = 5, Maxv[nMaxlength]=9
    LIS[8] = LIS[替換掉的index] = 4;

-- 2 1 5 3 6 4 8 9 7
i 1 2 3 4 5 6 7 8 9
LIS 1 1 2 2 3 3 4 5 4
MaxV 2 1 1,5 1,3 1,3,6 1,3,4 1,3,4,8 1,3,4,8,9 1,3,4,7

java實現:

import java.util.*;

public class LIS {
    public static int lengthofLCS(int[] arr){
        // 輔助變量
        int[] MaxV = new int [arr.length+1]; // 記錄遞增子序列 LIS 的末尾元素最小值 
        int nMaxLength = 1; // 當前LIS的長度
        int [] LIS = new int[arr.length+1]; //LIS[i]記錄的是以第i個元素爲結尾的最長序列的長度
        // 初始化
        MaxV[0] = -100;
        MaxV[nMaxLength] = arr[0];
        LIS[0] = 0;LIS[1] = 1;

        for(int i=1;i<arr.length;i++){
            if(arr[i] >MaxV[nMaxLength]){
                MaxV[++nMaxLength] = arr[i];
                LIS[i] = LIS[i-1]+1;
            }
            else{
                // 新元素 更小,更有「潛力」,替換大的元素
                int index = binarySearch(MaxV,arr[i],0,nMaxLength);     
                //*     
                LIS[i] =index;
                MaxV[index] = arr[i];
            }
        }
        Arrays.sort(LIS);
        return LIS[LIS.length-1];
    }
    // 在MaxV數組中查找一個元素剛剛大於arr[i]
    // 返回這個元素的index
    public static int binarySearch(int []arr, int n, int start, int end){
        while(start<end){
            int mid = (start + end)/2;
            if(arr[mid]< n){
                start = mid+1;
            }
            else if(arr[mid]> n) {
                end = mid -1;
            }
            else 
                return mid;
        }
        return end;
    }
    public static void main(String[] args) {
        int[] arr = {2,1,5,3,6,4,8,9,7};
        System.out.println(lengthofLCS(arr));
    }

}

* : MaxV裏面的數組下標表明瞭長度爲index的最長子序列末尾元素,反過來就是末尾元素在MaxV裏對應的下標就是他子序列的長度


能夠轉化爲LCS的問題

  • 給一個字符串,求這個字符串最少增長几個字符能變成迴文
  • 要在一條河的南北兩邊的各個城市之間造若干座橋.橋兩邊的城市分別是 a(1)...a(n) 和 b(1)...b(n). 且南邊 a(1)...a(n) 是亂序的,北邊同理,可是要求 a(i) 只能夠和 b(i) 之間造橋, 同時兩座橋之間不能交叉. 但願能夠獲得一個儘可能多座橋的方案.

以我和藍盆友的討論作結:

- 一般DP是一個不算最好,可是比最直接的算法好不少的方法。 DP通常是O(n^2);可是若是想進一步優化 O(nlogn)就要考慮其餘的了

- 對,要想更好的方法就是要挖掘題目自己更加隱匿的性質了


想更一進步的支持我,請掃描下方的二維碼,你懂的~
圖片描述

相關文章
相關標籤/搜索