回文自動機一一處理回文串問題的有力武器
這幾天一直沉迷字符串數據結構
看了很多大佬的回文自動機學習筆記,稍微有點理解了,整理一下吧
1.概念
\(\quad\)a.大概: 同其他自動機一樣,回文自動機是個DAG,它用相當少(\(O(n)\))的空間復雜度就存儲了這個字符串的所有回文串信息。一個回文自動機包含不超過\(|S|\)個節點,每個節點都表示了這個字符串的一個不重復的回文子串,同時一個節點會有不超過字符集大小的邊連向其他節點,以及一條fail邊連向這個點的fail...這些都會在下面介紹
\(\quad\)b.森林: 和別的自動機不太一樣,回文自動機是有兩棵樹的森林:其中一棵是長度為偶數的回文串集合,另一棵是長度為奇數的回文串集合,這兩棵樹的根節點分別表示長度為0(空串)和-1(無實際含義,便於運算)的回文串;
\(\quad\)c.邊:自動機中每條有向邊都有一個字符類型的權值,起點的串左右分別加上這個字符得到的就是終點的串。舉個栗子:設一條邊權為\(c\)的邊連接的兩個點分別是\(A,B\),\(A\)表示回文串\(aba\),則\(B\)表示的回文串就是\(cabac\) 。特別的,如果\(A\)是那個長度為\(-1\)的根,\(B\)串就是這條邊的權值。。。
\(\quad\)d.點:當你插入一個字符的時候,插入的點代表的就是這個字符匹配的最長回文串,也就是說從根節點往下順着邊走,記着一個str一開始為空,一邊走一邊不停地往str左右兩邊添加新的字符,走到一個點,這個點代表的回文串就是str
\(\quad\)e.\(fail\)邊:每個點都有個fail邊,這條邊指向這個點所代表的回文串的 最長回文后綴 所在的那個點(最長回文后綴:串中滿足回文的最長的后綴,這個串自己不算)如果沒有,則指向0(就是那個根節點)。特別的,0的fail節點就是那個長度為-1的點。
2.構造:
\(\quad\)我是用的一個結構體存的,\(len,fail,son[26],siz\) 分別代表這個串的長,fail節點,連出來的每一條邊以及這個回文串的數量,如下
struct node{
int len,fail,son[26],siz;
};
node prt[maxn];
我們把兩個根下標設為0和1,並根據上面介紹的給他們賦值
prt[1].len=-1;
prt[0].fail=prt[1].fail=1;
然后我們就可以把點一個一個加入到回文自動機中,這可以用一個函數\(extend\)來實現,具體實現方法如下:
設我們以前插入的最后一個點為\(last\),這次要插入一個點x,首先要找到一個點\(cur\)為滿足前面的字符等於新加入字符的,\(last\)的最長的回文后綴,這個過程可以不停地在\(last\)的\(fail\)鏈上跑,因為\(fail\)所對應的正是串的最長回文后綴,這個可以用下面函數實現:
int getfail(int x){
while(s[n-prt[x].len-1]!=s[n]) x=prt[x].fail;
return x;
}
若\(cur\)已經包含權值為x的出邊了,我們就可以簡單地將出邊終點的權值++,繼續去加下一個點了。如果不包含權值x的邊,我們就需要新建一個點\(now\)並讓\(cur\)把邊連向他,\(now\)代表的長度自然是\(cur\)的長度+2,然后我們只要求出\(now\)的\(fail\)就完事了。
求\(fail\)的話可以用cur的\(fail\)來求,就用上面求\(cur\)的方法,但是不能用\(cur\)本身(想一想,為什么)
當然最后千萬不要忘記把\(last\)的值更新啊\(qwq\)
void extend(int x){
int cur=getfail(last);
int now=prt[cur].son[x];
if(!now){
now=++num;
prt[now].len=prt[cur].len+2;
prt[now].fail=prt[getfail(prt[cur].fail)].son[x];
prt[cur].son[x]=now;
}
prt[now].siz++;
last=now;
}
累計答案可以從下往上把回文串數目加起來,顯然上面的串一定是下面串的子串嘛\(qwq\)
void count(){
for(int i=num;i>=2;i--)
prt[prt[i].fail].siz+=prt[i].siz;
}
4.舉個栗子:
如圖,我們已經把串\(abab\)的回文自動機建好了,下面要添加一個點\(a\),此時\(last=5\)
首先求出\(cur\),\(last\)所代表的回文串\(bab\)前邊的字符正好與要加入的字符\(a\)相等,所以\(cur\)就是\(last\),我們發現\(cur\)不存在邊權為\(a\)的出邊,於是新建個點 6,從\(cur\)連一條邊\(a\)到 6;
6 的長度自然是5的長度+2\((a'bab'a)\)
然后求6的\(last\):5的\(fail\)指向3\((b)\),可以發現,3前面的那個字符\(a\)就是新加的字符(怎么那么巧...),於是我們把6的fail指向點3的\(a\)邊所指向的點4;
嗯,\(last\)更新為6,6的數量++,結束;
最后累加答案,
\(siz(6)=1\),\(siz(4)=1\)
\(siz(5)=1\)
\((siz(4)+=siz(6))=2\)
\((siz(3)+=siz(5))=2\)
\((siz(2)+=siz(4))=3\)
附:閑得自己也寫了個造圖的代碼。。。
void print(int x){
if(cz[x]) return;
cz[x]=1;
printf(" %d->%d[style=\"dashed\"];\n",x,sam[x].link);
for(int i=0;i<=25;i++)
if(sam[x].ch.count(i))
printf(" %d->%d[label=%d];\n",x,sam[x].ch[i],i),
print(sam[x].ch[i]);
}
void Vz(){
printf("digraph zhy{\n rankdir = LR;\n");
print(0);
printf("}\n");
}
5.例題(Luogu-1659):
\(\quad\)這道題的話就是把這些點按照長度從大到小排一遍序,然后前\(k\)個奇數長的乘起來就是答案啦,注意這題k較大,還要用快速冪,代碼:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
const int maxn=1e6+100,P=19930726;
struct node{
int len,fail,son[26],siz;
node(){
len=fail=0;
for(int i=0;i<=25;i++)
son[i]=0;
}
};
node prt[maxn];
int n,last,len,num;
ll ans=1,k;
char s[maxn];
ll poww(ll x,int y){
ll base=1;
while(y){
if(y&1) base*=x,base%=P;
x*=x,x%=P;
y>>=1;
}
return base;
}
bool cmp(node x,node y){
return x.len>y.len;
}
int getfail(int x){
while(s[n-prt[x].len-1]!=s[n]) x=prt[x].fail;
return x;
}
void extend(int x){
int cur=getfail(last);
if(!prt[cur].son[x]){
int now=++num;
prt[now].len=prt[cur].len+2;
prt[now].fail=prt[getfail(prt[cur].fail)].son[x];
prt[cur].son[x]=now;
}
prt[prt[cur].son[x]].siz++;
last=prt[cur].son[x];
}
int main(){
scanf("%d%d",&len,&k);
scanf("%s",s);
last=num=1,prt[1].len=-1;
prt[0].fail=prt[1].fail=1;
for(n=0;n<len;n++) extend(s[n]-'a');
for(int i=num;i>=2;i--)
prt[prt[i].fail].siz+=prt[i].siz,prt[prt[i].fail].siz%=P;
sort(prt+1,prt+num+1,cmp);
int now=1;
while(k){
if(now>num){
printf("-1\n");
return 0;
}
if(prt[now].len%2==0){
now++;
continue;
}
if(prt[now].siz<k){
k-=prt[now].siz;
ans*=poww(prt[now].len,prt[now].siz)%P;
ans%=P;
now++;
}
else{
ans*=poww(prt[now].len,k)%P;
ans%=P;
k=0;
}
}
printf("%lld\n",ans);
}