【题 目 】
给定两个字符串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);
}