最长公共子序列(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; }