Manacher 的一个优雅结论
做题遇到了一个 trick,想了两天,求助教练,LLM 无果后,在大佬的帮助下明白了。
考虑以下 Manacher 代码的改版:
int n=s.size(),C=0,R=-1;
vector<int> p(n);
for(int i=0;i<n;i++){
if(i<=R){//就是这样收缩
int j=2*C-i;
p[i]=p[j];
while(p[i]>R-i+1) p[i]--;
}
else p[i]=1;
while(i-p[i]>=0 && i+p[i]<n && s[i-p[i]]==s[i+p[i]]) p[i]++;
int right=i+p[i]-1;
if(right>=R) C=i,R=right;
}
其中相对于正常的 Manacher,将取 min 换成了朴素的收缩。这种改版,支持代码完成更多操作,减少数据结构的使用。
其实,这版代码的时间复杂度是线性的。
证明
也不知道之前怎么想出来的复杂野鸡证明还假了,现在补一个。
就是我们考虑找一个媒介做势能分析,就取维护的最靠右字符串长度好了,暂且叫它 \(\delta\)。
那么考虑一次成功的扩张,\(\delta\) 会加二,但是由于
if(right>=R) C=i,R=right;
,\(\delta\) 最多才会加二。
考虑缩小,当一个字符串从另一个字符串对称过来时,我们画个图。

那么需要缩短的,就是蓝色的这一段。

然后此时的最靠右字符串,也就是 \(\delta\),是白色这一段。

那么紫色字符串每收缩一次,白色字符串也会至少收缩一次,\(\delta\) 至少减一。
马拉车总共会扩张 \(\mathcal{O}(N)\) 次,注意到 \(\delta\) 一直非负,那么收缩操作最多也只能进行 \(\mathcal{O}(N)\) 次,总复杂度线性。
一道练习题
#include<bits/stdc++.h>
#define ll __int128
using namespace std;
/*
FACE THE FEAR, MAKE THE FUTURE.
P12977 泪雨 Namid[A]me
需要维护:括号数量,当前总贡献(合法子回文串括号数量之和),区间长度。
*/
const int N=5e6+7;
int n; string s;
ll p[N<<1],contr[N<<1],num[N<<1];
ll c,r,ans;
int skl;
void pre(){
string t="$#";
for(char ch:s) t+=ch,t+="#";
t+="$"; s=t,n=s.size();
}
void work(){
for(int i=1;i<n;i++){
if(i<=r){
int j=2*c-i;
while(p[j]>r-i+1){
if(s[j+p[j]-1]=='#'&&num[j]*2>=p[j]-1) contr[j]-=num[j];
if(s[j+p[j]-1]=='?') num[j]-=2;
p[j]--;
} contr[i]=contr[j],num[i]=num[j],p[i]=p[j];
}
else p[i]=1,num[i]=(s[i]=='?');
while(s[i-p[i]]!='$'&&s[i+p[i]]!='$'&&s[i-p[i]]==s[i+p[i]]){
num[i]+=2*(s[i+p[i]]=='?'); p[i]++;
if(s[i+p[i]-1]=='#') contr[i]+=(2*num[i]>=p[i]-1)*num[i];
}
if(i+p[i]-1>=r) r=i+p[i]-1,c=i;
ans+=contr[i]*i/2;
}
}
int main(){
cin>>skl>>s;
pre();
work();
if(ans==0) cout<<0;
stack<int> stk;
while(ans){
stk.push(ans%10);
ans/=10;
} while(stk.size()) cout<<stk.top(),stk.pop();
return 0;
}

浙公网安备 33010602011771号