
【題 目 】
給定兩個字符串str1和 str2,返回兩個字符串的最長公共子串。
【舉 例 】
str1=”1AB2345CD”, str2=”12345EF”,返回”2345″。
【要 求 】
如 果 str1長 度 為 M , str2長 度 為 N , 實現時間復雜度為 O ( M x N ),額外空間復雜度為
0(1)的方法。
【難 度 】
★ ★ ★ ☆
解答
經典動態規划的方法可以做到時間復雜度為 O ( M*N ),額外空間復雜度O( M*N ),經過優化之后的實現可以把額外空間復雜度從O( M*N )降至 0 (1 ),我們先來介紹經典方法。
首先需要生成動態規划表。生成大小為 M*N 的矩陣dp ,行數為M, 列數為 N 。 dp[i][j]的含義是,在必須把str1[i]和 str2[j]當作公共子串最后一個字符的情況下,公共子串最長能有多長。比如,str1=”A1234B”, str2=”CD1234″, dp[3][4]的含義是在必須把 str1[3]當作公共子串最后一個字符的情況下,公共子串最長能有多長。這種情況下的最長公共子串為”1 2 3 “,所 以 dp[3][4]為 3。再如, str1=”A12E4B”, str2=”CD12F4″,dp[3][4]的含義是在必須把str1[3]( 即’E ‘) 和 str2[4]( 即’F’)當作公共子串最后一個字符的情況下,公共子串最長能有多長。這種情況下根本不能構成公共子串,所 以 dp[3][4]為 0。
介紹了 dp[i][j]的意義后,接下來介紹dp[i][j]怎么求。具體過程如下:
- 矩 陣 dp第一列即dp[0..M-1][0]。對某一個位置(i,0)來說,如果 str1[i]=str2[0] , 令dp[i][0]=1, 否則令 dp[i][0]=0。比如 str1=”ABAC”, ,str2[0]=”A”。dp 矩陣第一列上的值依次為 dp[0][0]=1, dp[1][0]=0, dp[2][0]=1, dp[3][0]=0
- 矩 陣 d p 第 一 行 即 dp[0][0..N-1]與 步 驟 1 同理。對某一個位置(0,j)來 說 ,如果str1[0]== str2[j] . 令 dp[0][j]=1, 否則令 dp[0][j]=0。
- 其他位置按照從左到右,再從上到下來計算,dp[i][j]的值只可能有兩種情況。
• 如 果 str1[i]!=str2[j],說明在必須把str1[i]和 str2[j] 當作公共子串最后一個字符是不 可能的, 令 dp[i][j]=0。
• 如 果 str1[i]==str2[j],說 明 str1[i]和 str2[j]可以作為公共子串的最后一個字符,從最 后 一 個 字 符 向 左 能 擴 多 大 的 長 度 呢 ? 就 是 dp [i-1][j-1]的 值 , 所 以 令dp[i][j]=dp[i-1][j-1]+1
如果 str1=”abcde”,str2=”bebcd”, 計算的 dp 矩陣如下:

public static int[][] getdp(char[] str1, char[] str2) {
int[][] dp = new int[str1.length][str2.length];
for (int i = 0; i < str1.length; i++) {
if (str1[i] == str2[0]) {
dp[i][0] = 1;
}
}
for (int j = 1; j < str2.length; j++) {
if (str1[0] == str2[j]) {
dp[0][j] = 1;
}
}
for (int i = 1; i < str1.length; i++) {
for (int j = 1; j < str2.length; j++) {
if (str1[i] == str2[j]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
}
}
}
return dp;
}
生成動態規划表dp之后,得到最長公共子串是非常容易的。比如,上邊生成的dp中,最 大 值 是 dp[3][4]==3,說明最長公共子串的長度為3。最長公共子串的最后一個字符是str1[3],當然也是str2[4],因為兩個字符一樣。那么最長公共子串為從strl[3]開始向左一共3 字節的子串,即 strl[1..3],當然也是str2[2..4]。總之,遍 歷 dp找到最大值及其位置,最長公共子串自然可以得到。具體過程請參看如下代碼中的Icstl方法,也是整個過程的主方法。
public static String lcst1(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 end = 0;
int max = 0;
for (int i = 0; i < chs1.length; i++) {
for (int j = 0; j < chs2.length; j++) {
if (dp[i][j] > max) {
end = i;
max = dp[i][j];
}
}
}
return str1.substring(end - max + 1, end + 1);
}
經典動態規划的方法需要大小為MxN的 dp矩陣,但實際上是可以減小至o(1)的,因為我們注意到計算每一個dp[i][j]的時候,最多只需要其左上方d p[i-l][j-l]的值,所以按照斜線方向來計算所有的值,只需要一個變量就可以計算出所有位置的值,如 圖 所示

每一條斜線在計算之前生成整型變量len, len表示左上方位置的值,初 始 時 len=0。從斜線最左上的位置幵始向右下方依次計算每個位置的值,假設計算到位置(i,j),此 時 len表示 位 置 的 值 。如果 strl [i]=str2[j],那么位置( i-1,j-1 )的值為 len+l,如果 strl[i]!=str2[j] ,那么位置(i,j)的值為0。計算后將len更新成位置(i,j)的值,然后計算下一個位置,即(i+1,j+1) 位置的值。依次計算下去就可以得到斜線上每個位置的值,然后算下一條斜線。用全局變量 max記錄所有位置的值中的最大值3 最大值出現時,用全局變量end記錄其位置即可。
具體過程請參看如下代碼中的lcst2方法
public static String lcst2(String str1, String str2) {
if (str1 == null || str2 == null || str1.equals("") || str2.equals("")) {
return "";
}
char[] chs1 = str1.toCharArray();
char[] chs2 = str2.toCharArray();
int row = 0; // 斜線開始位置的行
int col = chs2.length - 1; // 斜線開始位置的列
int max = 0; // 記錄最大長度
int end = 0; // 最大長度更新時,記錄子串的結尾位置
while (row < chs1.length) {
int i = row;
int j = col;
int len = 0;
// 從(i,j)開始向右下方遍歷
while (i < chs1.length && j < chs2.length) {
if (chs1[i] != chs2[j]) {
len = 0;
} else {
len++;
}
// 記錄最大值,以及結束字符的位置
if (len > max) {
end = i;
max = len;
}
i++;
j++;
}
if (col > 0) { // 斜線開始位置的列先向左移動
col--;
} else { // 列移動到最左之后,行向下移動
row++;
}
}
return str1.substring(end - max + 1, end + 1);
}