迴文自動機html
建議先學習AC自動機:AC自動機講解超詳細c++
迴文自動機,顧名思義,用來處理迴文串的自動機。數組
1.求\(S\)串內本質不一樣的迴文串個數學習
2.求\(S\)串內本質不一樣的迴文串出現次數spa
3.最小回文劃分指針
4.\(S\)串中如下標\(i\)結尾的最長迴文串長度code
看看本身感悟一下。感受特別形象,都不用解釋了啊htm
仍是稍微解釋一下:blog
1.迴文數上每個節點表明了原串上出現過的一個本質不一樣迴文子串,原串上的每個迴文子串都在迴文樹上有對應。迴文樹上每個點表明的串都是迴文串。排序
2.迴文樹分兩部分,奇和偶,奇樹上的點表明的迴文串長度爲奇數,偶樹上的爲偶
3.兒子節點表明串長度爲父親節點表明串長度\(+2\)
4.和\(Trie\)類似的其餘性質,不說了
學過AC自動機的OIer們應該就很熟悉啦QwQ
\(Fail\)指針含義:這個節點所表明的迴文串的最長迴文後綴
通常作許多PAM題目經常使用的東西
\(Trans\)指針含義:小於等於當前節點長度一半的最長迴文後綴
咱們要維護如下信息
char s[maxn]; //原串 int fail[maxn]; //fail指針 int len[maxn]; //該節點表示的字符串長度 int tree[maxn][26]; //同Trie,指向兒子 int trans[maxn]; //trans指針 int tot,pre; //tot表明節點數,pre表明上次插入字符後指向的迴文樹位置
其中\(fail,len,tree,trans\)爲PAM上的信息
構建PAM的方法爲增量,即一個一個加入字符構建PAM
奇樹和偶樹的根長度\(len\)分別爲\(-1\)和\(0\)
設當前咱們插入原串中\(i\)位置的字符\(u\)
那麼以\(i\)爲結尾的最長迴文串應該爲(以\(i-1\)爲結尾的最長迴文串\(+u\)),而且那個迴文串要知足前一個字符等於\(u\)(否則就不是迴文串了啊)
要找到那個點很是簡單,不斷從\(pre\)開始跳\(fail\),直到找到一個知足\(s[i-len[x]-1]==u\) 的節點\(Fail\) ,那麼從\(Fail\)建一個\(u\)兒子便可以表示新的迴文串。
新點的\(fail\)怎麼求呢。
明顯爲從\(pre\)開始跳\(fail\),找到{ [第二個(知足\(s[i-len[x]-1]==u\)) 的節點\(x\) ]的\(u\)兒子 }
也就是從\(Fail\)開始跳\(fail\),找到{ [第一個(知足\(s[i-len[x]-1]==u\)) 的節點\(x\) ]的\(u\)兒子 }
跳到根記得判斷
特別提醒:節點\(1\)爲奇根,節點\(0\)爲偶根,\(fail[0]=1\) , \(len[1]=-1\)
時間複雜度證實參考OIwiki:OIwiki-PAM
放代碼理解:
int getfail(int x,int i){ //從x開始跳fail,知足字符s[i]的節點 while(i-len[x]-1<0||s[i-len[x]-1]!=s[i])x=fail[x]; return x; } void insert(int u,int i){ int Fail=getfail(pre,i); //找到符合要求的點 if(!tree[Fail][u]){ //沒建過就新建節點 len[++tot]=len[Fail]+2; //長度天然是父親長度+2 fail[tot]=tree[getfail(fail[Fail],i)][u]; //fail爲知足條件的次短迴文串+u tree[Fail][u]=tot; //認兒子 } pre=tree[Fail][u]; //更新pre }
至於\(trans\)維護也和\(fail\)差很少
根據\(trans\)的定義去推一下怎麼搞吧
放一下完整代碼:
char s[maxn]; //原串 int fail[maxn]; //fail指針 int len[maxn]; //該節點表示的字符串長度 int tree[maxn][26]; //同Trie,指向兒子 int trans[maxn]; //trans指針 int tot,pre; //tot表明節點數,pre表明上次插入字符後指向的迴文樹位置 int getfail(int x,int i){ //從x開始跳fail,知足字符s[i]的節點 while(i-len[x]-1<0||s[i-len[x]-1]!=s[i])x=fail[x]; return x; } int gettrans(int x,int i){ while(((len[x]+2)<<1)>len[tot]||s[i-len[x]-1]!=s[i])x=fail[x]; return x; } void insert(int u,int i){ int Fail=getfail(pre,i); //找到符合要求的點 if(!tree[Fail][u]){ //沒建過就新建節點 len[++tot]=len[Fail]+2; //長度天然是父親長度+2 fail[tot]=tree[getfail(fail[Fail],i)][u]; //fail爲知足條件的次短迴文串+u tree[Fail][u]=tot; //指兒子 if(len[tot]<=2)trans[tot]=fail[tot]; //特殊trans else{ int Trans=gettrans(trans[Fail],i); //求trans trans[tot]=tree[Trans][u]; } } pre=tree[Fail][u]; //更新pre }
求第 i 個整數表示原串以第 i 個字符結尾的迴文子串個數,強制在線
明顯:一個迴文串的答案等於其最長迴文後綴的答案\(+1\) (這超好理解的吧
那就在多維護一個信息\(ans\)表示答案,新建節點時更新便可
ans[tot]=ans[fail[tot]]+1;
答案爲lastans=ans[pre];
代碼:
#include<bits/stdc++.h> #define maxn 510001 using namespace std; char s[maxn]; int fail[maxn],len[maxn],ans[maxn],trie[maxn][26]; int pre,slen,lastans,tot; int getfail(int x,int i){ while(i-len[x]-1<0||s[i-len[x]-1]!=s[i])x=fail[x]; return x; } int main(){ scanf("%s",s);slen=strlen(s); fail[0]=1;len[1]=-1;tot=1; for(int i=0;i<slen;i++){ if(i>=1)s[i]=(s[i]-97+lastans)%26+97; int u=s[i]-'a'; int Fail=getfail(pre,i); if(!trie[Fail][u]){ fail[++tot]=trie[getfail(fail[Fail],i)][u]; trie[Fail][u]=tot; len[tot]=len[Fail]+2; ans[tot]=ans[fail[tot]]+1; } pre=trie[Fail][u]; lastans=ans[pre]; printf("%d ",lastans); } return 0; }
學好\(trans\)指針,秒切此題
明顯:當存在\(i\)知足\(len[trans[i]]*2==len[i]\)而且知足題意中\(len[trans[i]]%2==0\)即爲符合題意的串,取最長便可。
代碼:真·模板
#include<bits/stdc++.h> #define maxn 510001 using namespace std; char s[maxn]; //原串 int fail[maxn]; //fail指針 int len[maxn]; //該節點表示的字符串長度 int tree[maxn][26]; //同Trie,指向兒子 int trans[maxn]; //trans指針 int tot,pre; //tot表明節點數,pre表明上次插入字符後指向的迴文樹位置 int getfail(int x,int i){ //從x開始跳fail,知足字符s[i]的節點 while(i-len[x]-1<0||s[i-len[x]-1]!=s[i])x=fail[x]; return x; } int gettrans(int x,int i){ while(((len[x]+2)<<1)>len[tot]||s[i-len[x]-1]!=s[i])x=fail[x]; return x; } void insert(int u,int i){ int Fail=getfail(pre,i); //找到符合要求的點 if(!tree[Fail][u]){ //沒建過就新建節點 len[++tot]=len[Fail]+2; //長度天然是父親長度+2 fail[tot]=tree[getfail(fail[Fail],i)][u]; //fail爲知足條件的次短迴文串+u tree[Fail][u]=tot; //指兒子 if(len[tot]<=2)trans[tot]=fail[tot]; //特殊trans else{ int Trans=gettrans(trans[Fail],i); //求trans trans[tot]=tree[Trans][u]; } } pre=tree[Fail][u]; //更新pre } int slen,ans; int main(){ scanf("%d",&slen); scanf("%s",s); fail[0]=1;len[1]=-1;tot=1; for(int i=0;i<slen;i++)insert(s[i]-'a',i); for(int i=2;i<=tot;i++){ if(len[trans[i]]*2==len[i]&&len[trans[i]]%2==0) ans=max(ans,len[i]); } printf("%d\n",ans); return 0; }
題目描述: 順序和逆序讀起來徹底同樣的串叫作迴文串。好比`acbca`是迴文串,而`abc`不是(`abc`的順序爲`abc`,逆序爲`cba`,不相同)。 輸入長度爲n的串S,求S的最長雙迴文子串T,便可將T分爲兩部分X,Y,(|X|,|Y|≥1)且X和Y都是迴文串。
簡單PAM題
題解:
正着建一棵PAM,\(a[i]\)記錄當前位置\(i\)結尾最長迴文串長度
反着建一棵PAM,\(b[i]\)記錄當前位置\(i\)結尾最長迴文串長度
\(a[i]+b[i+1]\)即爲以\(i\)爲分界的雙迴文串
取\(a[i]+b[i+1]\)最大值即爲答案
代碼:封裝版PAM
#include<bits/stdc++.h> #define maxn 510001 using namespace std; char s[maxn]; int slen,a[maxn],b[maxn],ans; struct PAM{ int fail[maxn],len[maxn],trie[maxn][26]; int tot,pre; void init(){fail[0]=1;len[1]=-1;tot=1;pre=0;} int getfail(int x,int i){ while(i-len[x]-1<0||s[i-len[x]-1]!=s[i])x=fail[x]; return x; } void insert(int u,int i){ int Fail=getfail(pre,i); if(!trie[Fail][u]){ fail[++tot]=trie[getfail(fail[Fail],i)][u]; trie[Fail][u]=tot; len[tot]=len[Fail]+2; } pre=trie[Fail][u]; } }A,B; int main(){ scanf("%s",s);slen=strlen(s);A.init();B.init(); for(int i=0;i<slen;i++)A.insert(s[i]-'a',i),a[i]=A.len[A.pre]; reverse(s,s+slen); //翻轉 for(int i=0;i<slen;i++)B.insert(s[i]-'a',i),b[slen-i-1]=B.len[B.pre]; for(int i=0;i<slen-1;i++)ans=max(ans,a[i]+b[i+1]); printf("%d\n",ans); return 0; }
PAM好題,請好好思考
題意:
初始有一個空串,利用下面的操做構造給定串 \(S\) 。
一、串開頭或末尾加一個字符
二、串開頭或末尾加一個該串的逆串
求最小化操做數, \(∣\ S∣≤10^5\) 。
題解:
PAM上dp
一眼可得PAM
咱們令PAM上多記錄一個信息\(sum\),表示該節點表示串在原串上出現了多少次。
當咱們處理完了\(sum\),對於長度\(len\)爲奇數的節點的信息\(sum\)計入數組\(a[i]\).
\(a[i]\)爲長度爲\(i\)的迴文子串出現次數。
\(a[i]\)降序排序後累加答案快速冪處理一下便可,不需太多點撥
重點來了
講一下怎麼處理\(sum\)
咱們能夠發現當一個節點\(u\)的\(sum+1\),那麼\(fail[u]\)的\(sum\)也要\(+1\)
熟悉AC自動機的OIer能夠敏銳的察覺到能夠用拓撲排序了(例如我
建PAM的時候打個標記,最後統一一個拓撲排序向\(fail\)去更新\(sum\)便可
queue<int >q; //in數組爲fail入邊數量 void tuopu(){ for(int i=0;i<=tot;i++)if(in[i]==0)q.push(i); while(!q.empty()){ int u=q.front();q.pop(); sum[fail[u]]+=sum[u];in[fail[u]]--; if(in[fail[u]]==0)q.push(fail[u]); } }
好像沒什麼問題,多一個拓撲排序就好了
但真的如此嗎?
咱們觀察PAM和AC自動機的區別
AC自動機是建好\(Trie\)後再進行\(getFail\)的,\(fail\)的節點編號是會大於自身節點編號
而PAM不會出現這種狀況,PAM\(fail\)定義不一樣於AC自動機,構建使用增量法,保證了\(fail\)的節點編號必定小於自身節點編號。
因此就能夠不用拓撲排序了,直接一個\(for\)從後到前更新便可
for(int i=tot;i>=0;i--)sum[fail[i]]+=sum[i];
總代碼:
#include<bits/stdc++.h> #define maxn 1010001 #define ll long long #define mod 19930726 using namespace std; char s[maxn]; int fail[maxn],len[maxn],trie[maxn][26],trans[maxn]; long long sum[maxn]; int per,slen,tot; long long a[maxn],K,ans=1; int getfail(int x,int i){ while(i-len[x]-1<0||s[i-len[x]-1]!=s[i])x=fail[x]; return x; } int gettrans(int x,int i){ while(((len[x]+2)<<1)>len[tot]||s[i-len[x]-1]!=s[i])x=fail[x]; return x; } void insert(int u,int i){ int Fail=getfail(per,i); if(!trie[Fail][u]){ len[++tot]=len[Fail]+2; fail[tot]=trie[getfail(fail[Fail],i)][u]; trie[Fail][u]=tot; if(len[tot]<=2)trans[tot]=fail[tot]; else{ int Trans=gettrans(trans[Fail],i); trans[tot]=trie[Trans][u]; } } per=trie[Fail][u]; sum[per]++; //記錄sum } ll qpow(ll n,ll m){ ll ans=1ll; while(m){ if(m&1){ans=ans*n;ans%=mod;} n=n*n;n%=mod;m>>=1; }return ans%mod; } int main(){ scanf("%d%lld",&slen,&K); scanf("%s",s); fail[0]=1;len[1]=-1;tot=1; for(int i=0;i<slen;i++)insert(s[i]-'a',i); for(int i=tot;i>=1;i--)sum[fail[i]]+=sum[i]; //更新sum for(int i=2;i<=tot;i++)a[len[i]]+=sum[i],a[len[i]]%=mod; //長度處理 for(int i=slen;i>=1;i--){ //答案處理 if(i%2==1){ if(K>=a[i]){ ans*=qpow(i,a[i]);ans%=mod; K-=a[i]; }else{ ans*=qpow(i,K);ans%=mod; K-=K; break; } } } if(K==0) //判-1 printf("%lld\n",ans%mod); else printf("-1\n"); return 0; }
卡空間PAM,2010沒有PAM,因此都是馬拉車
衆所周知,PAM擁有十分優秀的時間複雜度,但空間複雜度lj得不行
但這題卡空間,因此得用到鄰接鏈表PAM
先講思路
題目要求相交的迴文子串對,這很難作
因而咱們求補集,求不相交的迴文子串對,再用總數減便可
求法和上文的最長雙迴文子串 相似
正反建一次PAM,存該位置結尾的迴文子串個數,而後加法改乘法
本身領悟一下,挺簡單的。
如今講一下鄰接鏈表PAM
咱們記邊結構體\(line\)
存\(3\)個信息:\(nx,to,w\) 分別表示上一條邊,這條邊通向的節點編號,這條邊是表明哪一個字符
數組\(fir[i]\)表示\(i\)伸出的最後一條邊的編號(頭插式
當咱們要尋找\(u\)的\(v\)兒子
咱們就像鄰接鏈表同樣找,直到有一條邊的\(w==v\)爲止
找不到記得指根
int getson(int u,int v){ for(int i=u;i!=-1;i=l[i].nx) if(l[i].w==v)return l[i].to; return -1; }
建點的時候把邊建上
void insert(int u,int i){ int Fail=getfail(pre,i),ls=getfail(fail[Fail],i); if(getson(fir[Fail],u)==-1){ if(getson(fir[ls],u)==-1)fail[++tot]=0; //找不到指根 else fail[++tot]=getson(fir[ls],u); //找到了 l[++cnt]=(line){fir[Fail],tot,u};fir[Fail]=cnt; //加邊 len[tot]=len[Fail]+2; ans[tot]=ans[fail[tot]]+1; //結尾迴文子串個數 pre=tot; }else pre=getson(fir[Fail],u); }
然鵝事實上你仍然過不了,你還要繼續壓空間,省掉一堆數組就能夠過啦!
總代碼:
#include<bits/stdc++.h> #define maxn 2000005 #define mod 51123987 using namespace std; char s[maxn]; int slen,b[maxn]; long long res; int fail[maxn],len[maxn],ans[maxn],fir[maxn]; struct line{int nx,to,w;}l[maxn]; int tot,pre,cnt; void init(){ memset(fir,-1,sizeof(fir));cnt=0; fail[0]=1;len[1]=-1;tot=1;pre=0; } int getfail(int x,int i){ while(i-len[x]-1<0||s[i-len[x]-1]!=s[i])x=fail[x]; return x; } int getson(int u,int v){ for(int i=u;i!=-1;i=l[i].nx) if(l[i].w==v)return l[i].to; return -1; } void insert(int u,int i){ int Fail=getfail(pre,i),ls=getfail(fail[Fail],i); if(getson(fir[Fail],u)==-1){ if(getson(fir[ls],u)==-1)fail[++tot]=0; else fail[++tot]=getson(fir[ls],u); l[++cnt]=(line){fir[Fail],tot,u};fir[Fail]=cnt; len[tot]=len[Fail]+2; ans[tot]=ans[fail[tot]]+1; pre=tot; }else pre=getson(fir[Fail],u); } int main(){ int n; scanf("%d",&n); scanf("%s",s);slen=strlen(s);init(); reverse(s,s+slen); for(int i=0;i<slen;i++)insert(s[i]-'a',i),b[slen-i-1]=ans[pre]; for(int i=slen-1;i>=0;i--)b[i]+=b[i+1],b[i]%=mod; reverse(s,s+slen);init(); for(int i=0;i<slen-1;i++){ insert(s[i]-'a',i);int x=ans[pre]; res+=(1ll*x*b[i+1])%mod,res%=mod; } printf("%lld\n",((1ll*b[0]*(b[0]-1)/2ll)%mod-res+mod)%mod); return 0; }
To be continue……