【luogu P5496】【模板】迴文自動機(PAM)(迴文樹)
【模板】迴文自動機(PAM)
題目連結:luogu P5496
題目大意
給你一個字串,要你對於字串的每個位置,求有多少個迴文串是在這個位置結尾的。
思路
為啥要用 PAM
首先我們想想處理字串問題有什麼方法:
KMP、AC 自動機
字尾陣列字尾自動機
Manacher
雜湊、DP、暴力等等。
但你發現如果要處理迴文串的問題,雜湊+二分複雜度太高,Manacher 只能針對整個串,在面對這樣的題的時候,似乎沒有什麼辦法。
於是,就有了一個叫做 PAM 的東西。
PAM 有啥用
它可以求一個字串的某個字首中出現了多少個不同的迴文串。
統計在一個字串中每個迴文串出現的次數。
(也就是說它可以求迴文串的個數)
它還可以求以下標 \(i\) 結尾的迴文串個數,也可以得出有哪些。
咋搞
那你同最後一個用處多多少少都能看出來,它是類似 AC 自動機一樣的東西,有一個 \(fail\) 指標,指向的位置就是它失配之後跳轉到的表示它最長字尾迴文串的點。
然後接著它有一些陣列:
\(len_i\) 表示第 \(i\) 個點表示的迴文串的長度。
\(nxt_{i,j}\) 表示在第 \(i\) 個點表示的迴文串兩邊都加 \(j\) 這個字串形成的新迴文串對於的點。(這個有點類似 Trie 數)
\(fail_i\) 就是我們前面說的失配指標。
\(sum_i\) 就是有多少個迴文串的結束位置是它的結束位置。(這個就是用來求本題的答案啦)
\(num_i\)
\(lst\) 就是以最後一個字元結尾的最長的迴文串的編號。
有的時候,我們還會順手維護一個 \(trans_i\),表示長度小於等於這個迴文串的一半最長迴文字尾。
迴文樹比較神奇的地方,就是它有兩個根:\(0\) 表示偶數長度的根,\(1\) 表示奇數長度的根。
然後由於你可能跳 \(fail\) 邊跳到空字串之後可能會由原來的偶數長度變成奇數長度,所以 \(fail_0=1,fail_1=0\)。
然後我們設 \(len_0=0,len_1=-1\),至於為什麼 \(len_1=-1\) 我們在後面會發現它的好處。
然後考慮在當前的字串後面加一個字元,考慮怎麼搞。
那首先肯定是跳 \(fail\) 邊直到碰到可以匹配這個新字串。
那你肯定會想如果一直都無法匹配,就要搞特判,但其實 \(len_1=-1\) 可以讓我們不用特判。
首先不難想到判斷是否能匹配是看 \(s_{n-len_{x}-1}\) 是否等於 \(s_n\)。(\(n\) 是當前字串長度,\(x\) 是現在跳到的位置)
那如果一直無法匹配,就會跑到 \(1\),那這個時候帶進去看:\(s_{n-len_x-1}\) 就是 \(s_{n-(-1)-1}\) 即 \(s_n\),所以自己肯定等於自己,就會跳出來。
接著就是匹配啦,那就會走到 \(nxt_{x,s_n}\),那如果有了我們就不用管,但如果沒有這個點,那我們就要新開一個點 \(now\),並維護關係。
首先看 \(len_{now}\),那就是從 \(len_x\) 左右兩邊都加了 \(s_n\) 這個字元,長度就加了 \(2\)。
而且這個時候也不同特判,如果它自己一個形成迴文串,那就是 \(len_{1}+2\),剛好就是 \(1\)。
接著你考慮維護 \(fail_{now}\),那跟 AC 自動機的維護方式一樣,你先不斷跳 \(fail\) 邊找到 \(fail_x\) 可以匹配的,然後它兩邊加 \(s_n\) 這個字元對於的點就是 \(fail_{now}\) 了。
接著就是連 \(nxt_{x,s_n}\),這個就不多說了,\(nxt_{x,s_n}=now\)。有的時候還要記錄父親,這個也沒什麼麻煩的,直接 \(fa_{now}=x\) 即可。
然後是 \(sum_{now}\),那它其實就是比 \(fail_{now}\) 多了一種迴文串(它自己),所以就是 \(sum_{fail_{now}}+1\)。
那接著是 \(trans_{now}\),那不難想到它也是類似 AC 自動機的匹配方式。
首先如果當前字串的長度小於等於 \(2\),那它要麼長度是 \(1\),要麼是空,所以就直接 \(trans_{now}=fail_{now}\)。
那如果長的,那我們就考慮繼續跳 \(fail\) 邊,但是是從 \(trans_{x}\) 開始跳。(因為你只是要一半,你從 \(fail_x\) 開始跳就太慢了會超時)
那首先跳到的要能匹配 \(s_n\),接著就是要加上兩邊的兩個 \(s_n\) 字元之後長度還不超過當前串的長度的一半,那跳到就退出,然後它匹配上 \(s_n\) 形成的迴文串對於的點就是我們要的了。
這裡再講講 \(count()\) 函式。
其實它就是從葉子到父親不斷的 DP 一下,因為如果一個 \(A\) 是 \(B\) 的子串,\(B\) 是 \(C\) 的子串,那 \(A\) 是 \(C\) 的子串。
所以在程式碼上就是倒序列舉點,然後 \(num_{fail_i}=num_{fail_i}+num_i\) 就可以了。
這道題
其實就是每次插入點,然後輸出 \(sum_{lst}\) 即可。
程式碼
#include<cstdio>
#include<cstring>
using namespace std;
struct PAM {
int len, nxt[26], fail, sum, num, trans;
}t[500002];
int sn, lastans, lst, tot, a[500001], n;
char s[500001];
int get_new(int l) {//建一個新的點
t[++tot].len = l;
for (int i = 0; i < 26; i++) t[tot].nxt[i] = 0;
t[tot].fail = 0; t[tot].sum = 0; t[tot].num = 0; t[tot].trans = 0;
return tot;
}
int get_fail(int x) {//像 AC 自動機一樣匹配
while (a[n - t[x].len - 1] != a[n]) x = t[x].fail;
return x;
}
void insert(int x) {
a[++n] = x;
int cur = get_fail(lst);
if (!t[cur].nxt[x]) {
int now = get_new(t[cur].len + 2);//兩邊都擴充套件一格,所以長度加了 2
t[now].fail = t[get_fail(t[cur].fail)].nxt[x];//建 fail 邊
t[cur].nxt[x] = now;//連兒子
t[now].sum = t[t[now].fail].sum + 1;//字尾個數增加了它這個串
if (t[now].len <= 2) t[now].trans = t[now].fail;//求 trans 陣列
else {
int tmp = t[cur].trans;//也是像 AC 自動機一樣跳 fail 邊直到找到要的
while (a[n - t[tmp].len - 1] != a[n] || ((t[tmp].len + 2) << 1) > t[now].len) tmp = t[tmp].fail;
t[now].trans = t[tmp].nxt[x];
}
}
lst = t[cur].nxt[x];
t[lst].num++;//統計出現次數
}
void count() {//這個是求出這個迴文串在這個字串中出現的次數
for (int i = tot; i >= 2; i--)
t[t[i].fail].num += t[i].num;
}
int main() {
scanf("%s", s + 1);
sn = strlen(s + 1);
lst = 0; t[0].len = 0; t[1].len = -1; tot = 2; a[0] = -1;//初始化
t[1].fail = 0; t[0].fail = 1;//注意奇偶根的 fail 邊是互相連著的
for (int i = 1; i <= sn; i++) {
insert((s[i] - 97 + lastans) % 26);
lastans = t[lst].sum;
printf("%d ", lastans);
}
return 0;
}