- KMP,扩展KMP和Manacher就不写了,感觉没多大意思。
- 之前感觉后缀自动机简直可以解决一切,所以不怎么写后缀数组。
- 马拉车主要是通过对称中心解决问题,有的时候要通过回文串的边界解决问题,这个时候回文树就用到了,又好写,又强大。
- 之前整理过一篇后缀自动机的。感觉还要整理一下,顺便把回文树,后缀数组也整理一下
- 很久没写专题了,如果有空,把回文自动机也学习一下。
一:回文树
前置技能: 马拉车,KMP。 其实也没关系,就是对比一下。
马拉车是利用对称的思想,得到每个点或者空格为对称中心的最长回文。
KMP有失配指针,回文树也有,指向失配后最长的回文串。
前置理论: 一个字符串的本质不同的回文串最多有|S|个。所以,回文树的空间是线性的。
保存信息:我们把每个回文串对应到一个节点里。
struct node{ int len,num,fail,son[26],dep; }t[maxn];
其中,len是回文串的长度;num是出现次数;son是儿子指针,用于匹配,看是否能增加长度;dep是指这个回文串失配次数,即以它的最后一个字符为尾的回文串个数; fail是失配指针;
功能:我们可以得到所有的回文串; 所有本质不同的回文串; 回文串的出现次数; 以某个位置结尾的回文串个数;
实现:首先,初始化两个节点1号和0号,分别表示奇数偶数长度的回文串。 他们都指向1号节点,而1号节点的长度设置尾-1,这样的话,确保每个位置结尾的回文串长度至少是-1+2=1;
void init() { tot=last=1; t[0].len=0; t[1].len=-1; t[0].fail=t[1].fail=1; }
完整代码:
struct PAT { struct node{ int len,num,fail,son[26]; }t[maxn]; int last,n,tot,s[maxn]; void init() { memset(t,0,sizeof(t)); tot=last=1; n=0; t[0].len=0; t[1].len=-1; t[0].fail=t[1].fail=1; s[0]=-1; } int add(int c){ int p=last; s[++n]=c; while(s[n]!=s[n-1-t[p].len]) p=t[p].fail; if(!t[p].son[c]){ int v=++tot,k=t[p].fail; while(s[n]!=s[n-t[k].len-1]) k=t[k].fail; t[v].fail=t[k].son[c]; t[v].len=t[p].len+2; t[v].num=t[t[v].fail].num+1; t[p].son[c]=v; } last=t[p].son[c]; return t[last].num; } }T;
例题一:HDU5658:CA Loves Palindromic
题意:给定字符串S,|S|<1000;Q次询问区间不用本质的回文串数量。
思路:以每个左端点建立回文树即可。
#include<bits/stdc++.h> #define rep(i,a,b) for(int i=a;i<=b;i++) using namespace std; const int maxn=1010; char c[maxn]; int bb[maxn]; int N,Q,fcy[maxn][maxn]; struct PT { struct node{ int fail,len,son[26]; }t[maxn]; int tot,last; void init() { memset(t,0,sizeof(t)); t[0].fail=t[1].fail=1; t[1].len=-1; last=1; tot=1; bb[0]=-1; bb[1]=-2; } void add(int s,int n) { int p=last; bb[n]=s; while(bb[n-t[p].len-1]!=bb[n]) p=t[p].fail; if(!t[p].son[s]) { int v=++tot,k=t[p].fail; t[v].len=t[p].len+2; while(bb[n-t[k].len-1]!=bb[n]) k=t[k].fail; t[v].fail=t[k].son[s]; t[p].son[s]=v; } last=t[p].son[s]; } }T; void solve() { rep(i,1,N){ T.init(); rep(j,i,N) { T.add(c[j]-'a',j-i+1); fcy[i][j]=T.tot-1; } } scanf("%d",&Q); rep(i,1,Q){ int L,R; scanf("%d%d",&L,&R); printf("%d\n",fcy[L][R]); } } int main() { int T;scanf("%d",&T); while(T--){ memset(c,0,sizeof(c)); scanf("%s",c+1); N=strlen(c+1); solve(); } return 0; }
例题二:HDU - 5157 :Harry and magic string
题意: 多组输入,每次给定字符串S(|S|<1e5),求多少对不相交的回文串。
思路:可以用回文树求出以每个位置结尾的回文串数,那么累加得到前缀和; 倒着再做一遍得到每个位置为开头的回文串数,乘正向求出的前缀和即可。
#include<bits/stdc++.h> #define ll long long #define rep(i,a,b) for(int i=a;i<=b;i++) #define rep2(i,a,b) for(int i=a;i>=b;i--) using namespace std; const int maxn=100010; struct PAT { struct node{ int len,num,fail,son[26]; }t[maxn]; int last,n,tot,s[maxn]; void init() { memset(t,0,sizeof(t)); tot=last=1; n=0; t[0].len=0; t[1].len=-1; t[0].fail=t[1].fail=1; s[0]=-1; } int add(int c){ int p=last; s[++n]=c; while(s[n]!=s[n-1-t[p].len]) p=t[p].fail; if(!t[p].son[c]){ int v=++tot,k=t[p].fail; while(s[n]!=s[n-t[k].len-1]) k=t[k].fail; t[v].fail=t[k].son[c]; t[v].len=t[p].len+2; t[v].num=t[t[v].fail].num+1; t[p].son[c]=v; } last=t[p].son[c]; return t[last].num; } }T; ll ans,sum[maxn];char c[maxn]; int main() { while(~scanf("%s",c+1)){ int N=strlen(c+1); T.init(); ans=0; rep(i,1,N) sum[i]=sum[i-1]+T.add(c[i]-'a'); T.init(); rep2(i,N,1) ans+=sum[i-1]*T.add(c[i]-'a'); printf("%lld\n",ans); } return 0; }
题意:累计i*k的和,如果[i,j],[j+1,k]都是回文串;
思路:统计以j为结尾的回文串个数numj,以及他们的长度和addj; 以j+1为首....;那么这个位置的贡献就是(numj*(j+1)-addj)*(numj+1*j+addj+1);
(此题要节约空间,所以不要开longlong的数组。
#include<bits/stdc++.h> #define ll long long #define rep(i,a,b) for(int i=a;i<=b;i++) #define rep2(i,a,b) for(int i=a;i>=b;i--) using namespace std; const int maxn=1000002; const int Mod=1e9+7; struct PAT { struct node{ int len,num,fail,son[26],add; }t[maxn]; int last,n,tot,s[maxn]; void init() { memset(t,0,sizeof(t)); tot=last=1; n=0; t[0].len=0; t[1].len=-1; t[0].fail=t[1].fail=1; s[0]=-1; } void add(int c){ int p=last; s[++n]=c; while(s[n]!=s[n-1-t[p].len]) p=t[p].fail; if(!t[p].son[c]){ int v=++tot,k=t[p].fail; while(s[n]!=s[n-t[k].len-1]) k=t[k].fail; t[v].fail=t[k].son[c]; t[v].len=t[p].len+2; t[v].num=t[t[v].fail].num+1; t[v].add=(t[t[v].fail].add+t[v].len)%Mod; t[p].son[c]=v; } last=t[p].son[c]; } }T; int ans,sum[maxn];char c[maxn]; int main() { while(~scanf("%s",c+1)){ int N=strlen(c+1); T.init(); ans=0; rep(i,1,N) { T.add(c[i]-'a'); sum[i]=(1LL*T.t[T.last].num*(i+1)-T.t[T.last].add)%Mod; } T.init(); rep2(i,N,1){ T.add(c[i]-'a'); ans+=(ll)(T.t[T.last].add+1LL*T.t[T.last].num*(i-1)%Mod)*sum[i-1]%Mod; ans%=Mod; } printf("%d\n",ans); } return 0; }
例题四:HDU - 5421:Victor and String
题意:多组输入,开始字符串为空,支持4中操作: 1,在字符串首加字符; 2,在字符串尾加字符; 3,查询字符串不同本质的回文串个数; 4,查询回文串个数总和
思路:因为支持首尾加入,所以和常规的回文树有些不同。 参考了YYB的博客。 发现首尾互相影响,当且仅当整个字符串是回文串。 其他情况两头正常加即可。
也就是现在有两个last,除了完全对称,其他操作都一样。
#include<bits/stdc++.h> #define ll long long #define rep(i,a,b) for(int i=a;i<=b;i++) #define rep2(i,a,b) for(int i=a;i>=b;i--) using namespace std; const int maxn=100010; struct PAT { struct node{ int len,num,fail,son[26]; }t[maxn]; int n,tot,s[maxn<<1],L,R,suf,pre; ll ans; void init() { memset(t,0,sizeof(t)); memset(s,-1,sizeof(s)); tot=1; L=100000; R=L-1; suf=pre=0; ans=0; t[1].len=-1; t[0].fail=t[1].fail=1; } void add(int c,int n,int &last,int op){ int p=last; s[n]=c; while(s[n]!=s[n-op-op*t[p].len]) p=t[p].fail; if(!t[p].son[c]){ int v=++tot,k=t[p].fail; while(s[n]!=s[n-op*t[k].len-op]) k=t[k].fail; t[v].fail=t[k].son[c]; t[v].len=t[p].len+2; t[v].num=t[t[v].fail].num+1; t[p].son[c]=v; } last=t[p].son[c]; ans+=t[last].num; if(t[last].len==R-L+1) suf=pre=last; } }T; int main() { int N; while(~scanf("%d",&N)){ T.init(); int opt; rep(i,1,N){ scanf("%d",&opt); char s[3]; if(opt==1){ scanf("%s",s); T.add(s[0]-'a',--T.L,T.pre,-1); } else if(opt==2){ scanf("%s",s); T.add(s[0]-'a',++T.R,T.suf,1); } else if(opt==3) printf("%d\n",T.tot-1); else printf("%lld\n",T.ans); } } return 0; }
例题五:Gym - 101981M:(南京) Mediocre String Problem
题意:给字符串S和T,累计(i结尾的回文串个数)*(i+1开始匹配T的长度)。
思路:第一部分用回文树,第二部分exkmp。
#include<bits/stdc++.h> #define ll long long #define rep(i,a,b) for(int i=a;i<=b;i++) using namespace std; const int maxn=1000010; char S[maxn],T[maxn]; struct PT { struct in{ int dep,fail,len,son[26]; }p[maxn]; int cnt,last; void init() { //memset(p,0,sizeof(p)); cnt=last=1;p[0].dep=p[1].dep=0; p[0].fail=p[1].fail=1; p[0].len=0; p[1].len=-1; } int add(int c,int n) { int np=last; while(S[n]!=S[n-1-p[np].len]) np=p[np].fail; if(!p[np].son[c]){ int v=++cnt,k=p[np].fail; p[v].len=p[np].len+2; while(S[n]!=S[n-p[k].len-1]) k=p[k].fail; p[v].fail=p[k].son[c]; p[np].son[c]=v; //这一句放前面会出现矛盾,因为np可能=k p[v].dep=p[p[v].fail].dep+1; } last=p[np].son[c]; return p[last].dep; } }Tree; int N,M,num[maxn],Next[maxn],extand[maxn]; ll ans; void getnext(){// next[i]: 以第i位置开始的子串与T的公共前缀长度 int i,length=strlen(T+1); Next[1]=length; for(i=0;i+1<length&&T[i+1]==T[i+2];i++); Next[2]=i; int a=2; //! for(int k=3;k<=length;k++){//长度+1,位置-1。 int p=a+Next[a]-1, L=Next[k-a+1]; if(L>=p-k+1){ int j=(p-k+1)>0?(p-k+1):0;//中断后可能是负的 while(k+j<=length&&T[k+j]==T[j+1]) j++;// 枚举(p+1,length) 与(p-k+1,length) 区间比较 Next[k]=j, a=k; } else Next[k]=L; } } void getextand(){ memset(Next,0,sizeof(Next)); getnext(); int Slen=strlen(S+1),Tlen=strlen(T+1),a=0; int MinLen=Slen>Tlen?Tlen:Slen; while(a<MinLen&&S[a+1]==T[a+1]) a++; extand[1]=a; a=1; for(int k=2;k<=Slen;k++){ int p=a+extand[a]-1,L=Next[k-a+1]; if(L>=p-k+1){ int j=(p-k+1)>0?(p-k+1):0; while(k+j<=Slen&&j+1<=Tlen&&S[k+j]==T[j+1]) j++; extand[k]=j;a=k; } else extand[k]=L; } } int main() { scanf("%s%s",S+1,T+1); N=strlen(S+1); M=strlen(T+1); reverse(S+1,S+N+1); Tree.init(); rep(i,1,N) num[i]=Tree.add(S[i]-'a',i); getextand(); rep(i,1,N) ans+=(ll)num[i-1]*extand[i]; printf("%lld\n",ans); return 0; }