基础概念:
子串:
在一个字符串s中,取任意i<=j,那么从i到j的这一段就叫做s的一个子串
后缀:
从字符串s的某个位置i到字符串末尾的子串,
suff[i]:
以s的第i个字符为第一个元素的后缀
后缀数组:
现有字符串s,长度为n
把s的全部后缀按照字典序排序,总共有n个后缀
sa[i]:
表示排名为i的后缀的起始位置的下标——根据排名来查询后缀起始位置
rak[i]:
表示起始位置为i的后缀的排名——根据起始位置来查询后缀排名
LCP:
LCP[i][j]:表示suff[sa[i]](排名为i的后缀)与suff[sa[j]](排名为j的后缀)的最长公共前缀的长度
LCP[i][j]=LCP[j][j]
LCP[i][i]=Len[sa[i]]=n-sa[i]+1;
该式子的意思为,自身与自身的最长公共前缀就是自身,其长度为排名为i的后缀的长度,而其长度等于n减去起始位置的下标再加1
LCP性质:
当i<j<k时
LCP[i][j]>=LCP[i][k]
显然由字典序排序的单调性可知,离i越近的后缀与i的前缀重合度就会越大,即长度越长
Height[i]:
表示LCP[i][i-1],即排名为i的后缀与排名为i-1的后缀的最长公共前缀
后缀数组实现:DC3 O(n)
DA O(nlogn)
倍增实现:
倍增思路:
对以每个字符为开头的长度为2^k的子字符串进行排序,即为rank值
k从0开始,每次+1
当2^k的长度大于n以后
以第一个字符开头的长度为2^k的子字符串已经包含了整个字符串
所以每个字符开始的长度为2^k的子字符串就相当于所有的后缀
对于靠后的起始字符的子串而言,若其到字符串末尾的长度小于2^k,那么就取它的实际长度
并且我们对这些子字符串都已经用一定的方法比较出了大小
即rank值里面没有相同的值
那么此时的rank值就是最后的结果
那么如何得到这个rank值呢?
我们利用倍增的思想
我们对于n个长度为2^k的子字符串进行rank值排序
当k等于0的时候,子串长度为1,直接按照字典序排出rank值大小即可
假设我们要比较a和b两个长度为2^k的子字符串
只需要将其划分成为两个2^(k-1)长度的子字符串
即,将a划分为a1和a2,将b划分为b1和b2,长度均为2^(k-1)
那么我们先比较a1和b1的rank值大小
若rank[a1]!=rank[a2],则已经比较出a和b的字典序大小
若rank[a1]==rank[a2],我们再进而比较a2和b2的rank值
由于两个子串rank值不可能相同,则一定能够通过rank[a2]和rank[b2]比较出a和b的大小
这样,当2^k>n时,我们就得到了sa数组
后缀数组的应用举例:
后缀数组为排序后的数组,具有一定的单调性
求无重复子串:
对于一个长度为n的字符串而言,其最多有n*(n+1)/2个不重复子串
思考过程:
对于每一个子串,我们可以分其以不同的起始位置讨论
以下标为1的开头的子串有n个
以下标为2的开头的子串有n-1个
以此类推
最后就是一个1+2+3+……+n的求和
故有n*(n+1)/2个子串
对于s中的每一个子串
一定存在于每一个后缀的前缀中
即每一个后缀的前缀都会是一个子串
那么我们只需要将重复的前缀剔除,剩余的后缀所贡献的前缀的总和就是无重复子串的个数
由于字典序排序问题
相邻的两个后缀的前缀重合度一定是最大的
即对于排名为i的后缀而言
其能贡献的子串个数为n-sa[i]+1-Height[i]
求可重叠的k次最长重复子串
由上面第一道题的讨论已知
每一个子串一定存在于每一个后缀的前缀中
那么要求可重复的子串
就是求后缀数组中可重复的前缀
这就是height数组的含义
由于按字典序排序后
后缀的前缀重叠度最大
当i<j<k时
LCP[i][j]>=LCP[i][k]
因此选一个连续的区间的前缀
他们的可重叠度是最大的
单调性——二分——枚举最长长度
题目链接:
https://www.luogu.com.cn/problem/P3809
模版代码:
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int N=1E6+10;
char s[N];
int wa[N],wb[N],wss[N],wv[N],sa[N];
int cmp(int* r,int a,int b,int l){
return r[a]==r[b]&&r[a+l]==r[b+l];
}
void da(int* r,int* sa,int n,int M){
int i,j,p,*x=wa,*y=wb,*t;
//这里的字符串数组r,传入的是原串s+1
//从r[0]到r[n-1],长度为n
//最大值小于M
//约定除了r[n-1]外的r[i]都大于0,且r[n-1]等于0
for(i=0;i<n;i++) wss[x[i]=r[i]]++;
for(i=1;i<M;i++) wss[i]+=wss[i-1];
//至此,wss[i]表示的是字母关键值为i的字母前面有wss[i]-1个字母,即自己的排名为wss[i]-1
for(i=n-1;i>=0;i--) sa[--wss[x[i]]]=i;
//此时wa数组里存的是初始关键值(rank值),即为ASCII码的大小排序
//按照关键值大小,由后到前地排入sa中,sa的值为i
//因为x[i]为关键值大小,映射的字母位置为i
//排名从0到n-1
//sa[i]的值也是从0到n-1
//j为倍增子串长度,每次*=2
//第一次进入该循环时,M仍然是da函数初始化的M
for(j=1,p=1;p<n;j*=2,M=p){
//第一次基数排序
//把长度小于j的后缀的起始字母位置按相对位置不变地记录到y数组中
//由于他们无法接上第二子串,所以他们第二关键值为0,是最小的,所以排在最前面
for(p=0,i=n-j;i<n;i++) y[p++]=i;
//把长度大于等于j的后缀的起始字母位置按相对后缀字典序排名不变地记录到y数组中
//这里往前错位j个字母,将第sa[i]个字母的关键值给予第sa[i]-j个字母
//即排名为i的起始字母的关键值是第sa[i]-j,也就是他前j个字母的第二关键值
for(i=0;i<n;i++) if(sa[i]>=j) y[p++]=sa[i]-j;
//清零计数
for(i=0;i<M;i++) wss[i]=0;
//wv[i]为关键值大小,映射的字母位置是y[i]
//wv[i]表示经过第一次基数排序后
//第二关键值排名为i的后缀的起始字母位置为y[i]
for(i=0;i<n;i++) wss[wv[i]=x[y[i]]]++;
for(i=1;i<M;i++) wss[i]+=wss[i-1];
//在第二关键值排名的基础上进行第一关键值的排名
for(i=n-1;i>=0;i--) sa[--wss[wv[i]]]=y[i];
//更新rank数组,即更新x数组
//由于y数组已经没有用了,马上就会被下次循环覆盖
//所以我们这里借用y暂时成为旧x来更新x
//由于以sa[i]开头的长度为j的子串可能相同
//故rank值可能相同
//所以需要比较两个字符串是否相同
//相同的话,把rank值更新为p-1(由于上次p自增过),即与上一个字符串(即与自己相同的字符串)的rank值相同
//不同的话,更新为p,并p自增
for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1;i<n;i++)
x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;
// 这样,就把rank值更新完了,且rank的最大值不会超过p,因此在外层循环中,可以把上界M设为p,加快循环
//同时,当有n个不同的rank值时,即p==n时
//说明每个子串均不相同,此时可以结束外层循环
// cmp函数的解释:
// 两个排名相邻的子串相同,当且仅当他们第一子串的关键值与第二子串的关键值均相同
//
}
}
int cal[N];
int main(){
scanf("%s",s+1);
int n=strlen(s+1);
for(int i=1;i<=n;i++) cal[i]=s[i]-'0'+1;
//定义cal[n+1]等于0
//好处在于:
//当cmp函数判定sa[i-1]和sa[i]的第一子串的关键值时
//若r[a]==r[b]
//则说明他们的第一子串一定不包括cal[n+1]这个字符(全场唯一一个0)
//因此,可以放心地调用r[a+l]与r[b+l],而不用担心越界
cal[n+1]=0;
//传入时传入数组长度为n+1,即包括最后的0
da(cal+1,sa,n+1,'z'-'0'+2);
//由于cal[n+1]的存在,他一定排在第0位
//因此sa数组排名从1开始
//又由于传入的为s+1,因此s+1的下标0对应原字符串s的下标1
//因此sa[i]+1为对应起始字母位置
for(int i=1;i<=n;i++) printf("%d ",sa[i]+1);
return 0;
}
//顺便写上rak数组和height数组的计算方式
int rak[N], height[N];
void calheight(int *r, int *sa, int n)
{
int i, j, k = 0;
// k表示最长公共前缀长度
//求rak
//由于sa[i]表示的是排名为i的后缀的起始字母的位置
//所以位置为sa[i]的字母起始的后缀的排名为i
for (int i = 1; i <= n; i++) rak[sa[i]] = i;
//求height
//用到性质:height[i]>=height[i-1]-1
for(i=0;i<n;height[rak[i++]]=k)
for(k?k--:0,j=sa[rak[i]-1];r[i+k]==r[j+k];k++)
//顺便更新sa
for(i=n;i;i--) rak[i]=rak[i-1],sa[i]++;
}