PAM學習小結

PAM

迴文自動機html

建議先學習AC自動機:AC自動機講解超詳細c++

迴文自動機,顧名思義,用來處理迴文串的自動機。數組

功能:

1.求\(S\)串內本質不一樣的迴文串個數學習

2.求\(S\)串內本質不一樣的迴文串出現次數spa

3.最小回文劃分指針

4.\(S\)串中如下標\(i\)結尾的最長迴文串長度code

迴文樹

看看本身感悟一下。感受特別形象,都不用解釋了啊htm

仍是稍微解釋一下:blog

1.迴文數上每個節點表明了原串上出現過的一個本質不一樣迴文子串,原串上的每個迴文子串都在迴文樹上有對應。迴文樹上每個點表明的串都是迴文串。排序

2.迴文樹分兩部分,奇和偶,奇樹上的點表明的迴文串長度爲奇數,偶樹上的爲偶

3.兒子節點表明串長度爲父親節點表明串長度\(+2\)

4.和\(Trie\)類似的其餘性質,不說了

Fail指針

學過AC自動機的OIer們應該就很熟悉啦QwQ

\(Fail\)指針含義:這個節點所表明的迴文串的最長迴文後綴

Trans指針

通常作許多PAM題目經常使用的東西

\(Trans\)指針含義:小於等於當前節點長度一半最長迴文後綴

構建PAM

咱們要維護如下信息

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
}

應用

P5496【模板】迴文自動機(PAM)

求第 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;
}

P4287[SHOI2011]雙倍迴文

學好\(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;
}

P4555[國家集訓隊]最長雙迴文串

題目描述:

順序和逆序讀起來徹底同樣的串叫作迴文串。好比`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;
}

P4762[CERC2014]Virus synthesis

PAM好題,請好好思考

題意:

初始有一個空串,利用下面的操做構造給定串 \(S\)

一、串開頭或末尾加一個字符

二、串開頭或末尾加一個該串的逆串

求最小化操做數, \(∣\ S∣≤10^5\)

題解:

PAM上dp

P1659[國家集訓隊]拉拉隊排練

一眼可得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]);
	}
}

好像沒什麼問題,多一個拓撲排序就好了

但真的如此嗎?

咱們觀察PAMAC自動機的區別

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;
}

CF17E Palisection

卡空間PAM,2010沒有PAM,因此都是馬拉車

衆所周知,PAM擁有十分優秀的時間複雜度,但空間複雜度lj得不行

但這題卡空間,因此得用到鄰接鏈表PAM

先講思路

題目要求相交的迴文子串對,這很難作

因而咱們求補集,求不相交的迴文子串對,再用總數減便可

求法和上文的最長雙迴文子串 相似

正反建一次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……

相關文章
相關標籤/搜索