Template
算法逻辑:
对长度为1时进行排序 倍增长度k { 按第二关键字将第一关键字排序 求出2*k长度下的SA 求出2*k长度下的rank(即x数组) }
模板:
#include <bits/stdc++.h> using namespace std; #define X first #define Y second #define pb push_back #define debug(x) cerr<<#x<<"="<<x<<endl typedef double db; typedef long long ll; typedef pair<int,int> P; const int MAXN=1e6+10; char s[MAXN]; int len,lmt,cnt[MAXN],sa[MAXN],x[MAXN],y[MAXN],cur; void solve() { for(int i=1;i<=len;i++) cnt[x[i]=s[i]]++; for(int i=1;i<=lmt;i++) cnt[i]+=cnt[i-1]; for(int i=len;i>=1;i--) sa[cnt[x[i]]--]=i; for(int k=1;k<=len;k<<=1,lmt=cur) { cur=0; for(int i=len-k+1;i<=len;i++) y[++cur]=i; for(int i=1;i<=len;i++) if(sa[i]>k) y[++cur]=sa[i]-k; for(int i=1;i<=lmt;i++) cnt[i]=0; for(int i=1;i<=len;i++) cnt[x[i]]++; for(int i=1;i<=lmt;i++) cnt[i]+=cnt[i-1]; //一定要按第二关键字从大往小枚举! for(int i=len;i>=1;i--) sa[cnt[x[y[i]]]--]=y[i]; swap(x,y);cur=1;x[sa[1]]=1; for(int i=2;i<=len;i++) x[sa[i]]=(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k])?cur:++cur; if(cur==len) break; } } int main() { scanf("%s",s+1); len=strlen(s+1);lmt=130; solve(); for(int i=1;i<=len;i++) printf("%d ",sa[i]); return 0; }
注意:
1、可以利用基数排序直接算出排名
求出前缀和后其实就表示了该权值下的排名上界,每次将上界减一
因此要按第二关键字从大到小算SA
2、当已经出现$len$个$rank$时即可退出
Exercises
1、[BZOJ 4892]Dna
一共只有$n-m+1$个可能的串,加速比对过程即可
由于最大失配次数只有3次,因此可以利用$lcp$加速比对连续相同的部分!
复杂度降为$O(n*3*log(n))$
2、[Gym 101194F]
先用处理多字符串的套路连起来
找到第一个字符串的每个后缀在$rank$中最接近的不同字符串的后缀
那么对于后缀$i$的最短长度即为$max(lcp(rk[i],l[rk[i]]),lcp(rk[i],r[rk[i]]))+1$
为保证字典序最小用$lcp$来$O(logn)$进行比较
3、[BZOJ 3277]
先连起来,对每个后缀$x$二分可行长度$len$
判断长度是否可行,就是判断$lcp(x,y)\ge len$的$y$是否存在于$k$个字符串
由于$lcp$的单调性,可以左右分别二分出$y$的可行区间$[L,R]$
通过预处理出$least[R]$表示包含$k$个字符串的最右点,判断是否$L\le least[R]$即可
Tip1:可以发现同一字符串中$len[i]\ge len[i-1]-1$,这样像推$height$一样推$len$就能做到单$log$
Tip2:时刻注意自己枚举的是排名还是原串位置
4、[Gym 101955B]
关键点在于误差小于$1e-9$!
由于概率大于0.5的只能有一个字母,将原始字母设为概率最大后失配必然使得相乘概率小于0.5
也就是说,最多失配30个字母就可以不用统计了!这样就和前面的[BZOJ 4892]相同了
Tip:为保证精度可以用指数运算,这样就能使用前缀和了
5、[Gym 102028H]
知道本质不同的字符串的算法即可
问题转化为左端点固定,右端点为一个区间的最大值的和
可以发现对于此问题明显只有单调栈中的元素有分段的贡献
从而可以从后往前维护一个单调减的单调栈,每次退栈时用线段树区间修改即可
6、[BZOJ 3238]
求$\sum lcp(i,j)$可以转化为每个$height$的贡献
由于$lcp$的单调性,对于每个$i$用单调栈找到其提供贡献的区间$[L,R]$
最终答案即为$\sum (i-L+1)*(R-i+1)*h[i]$
Tip1:单调栈判断时保证一个小于等于一个小于,防止相等时重复计数
Tip2:注意$h[i]$是与前一位的$lcp$,计数时按两两间理解,自己乘自己是允许的
7、[BZOJ 4310]
在本质不同的字符串序列中二分排名,每次先算出该字符串的位置
判断就是从后往前每次加一个字符,只要字典序比二分串大就分割一段,判断段数是否满足
Tip:注意对两相同位置求$lcp$时特殊处理!
8、[BZOJ 3879]
将查询串按$rank$排序,算出两两间的$lcp$作为$height$
接下来就和[BZOJ 3238]完全相同了
9、[BZOJ 4278]
用$rank$的比较快速判断两后缀字典序的大小,贪心选择字典序小的即可
10、[BZOJ 2320]
要运用一个找最短循环节的结论:$n-nxt[n]$
由于$nxt[n]$就表示$[1,n-nxt[n]]$和$[nxt[n],n]$相等
于是想到枚举循环节长度$len$,通过$LCP,LCS$找满足条件的字符串
这时要使用一些技巧:每$len$长度设一个关键点,每次求相邻关键点间的$LCP,LCS$
由于循环节为$len$的字符串至少经过一个关键点,保证正确性,同时将复杂度控制在双$log$
Tip:由于极长重复串的个数为$O(n)$,暴力算$LCP$比较求字典序最小即可
11、[BZOJ 2119]
和上一题套路差不多,也是设关键点求左右匹配长度
但要注意此题还要保证计数无重复,因此向左向右最长只能匹配到$len$
最终每次的贡献的为$l+r-len+1$
Tip:此处也要保证左开右闭,求向左匹配时要错开一位
12、[BZOJ 2865]
利用$height$数组性质可知以$i$开头的不重复字符串的最短长度为$len=max(h[i],h[i+1])+1$
此时$[i,i+len]$可将$len$作为答案,$[i+len+1,n]$可将$x-i+1$作为答案,用两颗线段树维护即可
13、[BZOJ 2251]
此题求完SA直接枚举$[h[i]+1,len-sa[i]+1]$长度的话感觉复杂度不太对,但也不好卡
如果从大到小枚举$h$的值的话向右的长度明显是单调的,这样复杂度就是准确的了
14、[BZOJ 1031]
解决环形问题的套路:复制一遍求个SA即可
15、[BZOJ 1717]
经典模型:询问是否出现次数至少为$k$次的最长字符串
用单调栈维护一个长度为$k$的滑动窗口即可
16、[BZOJ 1692]
反向赋值一遍求出$rank$,比较字典序贪心选择
Tip:如果类型为$char$,赋极大值时不能赋值超过128!
17、[BZOJ 3230]
对本质不同字符串的处理:
用一个数组维护个数的前缀和,对于一个编号二分出起始位置,再算出终止位置
Tip:本质不同字符串的个数可能为$O(n^2)$种,注意可能爆$int$!
18、[BZOJ 2754]
可以发现对于每个询问$i$合法的是一个包含$rk[i]$的区间$[L,R]$
那么两问分别是求$[L,R]$内有来自于多少个字符串的后缀/每个字符串被多少个$[L,R]$包含
对于第一问:离线+树状数组,预处理出$nxt[i]$表示下一个同字符串在$rank$中的位置
对于第二问:建立开始和结束事件,每次统计$[pre[i],i]$间有多少个开始且未结束的询问
Tip:$nxt$可能为0,树状数组注意特判
19、[BZOJ 4698]
所谓同时加一个数相同其实就是差分数组相同
差分后做SA,如果区间$[L,R]$恰包含所有$n$个串,明显$L$随$R$单调
因此用单调栈维护下滑动窗口里$height$最小值就不用二分了(虽然没啥卵用)
Tip:一定注意要拿掉首位算最后再加一,否则会被卡(虽然并不会)
20、[BZOJ 4199]
首先要将询问的“至少”改为计算“恰好”,最后在做一个后缀和/后缀最大值即可
还是利用单调性质将$rank$按$h[i]$从大到小排序,每次合并$sa[id[i]],sa[id[i]-1]$的集合
由于保证两个集合组成任意一对都满足$lcp=h[i]$,并查集合并时维护下$sz,,mx,mn$即可
Tip:
此题也可以用单调栈做,维护$h[i]$单调减,每次退栈时就将当前集合与栈中集合合并
相当于栈中存的是起始点集合的末元素,退栈时仅完全计算了每个集合向后的答案
因此最后还要在单调栈中从右向左合并一遍计算每个集合向前的答案
Review
总结下后缀数组的套路和易错点
易错点:
1、要特殊处理位置相同时$lcp$的返回值
2、要时刻清晰现在枚举的$i$是否是排名,每个数组是基于什么的,需不需要套$rk/sa$
3、求得的$height$是$i,i-1$间的$lcp$,注意边界
套路:
1、将每个子串看作后缀的前缀来考虑
2、多个字符串匹配时串起来求SA
3、充分利用$lcp$的单调性/可二分性,同时考虑在维护单调栈时记录答案
4、使用$lcp,lcs$加速字符串匹配
5、计数问题时多从$height$的贡献角度考虑,注意边界