[题解]P7114 [NOIP2020] 字符串匹配

P7114 [NOIP2020] 字符串匹配

可以想到枚举\(AB\)的长度\(k\),然后再枚举\(AB\)的循环次数\(i\),用字符串哈希判断当前\(i\)是否合法。预处理出\(S\)前缀后缀中出现奇数次的字符个数,对于每个\((AB)^i\),查询出\(F(C)\),然后再计算\(F(A)\le F(C)\)\(A\)有多少个,累加答案。

计算\(A\)的数目可以开一个\([0,26]\)的树状数组用于查询长度\(<k\)的前缀中,出现奇数次的字符个数\(\le F(C)\)的个数。

这样子每组数据时间复杂度是\(O(n\log n\log |\Sigma|)\),其中\(O(n\log n)\)是调和级数\(\frac{2}{n}+\frac{3}{n}+\dots\)\(\times n\)\(O(\log |\Sigma|)\)是树状数组,\(|\Sigma|=26\)。无法通过,考虑如何优化。


观察发现,对于长度为\(k\)\(AB\)循环\(i\)次,当\(i\)同奇偶时,\(F(C)\)是固定值,自然满足\(F(A)\le F(C)\)\(A\)的个数也是确定的。

因此我们无需枚举每一个\(i\)累加答案,仅需算出\(i\)可能的最大值\(maxi\),在\([1,maxi]\)中奇数一共有\(\lfloor\frac{maxi+1}{2}\rfloor\)个,偶数一共有\(\lfloor\frac{maxi}{2}\rfloor\)个,奇数和偶数各在树状数组中查询一次即可,奇数\(i\)的答案是满足\(F(A)\le F(S[k+1,n])\)\(A\)的个数,偶数\(i\)的答案是满足\(F(A)\le F(S[1,n])\)\(A\)的个数。

最后我们考虑如何高效地求出\(maxi\)

  • 哈希+枚举:就是优化前的方法,不说了。

  • 哈希+二分:用字符串哈希二分找到\(S[1,n]\)\(S[k+1,n]\)的最长公共前缀\(d\),则有

    \[maxi=\min(\lfloor\frac{d}{k}\rfloor+1,\lfloor\frac{n-1}{k}\rfloor) \]

    解释:画个图推演一下可以发现,\(S[1,k]=S[k+1,2k]=S[2k+1,3k]=\dots=S[\lfloor\frac{d}{k}\rfloor\times k,\lfloor\frac{d}{k}\rfloor\times k+1]\),可以参见此题解;还需要注意\((AB)^{maxi}\)长度不能超过\(n-1\),所以需要再取一个\(\min\)

    时间复杂度是\(O(n\log_2 n\log |\Sigma|)\)

  • 扩展KMP(Z函数):可以发现我们所二分的最长公共前缀就是Z函数的定义,直接套用扩展KMP求出Z函数即可在线性时间复杂度内解决。关于扩展KMP的内容参见此文。时间复杂度\(O(n\log |\Sigma|)\)

因此跑一遍Z函数,然后根据第二条的公式逐个累加答案即可。

注意多测清空,否则后缀数组会出问题。还有树状数组不能存下标\(0\),所以需要整体\(+1\)再存取。

$O(n\log |\Sigma|)$ - 881ms
#include<bits/stdc++.h>
#define N 1048600
#define C 26
#define int long long
using namespace std;
int t,n,z[N],tpre[N],tsuf[N],pre[N],suf[N],sum[N],ans;
inline int lowbit(int x){return x&-x;}
string s;
struct BIT{
int sum[C+2];
void clear(){memset(sum,0,sizeof sum);}
void add(int x,int k){x++;while(x<=C+1) sum[x]+=k,x+=lowbit(x);}
int query(int x){x++;int ans=0;while(x) ans+=sum[x],x-=lowbit(x);return ans;}
}bit;
void getz(string s,int n){
z[1]=n;
for(int i=2,l=1,r=1;i<=n;i++){
if(i<=r&&z[i-l+1]<r-i+1) z[i]=z[i-l+1];
else{
z[i]=max(0ll,r-i+1);
while(i+z[i]<=n&&s[z[i]+1]==s[i+z[i]]) z[i]++;
}
if(i+z[i]-1>r) l=i,r=i+z[i]-1;
}
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr),cout.tie(nullptr);
cin>>t;
while(t--){
cin>>s;
n=s.size(),s=' '+s;
bit.clear(),ans=suf[n+1]=tsuf[n+1]=0;
getz(s,n);
for(int i=1;i<=n;i++)
pre[i]=pre[i-1]+((tpre[i-1]>>(s[i]-'a')&1)?-1:1),
tpre[i]=tpre[i-1]^(1<<(s[i]-'a'));
for(int i=n;i>=1;i--)
suf[i]=suf[i+1]+((tsuf[i+1]>>(s[i]-'a')&1)?-1:1),
tsuf[i]=tsuf[i+1]^(1<<(s[i]-'a'));
for(int k=2;k<n;k++){
int maxi=min(z[k+1]/k+1,(n-1)/k);
bit.add(pre[k-1],1);
int cnt1=(maxi+1)>>1,cnt2=maxi>>1;
ans+=cnt1*bit.query(suf[k+1]);
ans+=cnt2*bit.query(suf[1]);
}
cout<<ans<<"\n";
}
return 0;
}

注意到\(suf[1]\)始终不变,而\(suf[k+1]\)每次恰好变化\(1\),故我们可以放弃树状数组,实时更新两次查询的值,具体见代码。时间复杂度降到了严格的\(O(n+|\Sigma|)\)

$O(n+|\Sigma|)$ - 762ms
#include<bits/stdc++.h>
#define N 1048600
#define C 26
#define int long long
using namespace std;
int t,n,z[N],tpre[N],tsuf[N],pre[N],suf[N],sum[N],cnt[C+1],ans;
inline int lowbit(int x){return x&-x;}
string s;
void getz(string s,int n){
z[1]=n;
for(int i=2,l=1,r=1;i<=n;i++){
if(i<=r&&z[i-l+1]<r-i+1) z[i]=z[i-l+1];
else{
z[i]=max(0ll,r-i+1);
while(i+z[i]<=n&&s[z[i]+1]==s[i+z[i]]) z[i]++;
}
if(i+z[i]-1>r) l=i,r=i+z[i]-1;
}
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr),cout.tie(nullptr);
cin>>t;
while(t--){
cin>>s;
n=s.size(),s=' '+s;
ans=suf[n+1]=tsuf[n+1]=0;
memset(cnt,0,sizeof cnt);
getz(s,n);
for(int i=1;i<=n;i++)
pre[i]=pre[i-1]+((tpre[i-1]>>(s[i]-'a')&1)?-1:1),
tpre[i]=tpre[i-1]^(1<<(s[i]-'a'));
for(int i=n;i>=1;i--)
suf[i]=suf[i+1]+((tsuf[i+1]>>(s[i]-'a')&1)?-1:1),
tsuf[i]=tsuf[i+1]^(1<<(s[i]-'a'));
int q1=0,q2=0;
for(int k=2;k<n;k++){
int maxi=min(z[k+1]/k+1,(n-1)/k);
if(suf[k+1]>suf[k]) q1+=cnt[suf[k+1]];
else q1-=cnt[suf[k]];
cnt[pre[k-1]]++;
if(pre[k-1]<=suf[k+1]) q1++;
if(pre[k-1]<=suf[1]) q2++;
int cnt1=(maxi+1)>>1,cnt2=maxi>>1;
ans+=cnt1*q1+cnt2*q2;
}
cout<<ans<<"\n";
}
return 0;
}

两份代码中,我们求某段区间内出现奇数次的字符个数,都用了一些前缀异或和的思想,将\(s[1,i]\)中各字母出现情况压在\(f[i]\)中,奇数次为\(1\),偶数次为\(0\)

为了代码的可读性,我开了\(4\)个数组来处理出前缀和后缀的答案。实际上要求一个区间的答案,直接把\(f[r]\)\(f[l-1]\)异或再求一个\(\text{popcount}\)就可以了,代码如下(可读性稍差但效率高了不少):

$O(n+|\Sigma|)$ $\text{popcount}$ 优化 - 656ms
#include<bits/stdc++.h>
#define N 1048600
#define C 26
#define int long long
#define pc(x) (__builtin_popcountll(x))
using namespace std;
int t,n,z[N],pre[N],sum[N],cnt[C+1],ans;
inline int lowbit(int x){return x&-x;}
string s;
void getz(string s,int n){
z[1]=n;
for(int i=2,l=1,r=1;i<=n;i++){
if(i<=r&&z[i-l+1]<r-i+1) z[i]=z[i-l+1];
else{
z[i]=max(0ll,r-i+1);
while(i+z[i]<=n&&s[z[i]+1]==s[i+z[i]]) z[i]++;
}
if(i+z[i]-1>r) l=i,r=i+z[i]-1;
}
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr),cout.tie(nullptr);
cin>>t;
while(t--){
cin>>s;
n=s.size(),s=' '+s;
ans=0;
memset(cnt,0,sizeof cnt);
getz(s,n);
for(int i=1;i<=n;i++) pre[i]=pre[i-1]^(1<<(s[i]-'a'));
int q1=0,q2=0;
for(int k=2;k<n;k++){
int maxi=min(z[k+1]/k+1,(n-1)/k);
if(pc(pre[n]^pre[k])>pc(pre[n]^pre[k-1])) q1+=cnt[pc(pre[n]^pre[k])];
else q1-=cnt[pc(pre[n]^pre[k-1])];
cnt[pc(pre[k-1])]++;
if(pc(pre[k-1])<=pc(pre[n]^pre[k])) q1++;
if(pc(pre[k-1])<=pc(pre[n])) q2++;
int cnt1=(maxi+1)>>1,cnt2=maxi>>1;
ans+=cnt1*q1;
ans+=cnt2*q2;
}
cout<<ans<<"\n";
}
return 0;
}
posted @   Sinktank  阅读(29)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!
2025-2-27 8:11:26 TOP-BOTTOM-THEME
Enable/Disable Transition
Copyright © 2023 ~ 2024 Sinktank - 1328312655@qq.com
Illustration from 稲葉曇『リレイアウター/Relayouter/中继输出者』,by ぬくぬくにぎりめし.
点击右上角即可分享
微信分享提示