acm-后缀树组学习笔记

引言

本文主要介绍后缀数组的原理和一些典型例题

后缀数组的实现

基本原理

后缀数组主要是考虑用一个数组存放字符串s的所有后缀,然后再给这些后缀排个序。然后利用这个数组我们可以解决许多奇妙的问题。
首先是后缀的存放方式,显然不能真正地为每一个后缀开一个空间来存放,这空间不得爆炸…
因此我们考虑用后缀的第一个字母所在的下标来代表这个后缀本身。这里 s a [ i ] \mathbf{sa[i]} sa[i]来存放排好序的后缀,它代表的是排好序的后缀中的第 i \mathbf{i} i个后缀的首字母下标,相应地为了操作方便还要设置一个数组 r k [ i ] \mathbf{rk[i]} rk[i]代表首字母下标为 i \mathbf{i} i的后缀的排名,当然我们得让字符串s的下标从1开始才方便。

然后后缀数组最核心的操作也是最难理解的部分就是排序,如果直接用系统自带的 s o r t \mathbf{sort} sort排序那复杂度绝对爆炸,高达 O ( n 2 l o g ( n ) ) \mathbf{O(n^2log(n))} O(n2log(n)),考虑如何优化呢,这里介绍一种方法叫做倍增法。

利用倍增法,我们要从已知的信息来推出未知的信息,并且每次获取的总信息都是以往的两倍。在最初的时候,我们把每个后缀都看作一个字母,这个字母就是它的首字母,这样我们能得到初始的 r k \mathbf{rk} rk数组,方便起见我们直接令 r k [ i ] = s [ i ] \mathbf{rk[i]=s[i]} rk[i]=s[i],也就是说后缀 i \mathbf{i} i的排名由它的首字母决定,首字母大则排名大,首字母小则排名小,并且排名不是连续的,而且还存在重复的排名。此外需要明确的是,此时的 r k \mathbf{rk} rk数组仅仅代表后缀的首字母的排名,不代表后缀的最终排名,现在利用这个 r k \mathbf{rk} rk数组我们进行排序。

在每次排序中我们都会利用每个后缀的两个关键字来排序,第一个关键字是它的上一次的排名(即每个后缀前 w 个字母形成的字符串的排名,初始时 w = 1 \mathbf{w=1} w=1),第二个关键字是从它起始位置出发第 w+12w 位置的字母组成的字符串的排名。我们已经知道了其前 w 个字母形成的字符串的排名,对应就是 r k \mathbf{rk} rk数组中储存的信息,但怎么知道w+12w 位置的字母组成的字符串的排名呢?如果假设某个后缀的首字母位置为 i ,用 s [ i , j ] \mathbf{s[i,j]} s[i,j]代表 s \mathbf{s} s中第 i \mathbf{i} i位置到第 j \mathbf{j} j位置的字符串,则 s [ i + w , 2 w ] \mathbf{s[i+w,2w]} s[i+w,2w]对应的排名其实就是 r k [ i + w ] \mathbf{rk[i+w]} rk[i+w],因为 r k \mathbf{rk} rk数组储存了所有位置长度为 w \mathbf{w} w的字符串的排名关系,由于第二关键字的实质也是长度为 w \mathbf{w} w的字符串,故其信息其实也被储存在 r k \mathbf{rk} rk数组中。

明白了这一点后,我们对后缀排序的时候就可以按照第一关键字和第二关键字来比大小了,如果两个后缀第一关键字不同,那么第一关键字就可以直接决定其大小关系,如果相同的话就看第二关键字(如果还相同就任意排)。这样子每排一下序我们就可以把每个后缀的排序信息扩充一倍,只需要 l o g ( n ) \mathbf{log (n)} log(n)次排序我们就能得到所有后缀的排序关系了。

不过上面的分析还有个缺陷,那就是万一后缀的长度小等于 w \mathbf{w} w呢,那还如何比较呢?我们直接让第二关键字无穷小即可,具体代码实现的时候将 r k \mathbf{rk} rk数组开二倍大小,这样子 r k [ i + w ] \mathbf{rk[i+w]} rk[i+w]就等于0了。

于是我们的想法也就非常明确了,用首字母初始化 r k \mathbf{rk} rk数组后我们开始 l o g ( n ) \mathbf{log(n)} log(n)次排序,排序的对象是后缀,储存在 s a \mathbf{sa} sa中,每次排序都是双关键字排序,第一关键字是后缀 i \mathbf{i} i对应的上一次的排名rk[i],即从后缀 i 首字母开始的前 w 个字母的排名,第二关键字是后缀 i 的第 w+1到第 2w 位置的字符串的排名,也就是 r k [ i + w ] \mathbf{rk[i+w]} rk[i+w],由于 i + w \mathbf{i+w} i+w可能大于n,我们此时令其为无穷小即可。排好序以后,我们发现这是一个新的顺序关系,我们得根据这个新的顺序关系来确定每个后缀的新排名,即更新 r k \mathbf{rk} rk,更新的方式也很简单,按顺序遍历 s a \mathbf{sa} sa数组,由于 s a \mathbf{sa} sa数组储存的是排好序的后缀,故排名一定是单调不减的,如果两个后缀的一二关键字有一个不相等,那么谁在 s a \mathbf{sa} sa数组的前面谁 r k \mathbf{rk} rk就高,如果都相等就设置相同的 r k \mathbf{rk} rk,最终所有后缀的 r k \mathbf{rk} rk必定两两不同。
此外我们为了方便起见一般会在原字符串的末端加上一个不属于字符集的非常小的字符。
然后分析一下复杂度,倍增是 O ( l o g ( n ) ) \mathbf{O(log(n))} O(log(n))的,每次倍增都要排序,故总复杂度是 O ( n l o g ( n ) 2 ) \mathbf{O(nlog(n)^2)} O(nlog(n)2)的。
但在代码实现的时候还有一些小优化,具体见代码注释。
这里给出一份参考代码:

char s[maxn];
int sa[maxn],rk[maxn*2],tp[maxn*2];
bool cmp(int x,int y,int w){
    return tp[x]==tp[y]&&tp[x+w]==tp[y+w];
}
int get_sa(char *s,int len){
	s[++len]='$',s[len+1]='\0';//方便起见在最后加一个非常小的附加字符
	FOR(i,1,len+1)rk[i]=s[i],sa[i]=i;
	for(register int w=1;w<len;w<<=1){
		sort(sa+1,sa+1+len,[&](int x,int y){
			return rk[x]==rk[y]?rk[x+w]<rk[y+w]:rk[x]<rk[y];
		});
		memcpy(tp,rk,sizeof(int)*(len+1));
		int tot=0;
		FOR(i,1,len+1)rk[sa[i]]=cmp(sa[i],sa[i-1],w)?tot:++tot;//优化,避免访问不连续内存
		if(tot==len)break;//优化,当tot=len说明已经排好序了。
	}
	return len;
}
int main(){
	int len=rd(s+1);
	len=get_sa(s,len);
	wra(sa+2,len-1);
}

优化

利用基数排序在前面 O ( n l o g ( n ) 2 ) \mathbf{O(nlog(n)^2)} O(nlog(n)2)的基础上我们还可以优化掉一个 l o g ( n ) \mathbf{log(n)} log(n)
所谓基数排序就是一种类似桶排序的排序方式,我们先对所有的后缀分组,第一关键字相同的分到同一个组,也装在同一个桶里,每个桶也有个编号,第一关键字大的编号就大,第一关键字小的编号就小。
首先可以肯定的是如果两个后缀属于不同的桶,那么意味着它们第一关键字存在明确的大小关系,因此它们在排完序以后在数组中的相对关系也是明确的(即谁大谁小是明确的),因此我们可以按照桶的顺序将后缀一个一个放在后缀树组中。不过这里有个非常明显的问题,那就是该怎么比较同一个桶内的后缀谁大谁小呢?这就需要先预处理出每个后缀的第二关键字的排名了,同一个桶内的所有后缀的第一关键字是相同的,意味着我们只需要比较它们的第二关键字大小就能够确定该将哪个后缀先放到后缀数组中,然后再放其它的后缀。

通过以上分析,我们知道要事先预处理每个后缀第二关键字的排名,按照桶的编号从小到大(从大到小)遍历,每次从桶中取一个第二关键字最小(最大)的后缀放到后缀树组 s a \mathbf{sa} sa中,最后就能确定整个后缀树组。

不过这个流程并不是那么容易实现,尤其是想做到 O ( n ) \mathbf{O(n)} O(n),其中最难的地方在于如何 O ( 1 ) \mathbf{O(1)} O(1)确定一个桶中第二关键字最小的后缀是哪一个。于是这里用到了一个技巧性的写法,既然不能直接得到桶中的第二关键字最小的后缀,我们不如直接更改枚举的对象,也就是说我们不枚举桶的编号了,我们直接枚举第二关键字的排名,我们设置一个 t p \mathbf{tp} tp数组按第二关键字从小到大存放所有后缀。这样我们遍历 t p \mathbf{tp} tp数组的时候也就能够保证后缀的第二关键字是单调递增或单调递减的了。然后我们获取某个后缀的桶编号,只需要知道这个后缀的桶前面的所有桶中的后缀的总数目以及当前桶中未被放进 s a \mathbf{sa} sa数组中的后缀的数目即可。知道这些我们就足以确定这个后缀对应在 s a \mathbf{sa} sa数组中的位置了。

这里的逻辑可能会有点难理解,需要仔细领会,具体实现来说,一开始我们会初始化 c n t [ i ] \mathbf{cnt[i]} cnt[i]在第 i \mathbf{i} i个桶以及它之前的所有桶中的后缀的总数目,这个等价于是第i个桶之前的所有桶中的后缀数目再加上第i个桶内未被放入 s a \mathbf{sa} sa数组的后缀数目,然后我们从后往前(其实从前往后也行,但传统的写法是这样)枚举 t p \mathbf{tp} tp,这样就能保证第二关键字是递减的,由于在同一个桶中第二关键字大的对应放到 s a \mathbf{sa} sa数组中的时候编号也更大,故后缀 t p [ i ] \mathbf{tp[i]} tp[i]对应的在 s a \mathbf{sa} sa数组中的编号就是 t p [ i ] \mathbf{tp[i]} tp[i]所在的桶中的未被放入 s a \mathbf{sa} sa数组中的数目加上在 t p [ i ] \mathbf{tp[i]} tp[i]桶之前的所有桶中的后缀之和,由于 t p [ i ] \mathbf{tp[i]} tp[i]的桶编号是 r k [ t p [ i ] ] \mathbf{rk[tp[i]]} rk[tp[i]],故其在 s a \mathbf{sa} sa数组中的编号就是 c n t [ r k [ t p [ i ] ] ] \mathbf{cnt[rk[tp[i]]]} cnt[rk[tp[i]]],相应地我们可以写出放入 s a \mathbf{sa} sa数组的语句: s a [ c n t [ r k [ t p [ i ] ] ] ] = t p [ i ] \mathbf{sa[cnt[rk[tp[i]]]]=tp[i]} sa[cnt[rk[tp[i]]]]=tp[i]。然后当 t p [ i ] \mathbf{tp[i]} tp[i]被放到 s a \mathbf{sa} sa数组中之后自然地它的桶中的未被放入 s a \mathbf{sa} sa数组中的后缀数目要减一,也就是 c n t [ r k [ t p [ i ] ] ] − − \mathbf{cnt[rk[tp[i]]]--} cnt[rk[tp[i]]],不过一般为了方便我们会将“把 t p \mathbf{tp} tp放入 s a \mathbf{sa} sa数组”和“桶中的未被放入 s a \mathbf{sa} sa数组中的后缀数目减一”这两个操作合在一起,得到一个非常优美的语句: s a [ c n t [ r k [ t p [ i ] ] ] − − ] = t p [ i ] \mathbf{sa[cnt[rk[tp[i]]]--]=tp[i]} sa[cnt[rk[tp[i]]]]=tp[i]。这也是基数排序的核心语句,它的作用就是把当前枚举的后缀 t p [ i ] \mathbf{tp[i]} tp[i]放到后缀数组 s a \mathbf{sa} sa中并且让 t p [ i ] \mathbf{tp[i]} tp[i]所在桶的未放入 s a \mathbf{sa} sa后缀数目减一。

最后我们该如何预处理 t p \mathbf{tp} tp数组呢?根据前面的结论我们知道 s a \mathbf{sa} sa数组本质上储存的是所有长度为 w \mathbf{w} w的字符串的排序关系,而 t p \mathbf{tp} tp数组也是要处理出所有长度为 w \mathbf{w} w的字符串的排序关系,只不过 t p \mathbf{tp} tp相对 s a \mathbf{sa} sa存在 w \mathbf{w} w的偏移,也就是说 s a [ i ] \mathbf{sa[i]} sa[i]后缀对应的位置向前偏移 w \mathbf{w} w后对应的后缀的排名(按第二关键字)就是 i \mathbf{i} i,即 t p [ i ] = s a [ i ] − w \mathbf{tp[i]=sa[i]-w} tp[i]=sa[i]w,不过有些后缀显然会出现没有第二关键字的情况,我们就给它们特殊处理一下,提前把它们放到 t p \mathbf{tp} tp数组中,保证它们的第二关键字是最小的。

最后再总结一下大概流程吧(可能你到现在还是不理解,那就结合代码多看几遍吧),首先预处理出 t p \mathbf{tp} tp数组, t p [ i ] \mathbf{tp[i]} tp[i]代表第二关键字排名为 i \mathbf{i} i的后缀首字母在字符串中的位置,然后用一个桶 c n t \mathbf{cnt} cnt去装 r k \mathbf{rk} rk相同的后缀,为了方便起见,我们让桶 c n t [ i ] \mathbf{cnt[i]} cnt[i]装第i个桶之前的所有桶中的后缀数目以及第i个桶内的当前未被放入 s a \mathbf{sa} sa数组的后缀数目,处理方式也很简单,一开始先把每个后缀放到编号为 r k \mathbf{rk} rk的桶中,然后对桶求个前缀和即可。最后是核心语句用于排序: s a [ c n t [ r k [ t p [ i ] ] ] − − ] = t p [ i ] \mathbf{sa[cnt[rk[tp[i]]]--]=tp[i]} sa[cnt[rk[tp[i]]]]=tp[i],然后注意要从后往前枚举 t p [ i ] \mathbf{tp[i]} tp[i]

加入基数排序后算法的复杂度就变成了 O ( n l o g ( n ) ) \mathbf{O(nlog(n))} O(nlog(n)),另外还有个算法叫做DC3算法,可以把复杂度进一步降到 O ( n ) \mathbf{O(n)} O(n),但是由于该算法常数较大,相对倍增法优势不是很大(一般情况下是这样,也有专门卡倍增法的题),故不作介绍(其实是作者太懒了 ),感兴趣的可以去网上查找相关资料。

这里以LuoGuP3809 【模板】后缀排序为例给出一份参考代码:

char s[maxn];
int sa[maxn],rk[maxn<<1],tp[maxn<<1],cnt[maxn];//记得tp和rk开两倍
void radix_sort(int n,int m){//n代表字符串长,m代表字符集大小,即rk取值范围
    memset(cnt,0,sizeof(int)*(m+1));
    FOR(i,1,n+1)cnt[rk[i]]++;//把rk相同的放到同一个桶
    FOR(i,1,m+1)cnt[i]+=cnt[i-1];//求个前缀和
    ROF(i,n,1)sa[cnt[rk[tp[i]]]--]=tp[i];//上文中有介绍
}
bool cmp(int x,int y,int w){
    return tp[x]==tp[y]&&tp[x+w]==tp[y+w];
}
int get_sa(char *s,int len){
	s[++len]='$',s[len+1]='\0';
	int tot = 0;//字符集大小
	FOR(i,1,len+1){//初始化
	    rk[i]=s[i]-'$'+1;tp[i]=i;tot=max(tot,rk[i]);
	}
	radix_sort(len,tot);
	for(register int w=1;w<=len;w<<=1){
        register int tt=0;
		ROF(i,len,len-w+1)tp[++tt]=i;//将第二关键字为0的放进数组里(可以是任意顺序)
		FOR(i,1,len+1)if(sa[i]>w)tp[++tt]=sa[i]-w;//再把第二关键字不为0的有小到大一个一个放进tp中
        radix_sort(len,tot);//基数排序
		memcpy(tp,rk,sizeof(int)*(len+1));
		tot=0;
		FOR(i,1,len+1)rk[sa[i]]=cmp(sa[i],sa[i-1],w)?tot:++tot;//优化,避免访问不连续内存
		if(tot==len)break;//优化,当tot=len说明已经排好序了。
	}
	return len;
}
int main(){
	int len=rd(s+1);
	len=get_sa(s,len);
	wra(sa+2,len-1);
}

当然这份代码还可以优化,比如下面这份代码运气好跑了洛谷的第一(4
32ms)。

char s[maxn];
int sa[maxn],rk[maxn<<1],tp[maxn<<1],cnt[maxn];//记得tp和rk开两倍
bool cmp(int x,int y,int w){
    return tp[x]==tp[y]&&tp[x+w]==tp[y+w];
}
int get_sa(char *s,int n){
	s[++n]='$',s[n+1]='\0';
	int tot = 0;//字符集大小
	FOR(i,1,n+1){//初始化
	    rk[i]=s[i]-'$'+1;tp[i]=i;tot=max(tot,rk[i]);
	}
	memset(cnt,0,sizeof(int)*(tot+1));
    FOR(i,1,n+1)cnt[rk[i]]++;//把rk相同的放到同一个桶
    FOR(i,1,tot+1)cnt[i]+=cnt[i-1];//求个前缀和
    ROF(i,n,1)sa[cnt[rk[tp[i]]]--]=tp[i];
	for(register int w=1;w<n;w<<=1){
        register int tt=0;
		ROF(i,n,n-w+1)tp[++tt]=i;
		FOR(i,1,n+1)if(sa[i]>w)tp[++tt]=sa[i]-w;
        memset(cnt,0,sizeof(int)*(tot+1));
    	FOR(i,1,n+1)cnt[rk[i]]++;//把rk相同的放到同一个桶
    	FOR(i,1,tot+1)cnt[i]+=cnt[i-1];//求个前缀和
    	ROF(i,n,1)sa[cnt[rk[tp[i]]]--]=tp[i];
		memcpy(tp,rk,sizeof(int)*(n+1));tot=0;
		FOR(i,1,n+1)rk[sa[i]]=cmp(sa[i],sa[i-1],w)?tot:++tot;//优化,避免访问连续内存
		if(tot==n)break;//优化,当tot=len说明已经排好序了。
	}
	return n;
}

int main(){
	int len=rd(s+1);
	len=get_sa(s,len);
	wra(sa+2,len-1);
}

性质

要让后缀数组发挥真正的作用还需要引入一些定义。这里定义 l c p [ i ] \mathbf{lcp[i]} lcp[i]代表后缀数组 s a \mathbf{sa} sa中第 i \mathbf{i} i个后缀与第 i − 1 \mathbf{i-1} i1个后缀的最长公共前缀的长度,相应地定义 l c p ( i , j ) \mathbf{lcp(i,j)} lcp(i,j)作为第 i \mathbf{i} i个后缀与第 j \mathbf{j} j个后缀的最长公共前缀的长度。下面给出几条关于lcp的性质。
注意下面的证明中出现的后缀i统一理解为首字母位于s串中第i个位置的后缀,排名为i的后缀理解为在 s a \mathbf{sa} sa数组中的第 i \mathbf{i} i个后缀。

  1. 对于一个排名为 i \mathbf{i} i的后缀而言,假设另一个后缀排名为 j \mathbf{j} j,当 i ≥ j \mathbf{i\ge j} ij时,那么 i − j \mathbf{i-j} ij越大则 l c p ( i , j ) \mathbf{lcp(i,j)} lcp(i,j)也会越大(这里的越大指的是单调不减),同理当 i ≤ j \mathbf{i\le j} ij时, j − i \mathbf{j-i} ji越大则 l c p ( i , j ) \mathbf{lcp(i,j)} lcp(i,j)也会越大(这里的越大指的是单调不减)。
    证明:这很容易理解,排名差距越大说明相似度越小,如果仔细思考一下字符串的比较标准即可明白。

  2. l c p ( i , j ) = m i n { l c p ( i , k ) , l c p ( k , j ) } , i ≤ k ≤ j \mathbf{lcp(i,j)=min\{lcp(i,k),lcp(k,j)\},i\le k\le j} lcp(i,j)=min{lcp(i,k),lcp(k,j)},ikj
    证明:假设 m i n { l c p ( i , k ) , l c p ( k , j ) } = d \mathbf{min\{lcp(i,k),lcp(k,j)\}=d} min{lcp(i,k),lcp(k,j)}=d,且排名为 i , j , k \mathbf{i,j,k} i,j,k的后缀分别为 u , v , w \mathbf{u,v,w} u,v,w,则有 l c p ( i , k ) ≥ d , l c p ( k , j ) ≥ d \mathbf{lcp(i,k)\ge d,lcp(k,j)\ge d} lcp(i,k)d,lcp(k,j)d,也就是说 u \mathbf{u} u v \mathbf{v} v至少前d个字符相同, v \mathbf{v} v w \mathbf{w} w至少前d个字符相同,于是也就有 u \mathbf{u} u w \mathbf{w} w至少前 d \mathbf{d} d个字符相同,于是有 l c p ( i , j ) ≥ d \mathbf{lcp(i,j)\ge d} lcp(i,j)d。然后由于 m i n { l c p ( i , k ) , l c p ( k , j ) } = d \mathbf{min\{lcp(i,k),lcp(k,j)\}=d} min{lcp(i,k),lcp(k,j)}=d,故要么有 l c p ( i , k ) = d \mathbf{lcp(i,k)=d} lcp(i,k)=d,要么有 l c p ( k , j ) = d \mathbf{lcp(k,j)=d} lcp(k,j)=d,不妨设 l c p ( i , k ) = d \mathbf{lcp(i,k)=d} lcp(i,k)=d,根据性质1,排名差距越大,lcp也就越小(单调不增),故我们有 j − i ≥ k − i ⇒ l c p ( i , j ) ≤ l c p ( i , k ) = d \mathbf{j-i\ge k-i\Rightarrow lcp(i,j)\le lcp(i,k)=d} jikilcp(i,j)lcp(i,k)=d。结合前面我们得到的 l c p ( i , j ) ≥ d \mathbf{lcp(i,j)\ge d} lcp(i,j)d我们可以得出 l c p ( i , j ) = d \mathbf{lcp(i,j)=d} lcp(i,j)=d的结论。

  3. l c p ( i , j ) = m i n { l c p [ i + 1 ] , l c p [ i + 2 ] , . . . , l c p [ j ] } , ( j > i ) ) \mathbf{lcp(i,j)=min\{lcp[i+1],lcp[i+2],...,lcp[j]\},(j>i))} lcp(i,j)=min{lcp[i+1],lcp[i+2],...,lcp[j]},(j>i))
    证明:根据性质2我们知道 l c p ( i , i + 2 ) = m i n { l c p ( i , i + 1 ) , l c p ( i + 1 , i + 2 ) } = m i n { l c p [ i + 1 ] , l c p [ i + 2 ] } \mathbf{lcp(i,i+2)=min\{lcp(i,i+1),lcp(i+1,i+2)\}=min\{lcp[i+1],lcp[i+2]\}} lcp(i,i+2)=min{lcp(i,i+1),lcp(i+1,i+2)}=min{lcp[i+1],lcp[i+2]},于是又有 l c p ( i , i + 3 ) = m i n { l c p ( i , i + 2 ) , l c p ( i + 2 , i + 3 ) } = m i n { m i n { l c p [ i + 1 ] , l c p [ i + 2 ] } , l c p [ i + 3 ] } = m i n { l c p [ i + 1 ] , l c p [ i + 2 ] , l c p [ i + 3 ] } \mathbf{lcp(i,i+3)=min\{lcp(i,i+2),lcp(i+2,i+3)\}=min\{min\{lcp[i+1],lcp[i+2]\},lcp[i+3]\}=min\{lcp[i+1],lcp[i+2],lcp[i+3]\}} lcp(i,i+3)=min{lcp(i,i+2),lcp(i+2,i+3)}=min{min{lcp[i+1],lcp[i+2]},lcp[i+3]}=min{lcp[i+1],lcp[i+2],lcp[i+3]}
    由数学归纳法容易得知原命题是正确的。

  4. 假设 l c p [ r k [ i ] ] = k \mathbf{lcp[rk[i]]=k} lcp[rk[i]]=k,那么有 l c p [ r k [ i + 1 ] ] ≥ k − 1 \mathbf{lcp[rk[i+1]]\ge k-1} lcp[rk[i+1]]k1(在一些证明中会定义把 l c p [ r k [ i ] ] \mathbf{lcp[rk[i]]} lcp[rk[i]]记作 h [ i ] \mathbf{h[i]} h[i])。
    证明:首先要明确一点,那就是后缀i+1仅仅是比后缀i少了一个首字母。这里分两种情况讨论,当 k ≤ 1 \mathbf{k\le 1} k1的时候,显然有 l c p [ r k [ i + 1 ] ] ≥ 0 > − 1 \mathbf{lcp[rk[i+1]]\ge 0>-1} lcp[rk[i+1]]0>1,故公式正确。当 k ≥ 2 \mathbf{k\ge2} k2的时候,设排名为 r k [ i ] − 1 \mathbf{rk[i]-1} rk[i]1的这个后缀为 u \mathbf{u} u,我们发现后缀i与 u \mathbf{u} u的公共前缀存在至少两个相同的字母,那么当后缀i去掉它的首字母后它就变成了后缀i+1,假设 u \mathbf{u} u去掉首字母后就变成了 v \mathbf{v} v,可以肯定的是 v \mathbf{v} v也是一个后缀,并且 v \mathbf{v} v与后缀i+1的排名的相对关系和 u \mathbf{u} u与后缀i的排名的相对关系是相同的,又因为 u \mathbf{u} u的排名是比后缀i更小的,故 v \mathbf{v} v的排名也一定比后缀i+1更小,不妨假设 v \mathbf{v} v对应的是后缀j,那么一定有 r k [ j ] < r k [ i + 1 ] \mathbf{rk[j]<rk[i+1]} rk[j]<rk[i+1],根据性质1我们可以知道 l c p ( r k [ i + 1 ] , r k [ j ] ) ≤ l c p ( r k [ i ] , r k [ i + 1 ] ) \mathbf{lcp(rk[i+1],rk[j])\le lcp(rk[i],rk[i+1])} lcp(rk[i+1],rk[j])lcp(rk[i],rk[i+1]),而注意到 l c p ( r k [ i + 1 ] , r k [ j ] ) = u 与 后 缀 i 的 l c p − 1 , ( 因 为 后 缀 i + 1 与 后 缀 j 分 别 是 由 u 与 后 缀 i 去 掉 一 个 相 同 的 首 字 母 得 到 的 ) \mathbf{lcp(rk[i+1],rk[j])=u与后缀i的lcp-1,(因为后缀i+1与后缀j分别是由u与后缀i去掉一个相同的首字母得到的)} lcp(rk[i+1],rk[j])=uilcp1(i+1jui),故 l c p ( r k [ i ] , r k [ i + 1 ] ) ≥ l c p ( r k [ i + 1 ] , r k [ j ] ) = l c p ( r k [ i ] , r k [ i ] − 1 ) − 1 \mathbf{lcp(rk[i],rk[i+1])\ge lcp(rk[i+1],rk[j])=lcp(rk[i],rk[i]-1)-1} lcp(rk[i],rk[i+1])lcp(rk[i+1],rk[j])=lcp(rk[i],rk[i]1)1,即 l c p [ r k [ i + 1 ] ] ≥ l c p [ r k [ i ] ] − 1 = k − 1 \mathbf{lcp[rk[i+1]]\ge lcp[rk[i]]-1=k-1} lcp[rk[i+1]]lcp[rk[i]]1=k1

  5. 字符串s中的任意一个子串都一定是它的某个后缀的前缀。
    证明:显然正确。

lcp相关代码实现

方便起见下面我们把 l c p [ r k [ i ] ] \mathbf{lcp[rk[i]]} lcp[rk[i]]记作 h [ i ] \mathbf{h[i]} h[i]
根据性质4我们能够很快想到一个办法求解出 h \mathbf{h} h数组。即从小到大枚举字符串s的每个位置的后缀,根据 h [ i + 1 ] ≥ h [ i ] − 1 \mathbf{h[i+1]\ge h[i]-1} h[i+1]h[i]1我们可以每次都记录 h [ i ] \mathbf{h[i]} h[i]为k,然后枚举 i + 1 \mathbf{i+1} i+1的时候直接令 h [ i + 1 ] = k \mathbf{h[i+1]=k} h[i+1]=k,然后再此基础上去检查后缀i+1与排名为 r k [ i + 1 ] − 1 \mathbf{rk[i+1]-1} rk[i+1]1的后缀的lcp是否还能够增加。由于每次 k \mathbf{k} k只会减少1,且k的上界为 n \mathbf{n} n,故算法整体复杂度是 O ( n ) \mathbf{O(n)} O(n)的。
下面给出一份关于h数组求解的代码参考实现:

void get_h(int n){
    int k =0;
    FOR(i,1,n+1)rk[sa[i]]=i;//初始化rk数组
    FOR(i,1,n){//不用考虑第n个字符,因为它是$,即排名第一的字符,也就不存比它排名还靠前的字符
        int j=sa[rk[i]-1];
        while(s[i+k]==s[j+k])k++;//检测是否能继续扩大lcp
        h[i]=k;//设置成k即可
        k=max(k-1,0);//k--,但不能为负
    }
}

然后我们还会常常用到求解 l c p ( i , j ) \mathbf{lcp(i,j)} lcp(i,j),根据性质3我们容易想到用st表预处理 l c p [ ] \mathbf{lcp[]} lcp[]数组,这样就能做到 O ( 1 ) \mathbf{O(1)} O(1)查询,这里给出一份参考代码实现:

void init_lcp(int n){
    int k =0;lgn[1]=0;
    FOR(i,1,n+1)rk[sa[i]]=i;
    FOR(i,2,n+1)lgn[i]=lgn[i/2]+1;
    FOR(i,1,n){
        int j=sa[rk[i]-1];
        while(s[i+k]==s[j+k])k++;
        lcp[rk[i]]=k;//与h数组求解的唯一区别
        st[rk[i]][0]=lcp[rk[i]];
        k=max(k-1,0);
    }
    FOR(i,2,n+1)FOR(j,1,maxlog)st[i][j]=min(st[i][j-1],st[i+(1<<(j-1))][j-1]);
}
int get_lcp(int i,int j){
    if(i==j)return n-sa[i];
    if(i>j)swap(i,j);
    i++;
    int lg=lgn[j-i+1];
    wrn(i,1<<lg,j-(1<<lg)+1);
    return min(st[i][lg],st[j-(1<<lg)+1][lg]);
}

习题

例题一
题目来源:POJMilk Patterns

题面:
例题一题面
题解:本题让求重复次数大于等于k次的最长子串,允许有重叠。我们直接考虑在sa状数组中长度为k的一段连续后缀,所有这些后缀的lcp的最大值就是答案,考虑用st表。
代码:

int n,k,s[maxn],cnt[maxm],sa[maxn],rk[maxn*2],tp[maxn*2],st[maxn][maxlog],
	lgn[maxn];

void radix_sort(int n,int m){
	memset(cnt,0,sizeof(int)*(m+1));
	FOR(i,1,n+1)cnt[rk[i]]++;
	FOR(i,1,m+1)cnt[i]+=cnt[i-1];
	ROF(i,n,1)sa[cnt[rk[tp[i]]]--]=tp[i];
}
bool cmp(int x,int y,int w){
    return tp[x]==tp[y]&&tp[x+w]==tp[y+w];
}
int get_sa(int n){
	int tot=0;
	s[++n]=1;
	FOR(i,1,n+1)rk[n+i]=0,tp[i]=i,rk[i]=s[i],tot=max(tot,rk[i]);
	radix_sort(n,tot);
	for(register int w=1;w<n;w<<=1){
		register int tt=0;
		ROF(i,n,n-w+1)tp[++tt]=i;
		FOR(i,1,n+1)if(sa[i]>w)tp[++tt]=sa[i]-w;
		radix_sort(n,tot);
		memcpy(tp,rk,sizeof(int)*(n+1));tot=0;
		FOR(i,1,n+1)rk[sa[i]]=cmp(sa[i],sa[i-1],w)?tot:++tot;
		if(tot==n)break;
	}
	return n;
}
void init_lcp(){
	int k=0;
	FOR(i,1,n+1)rk[sa[i]]=i,lgn[i]=i==1?0:lgn[i/2]+1;
	FOR(i,1,n){
		int j=sa[rk[i]-1];
		while(j+k<n && s[j+k]==s[i+k])k++;
		st[rk[i]][0]=k;
		k=max(k-1,0);
	}
	FOR(j,1,maxlog){
		FOR(i,2,n+1){
			if(i+(1<<j)-1>n)break;
			st[i][j]=min(st[i][j-1],st[i+(1<<(j-1))][j-1]);
		}
	}
}
int lcp(int i,int j){
	if(i>j)swap(i,j);
	i++;int lg=lgn[j-i+1];
	return min(st[i][lg],st[j-(1<<lg)+1][lg]);
}
int main(){
	n=0;
	while(~scanf("%d%d",&n,&k)){
		FOR(i,1,n+1){
			rd(&s[i]);
			s[i]+=2;
		}
		n=get_sa(n);
		init_lcp();
		int ans=0;
		FOR(i,2,n-k+2){
			ans=max(ans,lcp(i,i+k-1));
		}
		wrn(ans);
	}
}

例题二
题目来源:POJMusical Theme

题面:
例题二题面
题解:首先对相邻项作差,本题就转化为求解至少重复两次的最长子串,并且要求这两个子串互不重叠。考虑求出 h \mathbf{h} h数组然后二分满足条件的最长子串,然后check是否存在连续的一短 h \mathbf{h} h值都大于等于mid-1,表明存在重复子串,但存在还不够,我们得保证其中有两个串是不重叠的,一次你我们要看这个区间内的sa值的最大差是否大于等于mid即可。
代码:

int n,a[maxn],s[maxn],sa[maxn],rk[maxn<<1],tp[maxn<<1],h[maxn],cnt[maxn];
bool cmp(int x,int y,int w){
	return tp[x]==tp[y]&&tp[x+w]==tp[y+w];
}
int main(){
	while(~scanf("%d",&n)&&n){
		FOR(i,1,n+1)rd(&a[i]),s[i]=a[i]-a[i-1]+100;s[1]=199,s[++n]=1;
		FOR(i,1,200)cnt[i]=0;
		FOR(i,1,n+1)cnt[rk[i]=s[i]]++,rk[n+i]=tp[n+i]=0;
		FOR(i,1,200)cnt[i]+=cnt[i-1];
		ROF(i,n,1)sa[cnt[rk[i]]--]=i;
		for(register int w = 1,m = 200,tot=0;w<n;w<<=1,tot=0){
			ROF(i,n,n-w+1)tp[++tot]=i;
			FOR(i,1,n+1)if(sa[i]>w)tp[++tot]=sa[i]-w;
			FOR(i,1,m)cnt[i]=0;//cnt记得清零,m注意容易改变,因为将m固定为200WA了好几发 
			FOR(i,1,n+1)cnt[rk[i]]++;
			FOR(i,1,m)cnt[i]+=cnt[i-1];
			ROF(i,n,1)sa[cnt[rk[tp[i]]]--]=tp[i];
			memcpy(tp,rk,sizeof(int)*(n+1));m=0;
			FOR(i,1,n+1)rk[sa[i]]=cmp(sa[i],sa[i-1],w)?m:++m;m++;
			if(m==n)break;
		}
		register int k=0,j,l=5,r=n>>1,mid,ans=0,minn,maxx;
		FOR(i,1,n+1)rk[sa[i]]=i;
		FOR(i,1,n){
			j=sa[rk[i]-1];
			while(i+k<n && s[i+k]==s[j+k])k++;
			h[rk[i]]=k;k=max(k-1,0);
		}
		while(l<=r){
			mid = l+r>>1,minn=inf,maxx=0;
			FOR(i,2,n+1){
				if(h[i]>=mid-1){
					minn=min(minn,min(sa[i],sa[i-1])),maxx=max(maxx,max(sa[i],sa[i-1]));
					if(maxx-minn>=mid){
						ans=(l=mid+1)-1;
						break;
					}
				}else minn=inf,maxx=0;
				if(i==n)r=mid-1;
			}
		}
		wrn(ans);
	}
}

例题三
题目来源:SPOJDistinct Substrings

题面:
例题二题面
题解:题意就是让求一个字符串的本质不同子串个数(不包括空串),由于每个子串都是某个后缀的前缀,假设所有的子串互不相同,那么我们直接求出它的所有后缀然后加上它的所有前缀的数量即可,但是其中一定有很多重复的,那么后缀数组的作用就在于去重,如果两个子串相同,那么以它们为前缀的后缀在后缀数组中的位置一定是相互靠近的,并且这两个后缀之间的lcp就至少为这两个子串的长度。因此我们只需要在后缀数组中不断加上后缀的长度,并且在这个过程中减去当前后缀与上个后缀的lcp即可。因此做法也很简单,求出 h \mathbf{h} h数组后遍历一遍,加上所有后缀长度并且减去所有h数组之和即可。

代码:

char s[maxn];
int rk[maxn<<1],tp[maxn<<1],cnt[maxn],sa[maxn],h[maxn];
bool cmp(int x,int y,int w){
	return tp[x]==tp[y] && tp[x+w]==tp[y+w];
}
int main(){
	int t;rd(&t);
	while(t--){
		int n=rd(s+1),m=0,k=0;
		s[++n]='$',s[n+1]='\0';
		FOR(i,1,200)cnt[i]=0;
		FOR(i,1,n+1)cnt[rk[i]=(s[i]-'$'+1)]++,m=max(m,rk[i]),rk[n+i]=tp[n+i]=0;
		FOR(i,1,m+1)cnt[i]+=cnt[i-1];
		ROF(i,n,1)sa[cnt[rk[i]]--]=i;
		for(register int w=1,tot=0;w<n;w<<=1,tot=0){
			FOR(i,n-w+1,n+1)tp[++tot]=i;
			FOR(i,1,n+1)if(sa[i]>w)tp[++tot]=sa[i]-w;
			FOR(i,1,m+1)cnt[i]=0;
			FOR(i,1,n+1)cnt[rk[i]]++;
			FOR(i,1,m+1)cnt[i]+=cnt[i-1];
			ROF(i,n,1)sa[cnt[rk[tp[i]]]--]=tp[i];
			memcpy(tp,rk,sizeof(int)*(n+1));m=0;
			FOR(i,1,n+1)rk[sa[i]]=cmp(sa[i],sa[i-1],w)?m:++m;
			if(m==n)break; 
		}
		FOR(i,1,n){
			register int j=sa[rk[i]-1];
			while(s[i+k]==s[j+k])k++;
			h[rk[i]]=k;k=max(k-1,0);
		}
		ll ans=0;
		FOR(i,2,n+1)ans+=n-sa[i]-h[i];
		wrn(ans);
	}
}

例题四
题目来源:URAL1297. Palindrome

题面:
例题四题面
题解:本题让求字符串s的一个最长回文子串,如果存在多个答案,输出首字母位置最小的一个。我们考虑将字符串对称一下,然后枚举回文子串的中心,考虑当前位置对应后缀的与其对称的位置对应的后缀之间的lcp,显然这两个公共部分可以形成回文子串,而且是以当前位置为中心点的最长回文子串,因此我们枚举每个中心点都求一下lcp即可,取最大值。注意要分奇偶讨论一下。
代码:

char s[maxn];
int sa[maxn],rk[maxn<<1],tp[maxn<<1],cnt[maxn],st[maxn][maxlog],lgn[maxn];
bool cmp(int x,int y,int w){
	return tp[x]==tp[y] && tp[x+w]==tp[y+w];
}
int lcp(int i,int j){//查询后缀i与后缀j之间的lcp 
	i=rk[i],j=rk[j];
	if(i>j)swap(i,j);i++;
	int lg=lgn[j-i+1];
	return min(st[i][lg],st[j-(1<<lg)+1][lg]);
}
int main(){
	int n=rd(s+1),nn=2*n+2,m=200,k=0;
	FOR(i,2,nn+1)lgn[i]=lgn[i/2]+1;
	s[n+1]='$';
	FOR(i,1,n+1)s[nn-i]=s[i];
	s[nn]='$'-1;
	FOR(i,1,nn+1)cnt[rk[i]=s[i]]++,rk[nn+i]=tp[nn+i]=0;
	FOR(i,1,m+1)cnt[i]+=cnt[i-1];
	ROF(i,nn,1)sa[cnt[rk[i]]--]=i;
	for(register int w=1,tot=0;w<nn;w<<=1,tot=0){
		FOR(i,nn-w+1,nn+1)tp[++tot]=i;
		FOR(i,1,nn+1)if(sa[i]>w)tp[++tot]=sa[i]-w;
		FOR(i,1,m+1)cnt[i]=0;
		FOR(i,1,nn+1)cnt[rk[i]]++;
		FOR(i,1,m+1)cnt[i]+=cnt[i-1];
		ROF(i,nn,1)sa[cnt[rk[tp[i]]]--]=tp[i];
		memcpy(tp,rk,sizeof(int)*(nn+1));m=0;
		FOR(i,1,nn+1)rk[sa[i]]=cmp(sa[i],sa[i-1],w)?m:++m;
		if(m==nn)break;
	}
	FOR(i,1,nn){
		register int j=sa[rk[i]-1];
		while(s[i+k]==s[j+k])k++;
		st[rk[i]][0]=k;k=max(k-1,0); 
	}
	FOR(j,1,maxlog)
	FOR(i,2,nn+1){
		if(i+(1<<j)-1>nn)break;
		st[i][j]=min(st[i][j-1],st[i+(1<<(j-1))][j-1]);
	} 
	s[nn+1]='\0';
	int ans=1,as=1;
	FOR(i,1,n){
		register int ans1=2*lcp(i,nn-i)-1,ans2=2*lcp(i+1,nn-i);
		if(ans<ans1){
			ans=ans1;
			as=i-((ans1+1)>>1)+1;
		}
		if(ans<ans2){
			ans=ans2;
			as=i-(ans2>>1)+1;
		}
	}
	FOR(i,as,as+ans)printf("%c",s[i]);
	wrn("");
}

例题五
题目来源:POJLong Long Message

题面:
例题五题面
题解:本题让求两个字符串的最长公共子串。跟例题四差不多,将两个串接在一起,注意中间要插入一个不再字符集中的小字符,末尾也要插入。然后求一下h数组,二分答案并check即可。如何check呢?考虑对于连续的一段h都大于等于mid,假设这段中的最小sa是小于等于第一个字符串的长度,最大sa则大于第一个字符串的长度,说明mid是可行的一个解。
代码:

char s[maxn];
int n,nn,sa[maxn],tp[maxn<<1],rk[maxn<<1],cnt[maxn],h[maxn];
bool cmp(int x,int y,int w){return tp[x]==tp[y]&&tp[x+w]==tp[y+w];}
int main(){
	register int m=200,k=0;
	n=rd(s+1);s[n+1]='a'-1;
	nn=rd(s+n+2)+n+1;s[++nn]='a'-2;
	FOR(i,1,nn+1)cnt[rk[i]=s[i]]++;
	FOR(i,1,m+1)cnt[i]+=cnt[i-1];
	ROF(i,nn,1)sa[cnt[rk[i]]--]=i;
	for(register int w=1,tot=0;w<nn;w<<=1,tot=0){
		FOR(i,nn-w+1,nn+1)tp[++tot]=i;
		FOR(i,1,nn+1)if(sa[i]>w)tp[++tot]=sa[i]-w;
		FOR(i,1,m+1)cnt[i]=0;
		FOR(i,1,nn+1)cnt[rk[i]]++;
		FOR(i,1,m+1)cnt[i]+=cnt[i-1];
		ROF(i,nn,1)sa[cnt[rk[tp[i]]]--]=tp[i];
		memcpy(tp,rk,sizeof(int)*(nn+1));m=0;
		FOR(i,1,nn+1)rk[sa[i]]=cmp(sa[i],sa[i-1],w)?m:++m;
		if(m==nn)break;
	}
	FOR(i,1,nn){
		register int j=sa[rk[i]-1];
		while(s[i+k]==s[j+k])k++;
		h[rk[i]]=k;k=max(k-1,0);
	}
	register int l=1,r=n,mid,ans=0,f1=0,f2=0;
	while(l<=r){
		mid=l+r>>1;
		f1=f2=0;
		FOR(i,2,nn+1){
			if(h[i]>=mid){
				f1=(sa[i-1]<=n || sa[i]<=n);
				f2=(sa[i-1]>=n+2 || sa[i]>=n+2);
				if(f1 && f2){
					ans=(l=mid+1)-1;
					break;
				}
			}else f1=f2=0;
			if(i==nn)r=mid-1;
		}
	}
	wrn(ans);
}

例题六
题目来源:POJPower Strings

题面:
例题六题面
题解:这题可以用kmp来做,但本文是关于后缀数组的内容,考虑用后缀数组来做。由于本题要用到DC3,作者还没学…所以暂时鸽了…
代码:

QAQ

例题七
题目来源:SPOJ687.Repeats

题面:
例题七题面
题解:本题让求重复次数最多的连续重复子串(不能重叠,一个紧接着接一个的重复)。这个做法来自一篇论文,达到了非常优秀的复杂度( O ( n l o g ( n ) ) \mathbf{O(nlog(n))} O(nlog(n))。我们考虑枚举这个重复子串的长度,假设它为len长,原字符串s长度为n。首先无论对任何字符串(非空)都一定满足连续重复串重复次数至少为1,故我们只考虑大于等于2的情况。对于长度为len的子串,如果它连续重复了两次,那么它至少覆盖了 s [ 1 ] , s [ l e n ] , s [ 2 l e n ] . . . , s [ ⌊ n l e n ⌋ l e n ] \mathbf{s[1],s[len],s[2len]...,s[\lfloor \frac n{len}\rfloor len]} s[1],s[len],s[2len]...,s[lennlen]字符中的连续的两个,即存在 i ∈ [ 1 , ⌊ n l e n ⌋ − 1 ] \mathbf{i\in[1,\lfloor \frac n{len}\rfloor-1]} i[1,lenn1]使得 s [ i ⋅ l e n ] , s [ ( i + 1 ) ⋅ l e n ] \mathbf{s[i\cdot len],s[(i+1)\cdot len]} s[ilen],s[(i+1)len]这两个字符被这个长度为len的子串所覆盖(因为连续重复两次的长度为len的子串的总长至少为 2 l e n \mathbf{2len} 2len)。于是我们从 s [ i ⋅ l e n ] , s [ ( i + 1 ) ⋅ l e n ] \mathbf{s[i\cdot len],s[(i+1)\cdot len]} s[ilen],s[(i+1)len]这两个字符出发向前后分别延伸,看最长能够延伸多少(即延伸多长使得从这两个字符出发形成的两个串是相等的)。假设向后延伸了 l e n 后 \mathbf{len_后} len(不包括3len和4len本身)如下图所示,
解释
如果 l e n 后 ≥ l e n − 1 \mathbf{len_后\ge len-1} lenlen1那么显然字符串 s [ 3 l e n , 3 l e n + 1 , . . . , 4 l e n + l e n 后 ] \mathbf{s[3len,3len+1,...,4len+len_后]} s[3len,3len+1,...,4len+len]显然是一个以 l e n \mathbf{len} len为周期的串,且重复次数为 l e n 后 + 1 l e n + 1 \mathbf{\frac{len_后+1}{len}+1} lenlen+1+1,如果再考虑向前延伸的长度为 l e n 前 \mathbf{len_前} len,那么重复总次数就为 l e n 后 + l e n 前 + 1 l e n + 1 \mathbf{\frac{len_后+len_前+1}{len}+1} lenlen+len+1+1,注意到这个式子本身保证了答案一定是大于等于一的,故必须要特判。显然 l e n 后 = l c p ( 3 l e n , 4 l e n ) − 1 \mathbf{len_后=lcp(3len,4len)-1} len=lcp(3len,4len)1,那么如何求出 l e n 前 \mathbf{len_前} len呢,我们可以反转字符串求一下lcp即可,不过这样太麻烦了,我们考虑 l e n 后 + 1 \mathbf{len_后+1} len+1能否被 l e n \mathbf{len} len整除,如果不能我们就考虑向前试着延伸 l e n − ( l e n 后 + 1 ) % l e n \mathbf{len-(len_后+1)\%len} len(len+1)%len的长度,即假设 l e n 前 = l e n − ( l e n 后 + 1 ) % l e n \mathbf{len_前=len-(len_后+1)\%len} len=len(len+1)%len,显然当 l e n 前 \mathbf{len_前} len小于这个值的时候不会对前计算结果产生贡献,故我么考虑假设 l e n 前 = l e n − ( l e n 后 + 1 ) % l e n \mathbf{len_前=len-(len_后+1)\%len} len=len(len+1)%len然后看它是否成立,当 l c p ( i ⋅ l e n − l e n 前 , ( i + 1 ) ⋅ l e n − l e n 前 ) = l e n 前 + l e n 后 + 1 + l e n \mathbf{lcp(i\cdot len-len_前,(i+1)\cdot len-len_前)=len_前+len_后+1+len} lcp(ilenlen,(i+1)lenlen)=len+len+1+len的时候显然假设就成立了,故我们的计算结果还可以加一,那能不能让 l e n 前 \mathbf{len_前} len再次加上一个 l e n \mathbf{len} len呢?如果稍加思考会发现这是没有必要的,因为你再让 l e n 前 \mathbf{len_前} len大一点就会使得子串覆盖 s [ ( i − 1 ) ⋅ l e n ] \mathbf{s[(i-1)\cdot len]} s[(i1)len],这种情况显然在我们上一次枚举 s [ ( i − 1 ) ⋅ l e n ] , s [ i ⋅ l e n ] \mathbf{s[(i-1)\cdot len],s[i\cdot len]} s[(i1)len],s[ilen]的时候就已经被考虑了。
这样枚举下来容易发现枚举加查询(查询用ST表做到 O ( 1 ) \mathbf{O(1)} O(1))的复杂度为 O ( n 1 + n 2 + n 3 + ⋯ + n n ) = O ( n l o g ( n ) ) \mathbf{O(\frac n1+\frac n2+\frac n3+\cdots+\frac nn)=O(nlog(n))} O(1n+2n+3n++nn)=O(nlog(n)),总复杂度不变也是 O ( n l o g ( n ) ) \mathbf{O(nlog(n))} O(nlog(n)),可以用远低于给定的时限通过本题。
代码:

char s[maxn];
int n,sa[maxn],rk[maxn<<1],tp[maxn<<1],cnt[maxn],st[maxn][maxlog],lgn[maxn];
bool cmp(int x,int y,int w){
	return tp[x]==tp[y] && tp[x+w]==tp[y+w];
}
int lcp(int i,int j){
	i=rk[i],j=rk[j];
	if(i>j)swap(i,j);i++;
	int lg=lgn[j-i+1];
	return min(st[i][lg],st[j-(1<<lg)+1][lg]);
}
int main(){
	FOR(i,2,maxn)lgn[i]=lgn[i/2]+1;
	int t;
	rd(&t);
	while(t--){
		rd(&n);FOR(i,1,n+1)rd(&s[i]);s[++n]='a'-1;
		register int m=3,k=0,ans=0;
		FOR(i,1,m+1)cnt[i]=0;
		FOR(i,1,n+1)cnt[rk[i]=s[i]-'a'+2]++,tp[n+i]=rk[n+i]=0;
		FOR(i,1,m+1)cnt[i]+=cnt[i-1];
		ROF(i,n,1)sa[cnt[rk[i]]--]=i;
		for(register int w=1,tot=0;w<n;w<<=1,tot=0){
			FOR(i,n-w+1,n+1)tp[++tot]=i;
			FOR(i,1,n+1)if(sa[i]>w)tp[++tot]=sa[i]-w;
			FOR(i,1,m+1)cnt[i]=0;
			FOR(i,1,n+1)cnt[rk[i]]++;
			FOR(i,1,m+1)cnt[i]+=cnt[i-1];
			ROF(i,n,1)sa[cnt[rk[tp[i]]]--]=tp[i];
			memcpy(tp,rk,sizeof(int)*(n+1));m=0;
			FOR(i,1,n+1)rk[sa[i]]=cmp(sa[i],sa[i-1],w)?m:++m;
			if(m==n)break;
		}
		FOR(i,1,n){
			register int j=sa[rk[i]-1];
			while(s[i+k]==s[j+k])k++;
			st[rk[i]][0]=k;k=max(k-1,0);
		}
		FOR(j,1,maxlog)FOR(i,2,n+1)if(i+(1<<j)-1>n)break;else st[i][j]=min(st[i][j-1],st[i+(1<<(j-1))][j-1]);
		FOR(len,1,n+1){
			for(register int i=1,j=1+len;j<n;i+=len,j+=len){
				k=lcp(i,j);
				register int res=k%len,lc;
				if(res && i!=1 && (lc=lcp(i-(len-res),j-(len-res)))==len-res+k)k=lc;
				k=k/len+1;
				ans=max(k,ans);
			}
		} 
		wrn(ans);
	}
}

例题八
题目来源:POJLife Forms

题面:
例题八题面
题解:本题让求最长的子串满足在一半以上的串中出现。考虑将所有串用不同的不在字符集中的字符连接起来,然后求出后缀数组,再二分这个最大长度即可。check的时候要看后缀数组中连续的满足h数组值大于二分长度的段中是否有大于一半的不同的字符串。
代码:

unsigned char s[maxn];
char ss[maxn];
int sa[maxn],tp[maxn<<1],rk[maxn],cnt[maxn],h[maxn],id[maxn],bl[105];
bool cmp(int x,int y,int w){
	return tp[x]==tp[y]&&tp[x+w]==tp[y+w];
}
void radix_sort(int n,int m){
	memset(cnt,0,sizeof(int)*(m+1));
	FOR(i,1,n+1)cnt[rk[i]]++;
	FOR(i,1,m+1)cnt[i]+=cnt[i-1];
	ROF(i,n,1)sa[cnt[rk[tp[i]]]--]=tp[i];
}
void get_sa(unsigned char *s,int n){
	register int m=0,tot=0;
	FOR(i,1,n+1)tp[i]=i,rk[i]=s[i],m=max(rk[i],m),tp[n+i]=0;
	radix_sort(n,m);
	for(register int w=1;w<n;w<<=1,tot=0){
		ROF(i,n,n-w+1)tp[++tot]=i;
		FOR(i,1,n+1)if(sa[i]>w)tp[++tot]=sa[i]-w;
		radix_sort(n,m);
		memcpy(tp,rk,sizeof(int)*(n+1));m=0;
		FOR(i,1,n+1)rk[sa[i]]=cmp(sa[i],sa[i-1],w)?m:++m;
		if(m==n)break;
	}
	tot=0;
	FOR(i,1,n){
		register int j=sa[rk[i]-1];
		while(s[i+tot]==s[j+tot])tot++;
		h[rk[i]]=tot;
		tot=max(tot-1,0);
	}
}
int n;
bool check(int x,int len){
	memset(bl,0,sizeof(bl));
	vector<int>rec;
	int num=0;
	FOR(i,2,len+1){
		if(h[i]>=x){
			if(!num){
				num++;
				rec.push_back(id[sa[i-1]]);
				if(num*2>n)return true;
				bl[id[sa[i-1]]]=1;
			}
			if(!bl[id[sa[i]]]){
				rec.push_back(id[sa[i]]);
				num++;
				if(num*2>n)return true;
				bl[id[sa[i]]]=1;
			}
		}else{
			FOR(i,0,rec.size())bl[rec[i]]=0;
			rec.clear();
			num=0;
		}
	}
	return false;
}
void print(int x,int len){
	memset(bl,0,sizeof(bl));
	vector<int>rec;
	int num=0;
	FOR(i,2,len+1){
		if(h[i]>=x){
			if(!num){
				num++;
				rec.push_back(id[sa[i-1]]);
				bl[id[sa[i-1]]]=1;
			}
			if(!bl[id[sa[i]]]){
				rec.push_back(id[sa[i]]);
				num++;
				bl[id[sa[i]]]=1;
			}
			if(num*2>n){
				FOR(j,0,x){
					printf("%c",ss[sa[i]+j]);
				}
				wrn("");
				num=-inf/4;
			}
		}else{
			FOR(i,0,rec.size())bl[rec[i]]=0;
			rec.clear();
			num=0;
		}
	}
}
int main(){
	int fg=0;
	while(~scanf("%d",&n)&&n){
		if(fg)wrn("");fg=1;
		if(n==1){
			rd(ss);
			wrn(ss);
			continue;
		}
		register int len=0,le,l=1,r=1000,ans=0;
		FOR(i,1,n+1){
			scanf("%s",s+len+1);
			le=0;
			while(s[len+1+le]!='\0')le++;
			len=le+len;
			ROF(j,len,len-le+1){
				ss[j]=s[j];
				s[j]=s[j]+100;
				id[j]=i;
			}
			s[++len]=n-i+1;
		}
		get_sa(s,len);
		while(l<=r){
			int mid=(l+r)>>1;
			if(ans==mid || check(mid,len)){
				ans=mid;
				l=mid+1;
			}else r=mid-1;
		}
		if(ans){
			print(ans,len);
		}else wrn("?");
	}
}

例题九
题目来源:SPOJPHRASES - Relevant Phrases of Annihilation

题面:
例题九题面
题解:还是套路的二分长度即可,check也不难想到可以直接维护不同串中h数组值大于等于x的后缀的最大坐标(mx)和最小坐标(mi),此外还要维护当前h数组值大于等于x的后缀所在不同串的种类数(num1)、满足(mx-mi>=1)的串的数量(num2)。如果num1=n并且num2=n就返回true即可,本题只是check函数稍微复杂一点。
代码:

char s[maxn];
int n,id[maxn],cnt[maxn],sa[maxn],tp[maxn<<1],rk[maxn],mi[15],mx[15],ok[15],
	h[maxn];

bool cmp(int x,int y,int w){
	return tp[x]==tp[y]&&tp[x+w]==tp[y+w];
}
void radix_sort(int n,int m){
	memset(cnt,0,sizeof(int)*(m+1));
	FOR(i,1,n+1)cnt[rk[i]]++;
	FOR(i,1,m+1)cnt[i]+=cnt[i-1];
	ROF(i,n,1)sa[cnt[rk[tp[i]]]--]=tp[i];
}
void get_sa(char *s,int n){
	register int tot=0,m=0;
	FOR(i,1,n+1)tp[i]=i,rk[i]=s[i],m=max(m,rk[i]),tp[n+i]=0;
	radix_sort(n,m);
	for(register int w=1;w<n;w<<=1,tot=0){
		ROF(i,n,n-w+1)tp[++tot]=i;
		FOR(i,1,n+1)if(sa[i]>w)tp[++tot]=sa[i]-w;
		radix_sort(n,m);
		memcpy(tp,rk,sizeof(int)*(n+1));m=0;
		FOR(i,1,n+1)rk[sa[i]]=cmp(sa[i],sa[i-1],w)?m:++m;
		if(m==n)break;
	}
	tot=0;
	FOR(i,1,n){
		register int j =sa[rk[i]-1];
		while(s[i+tot]==s[j+tot])tot++;
		h[rk[i]]=tot;
		tot=max(tot-1,0);
	}
}
bool check(int x,int len){
	register int num1=0,num2=0;
	memset(ok,0,sizeof(ok));
	memset(mi,0x3f,sizeof(mi));
	memset(mx,0,sizeof(mx));
	vector<int>rec1,rec2;
	FOR(i,2,len+1){
		if(h[i]>=x){
			if(!num1){
				num1++;
				mi[id[sa[i-1]]]=mx[id[sa[i-1]]]=sa[i-1];
				rec1.push_back(id[sa[i-1]]);
			}
			if(!mx[id[sa[i]]]){
				num1++;
				rec1.push_back(id[sa[i]]);
			} 
			mx[id[sa[i]]]=max(mx[id[sa[i]]],sa[i]);
			mi[id[sa[i]]]=min(mi[id[sa[i]]],sa[i]);
			if(mx[id[sa[i]]]-mi[id[sa[i]]]>=x){
				if(!ok[id[sa[i]]]){
					ok[id[sa[i]]]=1;
					num2++;
					rec2.push_back(id[sa[i]]);
				}
			}
			if(num1==n && num2==n)return true;
		}else{
			FOR(i,0,rec1.size())mx[rec1[i]]=0,mi[rec1[i]]=inf/4;
			FOR(i,0,rec2.size())ok[rec2[i]]=0;
			rec1.clear(),rec2.clear();
			num1=num2=0;
		}
	}
	return false;
}
int main(){
	int t;
	rd(&t);
	while(t--){
		rd(&n);
		register int len=0,le=0;
		ROF(i,n,1){
			len=(le=rd(s+len+1))+len;
			ROF(j,len,len-le+1)id[j]=i;
			s[++len]=i;
		}
		get_sa(s,len);
		int l=1,r=15000,ans=0;
		while(l<=r){
			int mid=l+r>>1;
			if(ans==mid || check(mid,len)){
				ans=mid;
				l=mid+1;
			}else r=mid-1;
		}
		wrn(ans);
	}
}

例题十
题目来源:CFD. Prefixes and Suffixes

题面:
例题十题面
题解:还是套路地二分,不过这次要结合lcp查询才行。我们考虑sa数组中的每一项与后缀rk[1]的lcp是否等于它本身,如果等于它本身说明这是一个符合条件的后缀(即整个字符串前缀等于后缀),然后再二分查找从它的位置开始向前和向后分别可以延伸多长,即满足h数组等于这个后缀的长度,而这就是它出现在整个字符串中(可重叠)的次数。
代码:

char s[maxn];
int n,cnt[maxn],tp[maxn<<1],rk[maxn],sa[maxn],st[maxn][maxlog],lgn[maxn];
bool cmp(int x,int y,int w){
	return tp[x]==tp[y]&&tp[x+w]==tp[y+w]; 
}
void radix_sort(int m){
	memset(cnt,0,sizeof(int)*(m+1));
	FOR(i,1,n+1)cnt[rk[i]]++;
	FOR(i,1,m+1)cnt[i]+=cnt[i-1];
	ROF(i,n,1)sa[cnt[rk[tp[i]]]--]=tp[i];
}
void get_sa(char *s){
	s[++n]='$';s[n+1]='\0';
	register int tot=0,m=0;
	FOR(i,1,n+1)tp[i]=i,rk[i]=s[i],m=max(rk[i],m);
	radix_sort(m);
	for(register int w=1;w<n;w<<=1,tot=0){
		ROF(i,n,n-w+1)tp[++tot]=i;
		FOR(i,1,n+1)if(sa[i]>w)tp[++tot]=sa[i]-w;
		radix_sort(m);
		memcpy(tp,rk,sizeof(int)*(n+1));m=0;
		FOR(i,1,n+1)rk[sa[i]]=cmp(sa[i],sa[i-1],w)?m:++m;
		if(m==n)break;
	}
	tot=0;
	FOR(i,1,n){
		register int j =sa[rk[i]-1];
		while(s[i+tot]==s[j+tot])tot++;
		st[rk[i]][0]=tot;
		tot=max(tot-1,0);
	}
	FOR(i,2,n+1)lgn[i]=lgn[i/2]+1;
	FOR(j,1,maxlog)
	FOR(i,2,n+1)if(i+(1<<j)-1>n)break;else st[i][j]=min(st[i][j-1],st[i+(1<<(j-1))][j-1]);
}
int lcp(int i,int j){
	if(i==j)return n-sa[i];
	if(i>j)swap(i,j);i++;
	int lg=lgn[j-i+1];
	return min(st[i][lg],st[j-(1<<lg)+1][lg]);
}
vector<pi >ans;
int main(){
	n=rd(s+1);
	get_sa(s);
	int ss=rk[1];
	FOR(i,2,n+1){
		if(lcp(i,ss)==n-sa[i]){
			register int l=1,r=i-1,up=0,down=0;
			while(l<=r){
				int mid=l+r>>1;
				if(up==mid || lcp(i-mid,i)>=n-sa[i]){
					up=mid;
					l=mid+1;
				}else r=mid-1;
			}
			l=1,r=n-i;
			while(l<=r){
				int mid=l+r>>1;
				if(down==mid || lcp(i+mid,i)>=n-sa[i]){
					down=mid;
					l=mid+1;
				}else r=mid-1;
			}
			ans.push_back(mk(n-sa[i],up+down+1));
		}
	}
	sort(ans.begin(),ans.end());
	wrn((int)ans.size());
	FOR(i,0,ans.size())wrn(ans[i].fi,ans[i].se);
}

例题十一
题目来源:POJCommon Substrings

题面:
例题十一题面
题解:这道题让求两个串之间满足长度大于等于k的公共子串的个数。我们考虑遍历h数组,在此过程中维护一个单调栈,单调栈中储存一个单调递增的h值序列,顾名思义,就是我们每次把当前的h值push进栈,不过在此之前我们还会做一些特殊的处理。首先如果当前的h所在的位置是第一个串,我们正常push进栈,为了维护单调性还要先剔除掉大于等于当前h值的栈内元素。由于我们要求的是第二个串的子串与第一个串的子串之间的lcp-k+1之和,故我们还需要设置一下push进栈的这个元素的权值,首先给每个元素设置一个 c t ct ct变量,代表与它相同的元素个数,然后给每个元素再设置一个 v a l val val变量,代表这个元素实际的权值,即最后我们要统计的那部分。权值是这样计算的: v a l = c t ⋅ ( h − k + 1 ) val=ct\cdot(h-k+1) val=ct(hk+1),虽然 c t ct ct代表相同元素的个数,事实上我们会将pop掉的元素的 c t ct ct都加到这个新的元素身上,因为这些pop掉的元素实际上它的大小都会变成和新push的元素的权值一样。然后维护一个栈内 s u m sum sum的和即可,不过我们不计算属于第二个串的 v a l val val,可以直接置为0,然后当遍历到第二个串的子串的时候累加一下 s u m sum sum,代表的是当前第二个串的子串与栈内第一个串的子串对总和产生的贡献。不过这样算一遍下来会有漏,因为没有计算第一个串的子串与在它之前的第二个串的子串的贡献,故我们可以维护两个单调栈,当然你也可以做两次遍历。

char s[maxn];
int len1,rk[maxn],tp[maxn<<1],sa[maxn],cnt[maxn],h[maxn],st[maxn],ct[maxn],st2[maxn],ct2[maxn];
ll val[maxn],val2[maxn];
void radix_sort(int n,int m){
	memset(cnt,0,sizeof(int)*(m+1));
	FOR(i,1,n+1)cnt[rk[i]]++;
	FOR(i,1,m+1)cnt[i]+=cnt[i-1];
	ROF(i,n,1)sa[cnt[rk[tp[i]]]--]=tp[i];
}
bool cmp(int x,int y,int w){
	return tp[x]==tp[y]&&tp[x+w]==tp[y+w];
}
void get_sa(char *s,int n){
	register int tot=0,m=0;
	FOR(i,1,n+1)tp[i]=i,rk[i]=s[i],m=max(m,rk[i]),tp[n+i]=0;
	radix_sort(n,m);
	for(register int w=1;w<n;w<<=1,tot=0){
		ROF(i,n,n-w+1)tp[++tot]=i;
		FOR(i,1,n+1)if(sa[i]>w)tp[++tot]=sa[i]-w;
		radix_sort(n,m);
		memcpy(tp,rk,sizeof(int)*(n+1));m=0;
		FOR(i,1,n+1)rk[sa[i]]=cmp(sa[i],sa[i-1],w)?m:++m;
		if(m==n)break;
	}
	tot=0;
	FOR(i,1,n){
		register int j=sa[rk[i]-1];
		while(s[i+tot]==s[j+tot])tot++;
		h[rk[i]]=tot;
		tot=max(tot-1,0);
	}
}
inline int id(int x){
	return sa[x]<=len1;
}
int main(){
	register int k;
	while(~scanf("%d",&k)&&k){
		register int len=rd(s+1),top=0,c,top2=0;
		len1=len;
		s[++len]='$';
		len=rd(s+len+1)+len;
		s[++len]='$'-1;
		get_sa(s,len);
		register ll ans=0,sum=0,sum2=0;
		FOR(i,2,len+1){//核心代码 
			if(h[i]>=k){
				c=id(i-1);//识别是第一个字符串还是第二个字符串 
				while(top && h[i]<=st[top]){//维持单调性 
					c+=ct[top];//加上这个被pop掉的元素的权值 
					sum-=val[top--];//sum同时减去权值*大小 
				}
				st[++top]=h[i];//放进栈内 
				val[top]=1ll*c*(h[i]-k+1);//设置新的元素的权值*大小 
				ct[top]=c;//设置新的元素的权值 
				sum+=val[top];//维护sum 
				if(!id(i))ans+=sum;//如果现在是第二个字符串就需要计算一下贡献 
				//下面的代码反着来一次即可,原理相同。 
				c=!id(i-1);
				while(top2 && h[i]<=st2[top2]){
					c+=ct2[top2];
					sum2-=val2[top2--];
				}
				st2[++top2]=h[i];
				val2[top2]=1ll*c*(h[i]-k+1);
				ct2[top2]=c;
				sum2+=val2[top2];
				if(id(i))ans+=sum2;
			}else{
				sum=top=sum2=top2=0;
			}
		}
		wrn(ans);
	}
} 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值