模板汇总4.1_字符串1
1.Hash
①Hash是什么
仅针对信息学竞赛来说,你可以将Hash看做是一个由字符串到整型的映射(比较好懂),常常用来判断两个字符串是否相同
②Hash的原理
Hash会将一个字符串转化为一个多于$k$进制的数,其中$k$为字符串中出现的字符的种类。
具体来说就是将字符串的每一位字符$c_1,c_2,c_3...c_n$分别乘上$k$的$0,1,2...n-1$次方,这样就将字符串转换成了一个$k$进制的整型,假如你的字符串里只有$26$个小写字母,你就可以把它们分别用$1$到$26$的整数代表。注意:不要将任何一个字符用$0$来代表,这样相当于丢失了信息,这也就是为什么要转化成一个多于$k$进制的数。
那么问题来了,如果这个字符串非常长,那么这个数很快就会存不下了,所以你需要将这个数对一个大数取模来存,为什么是大数呢?根据鸽巢原理,你的Hash在字符串数量达到$sqrt(n)$的级别时就会有很大的概率出锅,即两个不一样的字符串被映射到了一个整型上,这叫做Hash冲突,所以要选大数。
问题又来了:如果你的进制数和这个模数有不为$1$的公因子,你的Hash在取模时又会出锅,所以我们可以直接把模数取成一个大质数,进制数也取成质数,这样比较稳妥。还有一种风险很小的Hash方法:双Hash,即对两个质数取模,分别存下来Hash,判断时也用两个Hash判断。
另外,如果数据随机,你可以直接利用无符号整型的自然溢出取模,速度很快,当然出题人也可以很容易卡掉你。。。总体来说,我还是建议你写双哈希,基本没有锅,我常用的两个模数就是$1e9+7$和$2^{31}-1$,常见并不要紧,重要的是模数的大小;进制数的话我一般用$131$和$233$。不用大质数的原因一是在你对Hash做处理时常数会比较大,二是求子串哈希(见下面)可能会乘爆。。。
注意:$C++11$中有一个hash函数,所以在为有关Hash的函数和变量命名时不要直接写成hash
③Hash的时间复杂度
时间复杂度:$O(len)$
④Hash的具体实现
1 const long long mod1=1e9+7,mod2=(1ll<<31)-1; 2 void Hsh() 3 { 4 scanf("%s",s); int L=strlen(s); 5 for(int i=1;i<=L;i++) num[i]=rd[i-1]-'a'+1; 6 for(int i=1;i<=L;i++) hah1[i]=(hah1[i-1]*bas+num[i])%mod1; 7 for(int i=1;i<=L;i++) hah2[i]=(hah2[i-1]*bas+num[i])%mod2; 8 }
⑤Hash的$^{*1}$一些扩展内容
(*1:事实上这个对于Hash的实际应用很重要)
1.如何$O(1)$获取子串的Hash?直接上结论:
$Hash[l,r]=((Hash[1,r]-Hash[1,l-1]*base^{r-l+1})\%mod+mod)\%mod$
如果需要反复对子串求解$Hash$值,最好预处理$base$
的次幂
另外的,如何获取一个串删掉一位后的Hash?
$Hash_{del_i}=Hash[1,i-1]*bas^{len-i}+Hash[i+1,len]$
2.哈希表
均摊复杂度$O(1)$来查询一个哈希值是否出现过(改一改可以查询出现过几次之类的)
其实现类似于链表,你需要一个大小可以存下的模数作为表头,然后每次把哈希值对这个表头取模,顺着这条链一直走下去,直到找到一个空位置(插入)或者发现已经有这个哈希值的节点了(返回)。
我的写法是做两个哈希,一个用来匹配表头,一个是真正用来判断的哈希
放道用哈希表的题的代码
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 const int N=200050,M=2500; 6 const long long bs=1009,md=2333; 7 const long long bas=203339,mod=2147483647; 8 long long num[N],hah[N][2],huh[N][2],pw[N][2],val[N]; 9 int p[M],nxt[N],outp[N]; 10 int n,pos,cnt,ans; 11 long long Ghash(int l,int r,int t) 12 { 13 long long mdd=t?mod:md; 14 long long h1=((hah[r][t]-hah[l-1][t]*pw[r-l+1][t])%mdd+mdd)%mdd; 15 long long h2=((huh[l][t]-huh[r+1][t]*pw[r-l+1][t])%mdd+mdd)%mdd; 16 return max(h1,h2); 17 } 18 bool Fhash(long long has,long long hsh) 19 { 20 for(int i=p[has%md];i;i=nxt[i]) 21 if(val[i]==hsh) return true; 22 return false; 23 } 24 void Ihash(long long has,long long hsh) 25 { 26 if(Fhash(has,hsh)) return ; 27 nxt[++cnt]=p[pos=has%md]; 28 val[cnt]=hsh,p[pos]=cnt; 29 } 30 int main () 31 { 32 scanf("%d",&n),pw[0][0]=pw[0][1]=1; 33 for(int i=1;i<=n;i++) 34 { 35 scanf("%lld",&num[i]); 36 pw[i][0]=pw[i-1][0]*bs%md; 37 pw[i][1]=pw[i-1][1]*bas%mod; 38 } 39 for(int i=1;i<=n;i++) 40 { 41 hah[i][0]=(hah[i-1][0]*bs+num[i])%md; 42 hah[i][1]=(hah[i-1][1]*bas+num[i])%mod; 43 } 44 for(int i=n;i;i--) 45 { 46 huh[i][0]=(huh[i+1][0]*bs+num[i])%md; 47 huh[i][1]=(huh[i+1][1]*bas+num[i])%mod; 48 } 49 for(int i=1;i<=n;i++) 50 { 51 memset(p,0,sizeof p); cnt=0; 52 for(int j=1;j<=n-i+1;j+=i) 53 Ihash(Ghash(j,j+i-1,0),Ghash(j,j+i-1,1)); 54 if(cnt>ans) ans=cnt,outp[outp[0]=1]=i; 55 else if(cnt==ans) outp[++outp[0]]=i; 56 } 57 printf("%d %d\n",ans,outp[0]); 58 for(int i=1;i<=outp[0];i++) 59 printf("%d ",outp[i]); 60 return 0; 61 }
2.KMP字符串匹配
①KMP是什么
一种字符串算法,可以在$O(n)$时间内求出子串在母串中出现的位置。(你说模式串和文本串也行= =)
②KMP的原理
比起纯暴力匹配,KMP引入了一个很关键的东西,$next$数组(写的时候不要写成$next$,C++11会CE),$next[i]$表示在子串到$i$位置最长的前缀后缀相同的位置,提示匹配失败时要“跳”到子串的哪个位置去匹配,这样就避免了重新枚举匹配。所谓“跳”其实是子串的快速移动,举个例子
S----kaakakak
s----kak
匹配到第$3$位时匹配失败,假设我们已经求出了$next$数组,我们可以借助它直接把子串前两位移动到母串第四五位匹配
具体说来你要这样做:
找一个“指针”p,先对子串自己进行处理求$next$数组。一遍扫过子串(第一位可以不管),在指针p所指(子串)字符与当前(子串)字符不匹配时不断用$next[p]$更新p,最终若指针p所指字符与当前字符匹配后用++p更新$next[i+1]$,若无法匹配则令$next[i+1]=0$
之后扫一遍母串,仍然先在指针p所指(子串)字符与当前(母串)字符不匹配时不断用$next[p]$更新p。更新后判断是否匹配,若匹配则令p++。之后判断是否完成匹配即可,出现位置即$i-l+2$
③KMP的时间复杂度
时间复杂度:$O(n)$
④KMP的具体实现
1 #include<cstdio> 2 #include<cstring> 3 char A[n],a[n]; 4 int nxt[n]; 5 int main () 6 { 7 scanf("%s%s",A,a); 8 int L=strlen(A),l=strlen(a); 9 for(int i=1,p=0;i<l;i++) 10 { 11 while(p&&a[i]!=a[p]) p=nxt[p]; 12 nxt[i+1]=(a[i]==a[p])?++p:0; 13 } 14 for(int i=0,p=0;i<L;i++) 15 { 16 while(p&&A[i]!=a[p]) p=nxt[p]; 17 if(A[i]==a[p]) p++; 18 if(p==l&&i-l+2) printf("%d\n",i-l+2); 19 } 20 for(int i=1;i<=l;i++) 21 printf("%d ",nxt[i]); 22 return 0; 23 }
3.manacher算法
①manacher是什么
manacher算法可以在$O(n)$时间内求出串中的最长回文子串,同时会产生一个副产品:以各个位置为中心的最长回文半径。
②manacher的原理
朴素的回文串算法是怎么求回文串的呢?
扫一遍,在每个位置尝试两边同时扩展?不完全对,因为型如"2332"这样的回文串回文中心不在字符上,麻烦。。。
manacher是如何做的呢?
manacher算法会先进行一次预处理,在字符串头尾和每个字符间插入一个字符(只要是串里没出现过的都可以),这样以插入字符为中心匹配就能解决"2332"这样的问题了。
但是为什么说朴素算法低效呢?因为朴素算法中子串们被重复使用,每次都要一位位地挪太麻烦了,manacher是这样解决这个问题的:manacher维护一个“当前最右点”和最右点所对应的中心,匹配的时候先搞出来$r[i]$的最小值然后再暴力,可以证明本质不同的回文串最多有$n$个,然后复杂度$O(n)$
③manacher的时间复杂度
时间复杂度:$O(n)$
④manacher算法的具体实现
当$maxr$变化时说明出现了新的本质不同的回文串
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 const int N=22000020; 6 int len,maxr,mid,ans; 7 char s[N],S[N]; int r[N]; 8 int main () 9 { 10 scanf("%s",s+1),len=2*strlen(s+1)+1; 11 for(int i=1;i<=len;i++) 12 S[i]=(i&1)?'?':s[i>>1]; 13 S[0]='>',S[len+1]='<'; 14 for(int i=1;i<=len;i++) 15 { 16 r[i]=(maxr>=i)?min(r[2*mid-i],maxr-i+1):1; 17 while(S[i-r[i]]==S[i+r[i]]) r[i]++; 18 if(i+r[i]-1>maxr) maxr=i+r[i]-1,mid=i; 19 } 20 for(int i=1;i<=len;i++) 21 ans=max(ans,r[i]); 22 printf("%d",ans-1); 23 return 0; 24 }
4.Trie
①Trie是什么
一种数据结构,基本操作是支持对字符串的$O(len)$插入和查找
②Trie的原理
非常暴力,首先有一个根节点,然后每个节点挂着字符集大小个儿子,插入时从根节点出发看看当前节点有没有当前字母这个儿子,没有就新建,然后一路走下去即可,查询同理
③Trie的复杂度
时间复杂度$O(len)$
所需空间为$O(len*$字符集$)$
④具体实现
模板题 CF1070 BerOS File Suggestion
1 #include<cstdio> 2 #include<cctype> 3 #include<cstring> 4 #include<algorithm> 5 using namespace std; 6 const int N=10005; 7 char rd[10],str[N*28][10]; 8 int n,m,tot,len,nde,ans,flag; 9 int cnt[N*28],mark[N*28],trie[N*28][37]; 10 int turn(char ch) 11 { 12 if(isdigit(ch)) return ch-'0'; 13 else if(ch>='a'&&ch<='z') 14 return ch-'a'+10; 15 else return 36; 16 } 17 int main() 18 { 19 register int i,j,k,h; 20 scanf("%d",&n); 21 for(i=1;i<=n;i++) 22 { 23 scanf("%s",rd+1); 24 len=strlen(rd+1); 25 for(j=len;j;j--) 26 { 27 nde=0; 28 for(k=j;k<=len;k++) 29 { 30 int ch=turn(rd[k]); 31 if(!trie[nde][ch]) 32 trie[nde][ch]=++tot; 33 nde=trie[nde][ch]; 34 if(mark[nde]!=i) 35 { 36 cnt[nde]++,mark[nde]=i; 37 memset(str[nde],0,sizeof str[nde]); 38 for(h=1;h<=len;h++) str[nde][h]=rd[h]; 39 } 40 } 41 } 42 } 43 scanf("%d",&m); 44 for(i=1;i<=m;i++) 45 { 46 scanf("%s",rd+1); 47 len=strlen(rd+1),flag=true,nde=0; 48 for(j=1;j<=len;j++) 49 { 50 int ch=turn(rd[j]); 51 if(!trie[nde][ch]) 52 {flag=0; break;} 53 nde=trie[nde][ch]; 54 } 55 flag?printf("%d %s\n",cnt[nde],str[nde]+1):printf("0 -\n"); 56 } 57 return 0; 58 }
⑤Trie的一些拓展
显然那个最基本的操作完全不够用,甚至那个模板题都不是最基本的操作
那就先说那个模板题吧:如何将一个串的所有子串插入Trie?
显然只需要插入所有后缀,因为Trie是根据前缀建出来的
然后查询最长公共前缀/后缀(反过来做)
直接Trie上倍增找LCA,然后前面那段显然就是一样的,是在线的,单纯查询的复杂度是$O(\log len)$
最后是把串按字典序排序后输出,直接在Trie上跑就完了

浙公网安备 33010602011771号