最长公共子序列(LCS)最常见的算法是时间复杂度为O(n^2)的动态规划(DP)算法,但在James W. Hunt和Thomas G. Szymansky 的论文"A Fast Algorithm for Computing Longest Common Subsequence"中,给出了O(nlogn)下限的一种算法。
定理:设序列A长度为n,{A(i)},序列B长度为m,{B(i)},考虑A中所有元素在B中的序号,即A某元素在B的序号为{Pk1,Pk2,..},将这些序号按照降序排列,然后按照A中的顺序得到一个新序列,此新序列的最长严格递增子序列即对应为A、B的最长公共子序列。
举例来说,A={a,b,c,d,b},B={b,c,a,b},则a对应在B的序号为2,b对应序号为{4,0},c对应序号为1,d对应为空集,生成的新序列为{2,4, 0, 1, 4, 0},其最长严格递增子序列为{0,1,4},对应的公共子序列为{b, c, b}
原论文的证明过程较复杂,其实可以简单的通过一一对应来证明。即证明A、B的一个公共子序列和新序列的一个严格递增子序列一一对应。
(1) A、B的一个公共子序列对应新序列的一个严格递增子序列
假设A、B的某一个公共子序列长度为k,则其公共子序列在A和B中可以写为
{Ai1,Ai2, ..., Aik}
{Bj1,Bj2, ..., Bjk}
如此有Ai1 = Aj1,Ai2 = Aj2, ...., Aik = Ajk, 考虑元素Bj1在B中的序号P(Bj1),则有
P(Bj1)< P(Bj2) < ... < P(Bjk)
注意此严格递增子序列属于新序列的一个子序列,因此得证
(2) 新序列的一个严格递增子序列对应A、B的一个公共子序列
设新序列的一个严格递增子序列{P1,P2, ..., Pk},任意两个相信的P不可能属于A中同一个元素,因为A中某元素在B中的序号按照降序排列,但此序列为严格递增序列,矛盾。所以每个P均对应于A中不同位置的元素,设为{Ai1, Ai2, ..., Aik}。
因为P是严格递增序列,则每个P也对应B中唯一的一个元素,假设为{Bj1,Bj2, ..., Bjk},由P的定义可知Ai1= Aj1, Ai2 = Aj2, ...., Aik = Ajk,因此得证。
实现上比较复杂,有以下几个步骤:
(1) 对序列B排序
(2) 计算A中每个元素在B中的序号,并构成新序列
(3) 使用LIS的方法计算最长严格递增子序列
(4) 获取最长公共子序列
性能分析:
(1) 排序复杂度为nlogn
(2) 获取一个元素在B中的序号的复杂度,最小为logn,最大为n,获取所有元素的复杂度为 nlogn === n*n
(3) LIS 复杂度为nlogn
因此总体复杂度在nlogn 到 n*n logn之间,但如果(2) 步骤中A中元素在B中的序号对数很少时,性能相当优越,在实际测试时,string 中均为小写字母,长度为10000的情况下,这种方法比普通的LCS快一倍以上;如果string 中的字符扩展成char,即0-255,则这种方法比普通的LCS快至少一个数量级。以下为代码实现,可以参考:
/* ================================================================== */
/* NlogN for LCS, using LIS */
/* ================================================================== */
struct lcs_sort {
int val;
int pos;
};
struct lcs_lis_pos {
int pos_val;
int pos_1;
};
/* first, sort string2 */
static int qsort_func(const void *s1, const void *s2)
{
struct lcs_sort *ls1 = (struct lcs_sort *)s1, *ls2 = (struct lcs_sort *)s2;
if (ls1->val > ls2->val)
return 1;
else if (ls1->val < ls2->val)
return -1;
else {
if (ls1->pos > ls2->pos)
return -1;
else
return 1;
}
}
static void sort_lcs_string(char *string2, int n2, struct lcs_sort **ls_pp)
{
int i = 0;
struct lcs_sort *ls = NULL;
ls = malloc(sizeof(struct lcs_sort) * n2);
for (i = 0;i < n2;i++) {
ls[i].val = string2[i];
ls[i].pos = i;
}
qsort(ls, n2, sizeof(struct lcs_sort), qsort_func);
*ls_pp = ls;
}
/* second, find match list for char in string1 and construct a new sequence */
static int find_match_list(char ch, struct lcs_sort *ls, int l, int r, int *start)
{
int m = 0, len, len2;
int start1 = 0, start2 = 0;
if (l > r)
return 0;
else if (l == r) {
if (ch == ls[l].val) {
*start = r;
return 1;
}
return 0;
}
m = (l + r)>>1;
if (ls[m].val < ch)
len = find_match_list(ch, ls, m + 1, r, &start1);
else if (ls[m].val > ch)
len = find_match_list(ch, ls, l, m - 1, &start1);
else {
len = find_match_list(ch, ls, l, m - 1, &start1);
len2 = find_match_list(ch, ls, m + 1, r, &start2) + 1;
if (len == 0)
start1 = m;
len += len2;
}
*start = start1;
return len;
}
static int match_list_lcs(char *string1, int n1, struct lcs_sort *ls, int n2, struct lcs_lis_pos **lpos_pp)
{
int i = 0, j = 0, k = 0, len = 0, start = 0;
struct lcs_lis_pos *lpos = NULL;
lpos = malloc(sizeof(struct lcs_lis_pos) * n1 * n2);
for (i = 0;i < n1;i++) {
len = find_match_list(string1[i], ls, 0, n2 - 1, &start);
for (k = 0;k < len;k++) {
lpos[j].pos_val = ls[start + k].pos;
lpos[j++].pos_1 = i;
}
}
*lpos_pp = lpos;
return j;
}
/* third, find LIS for this new sequence */
static inline int find_pos(int *B, int len, int value)
{
int left = 0, right = len - 1, middle = 0;
while (left <= right) {
/* must finding strictly increasing sub sequence */
middle = (left + right)>>1;
if (B[middle] < value)
left = middle + 1;
else
right = middle - 1;
}
return left;
}
static int lis_nlogn(struct lcs_lis_pos *p, int len, int *inc_seq)
{
int i = 0, pos = 0, curr_len = 0;
int *L = NULL, *prev = NULL, *M = NULL;
if (len <= 0)
return 0;
L = malloc(len * sizeof(int));
M = malloc(len * sizeof(int));
prev = malloc(len * sizeof(int));
L[0] = p[0].pos_val;
M[0] = 0;
prev[0] = -1; /* the prev of the p[0] is NULL */
curr_len = 1;
/* Caculate prev and M */
for (i = 1;i < len;i++) {
pos = find_pos(L, curr_len, p[i].pos_val);
L[pos] = p[i].pos_val;
M[pos] = i;
if (pos > 0)
prev[i] = M[pos - 1];
else
prev[i] = -1;
if (pos + 1 > curr_len)
curr_len++;
}
/* Output increasing sequence */
pos = M[curr_len - 1];
for (i = curr_len - 1;i >= 0 && pos != -1;i--) {
inc_seq[i] = p[pos].pos_1;
pos = prev[pos];
}
return curr_len;
}
static int calc_lcs_using_lis(char *string1, int n1, char *string2, int n2)
{
int *inc_seq = NULL;
int list_len = 0, inc_len = 0, len = (n1 < n2) ? n1 : n2;
struct lcs_sort *ls = NULL;
struct lcs_lis_pos *lpos = NULL;
inc_seq = (int *)malloc(sizeof(int) * len);
/* first, sort string2 */
sort_lcs_string(string2, n2, &ls);
/* second, find match list for char in string1 and construct a new sequence */
list_len = match_list_lcs(string1, n1, ls, n2, &lpos);
/* third, find LIS for this new sequence */
inc_len = lis_nlogn(lpos, list_len, inc_seq);
/* end, get LCS */
#if 0
int i = 0;
printf("LCS is: ");
for (i = 0;i < inc_len;i++) {
inc_seq[i] = string1[inc_seq[i]];
printf("%c ", inc_seq[i]);
}
printf("\r\n");
#endif
free(ls);
free(lpos);
return inc_len;
}