1455:【例题1】Oulipo
典型例题:给出两个字符串s1,s2,求s1在s2中出现多少次
ps.求子串的hash公式
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1e6+10; const int INF=0x3fffffff; typedef long long LL; typedef unsigned long long ull; //字符串hash模板题 char s1[maxn],s2[maxn]; ull h[maxn]; ull power[maxn]; //每一步要乘的 ull b=27,mod=1<<31; //只有大写字母 int main(){ power[0]=1; for(int i=1;i<=1000000;i++) power[i]=power[i-1]*b; int t; scanf("%d",&t); while(t--){ scanf("%s",s1+1); scanf("%s",s2+1); int n=strlen(s1+1); int m=strlen(s2+1); h[0]=0; ull ans=0,s=0; for(int i=1;i<=m;i++){ //算出s2的hash值 每一位 h[i]=(h[i-1]*b+(ull)(s2[i]-'A'+1))%mod; } for(int i=1;i<=n;i++) s=(s*b+(ull)(s1[i]-'A'+1))%mod; //s1的hash值 for(int i=0;i<=m-n;i++){ if(s==h[i+n]-h[i]*power[n]) ans++; } printf("%d\n",ans); } return 0; }
1456:【例题2】图书管理
这道题可以直接用map来做,map<ull,int> a;
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=30010; const int INF=0x3fffffff; typedef long long LL; typedef unsigned long long ull; int n; int mod1=100005; int k1=233317; map<ull,int> a; //用map来做 char op[10],ss[210]; int main(){ scanf("%d",&n); //int num=0; while(n--){ scanf("%s ",op); gets(ss); ull num=0; for(int i=0;i<strlen(ss);i++) //求出hash值 num=(num*k1+(int)ss[i]); if(op[0]=='f'){ if(a[num]==1) printf("yes\n"); else printf("no\n"); } else a[num]=1; } return 0; }
1457:Power Strings
给定若干个长度 ≤106 的字符串,询问每个字符串最多是由多少个相同的子字符串重复连接而成的。如:ababab 则最多有 3 个 ab 连接而成。
abcd 1 aaaa 4 ababab 3 .
询问每个字符串最多是由多少个相同的子字符串重复连接而成的 求最短重复子序列(这样才能保证最多嘛
这道题试问我们一个串有几个循环节。循环节就是指相等的(真)前缀和(真)后缀的个数。我们知道,
kmp过程中的next[i]是这个意义:0-i-1位中相等的真前后缀个数。那么next[len]就是指0-len-1位中相等的真前后缀个数。
所以第一种做法就是,求next数组
所以要的最短重复子序列就是len-next[len]
if(len%(len-next[len])==0) printf("%d\n",len/(len-next[len]));
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1e6+10; const int INF=0x3fffffff; typedef long long LL; //询问每个字符串最多是由多少个相同的子字符串重复连接而成的 求最短重复子序列 //这道题试问我们一个串有几个循环节。循环节就是指相等的(真)前缀和(真)后缀的个数。我们知道, //kmp过程中的next[i]是这个意义:0-i-1位中相等的真前后缀个数。那么next[len]就是指0-len-1位中相等的真前后缀个数。 char s[maxn]; int next[maxn],len; void getnext(){ int j=-1; int i=0; next[0]=-1; while(i<len){ if(j==-1||s[i]==s[j]){ i++;j++; next[i]=j; } else j=next[j]; } } int main(){ while(1){ scanf("%s",s); if(s[0]=='.') break; memset(next,0,sizeof(next)); len=strlen(s); getnext(); if(len%(len-next[len])==0) printf("%d\n",len/(len-next[len])); else printf("1\n"); } return 0; }
第二种做法:普通做法,hash,判断连续k个字母的hash值是不是都是一样的,求子串的hash值
bool check(ull u,int k),分别是对应的hash值,还有就是这个重复子串的长度
//普通做法 #include<stdio.h> #include<string.h> using namespace std; const int maxn = 1e6+10; typedef unsigned long long ull; char s1[maxn]; ull power[maxn],h[maxn]; ull n,s,base=131,mod=1<<31; int check(ull v,int k){ for( ull i=0; i<n; i+=k ){ if(h[i+k]-h[i]*power[k]!=v) return 0; //计算字串的hash值 } return 1; } int main(){ power[0]=1; for( int i=1; i<=101000; i++ ) power[i]=power[i-1]*base; while(scanf("%s",s1+1)){ if(s1[1]=='.') break; n=strlen(s1+1); h[0]=0; for( int i=1; i<=n; i++ ) h[i]=h[i-1]*base+(ull)(s1[i]-'A'+1); for( int i=1; i<=n; i++ ){ if(n%i==0){ if(check(h[i],i)==1){ printf("%d\n",n/i);break; } } } } }
1458:Seek the Name, Seek the Fame
给定若干字符串(这些字符串总长 ≤4×105 ),在每个字符串中求出所有既是前缀又是后缀的子串长度。
例如:ababcababababcabab,既是前缀又是后缀的:ab,abab,ababcabab,ababcababababcabab。
前后缀:肯定就想到了next数组
这道题不需要重复求出next数组,只需要不断向前移动next
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=4e5+10; const int INF=0x3fffffff; typedef long long LL; typedef unsigned long long ull; //又是next数组? char s[maxn]; int next[maxn]; int len=0; void getnext(){ int i=0,j=-1; next[0]=-1; while(i<len){ if(j==-1||s[i]==s[j]) { i++;j++; next[i]=j; } else j=next[j]; } } int op[maxn]; int main(){ while(~scanf("%s",s)){ memset(next,0,sizeof(next)); len=strlen(s); getnext(); int num=0; memset(op,0,sizeof(op)); //不用循环做next数组,直接取呀!!! for(int i=len;next[i]!=-1;){ op[++num]=i; //len也写进来了 i=next[i]; } for(int i=num;i>=1;i--) printf("%d ",op[i]); printf("\n"); } return 0; }
1459:friends
根据题目意思,我们可以先求出字符串的HASH值,再枚举插入字符的位置然后判断字符串去掉这个字符后,用HASH计算两串是否相等的办法,判断能否分成两个相同部分。
分为三种情况:插在左边、插在右边、插在中间
然后计算三种情况下的hash值,看去掉当前这个字符,两边的hash值是不是相等
重点是怎样求中间空一个位置的串的hash值
#include<cmath> #include<cstdio> #include<cstring> #include<iostream> #include<algorithm> using namespace std; typedef unsigned long long ll; char s[2100000]; ll len,ans,sum,x,b=1e9+7,p[2100000],a[2100000]; int main() { scanf("%lld%s",&len,s+1); if(len%2==0) { printf("NOT POSSIBLE\n"); return 0; } p[0]=1; a[0]=ans=0; for(int i=1;i<=len+10;i++) p[i]=p[i-1]*b; for(int i=1;i<=len;i++) a[i]=a[i-1]*b+s[i]; for(int i=1;i<=len;i++) { bool bk=false; if(i<(len+1)/2) { if(a[i-1]*p[(len+1)/2-i]+a[(len+1)/2]-a[i]*p[(len+1)/2-i] ==a[len]-a[(len+1)/2]*p[len/2]) sum=a[len]-a[(len+1)/2]*p[len/2],x=i,bk=true; } else if(i>(len+1)/2) { if(a[len/2] ==(a[i-1]-a[len/2]*p[i-1-len/2])*p[len-i]+a[len]-a[i]*p[len-i]) sum=a[len/2],x=i,bk=true; } else if(i==(len+1)/2) { if(a[len/2]==a[len]-a[(len+1)/2]*p[len-i]) sum=a[len/2],x=i,bk=true; } if(ans&&bk) { if(ans!=sum) { printf("NOT UNIQUE\n"); return 0; } } else ans=sum; } if(!ans) printf("NOT POSSIBLE\n"); else { for(int i=1;i<=len/2;i++) { if(i<x) printf("%c",s[i]); else printf("%c",s[i+1]); } printf("\n"); } return 0; }
1460:A Horrible Poem
这道题也是求最短循环节的,但是有两个点会超时(怎么过的
P3538 [POI2012]OKR-A Horrible Poem - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
1、循环节一定是长度的约数
2、如果n是一个循环节,那么k*n也必定是一个循环节(关键所在)
3、n是[l,r]这一段的循环节 的充要条件是 [l,r-n]和[l+n,r]相同(利用这个性质我们在判断是否为循环节是可以做到O(1))
所以我们可以在求出这个区间的长度之后,判断它的每个约数是否是循环节(应用性质3),并且因为性质1,它的约数是循环节,原串一定也是。
所以只要不断除以质因数(相当于从大到小枚举约数),缩小L的长度,最后就是最小的长度。
而一个重要的点在于,用上面方法来枚举约数是为了避免tle
在求它的质因数的时候,可以通过线性筛的过程求得,将时间法度由根号n 降为logn。
注意这里枚举到全长。
这样通过nxt数组不断回跳,就可以找出所有质因数(多次乘方的也可以)
判定循环节的时候可以使用hash。
#include<cstdio> #include<cmath> #include<algorithm> #include<cstring> #define seed 13131 #define maxn 500005 using namespace std; typedef unsigned long long ull; char s[500005]; ull s1[500005]; ull ss[500005]; int sushu[500005]; int used[500005]; int nxt[500005]; int k; int ys[500005]; void pri() { for(int i = 2;i<= maxn;i++) { if(used[i] == 0) { sushu[++k] = i; nxt[i] = i; } for(int j = 1;j <= k && (long long)i*sushu[j]<= maxn;j++) { used[i*sushu[j]] = 1; nxt[i*sushu[j]] = sushu[j]; if(i%sushu[j] == 0) { break; } } } } int check(int l1,int r1,int l2,int r2) { if(s1[r1]-ss[r1-l1+1]*s1[l1-1] == s1[r2]-ss[r2-l2+1]*s1[l2-1]) { return 1; } return 0; } int main() { int n; scanf("%d", &n); scanf("%s",s+1); int q; scanf("%d", &q); for(int i = 1;i <= n;i++) { s1[i] = s1[i-1]*seed+s[i]-'a'+1; } ss[0] = 1; for(int i = 1;i <= n;i++) { ss[i] = ss[i-1]*seed; } pri(); for(int i = 1;i <= q;i++) { int l,r; scanf("%d%d", &l, &r); int len = r-l+1; int tt = 0; while(len != 1) { ys[++tt] = nxt[len]; len = len/nxt[len]; } len = r-l+1; for(int j = 1;j <= tt;j++) { int t = len/ys[j]; if(check(l,r-t,l+t,r) == 1) { len = t; } } printf("%d\n",len); } return 0; }
1461:Beads
看怎样除这个,能获得多种类型的子串
有一点要注意,就是正反串的hash值,1 2 3 和3 2 1是一样的,所以要正反都求一次hash值,用map来判断,但是有一点,应该是用h1*h2的值作为map的键值
不然一个map存正向的,一个map存反向的,会超时
可以想到存储格式:map
正向的子串hash:suml[j]-suml[j-i]*p[i];
反向的子串hash:sumr[j-i+1]-sumr[j+1]*p[i]
#include <bits/stdc++.h> typedef unsigned long long ll; using namespace std; int n,a[200005],ans[200005],total=1; ll p[200005],suml[200005],sumr[200005],b=1000000007; map<ll,int>mp; int main() { ios::sync_with_stdio(0); cin.tie(0); int i,j,c=0,maxn=1; cin>>n; p[0]=1; for(i=1;i<=n;i++) { cin>>a[i]; p[i]=p[i-1]*b,suml[i]=suml[i-1]*b+a[i]; } for(i=n;i>=1;i--) { sumr[i]=sumr[i+1]*b+a[i]; } for(i=1;i<=n;i++) /**< i用来表示字串长度,最大为n,不可以用n/2*/ { mp.clear(); c=0; for(j=i;j<=n;j+=i) { ll h1=suml[j]-suml[j-i]*p[i]; ll h2=sumr[j-i+1]-sumr[j+1]*p[i]; ll h3=h1*h2; /**< 正反认为同一个串,所以如果两串相同, 正向hash值h1和反向hash值h2的乘积的值必然一样 */ if(!mp[h3]) { c++; mp[h3]=1; } } if(c>maxn) { maxn=c; total=1; ans[total++]=i; } else if(c==maxn) { ans[total++]=i; } } cout<<maxn<<' '<<total-1<<endl; for(i=1;i<total;i++) cout<<ans[i]<<' '; return 0; }
1462:Antisymmetry
0\1取反和对称操作的前后顺序显然不影响,先考虑对称操作再考虑取反,于是可以发现子串长度显然是偶数而且关于对称轴0/1对称(一边为0则另一边为1)。
算法1: 枚举对称轴,二分+hash。时间复杂度O(nlog(n))。
#include<cstdio> #include<cmath> #include<algorithm> #include<cstring> #define seed 13131 #define maxn 500010 #define LL long long using namespace std; typedef unsigned long long ull; char s[maxn]; int a[maxn],b[maxn],n; LL ha[maxn],hb[maxn],p[maxn]; //原串和原串的hash 取反后的串和hash const int md=1e9+7; LL geta(int l,int r){ //原串的子串值 return (ha[r]-ha[l-1]*p[r-l+1]%md+md)%md; } LL getb(int l,int r){ return (hb[l]-hb[r+1]*p[r-l+1]%md+md)%md; } int query(int x){ int l=1,r=n/2; while(l<=r){ int mid=(l+r)>>1; if(mid>=1&&x+mid<=n&&geta(x-mid+1,x+mid)==getb(x-mid+1,x+mid)) l=mid+1; else r=mid-1; } return r; } int main(){ scanf("%d",&n); scanf("%s",s+1); for(int i=1;i<=n;i++) a[i]=s[i]-'0'; for(int i=1;i<=n;i++) b[i]=1-a[i]; ha[0]=1;hb[n+1]=1;p[0]=1; for(int i=1;i<=n;i++) ha[i]=(ha[i-1]*seed%md+a[i])%md,p[i]=p[i-1]*seed%md; for(int i=n;i>=1;i--) hb[i]=(hb[i+1]*seed%md+b[i])%md; LL ans=0; for(int i=1;i<=n;i++){ ans+=query(i); } printf("%lld\n",ans); return 0; }
算法2: manacher算法,O(n)的时间算出所有子串最长回文子串长度(此题改下判断条件就好)。
!!!!怎么想到马拉车算法的?因为要旋转,先不考虑取反,如果连在一起是对称的话,就说明是反的,所以想到了马拉车算法https://blog.csdn.net/yanghongMO/article/details/52673305
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn= 500010; const int INF=0x3fffffff; typedef long long LL; typedef unsigned long long ull; //先全部反转,两个串都求hash,然后比较子串,哪些两个hash值相等 int n; /* 0\1取反和对称操作的前后顺序显然不影响,先考虑对称操作再考虑取反,于是可以发现子串长度显然是偶数而且关于对称轴0/1对称(一边为0则另一边为1)。 算法1: 枚举对称轴,二分+hash。时间复杂度O(nlog(n))。 算法2: manacher算法,O(n)的时间算出所有子串最长回文子串长度(此题改下判断条件就好)。 */ //理解马拉车算法呀 //https://blog.csdn.net/yanghongMO/article/details/52673305 char c[maxn*2]; char s[maxn]; int len[maxn*2]; int main(){ scanf("%d %s",&n,s+1); c[0]='$'; for(int i=1;i<=n;i++){ c[i*2-1]='#'; c[i*2]=s[i]; } c[2*n+1]='#'; c[2*n+2]='$'; int mx=0,po=0; ull ans=0; for(int i=1;i<=2*n;i+=2){ if(mx>i) len[i]=min(mx-i,len[2*po-i]); else len[i]=1; while((c[i+len[i]]==c[i-len[i]]&&c[i+len[i]]=='#')||(c[i-len[i]]-'0'+c[i+len[i]]-'0'==1)) len[i]++; //反对称的要求就是从中间向两边扩展的每两个数相加都是1 //由于我们需要的是长度为偶数的回文串,所以只需考虑 ‘#’ 的位置(就是Manacher添加的那些字符). if(len[i]+i>mx){ mx=len[i]+i; po=i; } ans+=len[i]/2; } printf("%lld\n",ans); return 0; }
1463:门票
有三种方法,第一种会超时
(1)第一种就是用一个散列表,如果存在,就输出,不存在就保存
注意这个存储方法很像链式前向星
#include<cstdio> #include<vector> #include<cstring> using namespace std; const int P=100003; int a,b,c,x=1,s,last[P]; struct node { int x,pre; }; vector<node>f; //这里用vector是想优化一下空间,事实上数组即可 //有一个点通不过 //https://blog.csdn.net/bcr_233/article/details/100167093 int find() { for (int i=last[s]; ~i; i=f[i].pre) if (f[i].x==x) return 1; return 0; } int main() { scanf("%d%d%d",&a,&b,&c); memset(last,-1,sizeof last); f.push_back({1,-1}); last[1]=0; for (int i=1; i<=2000000; ++i) { x=(1ll*a*x+x%b)%c; s=x%P; if (find()) return printf("%d\n",i),0; f.push_back({x,last[s]}); last[s]=f.size()-1; } puts("-1"); return 0; }
(2)双hash,过了
双哈希一般是不会被卡掉的,直接认为是正确的就好了
不过这种做法空间是线性的,仍然需要占用 2MB+ 的空间,事实上我们有更优的解法。
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=2e6; const int INF=0x3fffffff; typedef long long LL; //双hash //双哈希一般是不会被卡掉的,直接认为是正确的就好了 //不过这种做法空间是线性的,仍然需要占用 2MB+ 的空间,事实上我们有更优的解法。 const int P1 =249439; const int P2 = 414977; int a,b,c; int vis1[P1],vis2[P2]; int p1,p2; int main(){ scanf("%d %d %d",&a,&b,&c); p1=1; p2=1; vis1[1]=1;vis2[1]=1; for(int i=1;i<=maxn;i++){ p1=(1ll*p1*a+p1%b)%c; p2=(1ll*p2*a+p2%b)%c; int m1=p1%P1; int m2=p2%P2; if(vis1[m1]&&vis1[m1]==vis2[m2]){ printf("%d\n",i); return 0; } if(!vis1[m1]) vis1[m1]=i; if(!vis2[m2]) vis2[m2]=i; //是同一个数产生的 } printf("-1\n"); return 0; }
(3)数学方法
观察数列的递推式可以看出,这个数列是存在循环节的。
//首先找到循环节长度,然后开始找循环开始的地方
//也就是循环节的起点,
根据题目要求,循环节长度不能超过 2e6,所以 a2e6一定在这个循环里面。
于是我们找到第一个和 a2e6等的元素,它们之间的距离就是循环节的长度 len
如果没有找到则说明循环节长度大于2e6,此时输出-1
于是我们可以分别从 a0和 alen开始同时往后计算,找到的第一对相等元素即是答案。
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=2e6; const int INF=0x3fffffff; typedef long long LL; //数学做法 //观察数列的递推式可以看出,这个数列是存在循环节的。 //首先找到循环节长度,然后开始找循环开始的地方 //也就是循环节的起点, /* 根据题目要求,循环节长度不能超过 2e6,所以 a22e6一定在这个循环里面。 于是我们找到第一个和 a2e6等的元素,它们之间的距离就是循环节的长度 len 如果没有找到则说明循环节长度大于2e6,此时输出-1 于是我们可以分别从 a0和 alen开始同时往后计算,找到的第一对相等元素即是答案。 */ int a,b,c; int s1=1,s2=1; int len=0; //循环节长度 int main(){ scanf("%d %d %d",&a,&b,&c); //首先计算a[inf] for(int i=1;i<=maxn;i++){ s1=(1ll*s1*a+s1%b)%c; } if(s1==1){ //首先判断a[0]是否与a[inf]相等 len=INF; } for(int i=1;i<maxn;i++){ //注意这里不能取等号 s2=(1ll*s2*a+s2%b)%c; if(s1==s2){ len=maxn-i; //记录两元素的距离 } } //这样更新以后len就是最短循环节长度 //循环结束后,len更新至最小值,即是循环节长度 if(!len){ printf("-1\n");return 0; //没有更新len,说明循环节长度大于inf } s1=s2=1; for(int i=1;i<=len;i++) s2=(1ll*s2*a+s2%b)%c; //计算a[len]的时候的值 for(int i=0;i<=maxn;i++){ if(s1==s2){ printf("%d",len+i); return 0; } s1=(1ll*s1*a+s1%b)%c; //分别从0、len开始算,直到算到相等的地方,这里就是循环开始的地方 s2=(1ll*s2*a+s2%b)%c; } printf("-1"); return 0; }
1464:收集雪花
用map就可以了,有点像尺取法
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1e6+10; const int INF=0x3fffffff; typedef long long LL; typedef unsigned long long ull; //可以直接用map //我这个连题目都能读错的混账 int n; int a[maxn],l=1,r; map<int,int> mp; //判断存不存在 int main(){ scanf("%d",&n); int len=0; for(r=1;r<=n;r++){ scanf("%d",&a[r]); if(mp.find(a[r])!=mp.end()){ //存在 while(a[l]!=a[r]) mp.erase(mp.find(a[l++])); //一直删掉直到相同,然后也把相同的删掉 l++; } else mp[a[r]]=1; len=max(len,r-l+1); //记录最长的种类数 } printf("%d",len); return 0; }