几大常见自动机...但我并不会....只会manacher,还只会板子....
安利好的博客
简单说一下,回文自动机有两颗tire树构成,根节点分别为0,1分别表示长度为偶数,奇数的回文串..
而且注意的时这里的节点都代表一个回文串,例如0->(a)2->(b)3.3号节点代表的是baab这个回文串.同样2号节点代表的是aa这个字符串.
这里再定义fail数组,代表的时当前节点构成的回文串与之匹配的最长后缀的节点.至于为什么是后缀,因为我们知道了回文自动机实质上实在原有回文串的基础上在两边都加上相同的字符来增加回文串的.
所以当前的节点如果无法与之前节点匹配的话,我们就只能用同一个后缀,但前面适当缩减来继续尝试匹配.特别的fail[0]=1.原因是0节点失配后,那就只能自己配自己了.
之后是len数组,表示当前节点构成的回文串的长度,特别的len[1]=-1,由于要保证1号是长度为奇数的回文串的根,况且这也保证了任何一个节点在1这里都能配对成功,因为是自己配自己.
还有我们还要维护一个last指针,表示上一个节点.因为我们是按照顺序给回文串增加长度的.所以这个是必要的.
之后口糊一下大概流程:
1.找到last只想的节点,尝试给他长度增1,即判断c[i-len[last]-1]是否与c[i]相等.否则,跳fail指针.
2.若当前last没有指向当前字符的边.新建节点.更新信息。.
3.更新last
代码大概长这样...
fail[0]=1;len[1]=-1;cnt=1; rep(i,1,n) { while(c[i-len[last]-1]!=c[i]) last=fail[last];//尝试为当前节点增长. int ch=c[i]-'a'; if(!tire[last][ch]) { tire[last][ch]=++cnt; //新建节点 len[cnt]=len[last]+2;//更新长度. int j=fail[last]; //fail指针不能是其本身 while(c[i-len[j]-1]!=c[i]) j=fail[j];//为其找到fail指针. fail[cnt]=tire[j][ch];// } last=tire[last][ch];//更新last }
这里再放几道例题:
https://www.luogu.com.cn/problem/P5496
这个是模板题,正好可以让我们熟悉下回文自动机基本上支持什么操作.
这个题要求以每个前缀的回文串的数量,我们在构造自动机时,加一个num数组,意思是当前节点的回文串的数量.
发现这个状态不能有父节点简单的继承过来。由于是以i结尾的回文串的数量,所以应该有fail[i]继承过来.
这里加深下对fail指针的理解,与此节点最长后缀,所有我们机场下fail指针即可.
#include<bits/stdc++.h> #define db double #define RE register #define ll long long #define P 1000000007 #define INF 1000000000 #define get(x) x=read() #define PLI pair<ll,int> #define PII pair<int,int> #define pb(x) push_back(x) #define ull unsigned long long #define put(x) printf("%d ",x) #define getc(a) scanf("%s",a+1) #define putl(x) printf("%lld ",x) #define rep(i,x,y) for(RE int i=x;i<=y;++i) #define fep(i,x,y) for(RE int i=x;i>=y;--i) #define go(x) for(int i=link[x],y=a[i].y;i;y=a[i=a[i].next].y) using namespace std; const int N=5e5+10; int len[N],fail[N],tire[N*26][26],num[N],cnt,ans; char c[N]; inline int read() { int x=0,ff=1; char ch=getchar(); while(!isdigit(ch)) {if(ch=='-') ff=-1;ch=getchar();} while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48);ch=getchar();} return x*ff; } inline void PAM() { fail[0]=1;len[1]=-1;cnt=1; int n=strlen(c+1),last=0; c[0]='#'; rep(i,1,n) { if(i!=1) c[i]=(char)((c[i]-97+ans)%26+97); while(c[i-len[last]-1]!=c[i]) last=fail[last]; int ch=c[i]-'a'; if(!tire[last][ch]) { len[++cnt]=len[last]+2; int j=fail[last]; while(c[i-len[j]-1]!=c[i]) j=fail[j]; fail[cnt]=tire[j][ch]; num[cnt]=num[fail[cnt]]+1; tire[last][ch]=cnt; } last=tire[last][ch]; printf("%d ",ans=num[last]); } } int main() { // freopen("1.in","r",stdin); getc(c); PAM(); return 0; }
这里再放一道题:
https://www.luogu.com.cn/problem/P4287
题目定义了什么是双倍回文,由于本身是个回文串,所以我们可以用回文自动机扫出来这个最长回文串,接下来就是怎么判定这是不是双倍回文串了.
由于前半部分和后半部分都是回文串,所以我们直接判定本身是回文串,前半部分或后半部分是回文串即可.(仔细思考,本身回文串说明前半部分与后半部分对称).
根据回文串自动机对后缀有着一些应用,所以我们选择判定后半部分.考虑后半部分==回文串等价于有无长度等与一半的后缀.由于回文自动机上的节点都是回文串,所以我们可以不用判定是否为回文串.
直接看他的后缀能否等于长度的一半.这样我们定义tarns为长度小于等于一半的最长后缀.直接判断即可.
#include<bits/stdc++.h> #define db double #define RE register #define ll long long #define P 1000000007 #define INF 1000000000 #define get(x) x=read() #define PLI pair<ll,int> #define PII pair<int,int> #define pb(x) push_back(x) #define ull unsigned long long #define put(x) printf("%d ",x) #define getc(a) scanf("%s",a+1) #define putl(x) printf("%lld ",x) #define rep(i,x,y) for(RE int i=x;i<=y;++i) #define fep(i,x,y) for(RE int i=x;i>=y;--i) #define go(x) for(int i=link[x],y=a[i].y;i;y=a[i=a[i].next].y) using namespace std; const int N=501000; int tire[N*26][26],len[N],fail[N],trans[N],tot=1; char c[N]; inline int read() { int x=0,ff=1; char ch=getchar(); while(!isdigit(ch)) {if(ch=='-') ff=-1;ch=getchar();} while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48);ch=getchar();} return x*ff; } int main() { //freopen("1.in","r",stdin); int get(n);getc(c); int ans=0,last=0; fail[0]=1;len[1]=-1; rep(i,1,n) { while(c[i-len[last]-1]!=c[i]) last=fail[last]; int ch=c[i]-'a'; if(!tire[last][ch]) { len[++tot]=len[last]+2; int j=fail[last]; while(c[i-len[j]-1]!=c[i]) j=fail[j]; fail[tot]=tire[j][ch]; if(len[tot]<=2) trans[tot]=fail[tot]; else { int k=trans[last]; while(c[i-len[k]-1]!=c[i]||(len[k]+2<<1)>len[tot]) k=fail[k]; trans[tot]=tire[k][ch]; } tire[last][ch]=tot; } last=tire[last][ch]; if(len[last]%4==0&&len[trans[last]]==len[last]/2) ans=max(ans,len[last]); } put(ans); return 0; }
https://www.luogu.com.cn/problem/P3649
板子题,这里记录下每个最长回文串出现的次数.
最后根据fail拓扑一下,累加答案即可.
#include<bits/stdc++.h> #define db double #define RE register #define ll long long #define P 1000000007 #define INF 1000000000 #define get(x) x=read() #define PLI pair<ll,int> #define PII pair<int,int> #define pb(x) push_back(x) #define ull unsigned long long #define put(x) printf("%d ",x) #define getc(a) scanf("%s",a+1) #define putl(x) printf("%lld ",x) #define rep(i,x,y) for(RE int i=x;i<=y;++i) #define fep(i,x,y) for(RE int i=x;i>=y;--i) #define go(x) for(int i=link[x],y=a[i].y;i;y=a[i=a[i].next].y) using namespace std; const int N=300010; int tire[N*26][26],fail[N],num[N],tot=1,len[N]; char c[N]; inline int read() { int x=0,ff=1; char ch=getchar(); while(!isdigit(ch)) {if(ch=='-') ff=-1;ch=getchar();} while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48);ch=getchar();} return x*ff; } int main() { //freopen("1.in","r",stdin); getc(c);int n=strlen(c+1); fail[0]=1;len[1]=-1; int last=0; ll ans=0; rep(i,1,n) { while(c[i-len[last]-1]!=c[i]) last=fail[last]; int ch=c[i]-'a'; if(!tire[last][ch]) { len[++tot]=len[last]+2; int j=fail[last]; while(c[i-len[j]-1]!=c[i]) j=fail[j]; fail[tot]=tire[j][ch]; tire[last][ch]=tot; } last=tire[last][ch]; num[last]++; } fep(i,tot,2) { num[fail[i]]+=num[i]; ans=max(ans,(ll)len[i]*num[i]); } putl(ans); return 0; }
小结一下:回文串自动机之所以强大,是因为它的fail指针将以i结尾的所有回文串都串联起来了.这样我们就可以通过这个关系来统计一些对我们有用的量.