马拉车(Manacher)与回文自动机(PAM)
回文串
反转串$R(S)$:一个字符串$S=S[1]S[2]…S[n]$,其反转串$R(S)=S[n]…S[2]S[1]$
回文串:满足$S=R(S)$的字符串
回文中心:奇数长度的回文串为$\dfrac{n+1}{2}$,偶数长度的回文串为$\dfrac{n}{2}$和$\dfrac{n}{2}+1$之间
回文半径:回文中心到左右端点的距离。
回文与Border:回文串$S$的回文前(后)缀等价于其Border。
Manacher
首先为了避免讨论奇偶长度,将字符串$S$的首尾以及相邻两字符之间插入一个$S$字符集以外的字符$*$,得到
$S^{*}=*S[1]*S[2]*…*S[n]*$。
在$S^*$上重新定义极长回文串为长度为奇数且最大,且满足$S=R(S)$的字符串。
定义$Len[i]$为以$i$为回文中心的最大回文半径,为Manacher的求解目标。
定义最右回文串$P$为所有已求得的回文串中,右端点最大的回文串。
设已经求出$Len[1],Len[2],…,Len[i-1]$,此时的$P$为$S^*[L,R]$,回文中心为$p$,对$i$和$p,R$的关系进行讨论:
- $i>R$,则从$i$处向左右暴力扩展,得到回文半径$Len[i]$以及新的$P$;
- $i\le R$,这就意味着$i\in(p,R]$,因此在$[L,p)$可以找到$i$的对称位置$j=2p-i$,而$Len[j]$是已经求过的,所以继续根据$Len[j]$的情况讨论:
- $Len[j]< j-L+1$,说明以$j$为回文中心的极长回文串并没有超出$P$的范围,所以$Len[i]=Len[j]$;
- $Len[j]\ge j-L+1$,说明以$j$为回文中心的极长回文串可能会超出$P$的范围,而根据$p$对称过去的部分在$R$后面,并没有考察过,因此$Len[i]$首先继承了$Len[j]$中不超过$P$的范围,即$j-L+1$这部分,然后从这个状态继续向左右暴力扩展。
1 | void Manacher(char s[], int len[], int n) { |
复杂度分析
对于$i>R$和$Len[j]\ge j-L+1$的情况,暴力扩展操作每进行一次都会使得$R$增加,而$R$的最大值为$\left|S^*\right|$,因此总复杂度为$O(\left|S^*\right|)$。
用途
首先是可以求$Len[i]$,即每个位置的回文半径。
可以求本质不同的回文子串的数量,因为本质不同的回文子串只会在$R$增大的时候增加,所以可以得到一个性质:
对于长度为$n$的字符串$S$,其本质不同的回文子串个数最多只有$n$个。
回文自动机(PAM)
PAM的结构与ACAM类似。
PAM的节点最多$n$个,每个节点表示了一种回文串。用$S(u)$表示节点$u$所代表的回文串。$len[u]=\left|S(u)\right|$
后继边:每个后继边上有一个字母$ch$。用$trans(u,ch)=v$表示$u$节点有后继边$ch$指向$v$节点,则有$S(v)=chS(u)ch$,且$len[v]=len[u]+2$。
失配边:每个节点都有一个失配边,用$fail[u]=v$,表示$u$节点的失配边指向了$v$节点。则有$S(v)$是$S(u)$的最大Border,即最长回文后缀。
需要注意的是,奇数与偶数的回文串是有差别的,因此我们的PAM中不能只有一个根,应该有表示奇数和偶数长度的两个根,偶数根的$len$为$0$,奇数根的$len$为$-1$,且这两个根的$fail$指针互相指向对方。
那么接下来依次考察$S$的每个字符来构造PAM,设当前我们所在的节点是$now$,那么我们就去考察$S[i]$与$S[i-len[now]-1]$是否相等,如果不相等就跳到$fail[now]$继续比较,直到比较成功,这里就会发现当除了根都无法匹配的时候,一定会在奇数根处匹配成功,这是符合要求的。
若在某处匹配成功,且此时$now$并没有$S[i]$的后继边,那么就需要添加一个新的节点。新的节点就需要求$fail$,从新节点的父节点处向上搜整条$fail$链,找到第一个匹配的位置,即为所求的$fail$链。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26struct PAM {
int c[N][26], fail[N], tot, rt0, rt1, len[N];
vector<int>to[N];
void build(char s[], int n) {
rt0 = ++tot, rt1 = ++tot;
len[rt0] = 0, len[rt1] = -1;
fail[rt0] = rt1;
fail[rt1] = rt0;
int now = rt0;
for (int i = 1; i <= n; ++i) {
int nt = s[i] - 'a';
while (i - len[now] - 1 < 0 || s[i] != s[i - len[now] - 1])
now = fail[now];
if (!c[now][nt]) {
c[now][nt] = ++tot;
len[tot] = len[now] + 2;
int x = fail[now];
while (i - len[x] - 1 < 0 || s[i] != s[i - len[x] - 1])
x = fail[x];
if (tot != c[x][nt])fail[tot] = c[x][nt], to[c[x][nt]].push_back(tot);
else fail[tot] = rt0, to[rt0].push_back(tot);
}
now = c[now][nt];
}
}
}A;
PAM Series
前面提到了一个性质:回文串的回文后缀就等价于回文串的border。
所以Border Series运用到PAM上,就有了PAM Series,即:某个回文串的回文后缀最多可能有$n$个,但是将这些回文后缀按长度排序,可以构成$O(\log n)$个等差数列。
后缀数组(SA)
首先需要了解几个字符串中的基本定义:
- 子串:字符串s中任意截取的一段字符串t称t为s的子串
- 后缀:字符串s中从任一位置到字符串末尾的子串称为后缀,记为suf(i)
- 后缀数组:将s的所有后缀按照字典序进行排序,后缀对应的下标组成后缀数组,记sa[i]表示排名为i的后缀起始位置,rk[i]表示起始位置为i的后缀排名
SA求法
这里给出一种常用解法。
基本思想:我们通过倍增的思想,对每个位置以长度为1的子串开始排序,每次将排序的长度翻倍,这样的话就能利用上一次排序的结果通过$\log n$次双关键字排序得到新的排序结果,而这里的排序我们选择基数排序来降低复杂度。
1 | void radix_sort() { |
LCP求法
后缀数组最重要的作用就是可以求LCP
LCP,即最长公共前缀,也就是两个字符串的前缀中相同且最长的子串长度。
在这里,我们定义LCP(i,j)为suf(sa[i])和suf(sa[j])的LCP,即排名分别为i和j的两个后缀的LCP。
显然我们有:
- $LCP(i,j)=LCP(j,i)$
- $LCP(i,i)=n-sa[i]+1$
接下来就需要证明几个重要的定理
LCP Lemma
设$x=suf(sa[i]),y=suf(sa[j]),z=suf(sa[k]),p=min(LCP(i,j),LCP(j,k))$
显然x与y的前p个字符相等,y与z的前p个字符相等,因此x与z的前p个字符相等,即$LCP(i,k)\ge p$
j在i和k之间,根据字典序,LCP(i,j)与LCP(j,k)都大于等于LCP(i,k),因此$LCP(i,k)\le p$
综上,$LCP(i,k)=p=min(LCP(i,j),LCP(j,k))$
LCP Theorem
根据LCP Lemma,我们不难得出
Height数组
height数组是求LCP中最重要的一环。
我们定义height[i]=LCP(i,i-1),为了描述方便,设h[i]=height[rk[i]]则有
我们设k=sa[rk[i-1]-1],显然LCP(rk[i-1],rk[k])=h[i-1]
现在我们来看看suf(k+1)与suf(i):
- $s[k]\not=s[i-1]$
此时h[i-1]=0,LCP Theorem显然成立 - $s[k]=s[i-1]$
此时suf(i)是suf(i-1)去掉首字符得到的,suf(k+1)是suf(k)去掉首字符得到的,因此suf(i)和suf(k+1)之间的排名关系与suf(i-1)和suf(k)之间的排名关系相同,所以rk[k+1]< rk[i],而且,LCP(rk[k+1],rk[i])=LCP(rk[k],rk[i-1])-1=h[i-1]-1
根据定义,h[i]=LCP(rk[i],rk[i]-1)
接下来只要证明$\max\{LCP(rk[i], j)|j< rk[i]\}=LCP(rk[i],rk[i]-1)$
又因为rk[k+1]<=rk[i]-1,就可得出h[i]>=LCP(rk[k+1],rk[i])=h[i-1]-1
而这个证明非常简单,根据字典序,suf(sa[j])的前h[i]个字符只能是suf(sa[rk[i]-1])的前h[i]个字符变小形成,这就保证了j减小的过程中LCP(rk[i],j)不会变大。
求LCP
有了上面的性质和定理,我们就可以在线性复杂度内求出height数组1
2
3
4
5
6
7
8
9
10
11
12void getheight() {
int k = 0;
for (int i = 1; i <= n; ++i)rk[sa[i]] = i;
for (int i = 1; i <= n; ++i) {
//这里i枚举的是位置而不是排名
if (rk[i] == 1)continue;
int j = sa[rk[i] - 1];
if (k)--k;//h[i]>=h[i-1]-1,因此从h[i-1]-1开始检验
while (i + k <= n && j + k <= n && s[i + k] == s[j + k])++k;//直接找
height[rk[i]] = k;
}
}
根据LCP Theorem和height的定义,$LCP(i,j)=\min\{height[k]|i<k\le j\}$
我们用ST表来维护height的区间最小值,即可做到O(1)查询LCP
1 | int LCP(int l, int r) {//l和r都是rk值 |