最長共通サブシーケンス(nlogn)
6656 ワード
最長共通サブシーケンス(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におけるシーケンス番号を{Pk 1,Pk 2,.}とし,これらのシーケンス番号を降順に並べ、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つ1つ対応していることを証明する.
(1)A,Bの共通のサブシーケンスは、新しいシーケンスの厳密な増分サブシーケンスに対応する
A、Bのある共通サブシーケンス長がkであると仮定すると、その共通サブシーケンスはAおよびBにおいて
{Ai1,Ai2, ..., Aik}
{Bj1,Bj2, ..., Bjk}
このようにAi 1=Aj 1,Ai 2=Aj 2,...,Aik=Ajkは、要素Bj 1のBにおけるシーケンス番号P(Bj 1)を考慮すると、
P(Bj1)< P(Bj2) < ... < P(Bjk)
この厳密な増分サブシーケンスは、新しいシーケンスのサブシーケンスに属することに注意してください.
(2)新しいシーケンスの厳密な増分サブシーケンスA,Bに対応する共通サブシーケンス
新しいシーケンスの1つの厳格な増分サブシーケンス{P 1,P 2,...,Pk}を設定し、いずれかの2つの信じられるPがAの中の同じ要素に属することはできない.Aの中のある要素のBの中のシーケンス番号は降順に配列されているが、このシーケンスは厳格な増分シーケンスであり、矛盾している.したがって,各PはA中の異なる位置の要素に対応し,{Ai 1,Ai 2,...,Aik}とする.
Pが厳密にインクリメントされたシーケンスであるため、各PもBの中で唯一の要素に対応し、{Bj 1,Bj 2,...,Bjk}と仮定し、Pの定義からAi 1=Aj 1,Ai 2=Aj 2,...,Aik=Ajkなので、証明します.
実装は複雑で、次の手順に従います.
(1)シーケンスBのソート
(2)Aの各要素のBにおけるシーケンス番号を計算し、新しいシーケンスを構成する
(3)LIS法を用いて最長厳格増分サブシーケンスを計算する
(4)最長共通サブシーケンスの取得
パフォーマンス分析:
(1)ソート複雑度nlogn
(2)1つの要素のBにおけるシーケンス番号の複雑度を取得し、最小はlogn、最大はn、すべての要素の複雑度を取得するのはnlogn==n*n
(3)LIS複雑度nlogn
従って、全体的な複雑度はnlognからn*n lognの間にあるが、(2)ステップでAの要素がBの中のシーケンス番号対数が少ない場合、性能はかなり優れており、実際のテストではstringでは小文字で、長さが10000の場合、この方法は通常のLCSより倍以上速い.stringの文字がchar、すなわち0〜255に拡張される場合、この方法は通常のLCSより少なくとも1桁速い.以下はコード実装です.参照してください.
定理:シーケンスAの長さをn,{A(i)}とし,シーケンスBの長さをm,{B(i)}とし,Aのすべての要素のBにおけるシーケンス番号,すなわちAのある要素のBにおけるシーケンス番号を{Pk 1,Pk 2,.}とし,これらのシーケンス番号を降順に並べ、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つ1つ対応していることを証明する.
(1)A,Bの共通のサブシーケンスは、新しいシーケンスの厳密な増分サブシーケンスに対応する
A、Bのある共通サブシーケンス長がkであると仮定すると、その共通サブシーケンスはAおよびBにおいて
{Ai1,Ai2, ..., Aik}
{Bj1,Bj2, ..., Bjk}
このようにAi 1=Aj 1,Ai 2=Aj 2,...,Aik=Ajkは、要素Bj 1のBにおけるシーケンス番号P(Bj 1)を考慮すると、
P(Bj1)< P(Bj2) < ... < P(Bjk)
この厳密な増分サブシーケンスは、新しいシーケンスのサブシーケンスに属することに注意してください.
(2)新しいシーケンスの厳密な増分サブシーケンスA,Bに対応する共通サブシーケンス
新しいシーケンスの1つの厳格な増分サブシーケンス{P 1,P 2,...,Pk}を設定し、いずれかの2つの信じられるPがAの中の同じ要素に属することはできない.Aの中のある要素のBの中のシーケンス番号は降順に配列されているが、このシーケンスは厳格な増分シーケンスであり、矛盾している.したがって,各PはA中の異なる位置の要素に対応し,{Ai 1,Ai 2,...,Aik}とする.
Pが厳密にインクリメントされたシーケンスであるため、各PもBの中で唯一の要素に対応し、{Bj 1,Bj 2,...,Bjk}と仮定し、Pの定義からAi 1=Aj 1,Ai 2=Aj 2,...,Aik=Ajkなので、証明します.
実装は複雑で、次の手順に従います.
(1)シーケンスBのソート
(2)Aの各要素のBにおけるシーケンス番号を計算し、新しいシーケンスを構成する
(3)LIS法を用いて最長厳格増分サブシーケンスを計算する
(4)最長共通サブシーケンスの取得
パフォーマンス分析:
(1)ソート複雑度nlogn
(2)1つの要素のBにおけるシーケンス番号の複雑度を取得し、最小はlogn、最大はn、すべての要素の複雑度を取得するのはnlogn==n*n
(3)LIS複雑度nlogn
従って、全体的な複雑度はnlognからn*n lognの間にあるが、(2)ステップでAの要素がBの中のシーケンス番号対数が少ない場合、性能はかなり優れており、実際のテストではstringでは小文字で、長さが10000の場合、この方法は通常のLCSより倍以上速い.stringの文字がchar、すなわち0〜255に拡張される場合、この方法は通常のLCSより少なくとも1桁速い.以下はコード実装です.参照してください.
/* ================================================================== */
/* 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
");
#endif
free(ls);
free(lpos);
return inc_len;
}