后缀数组 (Suffix Array)

后缀数组 (Suffix Array)

VectorVector

0x00 Problems:

先来看一道题吧:后缀排序 - 题目

这是一道模板题,就是要对一个字符串的所有后缀进行排序,并且输出排序好之后相邻的后缀的LCP长度,后缀树和后缀数组都可以,但是后缀树构造太难懂(作者太傻),只好打后缀数组了。

------------------------------------------------------2016.6.10--------------------------------------------------------

再来一道水题 BZOJ 1031 。只要把原本的串复制两份接起来,后缀排序即可。

0x01 Suffix Array:

首先,对于一个字符串 s,它的“后缀 i”表示“以下标为 i 开头的后缀”。而后缀数组就是所有后缀按照字典序从小到大排序后的结果。

如何构造后缀数组呢?最简单的就是快速排序一遍,这是极其暴力的,时间复杂度为O(n^{2} logn )。而倍增算法就很好的利用了每个后缀之间的联系,在O(nlogn) 时间内构造出后缀数组。

首先我们用基数排序求到每个字符串中的字符的名次。这里我们就以aabaaaab为例,如图:


第一轮之后的结果就是这样。那么第二轮,就是对每个后缀的前两个字符进行排序。因为每个单字符的名次已经得出。就相当于对一个二元组(x,y)进行排序,且以 x 为第一关键字,以 y 为第二关键字。这么排序之后就得到了这么一幅图:


接下来继续倍增,对前四个字符进行排序,此时依旧相当于对一个二元组(x,y)排序,排序规则相同。此时的 x 和 y 分别表示前两个字符的名次和第三、四个字符的名次(在第二次排序已经将其全部求出),所以同上进行排序,就能得到这样的一幅图:


这是我们可以发现每个后缀的名次已经完全不同,那么此时就可以退出。本来是还需要倍增的,但因为已经完全不同,继续倍增,结果也不会有变化。

那么给出后缀数组构造的代码:


gets(s); len = strlen(s);
// 对后缀的第一个字符进行基数排序,m 表示名次的最大值。
for (int i = 0; i < m; i++) buc[i] = 0; // buc 是一个桶
for (int i = 0; i < len; i++) buc[x[i] = s[i]]++;
for (int i = 1; i < m; i++) buc[i] += buc[i - 1];
for (int i = len - 1; i >= 0; i--) SA[--buc[x[i]]] = i;
for (int k = 1; k <= len; k <<= 1) { // k 倍增
  int p = 0;
  // y[i] 用来表示名次为 i 的后缀(j + k) 的 j 。接下来直接用 SA 对第二关键字排序

  for (int i = len - 1; i >= len - k; i--) y[p++] = i;
  // 后缀 len - k 及之后的所有后缀第二关键字最小。
  for (int i = 0; i < len; i++) if (SA[i] >= k) y[p++] = SA[i] - k;
  // SA[i] >= k 表示改后缀出现在 k 之后,那么就是第二关键字,
  // 且因为 SA 已排序好,这样便可以通过这个排序出第二关键字。
  for (int i = 0; i < m; i++) buc[i] = 0;
  for (int i = 0; i < len; i++) buc[x[y[i]]]++;
  for (int i = 1; i < m; i++) buc[i] += buc[i - 1];
  for (int i = len - 1; i >= 0; i--) SA[--buc[x[y[i]]]] = y[i];
  swap(x, y);
  p = 1; x[SA[0]] = 0;
  // 重新计算每个一元的名次。
  for (int i = 1; i < len; i++) {
    if (y[SA[i - 1]] == y[SA[i]] && y[SA[i - 1] + k] == y[SA[i] + k])
      x[SA[i]] = p - 1;
    else x[SA[i]] = p++;
  }
  if (p >= len) break; // 每个后缀的名次已经完全不同,不需要继续倍增
  m = p; // 更新名次的最大值。
}

那么这样我们就构造出了后缀数组。但本题还有一问,着我们要怎么解决呢?就需要两个辅助数组:rank[],height[]。rank[i] 用来记录后缀 i 在 SA 数组的位置。height[i] 记录后缀 SA[i] 和 SA[i - 1] 的 LCP 的长度。

首先很容易求出 rank 数组:


for (int i = 0; i < len; i++) rank[SA[i]] = i;

如何计算 height 数组呢?最简单的办法,相邻的两个后缀硬求一遍 LCP 时间复杂度 O(n^2),有一种更加高效的方法,只需要 O(n) 时间即可。先令一个辅助数组 h,其中 h[i] = height[rank[j]]。这里有一个神奇的性质:h[i] \geq h[i - 1] - 1.先来证明一下吧。


设排在后缀(i - 1)前一个的是后缀 k 。后缀(i - 1)和后缀 k 分别删除首字母后得到的是后缀 i 和后缀(k + 1)。因为后缀 k 排在 后缀(i - 1)之前,所以后缀(k + 1) 必定也排在后缀 i 之前,并且它们的 LCP 长度为h[i - 1] + 1。很显然,h[i - 1] - 1 是一系列 h 的最小值。包括排在后缀 i 之前的一个后缀 p 和 后缀 i 的 LCP 长度,即 h[i]。给出代码:


for (int i = 0; i < len; i++) {
  if (rank[i] == 0) {height[0] = 0; continue;} // 第一个后缀的 LCP 为 0。
  if (k) k--; // 从 k - 1 开始推
  int j = SA[rank[i] - 1];
  while (s[i + k] == s[j + k] && i + k < len && j + k < len) k++;
  height[rank[i]] = k;
}

到这里已经可以解决本题了。

0x02 后话

但其实后缀数组还有很多应用。

比如,可以通过对后缀数组的二分查找解决在线的多模版匹配问题。

还有,利用上述 height 数组可以利用 RMQ 来求出任意两个后缀的 LCP。

正如评论中@后缀自动机·张 所说的,还是SAM稳啊!

比如 Problem - 4622

struct SuffixAutomaton {
  int to[N][30], fail[N], step[N];
  int last, Tcnt, sum;
  int Q[N];
  int Qcnt;
  int Extend(int key) {
    step[++Tcnt] = step[last] + 1;
    int p = last, u = Tcnt;
    memset(to[u], 0, sizeof to[u]);
    for (; !to[p][key]; p = fail[p]) to[p][key] = u;
    if (!p) {
      fail[u] = 1;
    } else {
      int q = to[p][key];
      if (step[q] != step[p] + 1) {
        step[++Tcnt] = step[p] + 1;
        int v = Tcnt;
        memset(to[v], 0, sizeof to[v]);
        memcpy(to[v], to[q], sizeof to[q]);
        fail[v] = fail[q];
        fail[q] = fail[u] = v;
        for (; to[p][key] == q; p = fail[p])
          to[p][key] = v;
      } else {
        fail[u] = q;
      }
    }
    last = u;
    return sum += step[last] - step[fail[last]]; 
  }
  void Init(char *S, int len) {
    Tcnt = last = 1; sum = 0;
    for(int i = 0; i < len; i++)
       Extend(S[i] - st);
  }
  inline void Print(void) {
    for (int i = 0; i < Qcnt; i++)
      putchar(Q[i] + st);
    puts("");
  }
  void dfs(int u) {
    for (int i = 0; i < 26; i++)
      if (to[u][i]) {
        Q[Qcnt++] = i;
        Print();
        dfs(to[u][i]);
        --Qcnt;
      }
  }
  void Debug(void) {
    Qcnt = 0; dfs(0);
  }
  void Clear(void) {
    Tcnt = last = 1; sum = 0;
    memset(to[1], 0, sizeof to[1]);
  }
};


-----------------------------------------------------------完-----------------------------------------------------------

文章被以下专栏收录
  • 谈谈ACM算法和思想,希望小学生坐在马桶上也能听懂。

    进入专栏
23 条评论