字尾陣列學習筆記
阿新 • • 發佈:2020-12-26
## 作用
對於一個字串的字尾按照字典序進行排序
通常的求法是 $nlogn$ 的倍增做法
網上的部落格都很詳細
比如[這篇](https://www.cnblogs.com/lykkk/p/10520070.html) 和 [這篇](https://www.cnblogs.com/zwfymqz/p/8413523.html)
這裡只放一下板子,並說一下幾種常見的題型
``` cpp
#define rg register
const int maxn=1e6+5;
int n,m,sa[maxn],fir[maxn],sec[maxn],tax[maxn],hei[maxn];
void Qsort(){
for(rg int i=0;i<=m;i++) tax[i]=0;
for(rg int i=1;i<=n;i++) tax[fir[i]]++;
for(rg int i=1;i<=m;i++) tax[i]+=tax[i-1];
for(rg int i=n;i>=1;i--) sa[tax[fir[sec[i]]]--]=sec[i];
}
void getsa(){
m=10000;
for(rg int i=1;i<=n;i++) fir[i]=s[i],sec[i]=i;
Qsort();
for(rg int len=1,p=0;plen) sec[++p]=sa[i]-len;
Qsort();
std::swap(fir,sec);
fir[sa[1]]=p=1;
for(rg int i=2;i<=n;i++) fir[sa[i]]=(sec[sa[i]]==sec[sa[i-1]] && sec[sa[i]+len]==sec[sa[i-1]+len])?p:++p;
}
}
void getheight(){
rg int j,k=0;
for(rg int i=1;i<=n;i++){
if(k) k--;
j=sa[fir[i]-1];
while(s[i+k]==s[j+k]) k++;
hei[fir[i]]=k;
}
}
```
## 題型一:字尾排序
這種題一般給你一個長度為 $n$ 的字串,你可以選擇將最前面的字元移到最後,求字典序最小的方案
解決方法就是把原陣列複製一遍,然後跑一下字尾排序
後兩道 $USACO$ 的題則需要把原串翻轉接在最後,思想很巧妙
例題:[P4051 [JSOI2007]字元加密](https://www.luogu.com.cn/problem/P4051) [P1368 【模板】最小表示法](https://www.luogu.com.cn/problem/P1368) [P6140 [USACO07NOV]Best Cow Line S](https://www.luogu.com.cn/problem/P6140) [P2870 [USACO07DEC]Best Cow Line G](https://www.luogu.com.cn/problem/P2870)
## 題型二:不同性質子串個數
用總的子串的個數 $\frac{n(n+1)}{2}$ 減去重複的子串的個數 $\sum_{i=1}^{n}height[i]$
例題:[P2408 不同子串個數](https://www.luogu.com.cn/problem/P2408) [P4070 [SDOI2016]生成魔咒](https://www.luogu.com.cn/problem/P4070) [SP705 SUBST1 - New Distinct Substrings](https://www.luogu.com.cn/problem/SP705) [SP694 DISUBSTR - Distinct Substrings](https://www.luogu.com.cn/problem/SP694)
第二道題要稍稍做一下轉化,把向結尾加字元轉化成向前加字元
這樣每次只會有一個新的字尾加入,我們只需要用一個 $set$ 找該字尾的前驅後繼計算答案即可
## 題型三:利用$height$陣列的性質計算
對於$height$陣列,有如下的式子
$height[i]=LCP(sa[i−1],sa[i])$
$LCP(j,k)=min_{l=j+1}^kheightl$
例題:[P4248 [AHOI2013]差異](https://www.luogu.com.cn/problem/P4248) [#3879. SvT](https://darkbzoj.tk/problem/3879)
這兩道題都利用了$height$陣列第二個取 $min$ 的性質
對於 $height$ 陣列中的每一個值,記錄一下它向左和向右能做的最遠的貢獻,可以用單調棧實現
核心程式碼
``` cpp
sta[++tp]=1;
for(rg int i=2;i<=n;i++){
while(tp && heig[i]<=heig[sta[tp]]){
r[sta[tp]]=i;
tp--;
}
l[i]=sta[tp];
sta[++tp]=i;
}
while(tp){
r[sta[tp--]]=n+1;
}
ans=1LL*(n+1)*n*(n-1)/2;
for(rg int i=1;i<=n;i++){
ans-=2LL*(i-l[i])*(r[i]-i)*heig[i];
}
```
## 題型四:求不同串的最長的公共子串的長度
我們把這些串連成一個長串,在串與串相接的地方插入一個沒有出現過的特殊符號,防止出現重合的問題
然後求出整個串的 $height$ 陣列,並對於每一個$height$ 陣列染色,標記它屬於原來的哪一個串
然後用雙指標從前到後掃一遍,當記錄到的不同串的個數等於總的串的個數時取一下最大值
最後一道題還需要差分一下
例題:[SP1811 LCS - Longest Common Substring](https://www.luogu.com.cn/problem/SP1811) [SP10570 LONGCS - Longest Common Substring](https://www.luogu.com.cn/problem/SP10570) [SP1812 LCS2 - Longest Common Substring II](https://www.luogu.com.cn/problem/SP1812) [[SDOI2008]Sandy的卡片](https://www.luogu.com.cn/problem/P2463)
完整程式碼
``` cpp
#include
#include
#include
#define rg register
const int maxn=1e6+5;
int n,m,sa[maxn],fir[maxn],sec[maxn],tax[maxn],hei[maxn],t,l[maxn],r[maxn],col[maxn],cnt[maxn],js,q[maxn],head,tail,ans;
char s[maxn];
void Qsort(){
for(rg int i=0;i<=m;i++) tax[i]=0;
for(rg int i=1;i<=n;i++) tax[fir[i]]++;
for(rg int i=1;i<=m;i++) tax[i]+=tax[i-1];
for(rg int i=n;i>=1;i--) sa[tax[fir[sec[i]]]--]=sec[i];
}
void getsa(){
memset(sa,0,sizeof(sa));
memset(fir,0,sizeof(fir));
memset(sec,0,sizeof(sec));
m=300;
for(rg int i=1;i<=n;i++) fir[i]=s[i]-'0'+1,sec[i]=i;
Qsort();
for(rg int len=1,p=0;plen) sec[++p]=sa[i]-len;
Qsort();
std::swap(fir,sec);
fir[sa[1]]=p=1;
for(rg int i=2;i<=n;i++) fir[sa[i]]=(sec[sa[i]]==sec[sa[i-1]] && sec[sa[i]+len]==sec[sa[i-1]+len])?p:++p;
}
}
void getheight(){
memset(hei,0,sizeof(hei));
rg int j,k=0;
for(rg int i=1;i<=n;i++){
if(k) k--;
j=sa[fir[i]-1];
while(s[i+k]==s[j+k]) k++;
hei[fir[i]]=k;
}
}
void xg(rg int now,rg int op){
if(col[now]==0) return;
if(cnt[col[now]]==0) js++;
cnt[col[now]]+=op;
if(cnt[col[now]]==0) js--;
}
int T;
int main(){
scanf("%d",&T);
while(T--){
memset(col,0,sizeof(col));
memset(l,0,sizeof(l));
memset(r,0,sizeof(r));
memset(cnt,0,sizeof(cnt));
ans=js=0;
scanf("%d",&t);
rg int len;
for(rg int i=1;i<=t;i++){
l[i]=n+1;
scanf("%s",s+n+1);
len=strlen(s+n+1);
n+=len;
r[i]=n;
s[++n]='A'+i;
}
if(t==1){
printf("0\n");
return 0;
}
getsa();
getheight();
for(rg int i=1;i<=t;i++){
for(rg int j=l[i];j<=r[i];j++){
col[fir[j]]=i;
}
}
rg int nl=1;
xg(1,1);
for(rg int nr=2;nr<=n;nr++){
while(head<=tail && hei[nr]<=hei[q[tail]]) tail--;
q[++tail]=nr;
xg(nr,1);
if(js==t){
while(js==t && nl=1;i--){
f[i]=std::max(f[i],f[i+1]);
}
```
例題:[P2852 [USACO06DEC]Milk Patterns G](https://www.luogu.com.cn/problem/P2852) [SP8222 NSUBSTR - Substrings](https://www.luogu.com.cn/problem/SP8222)
## 題型七:一些綜合性比較強的題目
[P1117 [NOI2016]優秀的拆分](https://www.luogu.com.cn/problem/P1117)
主要考察怎麼利用字首和字尾的性質求類似於 $AA$ 的子串的個數
考慮列舉一個 $Len$ ,然後對於每個點求出他是否是一個 $2 \times Len$ 的 $AA$ 串的開頭 / 結尾。
我們每隔 $Len$ 放一個點,這樣每一個 長度為 $2 \times Len$ 的 $AA$ 串都至少會經過兩個相鄰的點。
所以再轉換為每兩個相鄰的點會對 $a, b$ 產生多少貢獻。
先求出這對相鄰點所代表的字首的最長公共字尾 $LCS$ 和 所代表的字尾的最長公共字首 $LCP$
如果 $LCP + LCS < Len$ 肯定不合法
否則給合法的區間整體加一
參考[洛谷題解](https://www.luogu.com.cn/problem/solution/P1117)
程式碼實現
``` cpp
#include
#include
#include
#define rg register
inline int read(){
rg int x=0,fh=1;
rg char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') fh=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*fh;
}
const int maxn=3e4+5;
int n,m,sa[maxn],fir[maxn],sec[maxn],tax[maxn],hei[maxn],hei1[maxn],hei2[maxn],fir1[maxn],fir2[maxn],lg[maxn],mmin1[maxn][20],mmin2[maxn][20],t;
char s[maxn];
void Qsort(){
for(rg int i=0;i<=m;i++) tax[i]=0;
for(rg int i=1;i<=n;i++) tax[fir[i]]++;
for(rg int i=1;i<=m;i++) tax[i]+=tax[i-1];
for(rg int i=n;i>=1;i--) sa[tax[fir[sec[i]]]--]=sec[i];
}
void getsa(){
memset(sa,0,sizeof(sa));
memset(fir,0,sizeof(fir));
memset(sec,0,sizeof(sec));
m=3e4+1;
for(rg int i=1;i<=n;i++) fir[i]=s[i],sec[i]=i;
Qsort();
for(rg int len=1,p=0;plen) sec[++p]=sa[i]-len;
Qsort();
memcpy(sec,fir,sizeof(fir));
fir[sa[1]]=p=1;
for(rg int i=2;i<=n;i++) fir[sa[i]]=(sec[sa[i]]==sec[sa[i-1]] && sec[sa[i]+len]==sec[sa[i-1]+len])?p:++p;
}
}
void getheight(){
memset(hei,0,sizeof(hei));
rg int j,k=0;
for(rg int i=1;i<=n;i++){
if(k) k--;
j=sa[fir[i]-1];
while(s[i+k]==s[j+k]) k++;
hei[fir[i]]=k;
}
}
int cf1[maxn],cf2[maxn];
long long ans;
int getans1(rg int l,rg int r){
rg int k=lg[r-l+1];
return std::min(mmin1[l][k],mmin1[r-(1<>1]+1;
t=read();
while(t--){
ans=0;
memset(cf1,0,sizeof(cf1));
memset(cf2,0,sizeof(cf2));
scanf("%s",s+1);
n=strlen(s+1);
getsa();
getheight();
memcpy(hei1,hei,sizeof(hei));
memcpy(fir1,fir,sizeof(fir));
std::reverse(s+1,s+1+n);
getsa();
getheight();
memcpy(hei2,hei,sizeof(hei));
memcpy(fir2,fir,sizeof(fir));
std::reverse(s+1,s+1+n);
for(rg int i=1;i<=n;i++) mmin1[i][0]=hei1[i],mmin2[i][0]=hei2[i];
for(rg int j=1;j<=15;j++){
for(rg int i=1;i+(1<ac2) std::swap(ac1,ac2);
ac3=getans2(ac1+1,ac2);
ac1=fir1[i],ac2=fir1[j];
if(ac1>ac2) std::swap(ac1,ac2);
ac4=getans1(ac1+1,ac2);
ac3=std::min(ac3,len);
ac4=std::min(ac4,len);
if(ac3+ac4-1