概述
一、適用問題
算法主要解決的是給出一個字符串, 複雜度下求出以字符串中任意一個節點爲中心所能擴展的最大距離。
二、算法解析
擴充字符串
爲了統一奇偶字符串,算法首先在每兩個字符(包括頭尾)之間加沒出現的字符(如*),這樣所有字符串長度就都是奇數了,簡化了問題。
具體 過程
記錄 表示 能向兩邊推(包括 )的最大距離,如果能求出 數組,那每一個迴文串就都確定了。
我們假設 已經求好了,現在要求 。假設之前能達到的最右邊爲 ,對應的中點爲 , 是 的對稱點。
- 第一種情況,,即 。
- 第二種情況,,先設 ,然後再繼續增加 ,並令 ,更新 。
由於 一定是遞增的,因此時間複雜度爲 ,可以發現一個串本質不同的迴文串最多有 個,因此只有 增加的時候纔會產生本質不同的迴文串。
算法特點
- 充分利用了迴文的性質,從而達到線性時間。
- 右端點遞增過程中產生了所有的本質不同迴文串,即一個串中本質不同迴文串最多隻有 個。
- 算法的核心在於求出每一個節點往兩邊推的最遠距離,所有涉及該算法的問題也都是在這個功能上做文章。
三、 模板
void Manacher(char s1[]){
int len, tot, R = 0, pos = 0;
//對字符串加'#'號
len = strlen(s1+1);
s2[0] = '$';
s2[ln=1]='#';
for(int i = 1; i <= len; i++)
s2[++ln] = s1[i], s2[++ln] = '#';
//求p[i]數組
for(int i = 1; i <= ln; i++){
if(R >= i) p[i] = min(p[pos*2-i],R-i+1);
if(p[i] + i > R){
for(; s2[R+1] == s2[i*2-R-1] && R+1 <= ln && i*2-R-1 <= ln; R++); //小心多組數據
pos = i;
p[i] = R-i+1;
}
}
}
迴文自動機概述
一、適用問題
迴文自動機其實可以當作迴文串問題的大殺器,主要可以解決以下問題:
- 本質不同迴文串個數
- 每個本質不同迴文串出現的次數
- 以某一個節點爲結尾的迴文串個數
- …
而且都是在 O(n) 的複雜度內解決了這些問題。但是要注意,迴文自動機並不能完全替代馬拉車算法,因爲馬拉車針對的是以一個節點爲中心所能擴展的最遠的距離,而回文自動機更多的是處理以一個節點爲結尾的迴文串數量。
二、算法解析
基礎思想
與自動機一樣,迴文自動機的構造也使⽤了樹的結構。
不同點在於迴文自動機有兩個根,一個是偶數⻓度回⽂串的根,一個是奇數⻓度回⽂串的根,簡稱爲奇根和偶根。自動機中每個節點表⽰⼀個回⽂串,且都有⼀個 指針,指向當前節點所表⽰的迴文串的最長迴文後綴(不包括其本身)。
具體構建過程
首先將偶根的 指針指向奇根,奇根的 指針指向偶根,然後令奇根的長度爲 ,偶根長度爲 。
在構建過程中,始終要記錄一個 指針,表示上一次插入之後所位於的迴文自動機節點。然後判斷插入當前節點之後是否能夠形成一個新的迴文串,如果不能則跳 指針,整體邏輯與自動機的構建沒有太大差別。
指針表示當前節點所代表的迴文串的最長後綴迴文串,此處指針的定義與自動機有一定區別,但本質目的相同,都是爲了儘可能地保存之前的匹配結果。
而 指針的構建是根據當前節點的父節點的 節點構建而來,與自動機的 構建沒有太大差別。
大致上就是這樣一個構建過程,具體細節可以查看下述模板,也可以查看 的圖解構建。
統計每個本質不同迴文串的出現次數
與自動機一樣,每個節點所代表字符串出現的次數要加上其所有 子孫出現的次數,因此需要倒序遍歷所有節點將節點出現次數累加到其 節點上。
三、迴文自動機模板
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
#define per(i,a,b) for(int i = a; i >= b; i--)
const int N = 1e5+10;
using namespace std;
struct PAM{
#define KIND 26
int n,last,tot;
int len[N],trie[N][KIND],fail[N],cnt[N],S[N],num[N];
//len[i]: 節點i所代表的迴文串長度, fail[i]: 當前迴文串的最長迴文後綴(不包括自身)
//cnt[i]: 節點i所代表的迴文串的個數, S[i]: 第i次添加的字符, num[i]: 以第i個字符爲結尾的迴文串個數
//last: 上一個字符構成最長迴文串的位置,方便下一個字符的插入
//tot: 總結點個數 = 本質不同的迴文串的個數+2, n: 插入字符的個數
int newnode(int l){
rep(i,0,KIND-1) trie[tot][i] = 0;
cnt[tot] = 0, len[tot] = l, num[tot] = 0;
return tot++;
}
inline void init(){
tot = n = last = 0, newnode(0), newnode(-1);
S[0] = -1, fail[0] = 1;
}
int get_fail(int x){ //獲取fail指針
while(S[n-len[x]-1] != S[n]) x = fail[x];
return x;
}
inline int insert(int c){ //插入字符
c -= 'a';
S[++n] = c;
int cur = get_fail(last);
//在節點cur前的字符與當前字符相同,即構成一個迴文串
if(!trie[cur][c]){ //這個迴文串沒有出現過
int now = newnode(len[cur]+2);
fail[now] = trie[get_fail(fail[cur])][c];
trie[cur][c] = now;
num[now] = num[fail[now]]+1; //更新以當前字符爲結尾的迴文串的個數
}
last = trie[cur][c];
cnt[last]++; //更新當前迴文串的個數
return num[last]; //返回以當前字符結尾的迴文串的個數
}
void count(){ //統計每個本質不同迴文串的個數
per(i,tot-1,0) cnt[fail[i]] += cnt[i];
}
}pam;
迴文串系列習題
1. P1659 [國家集訓隊] 拉拉隊排練
題意:
個字符,找出其中所有的奇長度迴文串,按長度降序排列,求出前 個迴文串長度的乘積。
思路:
模板題。直接對於所有本質不同迴文串求出其個數,然後用 存到 中,再利用快速冪計算答案即可。
代碼:
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
#define per(i,a,b) for(int i = a; i >= b; i--)
typedef long long ll;
const int N = 1e6+1000;
const ll mod = 19930726;
using namespace std;
struct PAM{
#define KIND 26
int n,last,tot;
int len[N],trie[N][KIND],fail[N],S[N],num[N];
ll cnt[N];
//len[i]: 節點i所代表的迴文串長度, fail[i]: 當前迴文串的最長迴文後綴(不包括自身)
//cnt[i]: 節點i所代表的迴文串的個數, S[i]: 第i次添加的字符, num[i]: 以第i個字符爲結尾的迴文串個數
//last: 上一個字符構成最長迴文串的位置,方便下一個字符的插入
//tot: 總結點個數 = 本質不同的迴文串的個數+2, n: 插入字符的個數
int newnode(int l){
rep(i,0,KIND-1) trie[tot][i] = 0;
cnt[tot] = 0, len[tot] = l, num[tot] = 0;
return tot++;
}
inline void init(){
tot = n = last = 0, newnode(0), newnode(-1);
S[0] = -1, fail[0] = 1;
}
int get_fail(int x){ //獲取fail指針
while(S[n-len[x]-1] != S[n]) x = fail[x];
return x;
}
inline int insert(int c){ //插入字符
c -= 'a';
S[++n] = c;
int cur = get_fail(last);
//在節點cur前的字符與當前字符相同,即構成一個迴文串
if(!trie[cur][c]){ //這個迴文串沒有出現過
int now = newnode(len[cur]+2);
fail[now] = trie[get_fail(fail[cur])][c];
trie[cur][c] = now;
num[now] = num[fail[now]]+1; //更新以當前字符爲結尾的迴文串的個數
}
last = trie[cur][c];
cnt[last]++; //更新當前迴文串的個數
return num[last]; //返回以當前字符結尾的迴文串的個數
}
void count(){ //統計每個本質不同迴文串的個數
per(i,tot-1,0) cnt[fail[i]] += cnt[i];
}
}pam;
int n;
ll k;
char buf[N];
vector<pair<int,ll> > v;
ll pow_mod(ll a,ll b){
ll ans = 1, base = a;
while(b){
if(b&1) ans = (ans*base)%mod;
base = (base*base)%mod;
b /= 2ll;
}
return ans;
}
int main(){
scanf("%d%lld",&n,&k);
scanf("%s",buf);
pam.init();
rep(i,0,n-1) pam.insert(buf[i]);
pam.count();
ll num = 0;
rep(i,0,pam.tot-1)
if(pam.len[i] > 0 && pam.len[i]%2 == 1){
v.push_back(make_pair(pam.len[i],pam.cnt[i]));
num += pam.cnt[i];
}
sort(v.begin(),v.end());
if(num < k) printf("-1\n");
else{
ll ans = 1;
int pos = v.size()-1;
while(k > 0){
if(k >= v[pos].second){
ans = (ans*pow_mod(v[pos].first,v[pos].second))%mod;
k -= v[pos].second;
}
else{
ans = (ans*pow_mod(v[pos].first,k))%mod;
k = 0;
}
pos--;
}
printf("%lld\n",ans);
}
return 0;
}
2. P3649 [APIO2014] 迴文串
題意:
給定一個長度爲 的字符串 ,我們定義 的一個子串的存在值爲這個子串在 中出現的次數乘以這個子串的長度,輸出所有迴文子串的最大存在值。
思路:
模板題。直接求出每一個子串的出現次數,然後對於所有節點統計答案即可。(模板題貼一份代碼即可)
3. P5496 迴文自動機(PAM)
題意:
給定一個長度爲 的字符串,求出以每個節點爲結尾的迴文子串個數。
思路:
模板題。 的 函數中自帶以該節點爲結尾的迴文子串數的求解。(模板題貼一份代碼即可,主要目的就是練練手)
4. P4287 [SHOI2011] 雙倍迴文
題意:
給定一個長度爲 的字符串,詢問該字符串中長度最長的雙倍迴文串的長度。雙倍迴文串指 ,如 即是雙倍迴文串,而 則不是。
思路:
由於只需要求最長迴文串,並不要求輸出個數,因此我們直接在 的 過程中求出答案即可。
首先明確一點, 右端點遞增過程中,涉及到了所有的本質不同迴文串,因此我們在每次右端點遞增時,令當前節點爲雙倍迴文串的中心節點,然後查詢左半部分是否也爲一個迴文串,如果是則更新答案。
變型:
當然此題也可以進行延伸,可以將查詢內容改成有多少個雙倍迴文串。如果詢問有多少個雙倍迴文串的話,我的想法是用迴文自動機+ 算法進行解決。
首先 求出每一個節點爲中心所能擴展的最遠距離。然後在根據當前字符串建立迴文自動機,並且在構建時對於每個節點記錄構成該節點的末尾座標 ,之後再遍歷迴文自動機上的所有節點,利用 計算的 數組來驗證該節點所代表的迴文串是否是雙倍迴文串即可。
代碼:
#include <bits/stdc++.h>
const int N = 5e5+100;
using namespace std;
int n,tot,p[2*N],R,pos,ans;
char s[N],s2[2*N];
void manacher(){
s2[0] = '$';
s2[tot = 1] = '#';
for(int i = 1; i <= n; i++)
s2[++tot] = s[i], s2[++tot] = '#';
for(int i = 1; i <= tot; i++){
if(R >= i) p[i] = min(p[pos*2-i],R-i+1);
if(p[i] + i > R){
for(; s2[R+1] == s2[i*2-R-1]; R++){
int y = (3*i-R-1)/2;
if(s2[i] == '#' && s2[R+1] == '#' && (R+1-i)%4 == 0 && p[y] >= i-y+1) ans = max(ans,2*(i-y));
}
pos = i;
p[i] = R-i+1;
}
}
printf("%d\n",ans);
}
int main(){
scanf("%d",&n); scanf("%s",s+1);
manacher();
}
5. iChandu
題意:
給定一個長度爲 的字符串,先可以選擇該字符串中的一個節點,將其替換成 符號,問替換之後該字符串中本質不同迴文串個數。
思路:
一個比較明顯的考慮方式就是枚舉替換的位置,然後計算該位置改變後增加的迴文串數量與減少的迴文串數量。
首先我們考慮增加的迴文串數量,顯然就是以該節點爲中心所形成的迴文串總數,因此我們直接用馬拉車的 數組進行求解即可。
然後再來考慮減少的迴文串數量。我們對於迴文自動機中所有本質不同迴文串的末尾出現位置記錄一個 和 ,表示最遠出現的位置和最早出現的位置,記該回文串長度爲 。則如果 ,則修改位置出現在 中時,本質不同迴文串數量就會減 ,因此我們維護一個差分數組, 加 , 減 ,然後對於每個自動機上的節點都這樣處理一遍即可。
最後再枚舉替換點統計答案。
代碼:
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
#define per(i,a,b) for(int i = a; i >= b; i--)
const int N = 1e5+10;
using namespace std;
char buf[N],s2[2*N];
int sum[N],p[2*N];
void dbg() {cout << "\n";}
template<typename T, typename... A> void dbg(T a, A... x) {cout << a << ' '; dbg(x...);}
#define logs(x...) {cout << #x << " -> "; dbg(x);}
struct PAM{
#define KIND 26
int n,last,tot;
int len[N],trie[N][KIND],fail[N],cnt[N],S[N],num[N],mx[N],mi[N];
//len[i]: 節點i所代表的迴文串長度, fail[i]: 當前迴文串的最長迴文後綴(不包括自身)
//cnt[i]: 節點i所代表的迴文串的個數, S[i]: 第i次添加的字符, num[i]: 以第i個字符爲結尾的迴文串個數
//last: 上一個字符構成最長迴文串的位置,方便下一個字符的插入
//tot: 總結點個數 = 本質不同的迴文串的個數+2, n: 插入字符的個數
int newnode(int l){
rep(i,0,KIND-1) trie[tot][i] = 0;
cnt[tot] = 0, len[tot] = l, num[tot] = 0, mx[tot] = 0, mi[tot] = 2*1e5;
return tot++;
}
inline void init(){
tot = n = last = 0, newnode(0), newnode(-1);
S[0] = -1, fail[0] = 1;
}
int get_fail(int x){ //獲取fail指針
while(S[n-len[x]-1] != S[n]) x = fail[x];
return x;
}
inline int insert(int c,int pos){ //插入字符
c -= 'a';
S[++n] = c;
int cur = get_fail(last);
//在節點cur前的字符與當前字符相同,即構成一個迴文串
if(!trie[cur][c]){ //這個迴文串沒有出現過
int now = newnode(len[cur]+2);
fail[now] = trie[get_fail(fail[cur])][c];
trie[cur][c] = now;
num[now] = num[fail[now]]+1; //更新以當前字符爲結尾的迴文串的個數
}
last = trie[cur][c];
cnt[last]++; //更新當前迴文串的個數
mx[last] = max(mx[last],pos);
mi[last] = min(mi[last],pos); //更新每個迴文串的最早/晚出現位置
return num[last]; //返回以當前字符結尾的迴文串的個數
}
void count(){ //統計每個本質不同迴文串的個數
per(i,tot-1,2){
cnt[fail[i]] += cnt[i];
mx[fail[i]] = max(mx[fail[i]],mx[i]);
mi[fail[i]] = min(mi[fail[i]],mi[i]);
if(mi[i] >= mx[i]-len[i]+1){
sum[mx[i]-len[i]+1] += 1;
sum[mi[i]+1] += -1;
}
}
}
}pam;
void manacher(){
int len = strlen(buf+1), tot = 1, R = 0, pos = 0;
s2[0] = '$'; s2[tot = 1] = '#';
for(int i = 1; i <= len; i++)
s2[++tot] = buf[i], s2[++tot] = '#';
for(int i = 1; i <= tot; i++){
if(R >= i) p[i] = min(p[pos*2-i],R-i+1);
if(p[i] + i > R){
for(; s2[R+1] == s2[i*2-R-1] && R+1 <= tot && i*2-R-1 <= tot; R++);
pos = i;
p[i] = R-i+1;
}
}
}
int main(){
int _; scanf("%d",&_);
while(_--){
int ans = 0, ct = 0;
scanf("%s",buf+1);
pam.init();
int len = strlen(buf+1);
rep(i,0,len) sum[i] = 0;
rep(i,1,len) pam.insert(buf[i],i);
pam.count();
manacher();
rep(i,1,len){
sum[i] += sum[i-1];
int to = pam.tot-2-sum[i]+((p[i*2]-2)/2+1);
if(ans < pam.tot-2-sum[i]+((p[i*2]-2)/2+1)){
ans = pam.tot-2-sum[i]+((p[i*2]-2)/2+1);
ct = 1;
}
else if(ans == pam.tot-2-sum[i]+((p[i*2]-2)/2+1)) ct++;
}
printf("%d %d\n",ans,ct);
}
return 0;
}
6. The Problem to Slow Down You
題意:
給定兩個字符串,詢問滿足條件的四元組個數, 滿足條件當且僅當 ,且 爲迴文串。
思路:
建兩顆迴文自動機,然後從奇根和偶根分別遍歷,同時遍歷兩棵樹,如果到達相同位置則計算對答案的貢獻,也算是比較裸的一道題目。
代碼:
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
#define per(i,a,b) for(int i = a; i >= b; i--)
const int N = 4*1e5+10;
typedef long long ll;
using namespace std;
char buf1[N],buf2[N];
ll ans = 0;
struct PAM{
#define KIND 26
int n,last,tot;
int len[N],trie[N][KIND],fail[N],S[N],num[N];
ll cnt[N];
//len[i]: 節點i所代表的迴文串長度, fail[i]: 當前迴文串的最長迴文後綴(不包括自身)
//cnt[i]: 節點i所代表的迴文串的個數, S[i]: 第i次添加的字符, num[i]: 以第i個字符爲結尾的迴文串個數
//last: 上一個字符構成最長迴文串的位置,方便下一個字符的插入
//tot: 總結點個數 = 本質不同的迴文串的個數+2, n: 插入字符的個數
int newnode(int l){
rep(i,0,KIND-1) trie[tot][i] = 0;
cnt[tot] = 0, len[tot] = l, num[tot] = 0;
return tot++;
}
inline void init(){
tot = n = last = 0, newnode(0), newnode(-1);
S[0] = -1, fail[0] = 1;
}
int get_fail(int x){ //獲取fail指針
while(S[n-len[x]-1] != S[n]) x = fail[x];
return x;
}
inline int insert(int c){ //插入字符
c -= 'a';
S[++n] = c;
int cur = get_fail(last);
//在節點cur前的字符與當前字符相同,即構成一個迴文串
if(!trie[cur][c]){ //這個迴文串沒有出現過
int now = newnode(len[cur]+2);
fail[now] = trie[get_fail(fail[cur])][c];
trie[cur][c] = now;
num[now] = num[fail[now]]+1; //更新以當前字符爲結尾的迴文串的個數
}
last = trie[cur][c];
cnt[last]++; //更新當前迴文串的個數
return num[last]; //返回以當前字符結尾的迴文串的個數
}
void count(){ //統計每個本質不同迴文串的個數
per(i,tot-1,2) cnt[fail[i]] += cnt[i];
}
}pam[2];
void dfs(int t1,int t2){
rep(i,0,KIND-1){
int y1 = pam[0].trie[t1][i], y2 = pam[1].trie[t2][i];
if(y1 && y2){
dfs(y1,y2);
ans = ans+pam[0].cnt[y1]*pam[1].cnt[y2];
}
}
}
int main(){
int _; scanf("%d",&_);
rep(Ca,1,_){
ans = 0;
pam[0].init(); pam[1].init();
scanf("%s",buf1); scanf("%s",buf2);
int len1 = strlen(buf1), len2 = strlen(buf2);
rep(i,0,len1-1) pam[0].insert(buf1[i]);
rep(i,0,len2-1) pam[1].insert(buf2[i]);
pam[0].count(); pam[1].count();
dfs(0,0); dfs(1,1);
printf("Case #%d: %lld\n",Ca,ans);
}
return 0;
}
7. Finding Palindromes
題意:
給出 個字符串,求解在其中 個拼接中,迴文串的數量。
思路:
考慮馬拉車+字典樹解決該問題。
兩個串拼在一起是迴文串,即 與 拼接之後爲迴文串,主要有兩種情況。
- ,則將 與 的反串進行匹配, 中剩下的未匹配部分爲迴文串。
- ,則將 與 的反串進行匹配, 中剩下的未匹配部分爲迴文串。
分類完之後即可用馬拉車算法求出迴文串範圍,然後用字典樹進行反串匹配。
代碼:
此題細節很多,非常容易寫到一半進入暴躁模式…
還有就是我對拍了 組數據還沒拍出錯,但是就是過不了是爲什麼!暴躁!(現在過了…是空間開小了…腦子不清醒的作品…悄咪咪地放上代碼…)
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <vector>
#define mem(a,b) memset(a,b,sizeof a);
#define rep(i,a,b) for(int i = a; i <= b; i++)
#define per(i,a,b) for(int i = a; i >= b; i--)
#define __ ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)
typedef long long ll;
typedef double db;
const int N = 2e6+1000;
const db EPS = 1e-9;
using namespace std;
int n,trie[N][26],tot,p[2*N],flag[N],L[N],sum1[N],sum2[N];
char s[N],s1[N],s2[2*N];
ll ans = 0;
void init(char base[],int len,int op = 0){
//manacher
int ln = 1, R = 0, pos = 0;
rep(i,1,len) s1[i] = base[i-1];
s2[0] = '$'; s2[ln = 1] = '#';
for(int i = 1; i <= len; i++)
s2[++ln] = s1[i], s2[++ln] = '#';
for(int i = 1; i <= ln; i++){
if(R >= i) p[i] = min(p[pos*2-i],R-i+1);
if(p[i]+i > R){
for(; s2[R+1] == s2[i*2-R-1] && R+1 <= ln && i*2-R-1 <= ln; R++);
pos = i;
p[i] = R-i+1;
}
}
//預處理
if(op){
for(int i = 1; i <= len; i++){
int tp = i+(len-i)/2;
flag[i] = 0;
tp *= 2;
if((len-i) % 2 == 1) tp += 2;
else tp++;
if(p[tp] >= ln-tp+1) flag[i] = 1;
}
return;
}
for(int i = 1; i <= len; i++){
int tp = i-(i-1)/2;
flag[i] = 0;
tp *= 2;
if((i-1)%2 == 1) tp -= 2;
else tp--;
if(i != 1 && p[tp] >= tp) flag[i] = 1;
}
//插入字典樹
int now = 0;
for(int i = len; i >= 1; i--){
if(!trie[now][s1[i]-'a']){
trie[now][s1[i]-'a'] = ++tot, sum1[tot] = sum2[tot] = 0;
rep(j,0,25) trie[tot][j] = 0;
}
now = trie[now][s1[i]-'a'];
if(flag[i]) sum2[now]++; //後半部分迴文
if(i == 1) sum1[now]++; //末尾
}
}
void solve(char base[],int len){
init(base,len,1);
rep(i,1,len) s1[i] = base[i-1];
int now = 0;
for(int i = 1; i <= len; i++){
if(!trie[now][s1[i]-'a']) return;
now = trie[now][s1[i]-'a'];
if(i == len) ans += sum2[now]+sum1[now];
else if(flag[i]) ans += sum1[now];
}
}
int main()
{
while(~scanf("%d",&n)){
tot = 0; ans = 0;
rep(i,0,25) trie[0][i] = 0;
int l = 0;
rep(i,1,n){
int tp; scanf("%d%s",&tp,s+l);
init(s+l,tp);
L[i] = l; L[i+1] = l+tp;
l += tp;
}
ans = 0;
rep(i,1,n) solve(s+L[i],L[i+1]-L[i]);
printf("%lld\n",ans);
}
return 0;
}
後記
的旅行充滿荊棘但一擡頭便能看見無數束光,繼續堅持,努力前行!???
本篇博客到這裏就結束了,祝大家 愉快,一起愛上回文串把!(๑•̀ㅂ•́)و✧