雖是讀書筆記,可是如轉載請註明出處 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)
算法
一些定義:
字符串 X
, Y
長度 分別m
,n
segmentfault
子串:字符串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
定義:
一個數列 S,若是分別是兩個或多個已知數列的子序列,且是全部符合此條件序列中最長的,則 S 稱爲已知序列的最長公共子序列。
例如:輸入兩個字符串 BDCABA 和 ABCBDAB,字符串 BCBA 和 BDAB 都是是它們的最長公共子序列,則輸出它們的長度 4,並打印任意一個子序列. (Note: 不要求連續)ide
判斷字符串類似度的方法之一 - LCS 最長公共子序列越長,越類似。優化
對於通常性的 LCS 問題(即任意數量的序列)是屬於 NP-hard。但當序列的數量肯定時,問題可使用動態規劃(Dynamic Programming)在多項式時間解決。可達時間複雜度:O(m*n)
July 10分鐘講LCS視頻,spa
最優子結構性質:
設序列 X=<x1, x2, …, xm>
和 Y=<y1, y2, …, yn>
的一個最長公共子序列 Z=<z1, z2, …, zk>
,則:code
xm = yn
,則 zk = xm = yn
則 Zk-1
是 Xm-1
和 Yn-1
的最長公共子序列;xm ≠ yn
, 要麼Z
是 Xm-1
和 Y
的最長公共子序列,要麼 Z
是X
和 Yn-1
的最長公共子序列。xm ≠ yn
且 zk≠xm
,則 Z
是 Xm-1
和 Y
的最長公共子序列;xm ≠ yn 且 zk ≠yn
,則 Z
是X
和 Yn-1
的最長公共子序列。遞歸結構:
遞歸結構容易看到最長公共子序列問題具備子問題重疊性質。例如,在計算 X
和 Y
的最長公共子序列時,可能要計算出 X
和 Yn-1
及 Xm-1
和 Y
的最長公共子序列。而這兩個子問題都包含一個公共子問題,即計算 Xm-1
和 Yn-1
的最長公共子序列。
遞歸結構容易看到最長公共子序列問題具備子問題重疊性質。例如,在計算 X
和 Y
的最長公共子序列時,可能要計算出 X
和 Yn-1
及 Xm-1
和 Y
的最長公共子序列。而這兩個子問題都包含一個公共子問題,即計算Xm-1
和 Yn-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]
表示 以 Xi
和 Yj
結尾的最長公共子串的長度,由於要求子串連續,因此對於 Xi
與 Yj
來說,它們要麼與以前的公共子串構成新的公共子串;要麼就是不構成公共子串。故狀態轉移方程
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)。 總的來講使用後綴數組對數據作一些 「預處理」,在效率上仍是能提高很多的。
問題描述:找出一個n個數的序列的最長單調遞增子序列: 好比A = {5,6,7,1,2,8}
的LIS是5,6,7,8
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]; }
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++ = 2
, MaxV[nMaxLength]=5
, LIS[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 = 3
,MaxV[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裏對應的下標就是他子序列的長度
以我和藍盆友的討論作結:
- 一般DP是一個不算最好,可是比最直接的算法好不少的方法。 DP通常是O(n^2);可是若是想進一步優化 O(nlogn)就要考慮其餘的了
- 對,要想更好的方法就是要挖掘題目自己更加隱匿的性質了
想更一進步的支持我,請掃描下方的二維碼,你懂的~