题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1403
解题思路:数据100000,普通LCS超时。
用后缀数组 N*logN 求得LCS
首先一定要了解一下基数排序。
参考的是罗穗骞大佬的论文,放个链接:https://wenku.baidu.com/view/5b886b1ea76e58fafab00374.html
对其中一些地方的理解:
为了形象,想象排好序之后所有后缀字符串装在桶里,比如sa[1]表示排好序后最小的,也就是第一个被倒出桶的
朋友你最好也画个桶,画一行字符串,不然容易晕。
一、求解sa数组需要了解的(论文中 void da函数部分)
①sa[i] 表示 桶中第i个倒出的后缀在实际字符串的位置
②rank[i] 表示 实际字符串中从位置i开始的后缀在桶中 排第几 / 分值(类似于整数排序当前位的数字大小)
③rank[sa[i]] = i ,
sa[rank[i]] = i;
④rank[sa[i]+j] 表示桶中第i个倒出的后缀字符串 在实际字符串的位置后移j个位置 代表的后缀字符串 在桶中的排第几/分值
⑤sa[rank[i]+j] 表示i位置开头的后缀字符串在桶中的位置(排第几)后移j个 代表的后缀字符串 在实际字符串中的位置
二、我认为难点应该在这部分代码:(微改)
for (j=1,p=1;p<n;j<<=1,m=p){
for (p=0,i=n-j;i<n;i++) y[p++] = i;
for (i=0;i<n;i++) if (sa[i]>=j) y[p++] = sa[i]-j;
for (i=0;i<m;i++) cnt[i] = 0;
for (i=0;i<n;i++) cnt[x[y[i]]]++;
for (i=1;i<m;i++) cnt[i] += cnt[i-1];
for (i=n-1;i>=0;i--) sa[--cnt[x[y[i]]]] = y[i];
swap(x,y); ///y储存上一轮rank值
x[sa[0]] = 0;
p = 1;
for (i=1;i<n;i++)
x[sa[i]] = ( y[sa[i]]==y[sa[i-1]] && y[sa[i]+j]==y[sa[i-1]+j] )? p-1:p++;
}
这个for循环分为三个部分:
①前两个for,根据上一轮的sa求出本轮排序第二关键字的sa(y数组)。
上一轮的桶中如果sa[i] 倒出 某一位置开头的后缀字符串,现在应该倒出sa[i]-j开头的字符串,因为这是sa[i]-j开头的字符串的第二关键字!
②中间四个for,根据第二关键字搞出的“新的一轮的桶”(y数组)来求解sa数组。
x[y[i]] 表示当前倒出的字符串的rank值。
③根据sa求解本轮rank数组(x数组),本来直接rank[sa[i]] = i。
不过,因为sa数组的值因为基数排序严格不同,但是实际上会有第一第二关键字完全相同的情况,so,需要再搞一搞。
三、height[]性质的粗略理解,以及h[i]>=h[i-1]-1的粗略证明
①为什么i开头的后缀字符串和j开头的后缀字符串的最长公共前缀(区别于LCS)等于 height[rank[i]+1] ~ height[rank[j]]
中的最小值?
求这个的过程就是桶中找到i后缀字符串的位置,j字符串的位置,然后中间夹着一堆,两两比较过去。
拿 aabaaaab来说,排好序之后:
aaaab
aaab
aab
aabaaaab
ab
abaaaab
b
baaaab
假设我们求的是aaab和abaaaab的后缀,aaab与aab比较后,当前height最小值为2,然后aab和aabaaaab比较,最小值=min(2,3)=2,还是2,直到传递到abaaaab,这个最小值其实就是,两两比较在传递下去时,aaab和当前在比较的字符串的
公共前缀的大小。
②h[i] >= h[i-1]-1的证明
首先这个结论是为了减少构建height数组是字符比较次数。
继续看上面那个字符串,举个例子
aabaaaab和aab比较,height为3
接下去求解aabaaaab把“头”砍掉,求
abaaaab,我们知道桶中abaaaab上一位(也就是比较对象),至少前两位会是ab,也就是前(3-1)=2 个字符不用比
具体是不是ab不确定,可能会有abXXXX什么的,但,最不济就是ab
四、LCS的求法
具体方法是两个字符串拼接成一个。中间断点记做len
遍历height数组,只要height[i]所比较的两个后缀实际位置一个大于len,一个小于len,LCS=max(height[i])
这个真不会证明。知道的朋友可以评论告诉我一下。
先放着不管吧,以后刷多了题再回来想想。
五、代码
#include<cstdio>
#include<algorithm>
#include<cstring>
#define debug(x) printf("----Line%s----\n",#x)
using namespace std;
const int N = 1e5+5;
int wa[N<<1],wb[N<<1],sa[N<<1],rnk[N<<1],cnt[N<<1],height[N<<1];
char s[N<<1];
void build(int n,int m)
{
int *x=wa,*y=wb,p,i,j;
for (i=0;i<m;i++) cnt[i] = 0;
for (i=0;i<n;i++) cnt[x[i]=s[i]]++;
for (i=1;i<m;i++) cnt[i] += cnt[i-1];
for (i=n-1;i>=0;i--) sa[--cnt[x[i]]] = i;
for (j=1,p=1;p<n;j<<=1,m=p){
for (p=0,i=n-j;i<n;i++) y[p++] = i;
for (i=0;i<n;i++) if (sa[i]>=j) y[p++] = sa[i]-j;
for (i=0;i<m;i++) cnt[i] = 0;
for (i=0;i<n;i++) cnt[x[y[i]]]++;
for (i=1;i<m;i++) cnt[i] += cnt[i-1];
for (i=n-1;i>=0;i--) sa[--cnt[x[y[i]]]] = y[i];
swap(x,y);
x[sa[0]] = 0;
p = 1;
for (i=1;i<n;i++)
x[sa[i]] = ( y[sa[i]]==y[sa[i-1]] && y[sa[i]+j]==y[sa[i-1]+j] )? p-1:p++;
}
}
void getheight(int n)
{
int k=0;
for (int i=0;i<n;i++) rnk[sa[i]] = i;
for (int i=0;i<n;i++){
if (rnk[i]==0){height[rnk[i]]=k=0;continue;}
if (k) k--;
int j = sa[rnk[i]-1];
while (i+k<n && j+k<n && s[i+k]==s[j+k]) k++;
height[rnk[i]] = k;
}
}
int main()
{
int n;
while (~scanf("%s",s)){
int len = strlen(s);
scanf("%s",s+len+1);
n = len + strlen(s+len+1) + 1;///+1很关键,虽然这题数据太水不+1也会AC
build(n,200);//debug(1);
getheight(n);//debug(2);
int ans = 0;
for (int i=1;i<n;i++)
if ( (sa[i]<len && sa[i-1]>len) || (sa[i]>len && sa[i-1]<len) )
ans = max(height[i],ans);
//debug(3);
printf("%d\n",ans);
}
return 0;
}
六、后记
后缀数组这个东西真的挺难的,与其每天花大把时间来想这个,不如每天抽个半小时想想,慢慢就想通了。
碰到难学的东西可能这么学效率会比较高。
然后我只能讲出我思考时遇到的困惑,如果有些地方没考虑,深感抱歉!