本文參考自原文鏈接
迴文自動機,又叫回文樹,是由俄羅斯人 MikhailRubinchik於2014年夏發明的(http://codeforces.com/blog/entry/13959).
這是一種比較新的數據結構,在原文中已有詳細介紹與代碼實現。
迴文樹其實不是嚴格的樹形結構,因爲它有是兩棵樹,分別是偶數長度的迴文樹和奇數長度的迴文樹,樹中每個節點代表一個迴文串。
爲了方便,第一棵樹的根是一個長度爲0的串,第二棵就是爲-1的串,不要感到奇怪,就是-1。
可以證明,最多隻有n個結點(n是串的長度)。這個可以用Manacher算法來證明。
如果某結點代表的是串ccabacc,那麼它的父親代表的串就是去掉前後兩個字符cabac。
每個點還有一個fail指針,表示這個串的後綴中最長的迴文串,比如babab的fail指向bab,bab的指向b。
方法的思想和KMP,AC自動機很類似,如果你理解了KMP與AC自動機,那麼這個算法基本可以一看就懂。
- 數據說明
- len[i]:節點i的迴文串的長度
- next[i][c]:節點i的迴文串在兩邊添加字符c以後變成的迴文串的編號(和字典樹的next指針類似)
- fail[i]:類似於AC自動機的fail指針,指向失配後需要跳轉到的節點
- cnt[i]:節點i表示的迴文串在S中出現的次數(建樹時求出的不是完全的,count()加上子節點以後纔是正確的)
- num[i]:以節點i迴文串的末尾字符結尾的但不包含本條路徑上的迴文串的數目。(也就是fail指針路徑的深度)
- last:指向最新添加的迴文結點
- S[i]表示第i次添加的字符
- p表示添加的節點個數
注意:迴文自動機裏面的節點都表示的是一個迴文串!!!而不是一個下標/字符!
先貼一波代碼
#include <queue>
#include <cstdio>
#include <set>
#include <string>
#include <stack>
#include <cmath>
#include <climits>
#include <map>
#include <cstdlib>
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>
#define LL long long
#define ULL unsigned long long
using namespace std;
const int MAXN = 100005 ;
const int N = 26 ;
char s[MAXN];
struct Palindromic_Tree
{
int next[MAXN][N] ;//next指針,next指針和字典樹類似,指向的串爲當前串兩端加上同一個字符構成
int fail[MAXN] ;//fail指針,失配後跳轉到fail指針指向的節點
int cnt[MAXN] ;
int num[MAXN] ; // 當前節點通過fail指針到達0節點或1節點的步數(fail指針的深度)
int len[MAXN] ;//len[i]表示節點i表示的迴文串的長度
int S[MAXN] ;//存放添加的字符
int last ;//指向上一個字符所在的節點,方便下一次add
int n ;//字符數組指針
int p ;//節點指針
int newnode(int l) //新建節點
{
for(int i = 0 ; i < N ; ++ i) next[p][i] = 0 ;
cnt[p] = 0 ;
num[p] = 0 ;
len[p] = l ;
return p ++ ;
}
void init() //初始化
{
p = 0 ;
newnode(0) ;
newnode(-1) ;
last = 0 ;
n = 0 ;
S[n] = -1 ;//開頭放一個字符集中沒有的字符,減少特判
fail[0] = 1 ;
}
int get_fail(int x) //失配後,在迴文串x中的所有後綴裏找到一個串右端+s[n]依然構成迴文串
{ //這裏因爲一定是從長的找到最短的,所以找到的一定是最長的
while(S[n - len[x] - 1] != S[n]) x = fail[x] ;//判斷此時S[n-len[last]-1]是否等於S[n]
//即上一個串-1的位置和新添加的位置是否相同,相同則說明構成迴文,否則,last=fail[last]。
return x ;
}
void add(int c,int pos) //cur,last,now都代表一個字符串,而不是一個下標/字符
{
printf("%d:",p);
c -= 'a';
S[++ n] = c ; //n代表字符下標
int cur = get_fail(last) ; //通過上一個迴文串找這個迴文串的匹配位置
printf("%d ",cur); //c+cur+c代表以c結尾的最長的迴文串,cur對應原串中的位置就是以c前一個字符結尾的子串的位置
if(!next[cur][c]) //如果這個迴文串沒有出現過,說明出現了一個新的本質不同的迴文串
{
int now = newnode(len[cur] + 2) ; //新建節點
fail[now] = next[get_fail(fail[cur])][c] ; //和AC自動機一樣建立fail指針,以便失配後跳轉
next[cur][c] = now ;
num[now] = num[fail[now]] + 1 ;
for(int i=pos-len[now]+1; i<=pos; ++i) printf("%c",s[i]);
} last = next[cur][c] ;
cnt[last] ++ ;
putchar(10);
}
void count()
{
for(int i = p - 1 ; i >= 0 ; -- i) cnt[fail[i]] += cnt[i] ;
//父親累加兒子的cnt,因爲如果fail[v]=u,則u一定是v的子迴文串!
}
} run;
int main()
{
scanf("%s",&s);
int n=strlen(s);
run.init();
for(int i=0; i<n; i++) run.add(s[i],i);
run.count();
return 0;
}
看其他博客講的,都看不懂,可能是自己太笨了...
所以就直接拿代碼來啃,終於理解這個算法的原理。
迴文自動機,其實關鍵是理解fail,基礎就是我上面說的那句話————迴文自動機的節點表示的是一個迴文串!!!
然後難點就是fail[now] = next[get_fail(fail[cur])][c] ; 這句代碼
首先定義後綴迴文串:字符串的一個後綴(不包括字符串本身),該後綴是迴文串
//因爲這裏now=c+cur+c已經代表了以c結尾的最長的迴文串,那麼如果這個匹配失敗,我們
需要找到fail[now]=c+w+c——以c結尾的次長的迴文串,該回文串的一個性質——
該回文串fail[now]是now的最長迴文串後綴,因爲這樣我們才能保證在now匹配失敗後,
fail[now]能立即被匹配,不需要任何移動。這一步就可以藉助fail[cur]來完成
例如
bbabba
最後一個字符'a'的最長迴文now是"abba",
cur="bb",fail[cur]="b",然後在fail[cur]中沒有找到滿足條件的解,
一直搜到x=0,表示長度爲2的迴文串,len[x]=1,發現還是不對,
然後x=fail[0]=1,長度爲1的迴文串,len[x]=-1,發現就是他本身
那麼他的fail[now]="a"
abbaabba
最後一個字符'a'的now是"abbaabba",
cur="bbaabb",fail[cur]="bb"
他的fail[now]="abba"
然後next[get_fail(fail[cur])][c] 非空的原因見圖
看完這個還不懂得可以去看一下貼在上面原來大牛的博客
最後貼一波他的功能
1.求串S前綴0~i內本質不同迴文串的個數(兩個串長度不同或者長度相同且至少有一個字符不同便是本質不同)
2.求串S內每一個本質不同迴文串出現的次數
3.求串S內迴文串的個數(其實就是1和2結合起來)
4.求以下標i結尾的迴文串的個數
- len[i]:節點i的迴文串的長度
- next[i][c]:節點i的迴文串在兩邊添加字符c以後變成的迴文串的編號(和字典樹的next指針類似)
- fail[i]:類似於AC自動機的fail指針,指向失配後需要跳轉到的節點
- cnt[i]:節點i表示的迴文串在S中出現的次數(建樹時求出的不是完全的,count()加上子節點以後纔是正確的)
- num[i]:以節點i迴文串的末尾字符結尾的但不包含本條路徑上的迴文串的數目。(也就是fail指針路徑的深度)
- last:指向最新添加的迴文結點
- S[i]表示第i次添加的字符
- p表示添加的節點個數
-