考完了NOIP,虽然D2脑子进水,然而还是目测水到了一等奖,避免了GG。也是时候开启一些新的算法了。思来想去,还是搞一下后缀数组吧。
先简单说明后缀数组,是啥。字符串后缀知道吧。数组知道吧。后缀数组就是在将一个字符串的所有后缀按照常见的字典序排法,排一下。显然我们可以用stdsort来进行非常暴力的排序,然而复杂度为n^2logn,这并不优美。我们需要找到一种更高效的算法,有两种,一种是倍增,一种是Dc3,后者为线性复杂度,然而常数较大,并且难以理解,代码较长,所以先来研究一下第一种。
我们先来明确一下算法流程,先开一下下图
我们先根据原来的字符串,排序出每一位是第几大。
我们的算法叫倍增,怎么个倍增,我们是倍增每次比较字符串的长度。
然后我们设比较的长度为k
我们每次把i和i+k合并到一起,最后将新得到的数组进行排序,反复多次,知道数组内的每个排名都不相同就可以退出了。
然后我们如果在这用暴力的stl sort的话,显然复杂度不够优美,所以我们需要用到基数排序
并且基数排序还有一个优美的性质,就是在排序关键字相等时,不更改在原序列中的相对位置前后。
for (int i = 0;i < m;i++) c[i] = 0;
for (int i = 0;i < n;i++) c[x[i] = r[i]]++;
for (int i = 1;i < m;i++) c[i] += c[i - 1];
for (int i = n - 1;i >= 0;i--) sa[--c[x[i]]] = i;
我们看一下上述代码,首先我们用c记录出每个元素的个数。
然后我们将c数组做前缀和处理。
然后我们这样子来愉快的排好序,至于这么做为什么是对的,读者可以自己模拟思考,在这解释下,sa i 表示长度排第i的后缀是从原字符串的哪一位开始。
这样子我们只要在倍增的过程中,处理出第二关键字,并按照这个来进行排序,就可以优美的求出后缀数组了。
上面讲的肯定会有些不足,可以参考下下面的代码,我会给予详细的注释,读者可以自行思考。
void sa_init()
{
int p,*x = t1,*y = t2;
//这里用两个指针来方便交换数组,参考下面swap
//先根据最初的,每一个字符进行基数排序
for (int i = 0;i < m;i++) c[i] = 0;
for (int i = 0;i < n;i++) c[x[i] = r[i]]++;
for (int i = 1;i < m;i++) c[i] += c[i - 1];
for (int i = n - 1;i >= 0;i--) sa[--c[x[i]]] = i;
for (int k = 1;k <= n;k <<= 1)//我们在进行倍增
{
//利用之前的排序结果直接根据第二关键字进行排序
p = 0;
for (int i = n - k;i < n;i++) y[p++] = i;
//显然n-k 到 n-1 并没有第二关键字 因为i + k已经超出范围了。
for (int i = 0;i < n;i++) if (sa[i] >= k) y[p++] = sa[i] - k;
//显然我们sa[i]是有序的。那我我么i是sa[i] - k的第二关键字。所以我们将y数组的第p位记录sa[i]的第一关键字在x中的下表。
//现在我们愉快的按照第二关键字,进行了排序。
//然后我们再根据第一关键字进行排序,由于基数排序的特性,我们保持了在第一关键字有序的情况下,第二关键字有序,并排出了sa数组。
for (int i = 0;i < m;i++) c[i] = 0;
for (int i = 0;i < n;i++) c[x[y[i]]]++;
for (int i = 1;i < m;i++) c[i] += c[i - 1];
for (int i = n - 1;i >= 0;i--) sa[--c[x[y[i]]]] = y[i];
//
swap(x,y);
//我们下然下次的x数组是这次的y,所以进行交换。
p = 1;
x[sa[0]] = 0;//提前处理边界,这个显然。
//下面是根据sa数组更新x数组内的排名。此时的y记录的就是原先x的排好的。
for (int i = 1;i < n;i++) x[sa[i]] = y[sa[i]] == y[sa[i - 1]] && y[sa[i] + k] == y[sa[i - 1] + k] ? p - 1 : p++;
if (p >= n) break;//如果凑够了n个,显然我们的排名已经不存在重复了。
m = p;//更新下次的基数排序的范围,因为我们基数排序拍的就是排名。
}
}
有一句话,后缀数组求出来没有用,关键是还要求出heigt数组,height数组表示什么呢,是字典序排序相邻的两个后缀的最大公共长度。
我们为什么要搞相邻的两位,因为他们的字典序最相近,可能的公共前缀也最长。
我们此处定义一下,height[i]表示sa[i]和sa[i + 1]的最长公共前缀长度。
我们先考虑暴力的求法,显然是n^2的做法,我们暴力的扫描这个sa数组,并且暴力的去判断相邻的字符串,但我们想一下,我们现在已经暴力的判断了最长的后缀和他的下一个后缀,然后我们求出他们的最长公共前缀长k,那么我们考虑第二长的后缀和他在sa数组的下一个后缀的最长前缀。然而我感觉我将不清楚,于是来一张自己画得图
最后给出详细的代码:
uoj有模板题~
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
const int MAXN = 200000;
int n,m = 30;
char s[MAXN];
int c[MAXN],t1[MAXN],t2[MAXN],sa[MAXN],r[MAXN],height[MAXN],rank[MAXN];
void sa_init()
{
int p,*x = t1,*y = t2;
//这里用两个指针来方便交换数组,参考下面swap
//先根据最初的,每一个字符进行基数排序
for (int i = 0;i < m;i++) c[i] = 0;
for (int i = 0;i < n;i++) c[x[i] = r[i]]++;
for (int i = 1;i < m;i++) c[i] += c[i - 1];
for (int i = n - 1;i >= 0;i--) sa[--c[x[i]]] = i;
for (int k = 1;k <= n;k <<= 1)//我们在进行倍增
{
//利用之前的排序结果直接根据第二关键字进行排序
p = 0;
for (int i = n - k;i < n;i++) y[p++] = i;
//显然n-k 到 n-1 并没有第二关键字 因为i + k已经超出范围了。
for (int i = 0;i < n;i++) if (sa[i] >= k) y[p++] = sa[i] - k;
//显然我们sa[i]是有序的。那我我么i是sa[i] - k的第二关键字。所以我们将y数组的第p位记录sa[i]的第一关键字在x中的下表。
//现在我们愉快的按照第二关键字,进行了排序。
//然后我们再根据第一关键字进行排序,由于基数排序的特性,我们保持了在第一关键字有序的情况下,第二关键字有序,并排出了sa数组。
for (int i = 0;i < m;i++) c[i] = 0;
for (int i = 0;i < n;i++) c[x[y[i]]]++;
for (int i = 1;i < m;i++) c[i] += c[i - 1];
for (int i = n - 1;i >= 0;i--) sa[--c[x[y[i]]]] = y[i];
//
swap(x,y);
//我们下然下次的x数组是这次的y,所以进行交换。
p = 1;
x[sa[0]] = 0;//提前处理边界,这个显然。
//下面是根据sa数组更新x数组内的排名。此时的y记录的就是原先x的排好的。
for (int i = 1;i < n;i++) x[sa[i]] = y[sa[i]] == y[sa[i - 1]] && y[sa[i] + k] == y[sa[i - 1] + k] ? p - 1 : p++;
if (p >= n) break;//如果凑够了n个,显然我们的排名已经不存在重复了。
m = p;//更新下次的基数排序的范围,因为我们基数排序拍的就是排名。
}
}
void rank_init()
{
for (int i = 0;i < n;i++) rank[sa[i]] = i;
}
int bl(int a, int b)
{
int ans = 0;
while(r[a++] == r[b++]) ++ans;
return ans;
}
void getht()
{
int cur = 0;
for(int i = 0; i < n; i++)
{
if(cur) --cur;
height[rank[i]] = cur = cur+bl(i+cur, sa[rank[i]+1]+cur);
}
}
int main()
{
scanf("%s",s);
n = strlen(s);
for (int i = 0;i < n;i++) r[i] = s[i] - 'a' + 2;
r[n] = 0;
n++;
sa_init();
rank_init();
n--;
getht();
for (int i = 1;i <= n;i++) printf("%d ",sa[i] + 1);
printf("\n");
for (int i = 1;i < n;i++) printf("%d ",height[i]);
printf("\n");
return 0;
}