心得
回文树,又名回文自动机,是一个东西
去年网络赛之后一直想学这个东西,一直碍于太难没学……
今天有个SA+PAM(后缀数组+回文自动机)的题,快100个队过,我就来补了……
一看我收藏的那篇讲解回文树的博客2W访问,看来你国网友都会了
原理感觉和AC自动机很类似,真的很巧妙啊,
由于now是从cur补字母c构成的回文串,
所以fail[now]要从fail[cur]失配时的最长后缀回文串状态补字母c
思路来源
https://blog.csdn.net/u013368721/article/details/42100363
知识点整理
功能:
1.求串S前缀0~i内本质不同回文串的个数(两个串长度不同或者长度相同且至少有一个字符不同便是本质不同)
2.求串S内每一个本质不同回文串出现的次数
3.求串S内回文串的个数(其实就是1和2结合起来)
4.求以下标i结尾的回文串的个数
1.len[i]表示编号为i的节点表示的回文串的长度(一个节点表示一个回文串)。
2.next[i][c]表示编号为i的节点表示的回文串在两边添加字符c以后变成的回文串的编号(和字典树类似)。
3.fail[i],fail指针,失配后跳转到fail指针指向的节点
即节点i失配以后,应跳转到编号为i的节点代表的回文串的最长后缀回文串j,
若编号为k的节点保存了回文串j,则fail[i]=k
4.cnt[i]表示编号为i的节点表示的回文字符串在整个字符串中出现了多少次。
(建树时求出的不是完全的,最后count()函数跑一遍以后才是正确的)
5.num[i]表示节点i表示的回文字符串中,有多少个本质不同的字符串(包括本身)。
6.last指向新添加一个字母后所形成的最长回文串表示的节点。
7.S[i]表示第i次添加的字符(一开始设S[0] = -1(可以是任意一个在串S中不会出现的字符))。
8.p表示添加的节点个数。
9.n表示添加的字符个数。
一开始回文树有两个节点,0表示偶数长度串的根和1表示奇数长度串的根,
且len[0] = 0,len[1] = -1,last = 0,S[0] = -1,n = 0,p = 2(添加了节点0、1)。
count函数用于计算cnt[i]
将节点x在fail指针树中将自己的cnt累加给父亲,从叶子开始倒着加,
最后就能得到串S中出现的每一个本质不同回文串的个数
如果只求本质不同回文串的种类数,答案就是回文树中除去虚节点0和虚节点1之外的节点数,即p-2
而所有奇回文串的根是cnt[1],偶回文串的根是cnt[0],且cnt[0]连向cnt[1]
那么虚节点cnt[1],记录的就是所有回文串出现的次数
复杂度
空间复杂度:O(N*字母种类数)
时间复杂度见到两种说法,一种说是O(N*字母种类数)
另一种说法是,长度为n的字符串本质不同的回文子串数目最多为n,所以时间复杂度:O(n)
例题
1.ural1960. Palindromes and Super Abilities
2.TsinsenA1280. 最长双回文串
3.TsinsenA1255. 拉拉队排练
4.TsinsenA1393. Palisection
5.2014-2015 ACM-ICPC, Asia Xian Regional Contest G The Problem to Slow Down You
板子整理1
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 100005 ;
const int N = 26 ;
char s[MAXN];
int len;
struct Palindromic_Tree {
int next[MAXN][N] ;//next指针,next指针和字典树类似,指向的串为当前串两端加上同一个字符构成
int fail[MAXN] ;//fail指针,失配后跳转到fail指针指向的节点
int cnt[MAXN] ;//cnt[i]表示编号为i的节点表示的回文字符串在整个字符串中出现了多少次。
//cnt[i]建树时求出的不是完全的,最后count()函数跑一遍以后才是正确的
int num[MAXN] ;//num[i]表示节点i表示的回文字符串中,有多少个本质不同的字符串(包括本身)
int v;
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 ) {//和KMP一样,失配后找一个尽量最长的
while ( S[n - len[x] - 1] != S[n] ) x = fail[x] ;
return x ;
}
void add ( int c ) {
c -= 'a' ;
S[++ n] = c ;
int cur = get_fail ( last ) ;//通过上一个回文串找这个回文串的匹配位置
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 ;
}
last = next[cur][c] ;
cnt[last] ++ ;
}
void count()//如果不count向上汇总 每个节点代表的回文串 当且仅当完全相同时才会被记次数
{
for ( int i = p - 1 ; i >= 0 ; -- i ) cnt[fail[i]] += cnt[i] ;
//fail[i]累加i的cnt,因为如果fail[v]=u,则u一定是套在v内的子回文串!
//所以 u自己出现了一次 在v中出现了一次 归回去能统计出u出现的次数
}
}a;
int main()
{
a.init();
scanf("%s",s);
len=strlen(s);
for(int i=0;i<len;++i)
a.add(s[i]);
a.count();//最后一定要把fail归到根
//才能统计出每个本质不同子串出现了多少次
printf("%d\n",a.cnt[1]);//输出所有回文串的个数
//所有子串的次数都加到了虚节点上
return 0;
}
板子整理2(福州大学模板)
#include<bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define mp make_pair
#define pb push_back
#define rep(i, a, b) for(int i=(a); i<(b); i++)
#define per(i, a, b) for(int i=(b)-1; i>=(a); i--)
#define sz(a) (int)a.size()
#define de(a) cout << #a << " = " << a << endl
#define dd(a) cout << #a << " = " << a << " "
#define all(a) a.begin(), a.end()
#define pw(x) (1ll<<(x))
#define endl "\n"
typedef long long ll;
typedef pair<int, int> pii;
typedef vector<int> vi;
typedef double db;
#define mem(a,x) memset(a,x,sizeof(a))
const int N=3e5+5;
char s[N];
int n;
inline void Put(string &s,vi &V) {
V.clear();
rep(i,0,sz(s)) V.pb(s[i]-'a');
}
const int M=26;
struct PAM{
int s[N],len[N],next[N][M],fail[N],cnt[N],dep[N],id[N],no[N],last,n,p,cur,now,f[N][20];
ll ans;
inline int new_node(int _l) { mem(next[p],0); cnt[p]=dep[p]=0,len[p]=_l; return p++; }
inline void Init() { new_node(p=0),new_node(s[0]=-1),fail[last=n=0]=1; }
inline int get_fail(int x) { for (; s[n-len[x]-1]!=s[n]; x=fail[x]); return x; }
inline void I(int c) {
c-='a',s[++n]=c,cur=get_fail(last);
if (!next[cur][c]) {
now=new_node(len[cur]+2);
fail[now]=next[get_fail(fail[cur])][c];
next[cur][c]=now;
dep[now]=dep[fail[now]]+1;
}
last=next[cur][c]; cnt[last]++;
id[n]=last,no[last]=n; //id[第n个字符]=第i个节点 no[第last个节点]=第n个字符 记录下在原串中的位置下标 从1开始
}
inline void Insert(char s[],int op=0,int _n=0) {//_n=0正序插入 _n=1倒序插入
if (!_n) _n=strlen(s); if (!op) rep(i,0,_n) I(s[i]); else per(i,0,_n) I(s[i]);
}
inline void count() { per(i,0,p) cnt[fail[i]]+=cnt[i]; }//短回文串fail[i]计入包含它的回文串i里fail[i]的出现次数
inline int Find(int x,int L) {//去找节点x的回文串长不超过l的最近祖先(可以为自己)编号 倍增lca
if (len[x]<=L) return x;
per(i,0,20) if (len[f[x][i]]>L) x=f[x][i];
return fail[x];
}
inline void init() {//fail树预处理倍增
mem(f,0);
rep(i,0,p) f[i][0]=fail[i];
rep(i,0,p) rep(j,1,20) f[i][j]=f[f[i][j-1]][j-1];
}
inline void Q() {
count();
rep(i,2,p) no[i]--;//为了对应0-(n-1)的下标
}
} TT;
int main() {
while (scanf("%s",s)!=EOF) {
n=strlen(s);
TT.Init(),TT.Insert(s),TT.Q();
}
return 0;
}