LCP|LCP
定义 LCP,最长公共前缀(并不是LCS最长公共子序列),这货其实一般是对于一个字符串的后缀而言的。
基于后缀数组的LCP
思想 因为和后缀有关,所以在后缀数组的基础上实现。
实现 定义H[i]表示SA[i-1]和SA[i]的最长公共前缀,rank[i]表示i的名次,那么不难发现SA[i]和SA[j](i<j)的最长公共前缀就是H[i+1],H[i+2],H[i+3],……,H[j]中的最小值,也就是RMQ问题!RMQ问题我们有办法快速求得,现在唯一的问题就是这个H数组怎么构造。
O(n^2)的算法很容易想到,但是立刻让之前辛辛苦苦的倍增算法白费了。所以我们需要另想办法,先用aaaabbaaab举个例子:
1aaaabbaaab
7aaabH[2]=3 h[7]=3
2aaabbaaabH[3]=3 h[2]=3
8aabH[4]=2 h[8]=2
3aabbaaabH[5]=3 h[3]=3
9abH[6]=1 h[9]=1
4abbaaabH[7]=2 h[4]=2
10 bH[8]=0 h[10]=0
6baaabH[9]=1 h[6]=1
5bbaaabH[10]=1 h[5]=1
求7(SA[2])和9(SA[6])的公共前缀,那么就是min(3,2,3,1)=1。
设h[i]=H[rank[i]],我们发现,h[i]>=h[i-1]-1!这难道是巧合吗?不是的,我们可以证明得到:(k是SA[rank[i-1]-1],也就是排序过后i-1的前一个)
文章图片
①当h[i-1]>1时
因为h[i-1]>1,说明至少有2个匹配到了。我们现在把第一个删除,就可以得到后缀k+1和i,那么后缀k+1肯定在i前面。显然SA[rank[k+1]]~SA[rank[i]]之间所有的后缀的前h[i-1]-1位是和k+1和i一样的(想一想,为什么)。又因为SA[rank[k+1]]~SA[rank[i]]的最长公共前缀是最小的h,所以每一个h都>=h[i-1]-1,由此得到h[i]>=h[i-1]-1。
【LCP|LCP】下面给出示意图:
文章图片
②当h[i-1]<=1时
之前k+1一定在i前面,而当h[i-1]<=1时,k+1就不一定在i前面了(因为把LCP删光了)。这种情况怎么办?我们换一种思路证明:因为h[i-1]<=1,所以h[i-1]-1<=0。又因为h[i]>=0,所以h[i]>=h[i-1]-1。
既然知道了这个性质,那么我们就算h,然后把h送给对应的H,这样就可以把H数组构造完成了。至于RMQ问题,就不需要多说了。
模板
#include
#include
#include
#include
using namespace std;
const int maxl=200000,maxt=18;
int len,SA[maxl+5],rk[maxl+5],t[maxl+5],ha[maxl+5],H[maxl+5],RMQ[maxl+5][maxt+5];
char now[maxl+5];
void make_SA(char* s)
{
int MAX=0;
len=strlen(s+1);
memset(ha,0,sizeof(ha));
for (int i=1;
i<=len;
i++) {ha[rk[i]=s[i]]++;
if (rk[i]>MAX) MAX=rk[i];
}
for (int i=1;
i<=MAX;
i++) ha[i]+=ha[i-1];
for (int i=len;
i>=1;
i--) SA[ha[rk[i]]--]=i;
for (int k=1;
k<=len;
k<<=1)
{
int p=0;
for (int i=len-k+1;
i<=len;
i++) t[++p]=i;
for (int i=1;
i<=len;
i++) if (SA[i]>k) t[++p]=SA[i]-k;
memset(ha,0,sizeof(ha));
for (int i=1;
i<=len;
i++) ha[rk[t[i]]]++;
for (int i=1;
i<=MAX;
i++) ha[i]+=ha[i-1];
for (int i=len;
i>=1;
i--) SA[ha[rk[t[i]]]--]=t[i];
memcpy(t,rk,sizeof(t));
p=1;
rk[SA[1]]=1;
for (int i=2;
i<=len;
i++)
if (t[SA[i-1]]==t[SA[i]]&&t[SA[i-1]+k]==t[SA[i]+k]) rk[SA[i]]=p;
else rk[SA[i]]=++p;
if (p==len) break;
MAX=p;
}
}
void make_H() //处理H数组
{
int k=0;
for (int i=1;
i<=len;
i++)
{
if (k) k--;
int j=SA[rk[i]-1];
if (j==0) continue;
while (now[i+k]==now[j+k]) k++;
RMQ[rk[i]][0]=H[rk[i]]=k;
}
int ln=log2(len);
//RMQ
for (int j=1;
j<=ln;
j++)
for (int i=1;
i<=len-(1<y) swap(x,y);
x++;
int j=log2(y-x+1);
return min(RMQ[x][j],RMQ[y-(1<
基于哈希的LCP ps:学习刘汝佳大神(Orz,%%%)蓝书上LCP之后写的
思想 如何快速比较字符串?有一种方法是Hash,所以求LCP时的验证可以用Hash实现。
ps:当然,Hash有风险,可能会判断错误,不过错误概率比较小。如果一些LCP问题能用稳定方法(如后缀数组)解决,就尽量不要用基于哈希的LCP。
实现 比如这样的一个字符串(len为5),我们构造一个H(i)表示i~len哈希值:
abcde (ASCII码为97 98 99 100 101)
H(5)=101
H(4)=H(5)*Base+100=101*Base+100
H(3)=H(4)*Base+99=101*Base^2+100*Base+99
H(2)=H(3)*Base+98=101*Base^3+100*Base^2+99*Base+98
H(1)=H(2)*Base+97=101*Base^4+100*Base^2+99*Base^2+98*Base+97
(毕竟是哈希……Base乱取即可)
有一个问题就是素数怎么取比较好?刘汝佳大神表示可以放在unsigned long long里让H自然溢出,就不需要特别选个素数了。
定义Hash(i,len)为从i位开始,向右推len位(包括i)的哈希值,不难发现:
Hash(i,len)=H(i)?H(i+len)?xlen
预处理H(i)和x^len之后就可以很快求出Hash值。求i和j的LCP,只需要二分枚举长度len,判断Hash(i,len)是否和Hash(j,len)相同就行了。
推荐阅读
- JS中的各种宽高度定义及其应用
- SpringBoot调用公共模块的自定义注解失效的解决
- python自定义封装带颜色的logging模块
- 列出所有自定义的function和view
- Spring|Spring Boot 自动配置的原理、核心注解以及利用自动配置实现了自定义 Starter 组件
- 运营是什么()
- 自定义MyAdapter
- Android自定义view实现圆环进度条效果
- Flutter自定义view|Flutter自定义view —— 闯关进度条
- js保留自定义小数点