20260725 - 马拉车 总结
你猜为什么一篇加上代码也只有不到一万三千字的总结拖了两个星期。
那当然是因为我太摆了。
马拉车是什么啊?我只知道 Manacher。
你说的对,但是。
Manacher 算法
Manacher 是一种求回文串的算法,它的具体做法在于先尝试继承曾经的答案,然后暴力扩展,均摊时间复杂度 \(O(n)\),非常优秀。具体细节不讲了,唉唉,我咋这么菜,没事看 Manacher 那篇就行,挺详细的,我忘了也是靠啃它捡起来的。
例题讲解
Manacher 好难啊啊啊。这次的讲解不会很详细,见谅嘟嘟嘟。
A - Manacher
板子。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 3e7+5;
int n,p[N],Ans;
string t,s;
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
int main(){
cin>>t;s=" ";
for(int i=0;i<t.size();i++){
s+=t[i];
if(i<t.size()-1)s+='#';
}
n=s.size()-1;
for(int i=1,l=0,r=-1;i<=n;i++){
int len=(i>r?1:min(p[l+r-i],r-i+1));
while(len<i&&i+len<=n&&s[i-len]==s[i+len])len++;
p[i]=len;
Ans=max(Ans,len-(s[i+len-1]=='#'));
if(i+len-1>r)l=i-len+1,r=i+len-1;
}
cout<<Ans<<"\n";
return 0;
}
B - ANT-Antisymmetry
还是板子,扩展判断的时候 \(=\) 改成 \(\not=\) 就行了。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 2e6+5;
LL n,p[N],Ans;
string t,s;
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
bool ok(int x,int y){
if(s[x]==s[y]&&s[x]=='#')return 1;
else return (s[x]!=s[y]);
}
int main(){
n=read();cin>>t;s=" ";
for(int i=0;i<n;i++){
s+=t[i];
if(i<n-1)s+='#';
}
n=s.size()-1;
for(int i=1,l=0,r=-1;i<=n;i++){
int len=(i>r?0:min(p[l+r-i],r-i+1ll));
while(len<i&&i+len<=n&&ok(i-len,i+len))len++;
p[i]=len;
if(s[i]=='#'){
//cout<<i<<":";
//cout<<p[i]<<"-("<<s[i+len-1]<<"=='#')\n";
Ans+=p[i]-(s[i+len-1]=='#');
}
if(i+len-1>r)l=i-len+1,r=i+len-1;
}
cout<<Ans/2<<"\n";
return 0;
}
C - 拉拉队排练
板子,存桶然后前缀和一下,快速幂算就行。这里只需要奇数长度回文串,所以字符串中间不需要插字符。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 2e6+5;
const LL MOD = 19930726;
LL n,k,p[N],c[N],Ans=1;
string t,s;
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
LL QP(LL x,LL y){
LL as=1;
while(y){
if(y&1)as=as*x%MOD;
x=x*x%MOD,y>>=1;
}return as;
}
int main(){
n=read(),k=read();
cin>>s;s=" "+s;
for(int i=1,l=0,r=-1;i<=n;i++){
int len=(i>r?1:min(p[l+r-i],r-i+1ll));
while(len<i&&i+len<=n&&s[i-len]==s[i+len])len++;
p[i]=len;
if(i+len-1>r)l=i-len+1,r=i+len-1;
}
for(int i=1;i<=n;i++)c[p[i]]++;
for(int i=n;i>=1;i--){
c[i]+=c[i+1];
LL tmp=min(c[i],k);
Ans=Ans*QP(2*i-1,tmp)%MOD,k-=tmp;
if(!k)break;
}
if(k)cout<<"-1\n";
else cout<<Ans<<"\n";
return 0;
}
D - 最长双回文串
充分彰显了我菜成一坨的题。其实还是很简单的,你找到回文串之后在其最左最后标记上,然后前后各扫一遍维护出每个节点作为回文串左端点和右端点分别能延伸出去多少,然后加法取 \(\max\) 即可。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 3e5+5;
int n,m,p[N],l[N],r[N],Ans=0;
string t,s;
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
int main(){
cin>>t;m=t.size();s=" #";
for(int i=0;i<t.size();i++)s+=t[i],s+='#';
n=s.size()-1;
for(int i=1,mid=0,R=0;i<=n;i++){
int len=(i>R?1:min(p[2*mid-i],R-i+1));
while(i-len>=1&&i+len<=n&&s[i-len]==s[i+len])len++;
p[i]=len;
if(i+len-1>R)mid=i,R=i+len-1;
l[i+len-1]=max(l[i+len-1],p[i]-1);
r[i-len+1]=max(r[i-len+1],p[i]-1);
}
//for(int i=1;i<=n;i++)cout<<p[i]<<" ";cout<<"\n";
//for(int i=1;i<=n;i++)cout<<l[i]<<" ";cout<<"\n";
//for(int i=1;i<=n;i++)cout<<r[i]<<" ";cout<<"\n";
for(int i=1;i<=n;i+=2)r[i]=max(r[i],r[i-2]-2);
for(int i=n;i>=1;i-=2)l[i]=max(l[i],l[i+2]-2);
for(int i=1;i<=n;i+=2)
if(l[i]&&r[i])Ans=max(Ans,l[i]+r[i]);
cout<<Ans<<"\n";
return 0;
}
E - 回文串计数
不需要用到 Manacher,直接暴力找出所有回文串然后标记各自作为左右端点的个数,枚举 \(r\) 和 \(L\) 做就行了。用 Manacher 属于自讨苦吃,反正最后还是 \(O(n^2)\) 的,不推荐。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 2005;
int n,p[N],cl[N],cr[N];
LL Ans;
string t,s;
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
int main(){
cin>>s;n=s.size(),s=" "+s;
for(int x=1;x<=n;x++){
int i=x,j=x;
while(i>=1&&j<=n&&s[i]==s[j])
cl[j]++,cr[i]++,i--,j++;
if(x==n)break;
i=x,j=x+1;
while(i>=1&&j<=n&&s[i]==s[j])
cl[j]++,cr[i]++,i--,j++;
}
for(int i=1;i<n;i++)
for(int j=i+1;j<=n;j++)
Ans+=cl[i]*cr[j];
cout<<Ans<<"\n";
return 0;
}
F - Prefix-Suffix Palindrome (Hard version)
不少人说这题很史,我表示不能理解。
把题目中的回文串拆成两个部分,首先首尾如果能凑回文串那就凑,直到凑不下去,可以证明这样一定不会变得更劣。删掉首尾匹配上的那部分,再在中间找一个靠着左边或者右边的回文串就行了。
代码实现很简单啊,核心代码不到三十行,说史的难道连一百行的题目都没做过吗???
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 2e6+5;
int T,n,pl,pr,p[N],bas,Ans;
int cs[N],ct[N];
string t,s;
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
int main(){
T=read();
while(T--){
cin>>t;
bas=0,Ans=0;
for(pl=0,pr=t.size()-1;pl<pr;pl++,pr--)
if(t[pl]!=t[pr])break;else bas++;
s=" ";
for(int i=pl;i<=pr;i++){
s+=t[i];
if(i<pr)s+='#';
}
n=s.size()-1;
for(int i=1,l=0,r=-1;i<=n;i++){
int len=(i>r?1:min(p[l+r-i],r-i+1));
while(len<i&&i+len<=n&&s[i-len]==s[i+len])len++;
p[i]=len;
if(i+len-1>r)l=i-len+1,r=i+len-1;
}
int ID=0;
for(int i=1;i<=n;i++)
if(p[i]==i||i+p[i]>n){
int len=p[i]-(s[i+p[i]-1]=='#');
if(len>Ans)Ans=len,ID=i;
}
string ans="";
for(int i=0;i<pl;i++)ans+=t[i];
for(int i=ID-p[ID]+1;i<=ID+p[ID]-1;i++)
if(s[i]!='#')ans+=s[i];
for(int i=pr+1;i<t.size();i++)ans+=t[i];
cout<<ans<<"\n";
}
return 0;
}
G - Palindromic characteristics
依旧不需要 Manacher 出手。首先暴力统计所有回文串,然后用类似区间 DP 的方式扫一遍,转移 \(dp_{i,j} = dp_{i,\frac{i+j}{2}} + 1\),就是折半的结果 \(+1\),最后用桶存一下,前缀和搞搞就行了。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 5005;
int n,dp[N][N],t[N];
bool vis[N][N];
string s;
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
int main(){
cin>>s;
n=s.size(),s=" "+s;
memset(vis,1,sizeof(vis));
for(int len=1;len<=n;len++)
for(int i=1;i+len-1<=n;i++){
int j=i+len-1;
vis[i][j]=vis[i+1][j-1]&&(s[i]==s[j]);
}
for(int len=1;len<=n;len++)
for(int i=1;i+len-1<=n;i++){
int j=i+len-1;
if(vis[i][j])dp[i][j]=dp[i][(i+j-1)/2]+1;
}
for(int i=1;i<=n;i++)
for(int j=i;j<=n;j++)
if(vis[i][j])t[dp[i][j]]++;
for(int i=n;i>=1;i--)t[i]+=t[i+1];
for(int i=1;i<=n;i++)
cout<<t[i]<<" ";cout<<"\n";
return 0;
}
H - Queries for Number of Palindromes
确定不是区间 DP 的题目吗?确定过了,是!
\(0\) 个 Manacher。暴力求出每个子串是否为回文,然后扫两遍区间 DP 求出答案,最后 \(O(1)\) 回答即可,非常简单。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 5005;
int n,Q,dp[N][N],cnt[N][N];
bool vis[N][N];string s;
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
int main(){
cin>>s;
n=s.size(),s=" "+s;
memset(vis,1,sizeof(vis));
for(int len=1;len<=n;len++)
for(int i=1;i+len-1<=n;i++){
int j=i+len-1;
vis[i][j]=vis[i+1][j-1]&&(s[i]==s[j]);
}
for(int len=1;len<=n;len++)
for(int i=1;i+len-1<=n;i++){
int j=i+len-1;
cnt[i][j]=cnt[i+1][j]+(int)vis[i][j];
}
for(int len=1;len<=n;len++)
for(int i=1;i+len-1<=n;i++){
int j=i+len-1;
dp[i][j]=dp[i][j-1]+cnt[i][j];
}
Q=read();
while(Q--){
int x=read(),y=read();
cout<<dp[x][y]<<"\n";
}
return 0;
}
I - 绿绿和串串
还挺有意思的一个题。首先你发现如果某个串翻转一次的前缀是 \(s\) 说明这个串由 \(s\) 的后缀回文串的一半加上剩下的前缀得来,有点绕口但没关系,而这个后缀回文串越大这个串就越短,非常棒啊;然后还有一种某个串翻转一次能得到一个经过若干次翻转能翻到前缀是 \(s\) 的串,这个就可以通过类似 \(vis\) 标记来搞定。
呃,我在说绕口令,没关系,发现这里的重点是求出后缀回文串,啊,这个好办,我们直接跑一通 Manacher,然后倒序扫一遍字符串,如果能直接翻过去(后缀回文串)就直接标记,否则就看你翻过去的位置是不是能够经过若干次翻转翻过去,也就是看有无标记,因为是倒着枚举的,所以这么干没啥问题,然后如果有标记也给当前位置打上标记。
说了一坨,然后就做完了,最后看所有有标记的字母节点,将其下标 \(\div 2\) 后输出即可(\(\div 2\) 可以转化为原下标)。
戳这里看代码
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 2e6+5;
int T,n,p[N];
bool vis[N];
string t,s;
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
int main(){
T=read();
while(T--){
cin>>t;s=" #";
for(char c:t)s+=c,s+='#';
n=s.size()-1,s+='%';
for(int i=1,l=0,r=-1;i<=n;i++){
int len=(i>r?1:min(p[l+r-i],r-i+1));
while(len<i&&i+len<=n&&s[i-len]==s[i+len])len++;
p[i]=len;
if(i+len-1>r)l=i-len+1,r=i+len-1;
}
for(int i=1;i<=n;i++)vis[i]=0;
for(int i=n;i>=1;i--)
if(i+p[i]-1==n)vis[i]=1;
else if(i==p[i]&&vis[i+p[i]-2])vis[i]=1;
for(int i=1;i<=n;i++)
if(s[i]>='a'&&s[i]<='z'&&vis[i])cout<<i/2<<" ";cout<<"\n";
}
return 0;
}
总结
Manacher 好难好难。
Manacher 一点也不好吃。
我不管了,反正我也总结不了什么,反正明天就要上课了,哈哈。

浙公网安备 33010602011771号