1. 程式人生 > >[字尾陣列][學習筆記]

[字尾陣列][學習筆記]

定義——來自百度百科

子串

一個字串中連續的一段成為這個字串的子串。

字尾

字尾是指從某個位置 i 開始到整個串末尾結束的一個特殊子串。字串 r 的從 第 i 個字元開始的字尾表示為 Suffix(i) ,也就是Suffix(i)=r[i,len(r)] 。

子串的大小

大小比較:關於字串的大小比較,是指通常所說的“字典順序”比較,也就是對於兩個字串u、v,令i 從1 開始順次比較u[i]和v[i],如果u[i]=v[i]則令 i 加1,否則若u[i]<v[i]則認為u<v,u[i]>v[i]則認為u>v(也就是v<u),比較結束。如果i>len(u)或者i>len(v)仍比較不出結果,那麼若len(u)<len(v) 則認為u<v ,若len(u)=len(v) 則認為u=v ,若len(u)>len(v)則u>v。

從字串的大小比較的定義來看,S 的兩個開頭位置不同的字尾u 和v 進行比較的結果不可能是相等,因為u=v 的必要條件len(u)=len(v)在這裡不可能滿足。

字尾陣列

在後綴陣列中,每個字尾我們都用它的起始位置來表示。sa[x]表示排名為x的字尾是哪一個(裡面存的就是這個字尾的起始位置)。

名次陣列

名次陣列是與字尾陣列對應的,rk[i]表示i這個字尾的排名。

演算法

那如何求字尾陣列呢,也就是對這n(字串長度)個字尾排序?

暴力做法就是對所有的字尾進行排序。這樣複雜度就成了\(O(n^2log(n))\)

求字尾陣列一般使用倍增演算法。

那麼怎麼倍增呢。

考慮我們暴力排序的時候,其實就是先比較每個字尾的第一位,但是可能會有第一位相同的,這時候再去比較第二位。依次類推。

假設我們現在已經對第一位排好序了,然後我們需要去比較第二位。好了,我們把前兩位都比較完了,還是有不能區分出名次的字尾,這時候去比較第三位???不不不

這時候我們發現,我們要繼續比較的第3位和第4位其實是其他的字尾的前兩位,也就是說我們之前的時候已經獲得他們的排名了。那麼何必重新比較呢。繼續利用之前比較的不是很好嗎。

來張圖

圖片還是來自百度百科,懶的畫圖

如圖可以看到,對於每次排序的時候都有兩個關鍵字,第一個關鍵字是之前排序的結果,第二個是我們在倍增之後獲得的序號。在按照第二關鍵字排序的時候一定要在第一關鍵字的基礎上。

這個其實很好懂,就像是平時排成績表。先按照總分排名次,總分相同的再按照資訊成績(笑)排。這裡也是一樣,第二關鍵字的作用就是對於之前無法區分出來的繼續進行區分。

實現

其實字尾陣列的原理並不是很難,難點在於程式碼的實現。其實可以使用快排,但是複雜度會多個log。為了保證他的高效,一般都會使用桶排。

具體實現見程式碼註釋吧。真的挺繞挺難懂的。

程式碼

/*
* @Author: wxyww
* @Date:   2018-12-18 11:09:32
* @Last Modified time: 2018-12-18 19:12:48
*/
#include<cstdio>
#include<iostream>
#include<cstdlib>
#include<cmath>
#include<ctime>
#include<cstring>
#include<bitset>
using namespace std;
typedef long long ll;
const int N = 1000000 + 100;
ll read() {
   ll x=0,f=1;char c=getchar();
   while(c<'0'||c>'9') {
      if(c=='-') f=-1;
      c=getchar();
   }
   while(c>='0'&&c<='9') {
      x=x*10+c-'0';
      c=getchar();
   }
   return x*f;
}
int c[N],sa[N],x[N],y[N];
//c為桶,sa[i]表示排名為i的元素的位置,x為當前陣列的樣子,y[i]為按照第二關鍵字排序排名為i的數字的第一關鍵字的位置
int s[N];
int n,m;
void work() {
   for(int i = 1;i <= n;++i) ++c[x[i] = s[i]];//處理出c陣列,並且把字串賦值到x上
   for(int i = 2;i <= m;++i) c[i] += c[i - 1];//對c[i]做字首和,來求出當前元素最靠後的排名,因為有重複的元素
   for(int i = n;i >= 1;--i) sa[c[x[i]]--] = i;//c[x[i]]--是因為使得每個排名都有元素,就先給他們人為安排一種順序
   for(int k = 1;k <= n;k <<= 1) {
      int num = 0;
      for(int i = n - k + 1;i <= n;++i) y[++num] = i;//對於沒有第二關鍵字的,他們的第二關鍵字是最靠前的
      for(int i = 1;i <= n;++i) if(sa[i] > k) y[++num] = sa[i] - k;//sa[i]儲存的是單獨這個元素的排名,所以只看第二關鍵字的話,就這麼排咯
      for(int i = 1;i <= m;++i) c[i] = 0;
      for(int i = 1;i <= n;++i) ++c[x[i]];//還是處理出c陣列
      for(int i = 2;i <= m;++i) c[i] += c[i - 1];
      //!!!結合第一關鍵字和第二關鍵字進行排名
      for(int i = n; i >= 1;--i) sa[c[x[y[i]]]--] = y[i];
      //這裡要好好思考,y[i]表示第二關鍵字排名為i的元素的位置。x[y[i]]表示這個元素真正是多少,c[x[y[i]]]表示這種元素現在應該排多少,sa[c[x[y[i]]]]就表示這個排名所對應的元素
      //!!!
      for(int i = 1; i <= n;++i) y[i] = 0;
      swap(x,y);
      //把x陣列清空,因為一會要按照新的排名重新給x賦值,並且把現在x的值賦給y,因為一會還要用
      x[sa[1]] = 1;
      num = 1;
      for(int i = 2;i <= n;++i) {
         if(y[sa[i]] == y[sa[i - 1]] && y[sa[i] + k] == y[sa[i - 1] + k]) 
            x[sa[i]] = num;
         //如果通過當前關鍵字進行篩選之後,兩個字尾還是無法區分出大小,那麼就把他們賦值成一樣大的,留給以後繼續區分
         else x[sa[i]] = ++num;
      }
      if(num == n) break;
      m = num;
   }  
   for(int i = 1;i <= n;++i) printf("%d ",sa[i]);
}
int main() {
   // scanf("%s",s + 1);
   // n = strlen(s + 1);
   // m = 122;
   n = read();
   for(int i = 1;i <= n;++i) s[i] = read();
   m = 200;
   work();
   return 0;
}

LCP

僅僅是對字尾進行排名,就顯示不出字尾陣列的強大了。他的另一個非常常用的作用就是求LCP。

什麼是LCP

LCP是指最長公共字首。也就是我們要對字尾求公共字首。

這有什麼作用呢。對字尾求公共字首,得到的就是一個字串內的重複子串啊。這在後面的一些題中會有用處。

一些性質

這裡我們用height[i]來表示排名為i的字尾與排名為i-1的字尾的LCP

為了方便,我們在借用定義一些陣列,rk[i]表示i這個字尾的排名(其實就是上面的名次陣列),h[i]表示height[rk[i]] (似乎就只能這麼表達了)。建議把這裡搞清楚。

性質一:\(\color {red}{h[i] >= h[i - 1] - 1}\)

就是說,排名為i的字尾與排名為i-1的字尾的最長公共字首大於等於排名為i-1的字尾與排名為i - 2的字尾 - 1.

性質二:\(\color{red}{LCP(i,k) = min\{ LCP(j,j - 1) \}(i < j \leq k)}\)

也就是說,排名為i的字尾與排名為k的字尾的最長公共字首,等於從i到k中所有串的最長公共字首中最小的那個。

利用性質一,我們可以求出來h陣列和height陣列,利用性質二,可以求出任意兩個串的LCP。

求LCP

依據性質一,我們可以把h陣列遞推出來。直接看程式碼其實就很明瞭

void get_height() {
   for(int i = 1;i <= n;++i) rk[sa[i]] = i;
   int k = 0;
   for(int i = 1;i <= n;++i) {
      if(rk[i] == 1) continue;
      if(k) --k;
      int j = sa[rk[i] - 1];
      while(j + k <= n && i + k <= n && a[j + k] == a[i + k]) ++k;
      height[rk[i]] = k;
   }
}

幾道例題

luogu3809

只需要求出來sa陣列就行了。

bzoj1692

題解

bzoj1717

題解

POJ1743

求不重疊的最長重複子串。

二分一下答案,看是不是有重疊,就行了。

程式碼(POJ1743)

/*
* @Author: wxyww
* @Date:   2018-12-18 15:20:59
* @Last Modified time: 2018-12-18 21:49:26
*/
#include<cstdio>
#include<iostream>
#include<cstdlib>
#include<cmath>
#include<ctime>
#include<bitset>
using namespace std;
typedef long long ll;
const int N = 20000 + 100,INF = 1e8;
ll read() {
   ll x=0,f=1;char c=getchar();
   while(c<'0'||c>'9') {
      if(c=='-') f=-1;
      c=getchar();
   }
   while(c>='0'&&c<='9') {
      x=x*10+c-'0';
      c=getchar();
   }
   return x*f;
}
int sa[N],rk[N],a[N],c[N],x[N],y[N];
int height[N],h[N];
int n,m;
void get_sa() {
   for(int i = 1;i <= m;++i) c[i] = 0;
   for(int i = 1;i <= n;++i) ++c[x[i] = a[i]];
   for(int i = 2;i <= m;++i) c[i] += c[i - 1]; 
   for(int i = n;i >= 1;--i) sa[c[x[i]]--] = i;
   for(int k = 1;k <= n;k <<= 1) {
      int num = 0;
      for(int i = n - k + 1;i <= n; ++i) y[++num] = i;
      for(int i = 1;i <= n;++i) if(sa[i] > k) y[++num] = sa[i] - k;
      for(int i = 2;i <= m;++i) c[i] = 0;
      for(int i = 1;i <= n;++i) ++c[x[i]];
      for(int i = 1;i <= m;++i) c[i] += c[i - 1];
      for(int i = n;i >= 1;--i) sa[c[x[y[i]]]--] = y[i];
      swap(x,y);
      num = 0;
      x[sa[1]] = ++num;
      for(int i = 2;i <= n;++i) {
         if(y[sa[i]] == y[sa[i - 1]] && y[sa[i] + k] == y[sa[i - 1] + k]) x[sa[i]] = num;
         else x[sa[i]] = ++num;
      }
      if(num == n) break;
      m = num;
   }
}
void get_height() {
   for(int i = 1;i <= n;++i) rk[sa[i]] = i;
   int k = 0;
   for(int i = 1;i <= n;++i) {
      if(rk[i] == 1) continue;
      if(k) --k;
      int j = sa[rk[i] - 1];
      while(j + k <= n && i + k <= n && a[j + k] == a[i + k]) ++k;
      height[rk[i]] = k;
   }
}
int check(int len) {
   int mn = INF,mx = -INF;
   for(int i = 1;i <= n;++i) {
      if(height[i] >= len) {
         mn = min(mn,min(sa[i],sa[i - 1]));
         mx = max(mx,max(sa[i],sa[i - 1]));
         if(mx - mn > len) return 1;
      }
      else mn = INF,mx = -INF;
   }
   return 0;
}
void erfen() {
   int ans = -1,l = 4,r = n;
   while(l <= r) {
      int mid = (l + r) >> 1;
      if(check(mid)) l = mid + 1,ans = mid;
      else r = mid - 1;
   }
    printf("%d\n",ans + 1);
}
int main() {
   while(1) {
      n = read();
      m = -1;
      if(n == 0) break;
      for(int i = 1;i <= n;++i) a[i] = read();
      for(int i = 1;i < n;++i) a[i] = a[i + 1] - a[i] + 100;
      m = 200;
      get_sa();

      get_height();
      erfen();
   }
   return 0;
}

/*
30
25 27 30 34 39 45 52 60 69 79 69 60 52 45 39 34 30 26 22 18 82 78 74 70 66 67 64 60 65 80
*/