模板汇总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 }
View Code

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 }
View Code

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 }
View Code

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 }
View Code

⑤Trie的一些拓展

显然那个最基本的操作完全不够用,甚至那个模板题都不是最基本的操作

那就先说那个模板题吧:如何将一个串的所有子串插入Trie?

显然只需要插入所有后缀,因为Trie是根据前缀建出来的

然后查询最长公共前缀/后缀(反过来做)

直接Trie上倍增找LCA,然后前面那段显然就是一样的,是在线的,单纯查询的复杂度是$O(\log len)$

最后是把串按字典序排序后输出,直接在Trie上跑就完了

posted @ 2018-09-18 17:30  Speranza_Leaf  阅读(217)  评论(0)    收藏  举报