回文串相关
1.manacher
为了避免长度奇偶性带来的麻烦,给每个字符前加一个特殊字符’#‘,为了避免非法访问可以在0号位置上再加个’@’
马拉车算法主要是计算
r
[
i
]
r[i]
r[i]表示以 i 为回文中心能向左右拓展的最长长度,称为回文半径
我们维护两个值mx和p,分别表示之前得到的回文串最右边界的位置和这个回文串的中心p
我们要计算先给
r
[
i
]
r[i]
r[i]赋一个下界,设 j 为 i 关于 p 的对称点,即 j=2p-i ,我们需要分以下三种情况讨论:
- i >= mx ,那么初值就是 1
- mx - i > r[j] ,那么初值就是r[j]
- mx - i <= r[j],那么初值就是mx-i
这三种情况合并起来可以写成
r
[
i
]
=
m
i
n
(
m
x
−
i
,
r
[
2
∗
p
−
i
]
)
r[i]=min(mx-i,r[2*p-i])
r[i]=min(mx−i,r[2∗p−i])
然后就是向右拓展合法的位置即可
因为每次向右拓展都会带来mx的更新,所以复杂度均摊$O(n)
P5446 [THUPC2018]绿绿和串串
发现我们要求的是一个串最长可以由哪个前缀操作构成,然后不断进行这个操作不知道怎么能发现,删去末尾最短的回文串(长度大于1) 的后一半即可
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=2e6+5;
int T,cnt,n;
char s[maxn],ss[maxn];
int r[maxn],vis[maxn];
int main()
{
// freopen("a.in","r",stdin);
// freopen("a.out","w",stdout);
scanf("%d",&T);
while(T--)
{
scanf("%s",s+1);
n=strlen(s+1);
cnt=0;
ss[0]='?'; ss[++cnt]='#';
for(int i=1;i<=n;i++) ss[++cnt]=s[i],ss[++cnt]='#';
ss[cnt+1]='~';
for(int i=0;i<=cnt;i++) vis[i]=0,r[i]=0;
int mx=0,mid=0;
for(int i=1;i<=cnt;i++)
{
if(i<=mx) r[i]=min(mx-i,r[mid*2-i]);
while(ss[i+r[i]]==ss[i-r[i]]) r[i]++;
if(i+r[i]>mx) mx=i+r[i]-1,mid=i;
// printf("%d ",r[i]);
}
// printf("\n");
for(int i=cnt;i>=1;i--)
{
if(i+r[i]-1==cnt) vis[i]=1;
else if(vis[i+r[i]-2] && i==r[i])
vis[i]=1;
}
for(int i=1;i<=cnt;i++)
if(ss[i]>='a' && ss[i]<='z' && vis[i])
printf("%d ",i/2);
printf("\n");
}
return 0;
}
ybtoj527.回文子串
看到这种区间性的操作,可以尝试使用线段树维护
给线段树上每个点维护st,ed,res,分别表示前k个字符构成的字符串,后k个字符构成的字符串,res表示区间的答案
修改操作因为是推平成一个字符,很好处理
重点是处理好pushup即可
对于合并两个区间,也就是要计算跨过mid 和 mid+1 两个位置和回文串个数,可以把ed[lson]和st[rson]拼接到一起,做一次manacher,统计跨过mid 和 mid+1的个数即可
时间复杂度 O ( q k l o g ∣ S ∣ ) O(qklog|S|) O(qklog∣S∣)
2.回文自动机 PAM
把回文串分为奇数长度和偶数长度两种,分别建立奇根和偶根,每个转移边相当于在两边同时加上字符c,得到的都是回文串
我们模仿后缀自动机的增量构造方式,依然是按顺序一个字符一个字符插入,维护fail边指向最长的回文后缀,同时给每个节点维护回文串长度
l
e
n
[
u
]
len[u]
len[u]。
插入一个字符先跳fail边直到
s
i
=
s
i
−
l
e
n
[
u
]
−
1
s_i=s_{i-len[u]-1}
si=si−len[u]−1,也就是找到一个可以通过两边增加字符
s
i
s_i
si得到另一个回文串的位置,如果这个串还没有出现过,就增加这个点,然后维护它的fail,方式依然是跳fail直到能匹配上为止。
注意把偶根的fail指向奇根,而奇根不需要管fail,因为奇根不可能实配。
这样就得到了回文自动机,可以证明节点数为
O
(
n
)
O(n)
O(n),每次插入一个字符最多只会新增一个回文串。
例题 P3649 [APIO2014]回文串
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=3e5+5;
char s[maxn];
struct PAM
{
int ch[26],len,cnt,fail;
}tr[maxn];
int last,tot;
ll ans;
int newnode(int x)
{
tr[++tot].len=x;
return tot;
}
int getfail(int x,int n)
{
while(s[n-tr[x].len-1]!=s[n]) x=tr[x].fail;
return x;
}
int main()
{
// freopen("a.in","r",stdin);
// freopen("a.out","w",stdout);
scanf("%s",s+1); s[0]=-1;
tr[0].fail=1; tr[0].len=0;
tr[1].len=-1; tot=1;
for(int i=1;s[i];i++)
{
s[i]-='a';
int p=getfail(last,i);
if(!tr[p].ch[s[i]])
{
int q=newnode(tr[p].len+2);
tr[q].fail=tr[getfail(tr[p].fail,i)].ch[s[i]];
tr[p].ch[s[i]]=q;
}
last=tr[p].ch[s[i]];
tr[last].cnt++;
}
for(int i=tot;i;i--)
tr[tr[i].fail].cnt+=tr[i].cnt,ans=max(ans,1LL*tr[i].cnt*tr[i].len);
printf("%lld\n",ans);
return 0;
}
P4287 [SHOI2011]双倍回文
每个节点维护一个
f
[
i
]
f[i]
f[i]表示小于等于一半长度的最长回文后缀长度
维护方式:如果这个点的fail本身长度小于等于一半更新即可
否则跳fail直到字符合法且长度合法为止
代码
P4555 [国家集训队]最长双回文串
正反建立两个回文自动机
分别求出
L
[
i
]
L[i]
L[i]和
R
[
i
]
R[i]
R[i]表示以 i 为左/右端点的最长回文串长度
那么
a
n
s
=
m
a
x
i
<
n
(
L
[
i
]
+
R
[
i
+
1
]
)
ans=max_{i<n}(L[i]+R[i+1])
ans=maxi<n(L[i]+R[i+1])
代码
P1659 [国家集训队]拉拉队排练
待完成!
3.border理论
对于字符串S,如果 p r e ( S , i ) = s u f ( S , i ) pre(S,i)=suf(S,i) pre(S,i)=suf(S,i),也就是长度为 i 的前后缀相等,就称 p r e ( S , i ) pre(S,i) pre(S,i)为S的一个border
定理:将字符串S的所有回文后缀按照长度排序后,可以划分成 l o g ∣ S ∣ log|S| log∣S∣段等差数列
引理1:对于回文串S,T是S的后缀,T是S的border的充要条件为T是回文串
引理2:对于S的borderT,如果 ∣ S ∣ ≤ 2 ∣ T ∣ |S| \le 2|T| ∣S∣≤2∣T∣ ,那么S是回文串的充要条件为T是回文串
引理3:对于回文串S, ∣ S ∣ − ∣ T ∣ |S|-|T| ∣S∣−∣T∣是S的最小周期的充要条件是T是S的最长回文真后缀
引理4:
例题 最小回文划分问题
给定一个字符串S,求最少把S划分为几段,能使得S的每一段均为回文串
很容易得到dp,设
f
i
f_i
fi表示前 i 位的答案,那么
f
i
=
m
i
n
{
f
j
+
1
}
,
s
[
j
+
1...
i
]
为
回
文
串
f_i=min \{f_j+1\},s[j+1...i]为回文串
fi=min{fj+1},s[j+1...i]为回文串
我们可以建立PAM,用
O
(
n
2
)
O(n^2)
O(n2)的时间复杂度暴力转移,也就是在当前节点暴力跳fail,每个人都尝试转移一次
#include <bits/stdc++.h>
#define N 500005
using namespace std;
struct Node
{
int len, fa, ch[26];
} nd[N << 1];
int ct = 1, last, diff[N], slink[N], dp[N], g[N];
char op[N];
inline int newNode(int x)
{
int p = ++ct;
nd[p].len = x;
return p;
}
inline void add(int c, int pos)
{
int u = last;
while (op[pos - nd[u].len - 1] != op[pos]) u = nd[u].fa;
if (nd[u].ch[c] == 0) {
int s = newNode(nd[u].len + 2), f = nd[u].fa;
while (op[pos - nd[f].len - 1] != op[pos]) f = nd[f].fa;
nd[s].fa = nd[f].ch[c], nd[u].ch[c] = s;
}
last = nd[u].ch[c];
diff[last] = nd[last].len - nd[nd[last].fa].len;
if (diff[last] == diff[nd[last].fa]) slink[last] = slink[nd[last].fa];
else slink[last] = nd[last].fa;
}
int main()
{
nd[0].len = 0, nd[0].fa = 1, nd[1].len = -1, nd[1].fa = 0;
scanf("%s", op + 1);
for (int i = 1; op[i]; i++) {
add(op[i] - 'a', i), dp[i] = 0x7fffffff;
for (int j = last; j > 1; j = slink[j]) {
g[j] = dp[i - nd[slink[j]].len - diff[j]];
if (diff[j] == diff[nd[j].fa]) g[j] = min(g[j], g[nd[j].fa]);
dp[i] = min(dp[i], g[j] + 1);
}
}
printf("%d", dp[strlen(op + 1)]);
return 0;
}