在这里放一些浅显易懂的证明吧。
kmp证明:
n e x t [ i ] next[i] next[i]表示“以i结尾的非前缀子串“与”前缀子串”能够匹配的最长长度,即:
n e x t [ i ] = m a x { j } , j < i , A [ 1 ∼ j ] = A [ i − j + 1 ∼ i ] next[i]=max\begin{Bmatrix}j\end{Bmatrix},j<i,A[1\sim j]=A[i-j+1\sim i] next[i]=max{j},j<i,A[1∼j]=A[i−j+1∼i]
称每一个满足以上两个条件的 j j j为 n e x t [ i ] next[i] next[i]的候选项。
引理:
若 j 0 j_0 j0为 n e x t [ i ] next[i] next[i]的一个候选项,即 j 0 < i , A [ 1 ∼ j 0 ] = A [ i − j 0 + 1 ∼ i ] j_0<i,A[1\sim j_0]=A[i-j_0+1\sim i] j0<i,A[1∼j0]=A[i−j0+1∼i],那么小于 j 0 j_0 j0的最大的 n e x t [ i ] next[i] next[i]的候选项是 n e x t [ j 0 ] next[j_0] next[j0]。换言之, n e x t [ j 0 ] + 1 ∼ j 0 − 1 next[j_0]+1\sim j_0-1 next[j0]+1∼j0−1之间都不是 n e x t [ i ] next[i] next[i]的候选项。
证明:
假设存在 n e x t [ j 0 ] < j 1 < j 0 next[j_0]<j_1<j_0 next[j0]<j1<j0使得 j 1 j_1 j1为 n e x t [ i ] next[i] next[i]的候选项,即 A [ 1 ∼ j 1 ] = A [ i − j 1 + 1 ∼ i ] A[1\sim j_1]=A[i-j_1+1\sim i] A[1∼j1]=A[i−j1+1∼i],由于 A [ 1 ∼ j 0 ] = A [ i − j 0 + 1 ∼ i ] A[1\sim j_0]=A[i-j_0+1\sim i] A[1∼j0]=A[i−j0+1∼i],取后 j 1 j_1 j1个字符,即: A [ j 0 − j 1 + 1 ∼ j 0 ] = A [ i − j 1 + 1 ∼ i ] = A [ 1 ∼ j 1 ] A[j_0-j_1+1\sim j_0]=A[i-j_1+1\sim i]=A[1\sim j_1] A[j0−j1+1∼j0]=A[i−j1+1∼i]=A[1∼j1],与 n e x t [ j 0 ] next[j_0] next[j0]“最大性"( n e x t [ j 0 ] next[j_0] next[j0]应为 j 1 j_1 j1)不符,故假设不成立。
另一方面,可以说明 n e x t [ j 0 ] next[j_0] next[j0]为 n e x t [ i ] next[i] next[i]的候选项。
A [ 1 ∼ n e x t [ j 0 ] ] = A [ j 0 − n e x t [ j 0 ] + 1 ∼ j 0 ] A[1\sim next[j_0]]=A[j_0-next[j_0]+1\sim j_0] A[1∼next[j0]]=A[j0−next[j0]+1∼j0]
A [ 1 ∼ j 0 ] = A [ i − j 0 + 1 ∼ i ] A[1\sim j_0]=A[i-j_0+1\sim i] A[1∼j0]=A[i−j0+1∼i]取后 n e x t [ j 0 ] next[j_0] next[j0]位,
即 A [ j 0 − n e x t [ j 0 ] + 1 ∼ j 0 ] = A [ i − n e x t [ j 0 ] + 1 ∼ i ] A[j_0-next[j_0]+1\sim j_0]=A[i-next[j_0]+1\sim i] A[j0−next[j0]+1∼j0]=A[i−next[j0]+1∼i]
因此可以得知, n e x t [ j 0 ] next[j_0] next[j0]为 n e x t [ i ] next[i] next[i]的候选项。
当正要计算 n e x t [ i ] next[i] next[i]的时候, n e x t [ i − 1 ] next[i-1] next[i−1]已经统计完了。
根据引理,可知 n e x t [ i − 1 ] next[i-1] next[i−1]的所有候选项都可以从 n e x t next next数组中得到。
根据定义,从大到小即为: n e x t [ n e x t [ i − 1 ] ] , n e x t [ n e x t [ n e x t [ i − 1 ] ] ] ⋯ ⋯ next[next[i-1]],next[next[next[i-1]]]\cdots \cdots next[next[i−1]],next[next[next[i−1]]]⋯⋯,若 j j j要满足是 n e x t [ i ] next[i] next[i]的候选项,那么首先要满足 j − 1 j-1 j−1是 n e x t [ i − 1 ] next[i-1] next[i−1]的候选项,因此我们只要从 n e x t [ i − 1 ] next[i-1] next[i−1]的候选项中找出适配的最大值,再加上 1 1 1,即为 n e x t [ i ] next[i] next[i];
贴个代码吧
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<cmath>
#include<cstring>
using namespace std;
const int N=1e6+10;
int nxt[N],n,m;//next居然是系统函数
char a[N],b[N];
void kmp()
{
nxt[1]=0;
for(int i=2,j=0;i<=n;i++)
{
while(j&&a[i]!=a[j+1])j=nxt[j];//j++
if(a[i]==a[j+1])++j;nxt[i]=j;
}
for(int i=1,j=0;i<=m;i++)
{
while(j&&b[i]!=a[j+1])j=nxt[j];
if(a[j+1]==b[i])++j;
if(j==n){printf("%d %d\n",i-n+1,i);return ;}
}
puts("NO");
}
int main()
{
scanf("%s%s",b+1,a+1);
n=strlen(a+1),m=strlen(b+1);
kmp();
return 0;
}
扩展KMP?(KMP+暴力)
真的莫得心思学这种愣头青算法。
回顾一下 kmp \operatorname{kmp} kmp的 n e x t next next数组,即为 A [ 1 ∼ n e x t [ i ] ] = A [ i − n e x t [ i ] + 1 ∼ i ] A[1\sim next[i]]=A[i-next[i]+1\sim i] A[1∼next[i]]=A[i−next[i]+1∼i]
首先人为定义一下: n = s t r l e n ( A + 1 ) , m = s t r l e n ( B + 1 ) n=strlen(A+1),m=strlen(B+1) n=strlen(A+1),m=strlen(B+1)
可是 EXKMP \operatorname{EXKMP} EXKMP贼优秀,是 A [ 1 ∼ n e x t [ i ] ] = A [ i ∼ i + n e x t [ i ] − 1 ] A[1\sim next[i]]=A[i\sim i+next[i]-1] A[1∼next[i]]=A[i∼i+next[i]−1]且保证 n e x t [ i ] next[i] next[i]最大
同样是自己弄自己,你咋那么优秀呢?
为了方便起见, n e x t [ 1 ] = n next[1]=n next[1]=n,直接重合。
接着我们要暴力弄一下 n e x t [ 2 ] next[2] next[2],如下图
就像这样,
n
e
x
t
[
2
]
=
6
next[2]=6
next[2]=6,即
A
[
1
∼
6
]
=
A
[
2
∼
7
]
A[1\sim 6]=A[2\sim 7]
A[1∼6]=A[2∼7],至于有什么用,等会就知道了。
这时候,再定义 k k k,这个 k k k表示的就是在当前搜索过的范围以内(因为这是线性算法,所以是从 1 1 1到 i i i)能到达最远(也就是说 k + n e x t [ k ] − 1 k+next[k]-1 k+next[k]−1最大)的编号。
定义一个
e
d
ed
ed,让它等于
k
+
n
e
x
t
[
k
]
−
1
k+next[k]-1
k+next[k]−1,那么由
n
e
x
nex
next这个数组的定义我们就可以得到一个等式:
A
[
1
∼
n
e
x
t
[
k
]
]
=
A
[
k
∼
e
d
]
A[1\sim next[k]]=A[k\sim ed]
A[1∼next[k]]=A[k∼ed],由于
e
d
=
k
+
n
e
x
t
[
k
]
−
1
ed=k+next[k]-1
ed=k+next[k]−1,则
n
e
x
t
[
k
]
=
e
d
−
k
+
1
next[k]=ed-k+1
next[k]=ed−k+1,故有
A
[
1
∼
e
d
−
k
+
1
]
=
A
[
k
∼
e
d
]
A[1\sim ed-k+1]=A[k\sim ed]
A[1∼ed−k+1]=A[k∼ed],代换来,代换去,还是那条柿子,如下图。
假设现在已经扫到第
i
i
i个位置了,且
e
d
>
i
ed>i
ed>i,则如下图:
那么,不是就有状态可以继承了吗?,即:
A
[
i
∼
e
d
]
A[i\sim ed]
A[i∼ed]=A[
i
−
k
+
1
∼
e
d
−
k
+
1
]
i-k+1\sim ed-k+1]
i−k+1∼ed−k+1]。
现在有 L = n e x t [ i − k + 1 ] L=next[i-k+1] L=next[i−k+1],那么 A [ 1 ∼ L ] = A [ i − k + 1 ∼ i − k + L ] A[1\sim L]=A[i-k+1\sim i-k+L] A[1∼L]=A[i−k+1∼i−k+L]
但是如果这样,不是有可能出现两种情况吗?
比如:
还有
所以要分类讨论,
当
e
d
−
k
+
1
>
i
−
k
+
L
ed-k+1>i-k+L
ed−k+1>i−k+L,即
根据 L = n e x t [ i − k + 1 ] L=next[i-k+1] L=next[i−k+1]的定义,即 A [ 1 ∼ L ] = A [ i − k + 1 ∼ i − k + L ] A[1\sim L]=A[i-k+1\sim i-k+L] A[1∼L]=A[i−k+1∼i−k+L],且L已经最大,
换种说法,也就是 A [ L + 1 ] A[L+1] A[L+1]不等于 A [ i − k + L + 1 ] A[i-k+L+1] A[i−k+L+1]。
又因为 A [ i ∼ e d ] A[i\sim ed] A[i∼ed]=A[ i − k + 1 ∼ e d − k + 1 ] i-k+1\sim ed-k+1] i−k+1∼ed−k+1],
可以说明 A [ i − k + 1 ∼ i − k + L ] = A [ i ∼ i + L − 1 ] = A [ 1 ∼ L ] A[i-k+1\sim i-k+L]=A[i\sim i+L-1]=A[1\sim L] A[i−k+1∼i−k+L]=A[i∼i+L−1]=A[1∼L],且 A [ i + L ] A[i+L] A[i+L]不等于 A [ L + 1 ] A[L+1] A[L+1]。因此 n e x t [ i ] = L next[i]=L next[i]=L即可。
另一种情况,即:
根据
n
e
x
t
[
k
]
next[k]
next[k]的定义,可以直接得到
A
[
e
d
+
1
]
A[ed+1]
A[ed+1]不等于
A
[
e
d
−
k
+
2
]
A[ed-k+2]
A[ed−k+2],令
n
e
x
t
[
i
]
=
e
d
−
i
+
1
next[i]=ed-i+1
next[i]=ed−i+1即可.
注意,有可能 e d < i ed<i ed<i
貌似?
还有等于的情况?
这是我们只能确定
A
[
1
∼
n
e
x
t
[
i
]
]
A[1\sim next[i]]
A[1∼next[i]]与
A
[
i
∼
i
+
n
e
x
t
[
i
]
−
1
]
A[i\sim i+next[i]-1]
A[i∼i+next[i]−1]中有
e
d
−
i
+
1
ed-i+1
ed−i+1长的公共前缀长度,
但是无法确定究竟有多长,那只好暴力了(滑稽)。
e x t e n d [ i ] extend[i] extend[i]的意义在于是求 B [ i , i + n − 1 ] B[i,i+n-1] B[i,i+n−1]与 A A A的最长公共前缀长度
求 e x t e n d extend extend数组类似,只需要记住一点, e d = k + e x t e n d [ k ] − 1 , L = n x t [ i − k + 1 ] ed=k+extend[k]-1,L=nxt[i-k+1] ed=k+extend[k]−1,L=nxt[i−k+1],这是由于 n x t [ i ] nxt[i] nxt[i]可能不存在,但由于 e x t e n d extend extend是统计 A A A串与 B B B串的最长公共前缀长度,不可能大于 A A A串的长度,则 n x t [ i − k + 1 ] nxt[i-k+1] nxt[i−k+1]始终合法,对于当前 i i i,求 e x t e n d extend extend的方法,类似于求 n x t nxt nxt的方法,也是分类讨论。如图,下面这段红色区间就是当前可以利用的区间,因为 b [ k , k + e x t e n d [ k ] − 1 ] b[k,k+extend[k]-1] b[k,k+extend[k]−1]与 b [ i , i + n ] b[i,i+n] b[i,i+n]重合了 b [ i , k + e x t e n d [ k ] − 1 ] b[i,k+extend[k]-1] b[i,k+extend[k]−1],类似这样。
接着分类讨论即可。
Code
#include<cstdio>
#include<cstring>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int N=1e6+10;
char a[N],b[N];
int n,m,nxt[N],extend[N];
int main()
{
scanf("%s%s",b+1,a+1);n=strlen(a+1),m=strlen(b+1);
nxt[1]=n;int x=1,k;
while(x+1<=n&&a[x]==a[x+1])++x;
nxt[2]=x-1;k=2;
for(int i=3;i<=n;i++)
{
int ed=k+nxt[k]-i,L=nxt[i-k+1];//int ed=k+nxt[k]-1;
if(ed>L)nxt[i]=L;
else
{
x=ed;
if(x<0)x=0;
while(x+i<=n&&a[x+1]==a[x+i])++x;//ed-i+1==L
nxt[i]=x;k=i;//i+nxt[i]-1>k+nxt[k]-1;
}
}
x=1;
while(x<=n&&a[x]==b[x])++x;
extend[1]=x-1;k=1;
for(int i=2;i<=m;i++)
{
int ed=k+extend[k]-i,L=nxt[i-k+1];//i-k+1<=n,A[1,nxt[i-k+1]
if(ed>L)extend[i]=L;
else
{
x=ed;
if(x<0)x=0;
while(x<n&&x+i<=m&&a[x+1]==b[x+i])++x;
extend[i]=x;k=i;
}
}
for(int i=1;i<=m;i++)printf("%d ",extend[i]);puts("");
return 0;
}
Manacher算法(马拉车算法)
马拉着车的算法果然不一样
马拉车用来解决回文串问题,简直跑得飞快(
O
(
n
)
O(n)
O(n)),(
O
(
n
2
)
O(n^2)
O(n2)大法好!)
回文串就是类似" b a a b {\color{blue}baab} baab"," a b g g b a {\color{red}abggba} abggba"的字符串,也就是从左往右读,或者从右往左读,都是一样的字符串。
但是回文串处理起来,要分奇偶。
但是将" b a a b {\color{blue}baab} baab",变成" # b # a # a # b # \#b\#a\#a\#b\# #b#a#a#b#"就可使回文串变成奇数。
运用这种方法,就可以避免奇偶的问题了。
那么如何实现快速找回文串呢?
(注:讲解中的
r
−
i
r-i
r−i实际上是
r
−
i
+
1
r-i+1
r−i+1,程序实现也是
r
−
i
r-i
r−i,其实这个
r
r
r可以代表为
r
r
r这个位置与
l
l
l不匹配,增减之后不影响结果。)
设现在够到最远的(也就是右端点最远)回文串的中心为 p o s pos pos,右端点为 r r r,设 p p p数组,记录回文长度,即: p [ i ] p[i] p[i]记录已i为中心的回文长度。
假设现在搜索到 i i i,且 i < r i< r i<r,如图,
若
p
[
j
]
>
r
−
i
p[j]>r-i
p[j]>r−i,如图,
根据回文串性质,
A
[
l
+
1
∼
j
]
=
A
[
r
−
1
∼
i
]
A[l+1 \sim j]=A[r-1\sim i]
A[l+1∼j]=A[r−1∼i],可是再往
l
l
l,
r
r
r扩展,就不行了(因为
A
[
l
]
A[l]
A[l]不等于
A
[
r
]
A[r]
A[r]),所以
p
[
i
]
p[i]
p[i]只能先等于
r
−
i
r-i
r−i,继承状态之后再扩展。
否则,如图,
直接继承 p [ j ] p[j] p[j]即可。
若 i ≥ R i\ge R i≥R,没得谈,只得暴力扩展。
Code
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstdlib>
#include<cstring>
using namespace std;
const int N=1e5+10;
int p[N<<1];
char a[N],b[N<<1];
void Manacher()
{
int len=strlen(a+1);
for(int i=1;i<=len;i++)b[2*i-1]='#',b[2*i]=a[i];
len=len*2+1;
b[len]='#';
int pos=0,r=0;
for(int i=1;i<=len;i++)
{
int j=2*pos-i;
if(i<r)p[i]=min(p[j],r-i);
else p[i]=1;
while(1<=i-p[i]&&i+p[i]<=len&&b[i-p[i]]==b[i+p[i]])++p[i];
if(i+p[i]>r)
{
pos=i;
r=i+p[i];
}
}
int ans=0;
for(int i=1;i<=len;i++)ans=max(ans,p[i]-1);
printf("%d\n",ans);
}
int main()
{
while(scanf("%s",a+1)!=EOF)Manacher();
return 0;
}
AC自动机
让我们首先学习一下 Trie \operatorname{Trie} Trie树,
来自百度,字典树又称单词查找树, Trie \operatorname{Trie} Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
实现原理,类似继承前缀,减少空间复杂度,如图。
有字符串"
a
a
b
,
a
b
,
b
{\color{blue}aab,ab,b}
aab,ab,b"
所以,
Trie
\operatorname{Trie}
Trie是一个十分简单的东西
A C AC AC自动机比 Trie \operatorname{Trie} Trie树多出一个 f a i l fail fail指针,指向同类型的其他节点,例如:一个字母 a 1 a_1 a1节点, f a i l fail fail指针指向且另外一个字母 a 2 a_2 a2,且另外一个字母 a 2 a_2 a2的父亲 f 2 f_2 f2,同样是 f 1 f_1 f1的 f a i l fail fail指针的集合里(有可能不是 f 1 f_1 f1的 f a i l fail fail所指向的那个。)
t
[
x
]
.
f
a
i
l
t[x].fail
t[x].fail的深度小于
x
x
x的深度,才能保证合法性(深度大不一定保证合法)
特别地,
t
[
0
]
t[0]
t[0]的所有儿子指向
t
[
0
]
t[0]
t[0]。
code
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<cstdlib>
using namespace std;
const int N=5e5+10;
const int M=1e6+10;
struct node{int c[27],s,fail;}t[N];
int cnt,ans,n;char s[M];
void clean(int x){t[x].s=t[x].fail=0;memset(t[x].c,-1,sizeof(t[x].c));}
void build(int root)
{
int x=root;int len=strlen(s+1);
for(int i=1;i<=len;i++)
{
int y=s[i]-'a'+1;
if(t[x].c[y]==-1){t[x].c[y]=++cnt;clean(cnt);}
x=t[x].c[y];
}
++t[x].s;
}
int q[N];
void bfs()
{
int l=1,r=2;
while(l!=r)
{
int x=q[l++];
for(int i=1;i<=26;i++)
{
if(t[x].c[i]==-1)continue;
int y=t[x].c[i];
if(!x)t[y].fail=0;//指向根节点
else
{
int k=t[x].fail;
while(k!=0&&t[k].c[i]==-1)k=t[k].fail;
t[y].fail=(t[k].c[i]==-1?0:t[k].c[i]);
}
q[r++]=y;
}
}
}
void solve()
{
int len=strlen(s+1);
int x=0;int k;ans=0;
for(int i=1;i<=len;i++)
{
int y=s[i]-'a'+1;
while(x!=0&&t[x].c[y]==-1)x=t[x].fail;
x=t[x].c[y];
if(x==-1){x=0;continue;}
k=x;
while(k)
{
ans+=t[k].s;
t[k].s=0;
k=t[k].fail;
}
}
printf("%d\n",ans);
}
int main()
{
int T;scanf("%d",&T);
while(T--)
{
scanf("%d",&n);cnt=0;clean(0);
for(int i=1;i<=n;i++)
{
scanf("%s",s+1);
build(0);
}
bfs();scanf("%s",s+1);
solve();
}
return 0;
}
后缀数组(怀疑人生系列)
思路挺好懂的。
前置知识
一点点的基数排序
一点点的字符串知识。
基数排序
大概学过桶排序吧,基数排序就是很多个桶,把数给分类一下,然后从小到大记录前缀和,再还原数就好了,桶够多 O ( n ) O(n) O(n)。
后缀
举个栗子吧。
比如 a b b a b a {\color{blue}abbaba} abbaba
它的所有后缀就是
a b b a b a abbaba abbaba, b b a b a bbaba bbaba, b a b a baba baba, a b a aba aba, b a ba ba, a a a
系统地讲,就是从某个位置 i i i到字符串结尾结束的一个特殊子串,即 A [ i ∼ n ] A[i\sim n] A[i∼n],记为 S u f f i x ( i ) Suffix(i) Suffix(i),以下简写为 s f ( i ) sf(i) sf(i)。
通常来讲,字典序排序,要从
1
∼
1\sim
1∼串长比较每一位的字母的ASCII码的大小。
(详细见百度)
将以上栗子排序之后,从小到大表示为
a , a b a , a b b a b a , b a , b a b a , b b a b a a,aba,abbaba,ba,baba,bbaba a,aba,abbaba,ba,baba,bbaba
比如这道题,
就是要求排序后的后缀第一个字母出现的位置。
好了,进入正题,
先弄两个数组出来,
一个后缀数组
S
A
SA
SA,保证
s
f
(
S
A
[
i
]
)
<
s
f
(
S
A
[
i
+
1
]
)
sf(SA[i]) <sf(SA[i+1])
sf(SA[i])<sf(SA[i+1]),
1
≤
i
<
n
1≤i<n
1≤i<n。
也就是将
A
A
A 的
n
n
n 个后缀从小到大进行排序之后把排好序的后缀的开头位置顺
次放入
S
A
SA
SA中(排名第几的是谁)
一个名次数组 R a n k Rank Rank,保存后缀 i i i在从小到大排列的所有后缀的名次(你排第几)
这两个数组是互逆的。
举个栗子: a a b a a a b {\color{red}aabaaab} aabaaab
引进一张罗穗骞dalao的图片
倍增算法(DA)
现设字符串长度为 n n n。
倍增算法的主要思路是:用倍增的方法对每个字符开始的长度为
2
k
2^k
2k 的子字
符串进行排序,求出排名,即
r
a
n
k
rank
rank 值。
k
k
k 从
1
1
1 开始,每次加
1
1
1,当
2
k
2^k
2k 大于
n
n
n以
后,每个字符开始的长度为
2
k
2^k
2k 的子字符串便相当于所有的后缀。并且这些子字
符串都一定已经比较出大小, 即
r
a
n
k
rank
rank 值中没有相同的值, 那么此时的
r
a
n
k
rank
rank 值就
是最后的结果。
每一次排序都利用上次长度为
2
k
−
1
2^{k-1}
2k−1的字符串的
r
a
n
k
rank
rank 值,那么长
度为
2
k
2^k
2k的字符串就可以用两个长度为
2
k
−
1
2^{k-1}
2k−1的字符串的排名作为关键字表示,然
后进行基数排序, 便得出了长度为
2
k
−
1
2^{k-1}
2k−1的字符串的
r
a
n
k
rank
rank 值。
如何找到 O ( 1 ) O(1) O(1)后一个长度为 2 k − 1 2^{k-1} 2k−1的字符串呢?
经过观察可以发现,后缀 i ( i ≤ n − 2 k + 1 ) i(i\le n-2^k+1) i(i≤n−2k+1)的第 2 k − 1 + 1 ∼ 2 k 2^{k-1}+1\sim 2^k 2k−1+1∼2k的字符恰好为后缀 i + 2 k − 1 i+2^{k-1} i+2k−1的前 2 k − 1 2^{k-1} 2k−1个字符。
以字符串“
a
a
b
a
a
a
a
b
aabaaaab
aabaaaab”
为例, 整个过程如图所示。 其中
x
,
y
x,y
x,y是表示长度为
2
k
−
1
2^{k-1}
2k−1的字符串的两个关键字
思路貌似就讲完了,实现的话
⋯
⋯
\cdots\cdots
⋯⋯
前方高能!
void DA(char *r,int n,int m)
{
int *x=wa,*y=wb,*t;
for(int i=1;i<=n;i++)++c[x[i]=r[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 j=1,p=1;p<n;j<<=1,m=p)
{
p=0;
for(int i=n-j+1;i<=n;i++)y[++p]=i;
//n-j+1~n是没有第二关键字的,它们的第二关键字就是它们自己
for(int i=1;i<=n;i++)if(sa[i]>j)y[++p]=sa[i]-j;
for(int i=1;i<=n;i++)wv[i]=x[y[i]];
for(int i=1;i<=m;i++)c[i]=0;
for(int i=1;i<=n;i++)++c[wv[i]];
for(int i=2;i<=m;i++)c[i]+=c[i-1];
for(int i=n;i>=1;i--)sa[c[wv[i]]--]=y[i];
t=x;x=y;y=t;p=1;x[sa[1]]=1;
for(int i=2;i<=n;i++)
x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p:++p;
Debug(x,sa,n);
}
for(int i=1;i<=n;i++)qw(sa[i]),putchar(' ');
}
T i p s Tips Tips:
y y y数组其实是整一个第二关键字排序后的一个编号结果(没有第二关键字的排在前面,之后按第一个关键字也就是 s a sa sa从小到大排下来的编号结果)
for(int i=n-j+1;i<=n;i++)y[++p]=i;
//n-j+1~n是没有第二关键字的,它们的第二关键字就是它们自己
for(int i=1;i<=n;i++)if(sa[i]>j)y[++p]=sa[i]-j;//排名要从小到大·
个人认为对于 i < j < k , x [ y [ i ] ] = x [ y [ j ] ] = x [ y [ k ] ] i<j<k,x[y[i]]=x[y[j]]=x[y[k]] i<j<k,x[y[i]]=x[y[j]]=x[y[k]],根据 y y y数组的性质,排序后, y [ i ] y[i] y[i]的排名< y [ j ] y[j] y[j]的排名< y [ z ] y[z] y[z]的排名
证明如下,
由于 i < j < k i<j<k i<j<k,设 y [ i ] = s a [ w 1 ] − t , y [ j ] = s a [ w 2 ] − t , y [ k ] = s a [ w 3 ] − t y[i]=sa[w_1]-t,y[j]=sa[w_2]-t,y[k]=sa[w_3]-t y[i]=sa[w1]−t,y[j]=sa[w2]−t,y[k]=sa[w3]−t,
可知 w 1 < w 2 < w 3 w_1<w_2<w_3 w1<w2<w3,即可说明后缀 s a [ w 1 ] − t sa[w_1]-t sa[w1]−t的第二关键字优于 s a [ w 2 ] − t sa[w_2]-t sa[w2]−t,同理,证毕。
因此我们可以得出, y y y数组的下标反应第二关键字(的排名),对应下标的值反应后缀的编号。
之后用第二关键字
y
y
y,对第一关键字进行排序,求出新的
s
a
sa
sa值。
(其实就是同一个堆中的元素,通过
y
y
y数组出现的早晚,来判断谁排在前,谁排在后。)
for(int i=1;i<=n;i++)wv[i]=x[y[i]];
for(int i=1;i<=m;i++)c[i]=0;
for(int i=1;i<=n;i++)++c[wv[i]];
for(int i=2;i<=m;i++)c[i]+=c[i-1];
for(int i=n;i>=1;i--)sa[c[wv[i]]--]=y[i];
code
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstdlib>
#include<cstring>
using namespace std;
const int N=1e6+10;
void qw(int x)
{
if(x/10)qw(x/10);
putchar(x%10+48);
}
char s[N];
int wa[N],wb[N],wv[N],c[N],sa[N];
bool cmp(int *r,int a,int b,int l){return r[a]==r[b]&&r[a+l]==r[b+l];}
void DA(char *r,int n,int m)
{
int *x=wa,*y=wb,*t;
for(int i=1;i<=n;i++)++c[x[i]=r[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 j=1,p=1;p<n;j<<=1,m=p)
{
p=0;
for(int i=n-j+1;i<=n;i++)y[++p]=i;
for(int i=1;i<=n;i++)if(sa[i]>j)y[++p]=sa[i]-j;
for(int i=1;i<=n;i++)wv[i]=x[y[i]];//第二关键字的sa[i]-j的第一关键字
for(int i=1;i<=m;i++)c[i]=0;
for(int i=1;i<=n;i++)++c[wv[i]];
for(int i=2;i<=m;i++)c[i]+=c[i-1];
for(int i=n;i>=1;i--)sa[c[wv[i]]--]=y[i];
t=x;x=y;y=t;p=1;x[sa[1]]=1;
for(int i=2;i<=n;i++)
x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p:++p;
}
for(int i=1;i<=n;i++)qw(sa[i]),putchar(' ');
}
int main()
{
scanf("%s",s+1);
int n=strlen(s+1),m=122;
DA(s,n,m);
return 0;
}
后缀数组的补充
height {\color{black}\text{height}} height数组:
定义 height[i]=suffix(sa[i-1]) \text{height[i]=suffix(sa[i-1])} height[i]=suffix(sa[i-1])和 suffix(sa[i]) \text{suffix(sa[i])} suffix(sa[i])的最长公共前缀,也就是排名相邻的两个后缀的最长公共前缀。
那么对于 j j j 和 k k k,不妨设 rank[j]<rank[k] \text{rank[j]<rank[k]} rank[j]<rank[k],则有以下性质: suffix(j) \text{suffix(j)} suffix(j) 和 suffix(k) \text{suffix(k)} suffix(k)的最长公共前缀为 height[rank[j]+1],height[rank[j]+2], height[rank[j]+3], … ,height[rank[k]] \text{height[rank[j]+1],height[rank[j]+2], height[rank[j]+3], … ,height[rank[k]]} height[rank[j]+1],height[rank[j]+2], height[rank[j]+3], … ,height[rank[k]]中的最小值(具有传递性,从小状态滚到大状态)。
设 r a n k [ k ] = r a n k [ j ] + 2 rank[k]=rank[j]+2 rank[k]=rank[j]+2,则 h e i g h t [ r a n k [ j + 1 ] ] height[rank[j+1]] height[rank[j+1]]表示为编号 j j j的后缀与排名 r a n k [ j + 1 ] rank[j+1] rank[j+1]的最长公共前缀, h e i g h t [ r a n k [ j + 2 ] ] height[rank[j+2]] height[rank[j+2]]表示为编号为 j + 1 j+1 j+1的后缀与排名 r a n k [ j + 2 ] rank[j+2] rank[j+2]的最长公共前缀,那么 m i n ( h e i g h t [ r a n k [ j + 1 ] ] , h e i g h t [ r a n k [ j + 2 ] ] ) min(height[rank[j+1]],height[rank[j+2]]) min(height[rank[j+1]],height[rank[j+2]])就可以作为编号为 j j j的后缀与编号为 k k k的最长公共前缀。
之后以此类推。
以下再引用罗穗骞dalao的图片
例如,字符串为“
aabaaaab
\text{aabaaaab}
aabaaaab” ,求后缀“
abaaaab
\text{abaaaab}
abaaaab” 和后缀“
aaab
\text{aaab}
aaab” 的最长公
共前缀,如图所示
如何高效求
height
\text{height}
height数组呢?
设
h
[
i
]
=
h
e
i
g
h
t
[
r
a
n
k
[
i
]
]
h[i]=height[rank[i]]
h[i]=height[rank[i]],也就是
s
u
f
f
i
x
(
i
)
suffix(i)
suffix(i)和在它前一名的后缀的最长公共前
缀。
则有 h [ i ] ≥ h [ i − 1 ] − 1 h[i]\ge h[i-1]-1 h[i]≥h[i−1]−1
证明:
s u f f i x ( k ) suffix(k) suffix(k)是 s u f f i x ( i − 1 ) suffix(i-1) suffix(i−1)的前一个排名的后缀,即 s a [ r a n k [ i − 1 ] − 1 ] = k sa[rank[i-1]-1]=k sa[rank[i−1]−1]=k,它们的最长公共前缀为 h [ i − 1 ] h[i-1] h[i−1],也就是 A [ k ∼ k + h [ i − 1 ] − 1 ] = A [ i − 1 ∼ i + h [ i − 1 ] − 2 ] A[k\sim k+h[i-1]-1]=A[i-1\sim i+h[i-1]-2] A[k∼k+h[i−1]−1]=A[i−1∼i+h[i−1]−2]。
那么 s u f f i x ( k + 1 ) suffix(k+1) suffix(k+1)将排在 s u f f i x ( i ) suffix(i) suffix(i)的前面(这里要求 h [ i − 1 ] > 1 h[i-1]>1 h[i−1]>1,如果 h [ i − 1 ] ≤ 1 h[i-1]≤1 h[i−1]≤1,原式显然成立) 并且 s u f f i x ( k + 1 ) suffix(k+1) suffix(k+1)和 s u f f i x ( i ) suffix(i) suffix(i)的最长公共前缀是 h [ i − 1 ] − 1 h[i-1]-1 h[i−1]−1, A [ k + 1 ∼ k + h [ i − 1 ] − 1 ] = A [ i ∼ i + h [ i − 1 ] − 2 ] A[k+1\sim k+h[i-1]-1]=A[i\sim i+h[i-1]-2] A[k+1∼k+h[i−1]−1]=A[i∼i+h[i−1]−2]。
根据上文性质, s u f f i x ( i ) suffix(i) suffix(i)和在它前一名的后缀的最长公共前缀至少是 h [ i − 1 ] − 1 h[i-1]-1 h[i−1]−1。按照 h [ 2 ] , h [ 3 ] , … … , h [ n ] h[2],h[3],……,h[n] h[2],h[3],……,h[n]的顺序计算,并利用 h 数组的性质,时间复杂度可以降为 O ( n ) O(n) O(n)
后缀自动机SAM
(这玩意真难学)
前置知识
没有。
由于篇幅很长,本人很懒,所以。