这里会显示出您选择的修订版和当前版本之间的差别。
两侧同时换到之前的修订记录 前一修订版 后一修订版 | 前一修订版 | ||
2020-2021:teams:legal_string:jxm2001:字符串_1 [2020/08/26 16:48] jxm2001 |
2020-2021:teams:legal_string:jxm2001:字符串_1 [2020/08/27 11:38] (当前版本) jxm2001 |
||
---|---|---|---|
行 21: | 行 21: | ||
如果 $s_{i+1}\neq s_{f(i)+1}$,则只能考虑第二长的真前后缀,暂时记为 $j$。 | 如果 $s_{i+1}\neq s_{f(i)+1}$,则只能考虑第二长的真前后缀,暂时记为 $j$。 | ||
- | 于是有 $s[1,j]=s[i-j+1,i]=s[f(i)-j+1,f(i)]$,发现 $j$ 是 $s[1,f(i)]$ 的真前后缀,于是有 $j=f(f(i))$。 | + | 根据已知,有 $s[1,j]=s[i-j+1,i]=s[f(i)-j+1,f(i)]$。 |
+ | |||
+ | 发现 $j$ 是 $s[1,f(i)]$ 的真前后缀,于是有 $j=f(f(i))$。 | ||
不停尝试,直到 $j=0$ 即没有满足条件真前后缀,可以停止尝试。 | 不停尝试,直到 $j=0$ 即没有满足条件真前后缀,可以停止尝试。 | ||
行 78: | 行 80: | ||
于是有结论若 $n-f(n)\nmid n$,则在 $s[1,n]$ 末尾补上长度为 $n-f(n)-(n\bmod {(n-f(n))})$ 可以构成以 $s[1,n-f(n)]$ 为最大周期的循环串。 | 于是有结论若 $n-f(n)\nmid n$,则在 $s[1,n]$ 末尾补上长度为 $n-f(n)-(n\bmod {(n-f(n))})$ 可以构成以 $s[1,n-f(n)]$ 为最大周期的循环串。 | ||
+ | |||
+ | === 前缀统计 === | ||
+ | |||
+ | 考虑对串 $s$,统计 $s$ 的每个前缀在 $s$ 中的出现次数。 | ||
+ | |||
+ | 先考虑统计所有形如 $s[l,r](l\gt 1)$ 的串对答案的贡献,发现这些串等价于所有 $s[1,r]$ 的真后缀集合。 | ||
+ | |||
+ | 考虑串 $s[1,i]$ 的所有真后缀,同时他们对 $f(i),f(f(i)),f(f(f(i)))\cdots$ 的前缀产生贡献。 | ||
+ | |||
+ | 于是从 $n$ 到 $1$ 依次转移真后缀的贡献,最后补上所有前缀的贡献即可。 | ||
+ | |||
+ | <code cpp> | ||
+ | int cnt[MAXN]; | ||
+ | void cal(int *s,int n){ | ||
+ | get_f(s,n); | ||
+ | _rep(i,1,n)cnt[f[i]]++; | ||
+ | for(int i=n;i;i--)cnt[f[i]]+=cnt[i]; | ||
+ | _rep(i,1,n)cnt[i]++; | ||
+ | } | ||
+ | </code> | ||
+ | |||
+ | 接下来考虑统计串 $t$ 的每个前缀在串 $s$ 中的出现次数。 | ||
+ | |||
+ | 事实上,直接先将与每个 $s[1,i]$ 的后缀匹配的 $t$ 的最长的前缀加入桶,然后按上述方式转移即可。 | ||
+ | |||
+ | === 不同子串统计 === | ||
+ | |||
+ | 一开始建立一个空串,考虑每次向空串中加入新字母。 | ||
+ | |||
+ | 假设当前串为 $s$,新加入的字母为 $c$,设 $t=s+c,t'=reverse(t),|t|=n$。 | ||
+ | |||
+ | 对串 $t'$ 跑 $\text{KMP}$ 算法,显然如果有 $f(i)=j$,则表示 $t'[1,j]=t'[i-j+1,i]$。 | ||
+ | |||
+ | 这等价于 $t[n-j+1,n]=t[n-i+1,n-i+j]$,于是 $t[n-j+1,n]$ 不是新子串。 | ||
+ | |||
+ | 设 $m=\max(f(i))$,于是有 $t'[1,i](i\gt m)$ 在 $t'$ 中唯一,同样也在 $t$ 中唯一,于是 $t[n-i+1,n](i\gt m)$ 为新子串。 | ||
==== 算法练习 ==== | ==== 算法练习 ==== | ||
行 140: | 行 178: | ||
int n=strlen(buf+1); | int n=strlen(buf+1); | ||
mem(dp,-1); | mem(dp,-1); | ||
- | dfs(1,n); | + | enter(dfs(1,n)); |
- | enter(dp[1][n]); | + | } |
+ | return 0; | ||
+ | } | ||
+ | </code> | ||
+ | </hidden> | ||
+ | |||
+ | === 习题二 === | ||
+ | |||
+ | [[https://www.luogu.com.cn/problem/CF808G|CF808G]] | ||
+ | |||
+ | == 题意 == | ||
+ | |||
+ | 给定一个字符串 $s$ 和模式串 $t$,$s$ 只包含 $26$ 个小写字母和 $\text{?}$,$t$ 只包含 $26$ 个小写字母。 | ||
+ | |||
+ | 将 $s$ 中的所有 $\text{?}$ 替换为任意小写字母,使得 $t$ 出现次数最大。 | ||
+ | |||
+ | == 题解 == | ||
+ | |||
+ | 不妨设 $|s|=n,|t|=m$。 | ||
+ | |||
+ | 设 $\text{dp}_1(i)$ 表示 $s[1,i]$ 的所有方案中 $t$ 出现的最大次数,$\text{dp}_2(i)$ 表示 $s[1,i]$ 的满足 $s$ 长度为 $m$ 的后缀为 $t$ 的方案中 $t$ 出现的最大次数。 | ||
+ | |||
+ | 首先有 $\text{dp}_1(i)=\max(\text{dp}_1(i-1),\text{dp}_2(i))$,表示 $s[1,i]$ 的长度为 $m$ 的后缀是否为 $t$。 | ||
+ | |||
+ | 接下来考虑 $\text{dp}_2(i)$,显然如果 $s[1,i]$ 的长度为 $m$ 的后缀无法变成 $t$,则 $\text{dp}_2(i)=0$。 | ||
+ | |||
+ | 否则考虑是否存在 $i-m\lt j\lt i$ 使得 $s[1,j]$ 的长度为 $m$ 的后缀为 $t$,如果不存在 $j$,则显然有 $\text{dp}_2(i)=\text{dp}_1(i-m)+1$。 | ||
+ | |||
+ | 如果存在 $j$ 满足上述条件,取最小的 $j$,则一定有 $\text{dp}_2(i)=\text{dp}_2(i-m+j)+1$。对其余所有 $j$,有 $\text{dp}_2(i)\ge\text{dp}_2(i-m+j)+1$。 | ||
+ | |||
+ | 同时对所有满足条件的 $j$,不难发现有 $t[1,m]$ 的长度为 $j$ 的前后缀相同。 | ||
+ | |||
+ | 由于无法判断 $j$ 是否满足上述条件,于是暴力跳 $t$ 的 $f$ 函数枚举每个 $j$ 即可。总时间复杂度 $O(nm)$。 | ||
+ | |||
+ | <hidden 查看代码> | ||
+ | <code cpp> | ||
+ | const int MAXN=1e5+5; | ||
+ | int f[MAXN]; | ||
+ | void get_f(char *s,int n){ | ||
+ | int pos=0; | ||
+ | f[1]=0; | ||
+ | _rep(i,2,n){ | ||
+ | while(pos&&s[i]!=s[pos+1])pos=f[pos]; | ||
+ | if(s[i]==s[pos+1])pos++; | ||
+ | f[i]=pos; | ||
+ | } | ||
+ | } | ||
+ | char s[MAXN],t[MAXN]; | ||
+ | int n,m,dp1[MAXN],dp2[MAXN]; | ||
+ | bool legal(int pos){ | ||
+ | for(int i=pos-m+1,j=1;j<=m;i++,j++){ | ||
+ | if(s[i]!=t[j]&&s[i]!='?')return false; | ||
+ | } | ||
+ | return true; | ||
+ | } | ||
+ | int main() | ||
+ | { | ||
+ | scanf("%s%s",s+1,t+1); | ||
+ | n=strlen(s+1),m=strlen(t+1); | ||
+ | get_f(t,m); | ||
+ | _rep(i,1,n){ | ||
+ | dp1[i]=dp1[i-1]; | ||
+ | if(legal(i)){ | ||
+ | dp2[i]=dp1[i-m]+1; | ||
+ | for(int j=f[m];j;j=f[j]) | ||
+ | dp2[i]=max(dp2[i],dp2[i-m+j]+1); | ||
+ | dp1[i]=max(dp1[i],dp2[i]); | ||
+ | } | ||
+ | } | ||
+ | enter(dp1[n]); | ||
+ | return 0; | ||
+ | } | ||
+ | </code> | ||
+ | </hidden> | ||
+ | |||
+ | ===== Z 函数 ===== | ||
+ | |||
+ | ==== 算法简介 ==== | ||
+ | |||
+ | 给定两个字符串 $S_1,S_2$,查询 $S_1,S_2$ 与 $S_2$ 每个后缀的 $\text{LCP}$。时间复杂度 $O(|S_1|+|S_2|)$。 | ||
+ | |||
+ | ==== 算法实现 ==== | ||
+ | |||
+ | 设 $z(i)$ 表示 $S[1,|S|],S[i,|S|]$ 的 $\text{LCA}$ 长度,先考虑求模式串 $S_2$ 的 $z$ 函数。 | ||
+ | |||
+ | 假设 $i\lt l$ 的 $z(i)$ 已知,考虑求 $z(l)$。直接暴力拓展 $[l,r]$,直到取到 $s_r=s_{r-l+1}$ 的最大值,易知此时 $z(l)=r-l+1$。 | ||
+ | |||
+ | 接下来考虑区间 $i\in [l+1,r]$,首先由于 $s_{r+1}\neq s_{r-l+2}$,于是 $z(i)\le r-i+1$。 | ||
+ | |||
+ | 同时由于 $s[l,r]$ 与 $s[1,r-l+1]$ 完全相同,于是若 $z(i-l+1)\le r-i+1$,显然有 $z(i)=z(i-l+1)$,否则有 $z(i)=r-i+1$。 | ||
+ | |||
+ | 于是可以在 $O(r-l)$ 时间求出 $r-l+1$ 个 $z$ 函数的值,于是求 $z$ 函数的时间复杂度为 $O(|S_2|)$。 | ||
+ | |||
+ | 接下来考虑求 $S_1$ 与 $S_2$ 每个后缀的 $\text{LCP}$,同样可以按上述方法求取,时间复杂度 $O(|S_1|)$。于是总时间复杂度 $O(|S_1|+|S_2|)$。 | ||
+ | |||
+ | <hidden 查看代码> | ||
+ | <code cpp> | ||
+ | const int MAXN=2e7+5; | ||
+ | namespace KMP{ | ||
+ | int z[MAXN],p[MAXN]; | ||
+ | void cmp(char *s1,int n,char *s2,int m){//s下标从1开始 | ||
+ | z[1]=m; | ||
+ | _rep(i,2,m)z[i]=0; | ||
+ | for(int i=2,l=0,r=0;i<=m;i++){ | ||
+ | if(i<=r)z[i]=min(z[i-l+1],r-i+1); | ||
+ | while(i+z[i]<=m&&s2[z[i]+1]==s2[i+z[i]])z[i]++; | ||
+ | if(i+z[i]-1>r)l=i,r=i+z[i]-1; | ||
+ | } | ||
+ | _rep(i,1,n)p[i]=0; | ||
+ | for(int i=1,l=0,r=0;i<=n;i++){ | ||
+ | if(i<=r)p[i]=min(z[i-l+1],r-i+1); | ||
+ | while(i+p[i]<=n&&s2[p[i]+1]==s1[i+p[i]])p[i]++; | ||
+ | if(i+p[i]-1>r)l=i,r=i+p[i]-1; | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | char a[MAXN],b[MAXN]; | ||
+ | int main() | ||
+ | { | ||
+ | gets(a+1);gets(b+1); | ||
+ | int n=strlen(a+1),m=strlen(b+1); | ||
+ | KMP::cmp(a,n,b,m); | ||
+ | LL ans1=0,ans2=0; | ||
+ | _rep(i,1,m) | ||
+ | ans1^=1LL*i*(KMP::z[i]+1); | ||
+ | _rep(i,1,n) | ||
+ | ans2^=1LL*i*(KMP::p[i]+1); | ||
+ | enter(ans1);enter(ans2); | ||
+ | return 0; | ||
+ | } | ||
+ | </code> | ||
+ | </hidden> | ||
+ | |||
+ | ==== 算法练习 ==== | ||
+ | |||
+ | === 习题一 === | ||
+ | |||
+ | [[https://www.luogu.com.cn/problem/CF126B|CF126B]] | ||
+ | |||
+ | == 题意 == | ||
+ | |||
+ | 给定字符串 $S$,求既是 $S$ 的前缀又是 $S$ 的后缀同时又在 $S$ 中间出现过的最长子串。 | ||
+ | |||
+ | == 题解 1 == | ||
+ | |||
+ | 考虑求出每个位置的 $z$ 函数,然后找到与前缀相同的在中间出现过的最长字串,有 $\text{pre}=\max_{1\lt i \lt n}(\min(z(i),n-i))$。 | ||
+ | |||
+ | 接下来从大到小枚举每个与前缀相同的后缀,判定条件为 $i+z(i)-1=n$。如果该后缀长度小于 $\text{pre}$,则得到答案。时间复杂度 $O(|S|)$。 | ||
+ | |||
+ | <hidden 查看代码> | ||
+ | <code cpp> | ||
+ | const int MAXN=1e6+5; | ||
+ | namespace KMP{ | ||
+ | int z[MAXN]; | ||
+ | void get_z(char *s,int n){ | ||
+ | z[1]=n; | ||
+ | _rep(i,2,n)z[i]=0; | ||
+ | for(int i=2,l=0,r=0;i<=n;i++){ | ||
+ | if(i<=r)z[i]=min(z[i-l+1],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; | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | char s[MAXN]; | ||
+ | int main() | ||
+ | { | ||
+ | gets(s+1); | ||
+ | int n=strlen(s+1); | ||
+ | KMP::get_z(s,n); | ||
+ | int pre=0,ans=0; | ||
+ | _for(i,2,n)pre=max(pre,min(KMP::z[i],n-i)); | ||
+ | _rep(i,2,n)if(i+KMP::z[i]-1==n&&KMP::z[i]<=pre){ | ||
+ | ans=KMP::z[i]; | ||
+ | break; | ||
+ | } | ||
+ | if(!ans)puts("Just a legend"); | ||
+ | else | ||
+ | _rep(i,1,ans)putchar(s[i]); | ||
+ | return 0; | ||
+ | } | ||
+ | </code> | ||
+ | </hidden> | ||
+ | |||
+ | == 题解 2 == | ||
+ | |||
+ | 考虑求出每个位置的 $f$ 函数,同样找到与前缀相同的在中间出现过的最长字串,有 $\text{pre}=\max_{1\lt i \lt n}f(i)$。 | ||
+ | |||
+ | 接下来从大到小枚举每个与前缀相同的后缀,初始为 $f(n)$,然后不断迭代 $f$ 即可。如果该后缀长度小于 $\text{pre}$,则得到答案。时间复杂度 $O(|S|)$。 | ||
+ | |||
+ | === 习题二 === | ||
+ | |||
+ | [[https://www.luogu.com.cn/problem/CF432D|CF432D]] | ||
+ | |||
+ | == 题意 == | ||
+ | |||
+ | 给你一个长度为 $n$ 的字符串。定义“完美子串”既是它的前缀也是它的后缀,求“完美子串”的个数且统计这些子串的在字符串中出现的次数。 | ||
+ | |||
+ | == 题解 1 == | ||
+ | |||
+ | 考虑求出每个位置的 $z$ 函数,统计每个前缀出现次数。关于 $z$ 函数统计前缀次数,如果 $s[l,r]=s[1,r-l+1]$,显然有 $s[l,r-1]=s[1,r-l]$。 | ||
+ | |||
+ | 于是可以直接用桶维护 $z$ 函数,然后求后缀和。 | ||
+ | |||
+ | 然后从大到小枚举每个与前缀相同的后缀并记录答案,判定条件为 $i+z(i)-1=n$。时间复杂度 $O(|S|)$。 | ||
+ | |||
+ | <hidden 查看代码> | ||
+ | <code cpp> | ||
+ | const int MAXN=1e5+5; | ||
+ | namespace KMP{ | ||
+ | int z[MAXN]; | ||
+ | void get_z(char *s,int n){ | ||
+ | z[1]=n; | ||
+ | _rep(i,2,n)z[i]=0; | ||
+ | for(int i=2,l=0,r=0;i<=n;i++){ | ||
+ | if(i<=r)z[i]=min(z[i-l+1],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; | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | int c[MAXN]; | ||
+ | char s[MAXN]; | ||
+ | int main() | ||
+ | { | ||
+ | gets(s+1); | ||
+ | int n=strlen(s+1); | ||
+ | KMP::get_z(s,n); | ||
+ | stack<int> s; | ||
+ | _rep(i,1,n){ | ||
+ | if(i+KMP::z[i]-1==n) | ||
+ | s.push(KMP::z[i]); | ||
+ | c[KMP::z[i]]++; | ||
+ | } | ||
+ | for(int i=n;i;i--)c[i-1]+=c[i]; | ||
+ | enter(s.size()); | ||
+ | while(!s.empty()){ | ||
+ | printf("%d %d\n",s.top(),c[s.top()]); | ||
+ | s.pop(); | ||
+ | } | ||
+ | return 0; | ||
+ | } | ||
+ | </code> | ||
+ | </hidden> | ||
+ | |||
+ | == 题解 2 == | ||
+ | |||
+ | 考虑求出每个位置的 $f$ 函数,统计每个前缀出现次数。 | ||
+ | |||
+ | 然后从大到小枚举每个与前缀相同的后缀并记录答案,初始为 $n$,然后不断迭代 $f$ 即可。时间复杂度 $O(|S|)$。 | ||
+ | |||
+ | <hidden 查看代码> | ||
+ | <code cpp> | ||
+ | const int MAXN=1e5+5; | ||
+ | namespace KMP{ | ||
+ | int f[MAXN]; | ||
+ | void get_f(char *s,int n){ | ||
+ | int pos=0; | ||
+ | f[1]=0; | ||
+ | _rep(i,2,n){ | ||
+ | while(pos&&s[i]!=s[pos+1])pos=f[pos]; | ||
+ | if(s[i]==s[pos+1])pos++; | ||
+ | f[i]=pos; | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | char s[MAXN]; | ||
+ | int c[MAXN]; | ||
+ | int main() | ||
+ | { | ||
+ | gets(s+1); | ||
+ | int n=strlen(s+1); | ||
+ | KMP::get_f(s,n); | ||
+ | _rep(i,1,n)c[KMP::f[i]]++; | ||
+ | stack<pair<int,int> >pt; | ||
+ | for(int i=n;i;i--)c[KMP::f[i]]+=c[i]; | ||
+ | for(int i=n;i;i=KMP::f[i]) | ||
+ | pt.push(make_pair(i,c[i]+1)); | ||
+ | enter(pt.size()); | ||
+ | while(!pt.empty()){ | ||
+ | printf("%d %d\n",pt.top().first,pt.top().second); | ||
+ | pt.pop(); | ||
} | } | ||
return 0; | return 0; |