基础 P3809 【模板】后缀排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 const int N=1e6 +100 ;char s[N];int sa[N],rk[N];int tp[N],tax[N];int n,m=75 ;int height[N];namespace SA{ inline void Rsort () { for (rei i=0 ;i<=m;++i) tax[i]=0 ; for (rei i=1 ;i<=n;++i) ++tax[ rk[i] ]; for (rei i=1 ;i<=m;++i) tax[i]+=tax[i-1 ]; for (rei i=n;i;--i) sa[ tax[ rk[ tp[i] ] ] -- ]=tp[i]; } inline void get_sa () { for (rei i=1 ;i<=n;++i) rk[i]=s[i]-'0' +1 ,tp[i]=i; Rsort (); for (rei w=1 ,p=0 ;p<n;m=p,w<<1 ){ p=0 ; for (rei i=1 ;i<=w;++i) tp[++p]=n-w+i; for (rei i=1 ;i<=n;++i) if (sa[i]>w) tp[++p]=sa[i]-w; Rsort (); swap (tp,rk); rk[ sa[1 ] ]=p=1 ; for (rei i=2 ;i<=n;++i) rk[ sa[i] ]=(tp[ sa[i-1 ] ]==tp[ sa[i] ] && tp[ sa[i-1 ]+w ]==tp[ sa[i]+w ]) ? p : ++p; } } inline void get_he () { rei k=0 ; for (rei i=1 ;i<=n;++i){ if (k) --k; rei j=sa[ rk[i]-1 ]; while (s[i+k]==s[j+k]) ++k; height[ rk[i] ]=k; printf ("%d\n" ,k); } } } int main () { scanf ("%s" ,s+1 ); n=strlen (s+1 ); SA::get_sa (); for (rei i=1 ;i<=n;++i) printf ("%d%c" ,sa[i],i==n ? 10 : 32 ); getchar (),getchar (); return 0 ; }
应用
两个后缀的最大公共前缀 $\text{lcp}$
$\text{lcp}(x,y)=\min\{height_{x+1},height_{x+2},…,height_{y}\}$ , $\text{rmq}$ 维护, $O(1)$ 查询
可重叠最长重复子串
$height_{\max}$
不可重叠最长重复子串
见例题 $1$
本质不同的子串数量
枚举每一个后缀,第 $i$ 个的贡献为 $len-sa_i+1-height_i$
例题
求不重叠的最长重复子串,两字符串相等当且仅当相同为值的两个字符差值一定(即经过转调)
显然联想到差分,即转化为差分数组下求相等的不重叠最大子串,而最大重复子串为 $height$ 数组中最大值
要求不重叠,对 $k$ 二分,转化为子串的存在问题
将 $height \geq k$ 的归为一组,若组内 $sa_{\max}-sa_{\min}\leq k$ 则存在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 rk[i]=s[i]+1 inline bool valid (int k){ rei MAX=-INF,MIN=INF; for (rei i=1 ;i<=n;++i){ if (height[i]<k) MAX=-INF,MIN=INF; MAX=max (MAX,sa[i]),MIN=min (MIN,sa[i]); if (MAX-MIN>k) return true ; } return false ; } int main () { .... rei l=0 ,r=n; while (l<=r){ rei mid=l+r>>1 ; valid (mid) ? (l=mid+1 ,ans=mid) : r=mid-1 ; } ... }
至少出现 $k$ 次的最长重复子串
$k$ 次,相当于选择 $k$ 个后缀后求出它们的 $\text{lcp}$
由 $\text{lcp}_{i,j}=\min\{height[rk[i+1]],…height[rk[j]]\}$ 可知,$k$ 个后缀在 $rk$ 上一定连续
二分即可
1 2 3 4 5 6 7 inline bool valid (int now,int cnt=0 ) { for (rei i=1 ;i<=n;++i){ if (height[i]<now) cnt=0 ; if (++cnt>=k) return true ; } return false ; }
本质不同的子串数量
双倍经验SP705 SUBST1 - New Distinct Substrings
每个后缀的前缀对应一个子串,枚举 $i$ 会产生 $n-sa_i+1$ 个,但需要减去和上一个的 $\text{lcp}$ 即,$height_i$
注意多测的数据 $\text{clear}$ ,要重置 $m$
1 2 3 4 5 6 7 8 9 10 inline void clear () { for (rei i=1 ;i<=n+10 ;++i) sa[i]=rk[i]=tp[i]=tax[i]=height[i]=0 ; ans=0 ; m=300 ; } int main () { ... for (rei i=1 ;i<=n;++i) ans+=n-sa[i]+1 -height[i]; ... }
求最长回文子串
将整个字符串反过来写在原字符串后,中间用特殊字符隔开,先求这个新字符串的莫两个后缀的最长公共前缀,再枚举每一位,计算以其为重心的最长回文子串,注意分奇偶讨论. $O(n\log n)$ 的后缀数组 $+$ $O(n)$ 的 $\text{RMQ}$
$\sout{O(n)的\text{Manacher}也好啊}$
注意: 插入的特殊字符为 $1$ 时:
由于 0
的 $\text{ACSII}$ 码值为 $48$
故会出现 rk[i]=1-48('0')+1
的情况,于是要改成如下:
for(rei i=1;i<=n;++i) rk[i]=s[i]-'0'+49,tp[i]=i;
qwq卡了一上午在这个了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 inline int lcq (int i,int j) { rei l=rk[i],r=rk[j]; if (l==r) return -INF; if (l>r) swap (l,r); ++l; rei k=log2 (r-l+1 ); return min (st[l][k],st[ r-(1 <<k)+1 ][k]); } int main () { ... for (rei i=1 ;i<=len;++i){ rei odd=lcq (i,2 *len+2 -i); if (ans<2 *odd-1 ) ans=2 *odd-1 ,pos=i-odd+1 ; rei even=lcq (i,2 *len+3 -i); if (ans<2 *even) ans=2 *even,pos=i-even; } ... }
求循环同构串的最小循环节/连续重复最小子串
$\sout{KMP:n\%(n-next[n])=0 \ ?\ \frac{n}{n-next[n]}\ :\ 1 }$
$\sout{Hash}$
枚举长度的约数 $k$ ,判断 $1$ 与 $k+1$ 的后缀的 $\text{lcp}$ 是否为 $n-k$
1 2 for (rei k=1 ;k<n;++k) if (n%k==0 && lcp (1 ,k+1 )==n-k) ans=n/k,k=n;printf ("%d\n" ,!ans ? 1 : ans);
重复次数最多的连续重复子串
枚举长度 $L$ ,求长度 $L$ 的子串最多能连续出现几次
显然可以出现一次,考虑出现至少 $2$ 次的情况。
以子串 $sub_s$ 在 $s$ 中连续出现 $2$ 次为例,$sub_s$ 一定包含字符 $s_0,s_L,s_{2L},…$ 中相邻的两个,设包含了字符 $s_{L\times i},s_{L\times (i+1)}$
那么看这两个字符往前往后各能匹配到多远,记录 $\frac{\max 长度}{L}$ 和 $sub_{s_1}$
1 2 3 4 5 6 7 8 9 10 11 12 13 for (rei L=1 ;(L<<1 )<=n;++L) for (rei i=0 ;L*(i+1 )+1 <=n;++i){ rei x=L*i+1 ,y=L*(i+1 )+1 ; if (s[x]!=s[y]) continue ; rei z=lcp (x,y); rei t1=y+z-1 ,t0=0 ; for (rei j=0 ;j<=L-1 ;++j){ if (x-j<1 || s[x-j]!=s[y-j]) break ; t0=x-j; rei now=((t1-t0+1 )/L); if (now>ans || (now==ans && rk[t0]<rk[al])) ans=now,al=t0,ar=t0+now*L-1 ; } }
给定 $n(n\leq 10)$ 个字符串,求其最长公共子串
双倍经验SP1811 LCS - Longest Common Substring
三倍经验SP1812 LCS2 - Longest Common Substring II
四倍经验SP10570 LONGCS - Longest Common Substring
(逃
论文中给出了两个字符串的做法,多字符串能从中推出:
子串具有连续性,字符串的任何一个子串都是这个字符串某个后缀的前缀,求多个字符串的公共子串相当于求每个字符串后缀的最长公共前缀的最大值
首先要把所有字符串连在一起,之间用题目限定的字符集以外的字符分隔
那么问题转化为:在 $height$ 数组上找连续一段使这一段包含来自每个字符串的至少一个后缀 ,设满足条件的第 $i$ 个区间为 $[l_i,r_i]$ ,最后的答案就是 $\displaystyle{\max_{l_i\leq x\leq r_i}height_i}$
先考虑计算区间:双指针扫一遍,数组记录第 $i$ 个字符串的后缀出现过的次数
再考虑计算答案:类比滑动窗口,维护一个单调队列即可
复杂度 $O(\sum |S| \log\sum|S|)$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 namespace color{ inline void build () { for (rei i=1 ;i<=cnt;++i) for (rei j=L[i];j<=R[i];++j) col[ rk[j] ]=i;} inline void add (int x) { if (!col[x]) return ; ++num[ col[x] ]; if (num[ col[x] ]==1 ) ++tot; } inline void del (int x) { if (!col[x]) return ; --num[ col[x] ]; if (!num[ col[x] ]) --tot; } } int main () { ... scanf ("%d" ,&cnt); for (rei i=1 ;i<=cnt;++i){ L[i]=n+1 ; scanf ("%s" ,s+n+1 ); n+=strlen (s+1 +n); R[i]=n; s[++n]=i+'0' ; } SA::get_sa (),SA::get_he (); color::build (); color::add (1 ); for (rei i=2 ;i<=n;++i){ while (head<tail && height[ q[tail] ]>=height[i]) --tail; q[++tail]=i; color::add (i); if (tot==cnt){ while (tot==cnt && l<i) color::del (l++); color::add (--l); } while (head<tail && q[head]<=l) ++head; if (tot==cnt) ans=max (ans,height[ q[head] ]); } printf ("%d\n" ,ans); ... }
其他乱七八糟不想用SAM写的
求 $\displaystyle{\sum_{1\leq i<j\leq n} i+j-2\times \text{lcp}(T_i,T_j)}$
原式:
$height$ 数组中 $[2,n]$ 内的每个区间给答案贡献区间最小值,单调栈即可
1 2 3 4 5 6 7 8 9 stk[top=1 ]=1 ; for (rei i=2 ;i<=n;++i){ while (top && height[ stk[top] ]>height[i]) R[ stk[top--] ]=i; L[i]=stk[top]; stk[++top]=i; } while (top) R[ stk[top--] ]=n+1 ;ans=(ll) (n-1 )*n*(n+1 )/2 ; for (rei i=2 ;i<=n;++i) ans-=(ll) 2 *(R[i]-i)*(i-L[i])*height[i];
求 $\displaystyle{\sum_{i,j\in \{E\},i<j} \text{lcp}(T_i,T_j)}$
这个类似于上面的内题,但这个是给定的点两两组合,而上一题是全部 $n$
类比上题中用 $height$ 将 $\text{lcp}$ 转化为 $RMQ$ 问题,并用单调栈求答案
考虑本题中是否能得到类似的 $h’$
$\sum t\leq 3\times 10^6$ 的范围与虚树类似,故进行类似虚树的转化:将给定的点按 $rk$ 排序去重,$h’_i=\text{lcp}(st[ a[i-1] ],st[ a[i] ])$ ,那么 $h’$ 与上题的 $height$ 由相同性质,单调栈即可
1 2 3 4 5 6 7 8 9 10 sort (a+1 ,a+1 +q,cmp);q=unique (a+1 ,a+1 +q)-a-1 ; for (rei i=q;i>=2 ;--i) a[i]=lcp (a[i-1 ],a[i]);a[1 ]=0 ,stk[++top]=1 ; for (rei i=2 ;i<=q;++i){ while (top && a[i]<a[ stk[top] ]) R[ stk[top--] ]=i-1 ; L[i]=stk[top]+1 ,stk[++top]=i; } while (top) R[ stk[top--] ]=q;for (rei i=2 ;i<=q;++i) ans+=(ll) a[i]*(R[i]-i+1 )*(i-L[i]+1 );