后缀数组讲解
后缀数组零基础讲解(倍增法)
最近几天好好复习了下SA,希望看完这篇博客能让你彻底明白SA(求height什么的有待填坑)
(偷偷告诉你们:其实学SA有个很简单的方法,那就是背下来!)
1.后缀数组的应用和优势(可以先看如何实现再看):
应用:后缀数组可以得到后缀的排名,如果将后缀排序后,则后缀是按照字典序排列的,那么就可以很好地求出任意两个后缀的最长公共前缀了。
假设排序后的后缀中第i个后缀为s'[i],如果s'[i]和s'[i-1]的最长公共前缀长度为A,s'[i-1]和s'[i-2]最长公共前缀长度为B,则s'[i]和s'[i-2]最长公共前缀长度为min(A,B)
下面用反证法证明:
如果s'[i]和s'[i-2]的最长公共前缀长度大于min(A,B)
则因为按照字典序排序,则可以知道s'[i-1]必定也含有该公共前缀,长度>min(A,B),与假设不符
证毕
据此可以得出任意两个后缀的最长公共前缀,设height[i]为排名为i的后缀与排名为i-1的后缀的最长公共前缀,则排名为L的后缀与排名为R的后缀的最长公共前缀长度为min(height[i])(i=L+1,L+2...,R)预处理+RMQ可以解决(如果真的零基础可以先放着看下面)
后缀数组不仅能求一个子串的后缀排序,还能搞多个子串,只要每个子串后加个不会出现在子串的字符即可,这样就可以实现很多功能了。
优势:后缀数组和SAM比有什么优势呢?SAM可以实现的东西很多,但SA不行,那SA还有什么学的必要吗?
这不废话吗,那你为什么再看我这个讲SA的博客(doge)
SA一般作为学SAM的过渡,SA学好了也能解决大部分问题,学的特别好甚至能乱搞一切SAM题(如果不是请告诉我qwq)
SA占用空间比SAM小很多,而且代码难度上来说也要小一些,理解难度也要小很多。
2.后缀数组的原理,实现
倍增法
假设我们已经得到了每个后缀按照前2x个字符排序的排名,则可以得出每个后缀按照前2x+1个字符排序的排名(第x次可以得出按前2x-1个字符比较的排名,因为第一次是比较1个),
令s'[i]表示开头位置为i的后缀,rk[i]表示s'[i]的排名(每次排序后更新,第i次排序时用第i-1次的rk,第i次排序后更新rk)
则如果我得到了第x次的rk,现在要比较s'[i]和s'[j]长度为2x+1的前缀的大小,那么我可以先比较rk[i]和rk[j],如果rk[i]和rk[j]不同可以直接得出大小关系,如果相同则继续比较rk[i+(1<<(x-1))]和rk[j+(1<<(x-1))]即将一个长度为2x+1的比较分为两次长度为2x的比较。
这样便能实现快速比较了,但是要如何排序呢?
这里就需要用到桶排序了
桶排序简单来说就是桶是按照顺序排列的,我要比较若干个数的话,就将这些数丢进对应编号的桶里面,然后按顺序从桶里取出来,那么这些数就是按照顺序排好的了。
举个栗子:3、1、4、5、3要排序,我可以设个数组a[N](N视数字最大值定,也可以用离散化减小N取值),然后a[3]++,a[1]++,a[4]++,a[5]++,a[3]++
再从1到N一次将桶里的数取出,如果桶里没有数不取,如果有多个取出多个。
当然桶排序也可以有多个关键字,就是会略微麻烦些,例如我们需要比较两次,并且优先第一次。
接下来是重头戏,请务必看清楚各个数组表示的意思!(注:所有数组指的都是当前的,即第x次比较则都为所有后缀长度为2x-1的前缀,需要每次排序后更新)
sa[i]表示排名为i的位置
rk[i]表示开头位置为i的后缀的排名
tp[i]表示第二关键字排名为i的位置
tax为桶数组,用于桶排序
下面上代码(请自行将代码拷贝仔细看清每一步!)
#include <bits/stdc++.h> using namespace std; const int N = 1e6 + 10; int rk[N], tp[N]; int sa[N], tax[N], m; char s[N]; int n; inline void qsort() {//桶排序 memset(tax + 1, 0, m << 2);//初始化 for (int i = 1; i <= n; ++i) ++tax[rk[i]];//根据第一关键字丢到桶里 for (int i = 1; i <= m; ++i) tax[i] += tax[i - 1];//求前缀和,因为还需要用第二关键字 //tax[i]即为所有rk<=i的数目,tax[i-1]即为所有rk<=i-1的数 //因此求出的sa数组中第tax[i-1]+1个到第tax[i]个即为rk=i的 for (int i = n; i; --i) sa[tax[rk[tp[i]]]--] = tp[i]; //这个需要好好理解,首先rk如果相同则所在的桶是相同的,那么我们直接根据第二关键字顺序将其取出即可 //tp[i]为第二关键字排名为i的位置 //假设tax[i-1]=A,tax[i]=B,则说明第i个桶有B-A个,说明有B-A个第一关键字即rk=i的, //此时我们如果先取出第二关键字最靠后的位置,那么sa[B]就是这个位置 //之后取出第二关键字次大的,那么sa[B-1]就是这个位置,依次类推 } inline void suffix_sort() { m = 200;//m为辅助用,用于记录当前的rk最大为多少,因为当两个后缀长度为2^i的前缀相同时rk算作相同 //是一个优化 for (int i = 1; i <= n; ++i) rk[i] = s[i], tp[i] = i;//初始第一关键字即为Ascall码大小,也可以自定义大小 //例如s只包含小写字母,rk[i]=s[i]-'a'+1即可,此时m可取26 //tp为第二关键字,当不存在时根据位置确定第二关键字 qsort(); for (int i = 1, p; p < n; m = p, i <<= 1) {//i即为上一次比较长度,i*2为这一次比较长度 p = 0; for (int j = 1; j <= i; ++j) tp[++p] = n - i + j;//因为上一次比较长度为i //后面开头位置为n-i+1,n-i+1...n的第二关键字都不存在,此时将位置作为第二关键字 //又因为不存在要优先考虑,因此放在存在第二关键字的前面 for (int j = 1; j <= n; ++j) if (sa[j] > i) tp[++p] = sa[j] - i;//上一次的sa可以求出这一次的第二关键字 //因为这里sa[j]表示上一次排名为j的位置 //而tp第二关键字实际上就是上一次排名为j的位置-i,因为第一关键字和第二关键字就是开头位置相隔i而已 qsort(), swap(rk, tp);//桶排序后,交换下rk,tp,因为接下来还需要用到上一次的rk以及求出这一次的rk rk[sa[1]] = p = 1; for (int j = 2; j <= n; ++j) rk[sa[j]] = (tp[sa[j]] == tp[sa[j - 1]] && tp[sa[j] + i] == tp[sa[j - 1] + i]) ? p : ++p; //此时的tp因为和上一次的rk交换,所以表示的是上一次的rk //如果上一次的时候第一关键字和第二关键字j和j-1相同,则这一次第一关键字j与j-1相同 } } int main() { scanf("%s", s + 1); n = strlen(s + 1); suffix_sort(); for (int i = 1; i <= n; ++i) printf("%d ", sa[i]); return 0; }
求height数组有待填坑!
SA相关题型有待填坑!

浙公网安备 33010602011771号