字尾陣列與基數排序
字串裡算難了。。參考了好多部落格
推薦https://blog.csdn.net/YxuanwKeith/article/details/50636898
每個陣列的含義:sa[i]:排在第i位的子串的位置,、
rank[i]:位置 i 的子串排在第幾位
height[i]:位置為sa[i]的字尾和位置為sa[i-1]的字尾的最長公共字首,即排名相鄰的兩個字尾的最長公共字首
height[rank[i]]:相鄰兩個字尾的最長公共字首
tax[i]:桶
tp[i]:基數排序輔助陣列,意義類似於sa[i],即按第二關鍵字順序排在第i位的子串的位置
rsort()函式:基數排序:用更新sa陣列
void rsort(){//桶排序,每個桶是第一關鍵字,桶內次序是第二關鍵字 for(int i=0;i<=m;i++)tax[i]=0;//桶清空 for(int i=1;i<=n;i++)tax[rank[tp[i]]]++;//把子串按照第二關鍵字次序放進桶中,越先放進桶內的第二關鍵字越小 for(int i=1;i<=m;i++)tax[i]+=tax[i-1];//求桶的字首和 for(int i=n;i>=1;i--)sa[tax[rank[tp[i]]]--]=tp[i];//按第二關鍵字倒序從桶中抽出子串,tp[i]即是對應子串的位置 }
suffix()函式:求出sa陣列,rank陣列,並最終求出height陣列
void suffix(){ for(int i=1;i<=n;i++) rank[i]=a[i],tp[i]=i; int p; m=127; rsort(); for(int w=1;p<n;w<<=1,m=p){//桶的個數更新 for(int i=n-w+1,p=0;i<=n;i++) tp[++p]=i;//長度越界的串第二關鍵字是0(即排在最前面) for(int i=1;i<=n;i++) if(sa[i]>w) tp[++p]=sa[i]-w;//未越界則用上一層的sa求出tp陣列 rsort();swap(rank,tp);rank[sa[1]]=1;p=1;//更新sa陣列,將舊rank轉給tp for(int i=2;i<=n;i++) rank[sa[i]]=cmp(tp,sa[i],sa[i-1],w)?p:++p;//離散化rank(把相等的字串的rank設定為相同),子串相等的用同一個序號 } //求出height陣列 int j,k=0; for(int i=1;i<=n;height[rank[i++]]=k) for(k=k?k-1:k,j=sa[rank[i]-1];a[i+k]==a[j+k];++k) ; }
如何求出height陣列
利用性質height[rank[i]] >= height[rank[i-1]]-1
證明:設suffix[k]是suffix[i-1]前一名的字尾,則其其最長公共字首是height[rank[i-1]],都去掉第一個字元,就變成suffix[k+1]和suffix[i],
顯然suffix[k+1]排在suffix[i]前面,並且其最長公共字首為height[rank[i-1]]-1(就是去掉了第一個字元,其他是一樣的)
那麼height[rank[i]] 要麼 就是 suffix[k+1], 要麼比suffix[k+1]排名更靠近suffix[i],所以height[rank[i]] >= height[rank[i-1]]-1
不用直接求height,而是通過height[rank[i]]間接求
//求出height陣列
int j,k=0; for(int i=1;i<=n;height[rank[i++]]=k) for(k=k?k-1:k,j=sa[rank[i]-1];a[i+k]==a[j+k];++k)//k=height[rank[i-1]]-1,j是證明中的k, ;
cmp()函式:比較兩個二元串是否完全相等
int cmp(int *f,int x,int y,int w){return f[x]==f[y] && f[x+w]==f[y+w];}
下面是例題:求字串中出現兩次及以上的子串
/* 字尾陣列:suffix[i]:字串第i-len的子串,即i開始的字尾 sa[i]:按字典序排在第i個的是第sa[i]個字尾 rank[i]:第i個字尾按字典序排在rank【i]位 height[i]=suffix(sa[i-1])和suffix(sa[i])的最長公共字首,即排在第i個字尾和第i-1個字尾的公共字首 性質1:rank[j]<rank[k]==>字尾j...k的最長公共字首是min(height[rank[j]+1],height[rank[j]+2]...height[rank[k]]) 假設字尾j的排名小於k,那麼這些排名連續的字尾的最長字首就是上面公式 性質2:height[rank[i]]>=height[rank[i-1]]-1 證明:設suffix[k]是排在suffix[i-1]前一名的字尾,則按照height陣列定義,其最長公共字首是height[rank[i-1]] 顯然suffix[k+1]排在suffix[i]前面,並且其最長公共字首為height[rank[i-1]]-1 所以height[rank[i]]即suffix[i]和其前一名的最長公共字首,必定不小於height[rank[i-1]]-1 可以按照height[rank[1]],height[rank[2]]...計算順序 所有後綴子串的字首子串即為原串的所有子串 怎麼構造rank和sa陣列?基數排序即可 怎麼構造height陣列?利用性質2即可 */ #include<iostream> #include<cstring> #include<cstdio> #include<algorihtm> using namespace std; #define maxn 100005 char ch[maxn],all[maxn]; int sa[maxn],rank[maxn],height[maxn],tax[maxn],tp[maxn],a[maxn],n,m; //rank[i]:第i個字尾的排名,sa[i]排名i的字尾的位置; //height[i]排名為i的字尾與排名為i-1的字尾的最長公共字首 //tax[i]基數排序輔助陣列(就是桶),tp[i],rank的輔助陣列(第二關鍵字),類似於sa陣列 //為什麼要增加一個tp陣列,因為前幾次排序後可能有好多個串的排序號是一樣的,這時sa[i]就無法精確表示第i個串是第幾個串 //通過增加tp陣列,tp[i]表示排名i的串是第幾個串,rank[tp[i]]表示i在雙關鍵字排序下的序號(rank[tp[i]]==rank[tp[j]]的情況) //a 原串 char str[maxn]; void rsort(){//基數排序 for(int i=0;i<=m;i++) tax[i]=0;//每個桶清空 for(int i=1;i<=n;i++) tax[rank[tp[i]]]++;//桶上累計 for(int i=1;i<=m;i++) tax[i]+=tax[i-1];//桶累加求字首和,以此求出所有每個桶對應的序號 for(int i=n;i>=1;i--) sa[tax[rank[tp[i]]]--]=tp[i];//找到那個桶的值,對應這個排名的就是第tp[i]個數,之後把桶裡的數減1即可! } int cmp(int *f,int x,int y,int w){return f[x]==f[y] && f[x+w]==f[y+w];}//比較兩個關鍵字 void suffix(){ for(int i=1;i<=n;i++) rank[i]=a[i],tp[i]=i; m=127,rsort();//一開始以單個字元為單位,所以m=127 for(int w=1,p=1;p<n;w<<=1,m=p){//倍增法,不斷更新rank //w:當前子串長度,m:當前離散後排名種類數 //tp:第二關鍵字,可以由上一次sa得到 p=0; for(int i=n-w+1;i<=n;i++) tp[++p]=i;//長度越界,第二關鍵字為0,退化為下標即可 for(int i=1;i<=n;i++) if(sa[i]>w) tp[++p]=sa[i]-w;//排名為p的第二關鍵字是屬於串中下標為a[i]-w的; //字串首w位不會變成第二關鍵字 //要使rank[sa[i]]=i,更新sa,用tp暫存上一輪的rank,離散rank rsort(),swap(rank,tp),rank[sa[1]]=p=1; //用已經完成的sa來更新rank,並離散化:把相等字串的rank設定為相同 for(int i=2;i<=n;i++) rank[sa[i]]=cmp(tp,sa[i],sa[i-1],w)?p,++p; } //求出height陣列 int j,k=0; for(int i=1;i<=n;height[rank[i++]]=k) for(k=k?k-1:k,j=sa[rank[i]-1];a[i+k]==a[j+k];++k)//k=height[rank[i-1]]-1,j是證明中的k, ; } void init(){ scanf("%s",str); n=strlen(str); for(int i=0;i<n;i++) a[i+1]=str[i]; } int main(){ init(); suffix(); int ans=height[2]; for(int i=3;i<=n;i++) ans+=max(height[i]-height[i-1],0); printf("%d\n",ans); } //詢問一個字串中有多少至少出現兩次的子串, //height[i]是兩個字尾的字首長度,為答案做的貢獻是以字首首字母開始的長度從1到height[i]個子串 //同時該貢獻要減去height[i-1]的貢獻,即如果首字母相同的情況下造成的重複計算 //但是如果這個差小於0,那麼顯然第i個字尾和第i-1個字尾首字母就不相同,那麼這種情況貢獻是0
關於height陣列的一些應用:height[i]是排名i和排名i-1的兩個字尾的最長公共字首
1.求一個串中兩個串的最大公共字首:就是height陣列,用rmq預處理,再O(1)查詢
2.一個串中可重疊的重複最長子串:即求任意兩個字尾的最長公共字首,而任意兩個字尾的最長公共字首都是height