学而不思则罔,思而不学则殆
题目
两个数组的,两个数组中都包含的最长序列
最长公共子序列,全称Longest Common Subsequence,简称LCS
基本概念
这里需要了解一下子序列和字=子串的概念。
类别 | 相同点 | 不同点 |
---|---|---|
子序列 | 元素保持跟父序列一致 | 元素可以不连续 |
子串 | 元素保持跟父序列一致 | 元素必须连续 |
可以说子串一定是子序列,但是子序列不一定是子串。
如图,更加形象一点:
算法思路
该问题我们首先想到用暴力算法解决。但是发现暴力算法不行,一个长度为n的数组的序列个数是2的n次方,两个数组求左右的子序列在一一对比是否相等,那么事件算法复杂度为(n,m分别为两个数组的长度):
O
(
2
n
∗
2
m
)
O(2^n*2^m)
O(2n∗2m)
这么看的话,时间复杂度太恐怖了,这条路走不通。
最长公共子序列算法在算法导论上有详细讲解,这里简单说一下核心思想。
假定两个序列为X={x1, x2, …, xn}和Y={y1, y2, …, ym),并设Z={z1, z2, …, zk}为X和Y的任意一个LCS。
C
[
i
,
j
]
=
{
0
if i=0 or j=0
C
[
i
−
1
,
j
−
1
]
+
1
if i,j>0 and x[i] = y[j]
m
a
x
(
C
[
i
−
1
,
j
]
,
C
[
i
,
j
−
1
]
)
if i,j>0 and x[i] != y[j]
C[i,j]= \begin{cases} 0 & \text {if i=0 or j=0}\\ C[i-1,j-1]+1 & \text {if i,j>0 and x[i] = y[j] }\\ max(C[i-1,j],C[i,j-1]) & \text {if i,j>0 and x[i] != y[j] }\\ \end{cases}
C[i,j]=⎩⎪⎨⎪⎧0C[i−1,j−1]+1max(C[i−1,j],C[i,j−1])if i=0 or j=0if i,j>0 and x[i] = y[j] if i,j>0 and x[i] != y[j]
1.如果X[n] == Y[m],则Z[k] == X[n] == Y[m] 且 且Z[k-1]是Xn-1和Ym-1的一个LCS。
2.如果X[n] != Y[m],则Z[k] != X[n] 蕴含Z是X[n-1]和Y得一个LCS。
3.如果X[n] != Y[m],则Z[k] != Y[m] 蕴含Z是Y[m-1]和X得一个LCS。
简单来说:
假如X的最后一个元素 与 Y的最后一个元素相等,那么X和Y的LCS就等于 {X减去最后一个元素} 与 {Y减去最后一个元素} 的 LCS 再加上 X和Y相等的最后一个元素。
假如X的最后一个元素 与Y的最后一个元素不等,那么X和Y的LCS就等于 : {X减去最后一个元素} 与 Y的LCS, {Y减去最后一个元素} 与 X 的LCS 中的最大的那个序列。
图解计算LCS
以X={2,4,5,6,7,8,8,9} Y={4,6,8,5,9,6,8,9,3}为例。
初始化
当i or j = 0 的时候C[i,j] = 0
然后一行一行的填写数据,根据递推公式:
第一行
第二行
第三行
第四行
第五行
第六行
第七行
第八行
第九行
构造LCS
我们根据递推公式建立上面的表格:
- C[9,8] = 5 且Y[9] != X[8],所以倒退回去,C[9,8]的值来源于C[8,8],(因为C[8,8]>C[9,7])
- C[8,8] = 5 且Y[8] == X[8],所以倒推回去,c[8][8]的值来源于 c[7][7]
- 然后以此类推,得出LCS{4,5,6,8,9}
- 在第一步的时候,如果遇到Y[i]!=X[j],当时C[i-1,j] = C[i,j-i]这种分支情况,如果不是求所有的LCS,可以只选择左走或者上走,如果要求所有的LCS就需要两个分支都考虑。
复杂度
时间复杂度
构建表时间复杂度为:
O
(
m
n
)
O(mn)
O(mn)
输出LCS:
O
(
m
+
n
)
O(m+n)
O(m+n)
整体复杂度为:
O
(
m
n
)
O(mn)
O(mn)
空间复杂度
辅助二维数组: O ( m n ) O(mn) O(mn)
Demo
public static void main(String[] args) {
//随机产生两个数组
//X={2,4,5,6,7,8,8,9} Y={4,6,8,5,9,6,8,9,3}
int1 = new int[]{2, 4, 5, 6, 7, 8, 8, 9};
int2 = new int[]{4, 6, 8, 5, 9, 6, 8, 9, 3};
lcs(int1, int2);
}
private static void lcs(int[] X, int[] Y) {
//辅助数据
int[][] flag = new int[Y.length + 1][X.length + 1];
//构建辅助表
for (int y = 0; y < Y.length; y++) {
for (int x = 0; x < X.length; x++) {
//x == y
if (Y[y] == X[x]) {
flag[y + 1][x + 1] = flag[y][x] + 1;
} else {
flag[y + 1][x + 1] = Math.max(flag[y][x + 1], flag[y + 1][x]);
}
}
}
//打印
for (int y = 0; y < flag.length; y++) {
System.out.println(Arrays.toString(flag[y]));
}
//计算LCS
int lcsNum = flag[Y.length][X.length];
int[] lcs = new int[lcsNum];
int lcsIndex = lcsNum;
for (int x = X.length, y = Y.length; x > 0 && y > 0 && lcsIndex > 0; ) {
//x y 是否相等
System.out.println(Y[y - 1] + " " + X[x - 1]);
//X[x] == Y[y]
if (X[x - 1] == Y[y - 1]) {
int index = --lcsIndex;
lcs[index] = X[x - 1];
System.out.println("index:" + index + " " + Arrays.toString(lcs));
x--;
y--;
} else {//X[x] != Y[y]
//得出最大值,这里只计算其中一情况,没有考虑相等的情况
if (flag[y - 1][x] > flag[y][x - 1]) {
y--;
} else {
x--;
}
}
}
System.out.println("lcs:" + Arrays.toString(lcs));
}
log信息:
[0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 1, 1, 1, 1, 1, 1, 1]
[0, 0, 1, 1, 2, 2, 2, 2, 2]
[0, 0, 1, 1, 2, 2, 3, 3, 3]
[0, 0, 1, 2, 2, 2, 3, 3, 3]
[0, 0, 1, 2, 2, 2, 3, 3, 4]
[0, 0, 1, 2, 3, 3, 3, 3, 4]
[0, 0, 1, 2, 3, 3, 4, 4, 4]
[0, 0, 1, 2, 3, 3, 4, 4, 5]
[0, 0, 1, 2, 3, 3, 4, 4, 5]
3 9
9 9
index:4 [0, 0, 0, 0, 9]
8 8
index:3 [0, 0, 0, 8, 9]
6 8
6 7
6 6
index:2 [0, 0, 6, 8, 9]
9 5
5 5
index:1 [0, 5, 6, 8, 9]
8 4
6 4
4 4
index:0 [4, 5, 6, 8, 9]
lcs:[4, 5, 6, 8, 9]
Process finished with exit code 0
最长子序列的应用
- 求最长(非)递增子序列
- 求最长(非)递减子序列
主要思路:根据原序列建立一个辅助序列,然后辅助序列跟原序列求最长公共子序列。