「學習筆記」字尾陣列SA
字尾會針對字串上的操作,就像這個題裡面 “ 把字串的所有非空字尾按字典序從小到大排序 ”
字尾排序就是構建字尾陣列的過程(就是把字尾排個序,排完序的陣列就叫字尾陣列)
字尾排序的實現主要依靠倍增法和基數排序來實現
原理
先說字尾排序
定義:
\(sa[i]:\) 排名為\(i\)的字尾的位置
\(rak[i]:\) 從第 \(i\) 個位置開始的字尾的排名,下文為了敘述方便,把從第\(i\)個位置開始的字尾簡稱為"字尾\(i\)"
\(tp[i]\):基數排序的第二關鍵字,意義與\(sa\)一樣
\(tax[i]\):\(i\)號元素出現了多少次,基數排序的桶
(構造字尾陣列還有DC3演算法(三分?)和建字尾樹,跑dfs序的兩種\(O(n)\)
觀察易得,\(rk\) 和 \(sa\) 是個互逆的陣列,也就是 \(rk[sa[i]]=i,sa[rk[i]]=i\)
如果直接sort會爆掉複雜度,因為我們的字串長度和比較的時候複雜度和長度相關
所以我們就對每一位做文章
先是基數排序,把所有的字尾按照第一位的字元排個序
然後考慮倍增
然後比較巧妙的一步就是在於“\(i\)號字尾的前\(\frac{w}{2}\)個字元形成的字串是\(i-\) \(\frac{w}{2}\)號字尾的後\(\frac{w}{2}\)個字元形成的字串”
這裡大概需要意會我們去掉了字尾\(i\)的後面部分
我們每一次執行迴圈中的內容的時候有一個部分排好序的字尾陣列(可以意會的)
這裡每一次考慮倍增新出來的那些位數,對它們進行基數排序就可以了
CODE
#include<bits/stdc++.h> using namespace std; #define int long long #define reg register namespace yspm{ inline int read(){ int res=0,f=1; char k; while(!isdigit(k=getchar())) if(k=='-') f=-1; while(isdigit(k)) res=res*10+k-'0',k=getchar(); return res*f; } const int N=1e6+10; int sa[N],rk[N],ton[N],sec[N],n,m,h[N]; char s[N]; inline void gb(){ memset(ton,0,sizeof(ton)); for(reg int i=1;i<=n;++i) ton[rk[i]]++; for(reg int i=1;i<=m;++i) ton[i]+=ton[i-1]; for(reg int i=n;i>=1;--i) sa[ton[rk[sec[i]]]--]=sec[i]; return ; } signed main(){ scanf("%s",s+1); n=strlen(s+1); m=75; for(reg int i=1;i<=n;++i) rk[i]=s[i]-'0'+1,sec[i]=i; gb(); for(reg int w=1,p=0;p<n;m=p,w<<=1){ p=0; for(reg int i=1;i<=w;++i) sec[++p]=n-w+i; for(reg int i=1;i<=n;++i) if(sa[i]>w) sec[++p]=sa[i]-w; gb(); swap(rk,sec); rk[sa[1]]=p=1; for(reg int i=2;i<=n;++i){ if(sec[sa[i]]==sec[sa[i-1]]&&sec[sa[i]+w]==sec[sa[i-1]+w]) rk[sa[i]]=p; else rk[sa[i]]=++p; } } int k=0; for(reg int i=1;i<=n;++i){ if(rk[i]==1){h[1]=k=0; continue;} int j=sa[rk[i]-1]; if(k>0) --k; while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k]) ++k; h[rk[i]]=k; } for(reg int i=1;i<=n;++i) printf("%lld ",h[i]); puts(""); return 0; } } signed main(){return yspm::main();}
然後字尾陣列比較重要的是 \(h\) 陣列和 \(height\) 陣列
\[height[i]=LCP(sa[i-1],sa[i]) \]\(height\) 陣列有以下性質:
若兩個下標 \(j,k\) 滿足 \(rk_j<rk_k\)
那麼\(LCP(j,k)=\min\limits_{l=j+1}^k height_l\)
這個感覺就比較有用了
然而不太好求
再定義:\(h_i=height[rk_i]\)
然後 \(h\) 陣列有以下性質:
\[h_i\ge h_{i-1}-1 \]關於上面兩條性質的證明?link
運用 \(h\) 陣列的性質,我們就可以在較低複雜度內暴力計算出 \(h,height\)
例題
\(Suffix\ Array\) 求本質不同的子串個數的方法:
求出來 \(sa\) 和 \(height\) 陣列,然後
\[ans=\sum _ {i=1}^n n+1-sa_i-height_i \]字尾陣列求任意子串 \(lcs\) 的方法:
把原串逆過來複制一次放到原串末尾(中間要新增字元的)
然後就又變成了 \(lcp\) 問題
求一些字尾的 \(lcp\) 和的問題
(好像可以用字尾虛樹,即 \(bzoj\ \ SvT\) 一題的全稱就是 \(Suffix\ virtual\ Tree\) ,那題目直接搞個樹然後每次按照消耗戰的做法做就行了)
用字尾陣列就單調棧就好了
套路題小結
\(1.\) 二分 \(rk\) 或者下標
\(JSOI2015\) 串分割中,觀察到相關的性質,減少列舉量,然後二分得解
\(HEOI2016\) 字串中,二分長度結合 \(rmq\) 及對 \(rk\) 用 \(rmq\) 或者主席樹的維護實現最大 \(lcp\) 的查詢
\(2.\) 差分
股市的預測 和 Sandy 的卡片 等題目中,題目涉及到趨勢的詢問,這時候差分再結合相應做法會比較巧妙
\(3.\) 兩串的 \(\cdots\)
考慮把第二個串複製一倍放到地一個後面,中間加入一個字典序大的字元進行維護
總的來看就是說字尾陣列更多的是一個工具,具體還是要靠分析性質和結合二分,資料結構等手段對題目進行維護