1. 程式人生 > >洛谷3809 SA模板 字尾陣列學習筆記(複習)

洛谷3809 SA模板 字尾陣列學習筆記(複習)

其實SA這個東西很久之前就聽過qwq
但是基本已經忘的差不多了

嚶嚶嚶
QWQ感覺自己不是很理解啊
所以寫不出來那種部落格
QWQ只能安利一些別人的部落格了



真的是講的非常好
不要在意名字
orz,膜拜他們

順便弄上自己的程式碼(裡面有一些需要注意的地方)

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<queue>
#include<map>
#include<set>
#define mk makr_pair
#define ll long long
using namespace std;
inline int read()
{
  int x=0,f=1;char ch=getchar();
  while (!isdigit(ch)) {if (ch=='-') f=-1;ch=getchar();}
  while (isdigit(ch)) {x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*f;
}
const int maxn = 2e6+1e2;
int wb[maxn];
int rk[maxn];
int sa[maxn],tmp[maxn];
char a[maxn];
int h[maxn],height[maxn];
int n;
void getsa()
{
 int *x=rk,*y=tmp;
 int s = 128;
 for (int i=1;i<=n;i++) x[i]=a[i],y[i]=i; //初始每個長度為1的字尾的rank是他自己的字元大小,第二關鍵字相當於空,那麼就順次賦值為i 
 for (int i=1;i<=s;i++) wb[i] =0;
 for (int i=1;i<=n;i++) wb[x[y[i]]]++; // 這裡其實基數排序的時候,x表示上一輪的rank,y[i]表示第二關鍵字排名為i的第一關鍵字的位置是多少
    for (int i=1;i<=s;i++) wb[i]+=wb[i-1];//做字首和就能更好的算出來排名,比如說有3個a,2個b,那麼自然第一個b的排名就要從4開始 
 for (int i=n;i>=1;i--) sa[wb[x[y[i]]]--]=y[i]; //只能感性理解了啊qwq之所以倒著列舉是為了保證在第一關鍵字相同的時候,第二關鍵字也是有序的 
 int p = 0;
 for (int j=1;p<n;j<<=1) //p是指本質不同的串的個數 
 {
  //x表示上一輪的rank
  //y表示排名為i的第二關鍵字的第一關鍵字的位置是多少(空優先) 
  p=0;
  //這裡可以這麼理解,如果一個串他的位置是大於n-j+1,那麼他一定是沒有第二關鍵字的。 
  for (int i=n-j+1;i<=n;i++) y[++p]=i; //第二關鍵字為空,就排名靠前 
     for (int i=1;i<=n;i++) if (sa[i]>j) y[++p]=sa[i]-j; //如果排名為i的位置是大於j的,那麼他可以成為一個第二關鍵字,並且第一關鍵字的位置應該是sa[i]-j;
  for (int i=1;i<=s;i++) wb[i]=0;
  for (int i=1;i<=n;i++) wb[x[y[i]]]++;
  for (int i=1;i<=s;i++) wb[i]+=wb[i-1];
  for (int i=n;i>=1;i--) sa[wb[x[y[i]]]--]=y[i]; //這裡i之所以從n開始,因為我們要保證排序第一關鍵字的時候,第二關鍵字一定也是符合原來的順序的,就是說,原來第二關鍵字大的,一定在後面(這個是基數排序的思想)
  swap(x,y);//交換之後,y表示上一輪的rank,x是一個新的陣列 
  p=1;
  x[sa[1]]=1;
  //若兩個串的兩部分在上一輪rank都相等的話, 那麼無法分辨,所以p不用加 
  for (int i=2;i<=n;i++) x[sa[i]] = (y[sa[i]]==y[sa[i-1]] && y[sa[i-1]+j]==y[sa[i]+j]) ? p : ++p;
  s=p;
 } 
 for (int i=1;i<=n;i++) rk[sa[i]]=i;
 h[0]=0;
 for (int i=1;i<=n;i++)
 {
  //h[i]表示i號字尾與它前一名的字尾的最長公共字首 
  //height[i]表示排名為i的字尾和排名為i-1的字尾的lcp 
  h[i]=max(h[i-1]-1,0);
  while (i+h[i]<=n && sa[rk[i]-1]+h[i]<=n && a[i+h[i]]==a[sa[rk[i]-1]+h[i]])
    h[i]++;
 }
 for (int i=1;i<=n;i++) height[i]=h[sa[i]];
}
int main()
{
  scanf("%s",a+1);
  n=strlen(a+1);
  getsa();
  for (int i=1;i<=n;i++) cout<<sa[i]<<" ";
  return 0;
}

Update

整理一些\(SA\)的小性質和經典應用。(會持續更新的)

1.求兩個字尾的\(lcp\) ,應該是\(min(height[rk[i]+1],height[rk[i]+2].....height[rk[j]])\)

2.對於排名為\(i\)的字尾,與它\(lcp\)最長的字尾應該是排名為\(i-1\),(可以理解為越靠前差異越多,越靠前,取\(min\)的區間就越長)

3.最長可重疊重複子串,應該是\(max(height[i])\)(這裡把子串看成字尾的字首,同時依據性質2就能得到)

4.給定一個子串,求不相同子串的個數,這裡要這麼考慮,按照字典序加入,每加入一個字串,會新增加\(n-sa[i]+1\)

個新的子串,但是會重複\(height[i]\)個,\((只有lcp會重複,同時依據性質2)\)

5.給定兩個串,求他們的最長公共子串。
將B串拼到A串後面,然後中間新增一個非法字元,然後直接想詢問最大的lcp(保證\(sa[i]和sa[i-1]\)分別位於兩個串即可)

6.給定兩個串,求他們的公共子串數目。

將B串拼到A串後面,然後中間新增一個非法字元,然後對於每個\(height\)用單調棧維護出左右最遠能擴充套件到哪裡。然後\(ans\)加上\(height[i]*(geta(i-1,l[i]-1)*getb(r[i],i)+getb(i-1,l[i]-1)*geta(r[i],i))\)

這裡之所以是這個式子的原因(第一要保證是一個端點屬於A串,一個屬於B串。另一個原因是因為對於一個擴充套件區間\([l,pos,r]\)來說,選擇字尾的右端點是在\([pos,r]\)而左端點是\([l-1,pos-1]\),因為字尾的選擇的左邊對於\(height\)是開區間,參考性質1。