[题解]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; }
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!