一、kmp
KMP算法,模式匹配算法,能够在线性时间内判定A[1-N]是否为B[1-M]的子串,并求出字符串A在字符串B中各次出现的位置
1.对A进行自我匹配,求出一个数组next,其中next[i]表示A中以i结尾的非前缀子串与A的前缀能够匹配的最长长度
即next[i]=max(j),其中j<i且A[i-j+1,i]=A[1,j]
2.对字符串A与B进行匹配,求出一个数组f,其中f[i]表示B中以i结尾的子串与A的前缀能够匹配的最长长度。
即f[i]=max(j),其中j<i且B[i-j+1,i]=A[1,j]
next数组的求法:
1.初始化next[1]=j=0,假设next[i,i-1]已求出,下面求解next[i]。
2.不断尝试扩展匹配长度j,如果扩展失败(下一个字符不相等),令j变为next[j],直至j为0(应该从头开始匹配)。
如果能够匹配成功,next[i]的长度就增加1。next[i]的值就是j
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 next[1]=0; 2 for(int i=2,j=0;i<=n;i++){ 3 while(j>0&&a[i]!=a[j+1]) j=next[j]; 4 if(a[i]==a[j+1]) j++; 5 next[i]=j; 6 } 7 for(int i=1,j=0;i<=m;i++){ 8 while(j>0&&(j==n||b[i]!=a[j+1])) j=next[j]; 9 if(b[i]==a[j+1]) j++; 10 f[i]=j; 11 //if(f[i]==n) 12 }
二、hash
快速判断两个字符串是否相等。
hsh[i]=(hsh[i-1]*k+str[i])%p(k进制,对p取模)
k可取|Σ|,或者取31,131,13131等数字。 p最好取较大的质数。
(998244353 23333333333333333 16个3) 如果不放心,可多取几个大质数p分别取模判断。
已知串ai 的每一个前缀6、的hash 值hi,求子串[l…r] 的hash 值
hash = (hr – hl-1 * b^r-l+1)mod p
已知串A 和串B 分别的Hash 值, 求串AB 的Hash 值。
Hash(AB) = Hash(A)*|b||B| + Hash(B)
Hash(“abcde”) = Hash(“abc”) *10^2 + Hash(“de”)
应用:最长公共前缀,比较字典序
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include<bits/stdc++.h> 2 using namespace std; 3 const int N=1005; 4 const int base=131; 5 const int mod=1e9+7; 6 char s[N],s1[N]; 7 unsigned long long hash[N],hash1[N],bin[N]; 8 unsigned long long cal(int l,int r) { 9 return (hash[r]-hash[l]*bin[r-l+1]%mod)%mod; 10 } 11 unsigned long long cal1(int l,int r) { 12 return (hash1[r]-hash1[l]*bin[r-l+1]%mod)%mod; 13 } 14 int len,len1,all,LCP; 15 int lcp() { 16 int l=0,r=min(len1,len),ans=0; 17 while(l<=r) { 18 int mid=(l+r)>>1; 19 if(cal(1,mid)==cal1(1,mid)) { 20 ans=mid; 21 l=mid+1; 22 } else r=mid-1; 23 } 24 return ans; 25 } 26 //inline int find(int l,int r,int f1,int f2) { 27 // if(l+1>=r) { 28 // if(get_hash(f1,f1+r-1)==get_hash(f2,f2+r-1)) return r; 29 // else return l; 30 // } 31 // int mid=(l+r)>>1; 32 // if(get_hash(f1,f1+mid-1)==get_hash(f2,f2+mid-1)) return find(mid,r,f1,f2); 33 // else return find(l,mid-1,f1,f2); 34 //} 35 void gethash() { 36 bin[0]=1; 37 for(int i=1; i<=all; i++) { 38 bin[i]=bin[i-1]*base; 39 } 40 for(int i=1; i<=len; i++) { 41 hash[i]=(hash[i-1]*base+s[i]-'a'+1)%mod; 42 } 43 for(int i=1; i<=len1; i++) { 44 hash1[i]=(hash1[i-1]*base+s1[i]-'a'+1)%mod; 45 } 46 } 47 void cmp() { 48 if(len==LCP) { 49 putchar('='); 50 } 51 if(s[LCP+1]>s1[LCP+1]) { 52 //cout<<s[LCP+1]<<' '<<s1[LCP+1]<<endl; 53 putchar('>'); 54 } 55 if(s[LCP+1]<s1[LCP+1]) { 56 //cout<<s[LCP+1]<<' '<<s1[LCP+1]<<endl; 57 putchar('<'); 58 } 59 } 60 int main() { 61 scanf("%s",s+1);scanf("%s",s1+1);len=strlen(s+1),len1=strlen(s1+1),all=max(len,len1)+1;gethash(); 62 LCP=lcp();cout<<lcp()<<endl; 63 cmp(); 64 //s[1,len-d]=s[d,len] 65 }
三、trie树
Trie 也称字典树,它将一个字符串集合对应到一棵有根树上。
树上每条边代表一个字符,将从根到任意顶点的路径上边的字符连起来就是该顶点代表的字符串。
Trie 中任意一个结点代表的字符串都是实际字符串集合中某些串的前缀
Trie 还将前缀相等的字符串压缩到一起,节省了存储空间
结点处还可以存储额外信息,比如该结点的字符串是否是字符串集合中的某个串
假设字符集是小写字母,则可简单的将 Trie 认为是一个 26 叉树,那么它的插入与查询操作都十分方便
时间复杂度:建树 O(字符串总长),查询:O(查询串总长)
查询字符串集合中是否含有某个字符串
将集合内字符串按照字典序排序求集合内两字符串的 LCP
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 void insert(char s[]){ 2 int now=0; 3 for(int i=0;s[i];i++){ 4 if(!c[now][s[i]-'a'])c[now][s[i]-'a']=++cnt; 5 now=c[now][s[i]-'a'],num[now]++; 6 } 7 } 8 void find(char s[]){ 9 int now=0; 10 for(int i=0;s[i];i++){ 11 now=c[now][s[i]-'a']; 12 printf("%c",s[i]); 13 if(num[now]==1)break; 14 }putchar('\n'); 15 }
四、AC自动机
构建 AC 自动机并用于匹配需要三个步骤:
将所有模式串建成一棵 Trie,
对 Trie 上所有结点构造失败指针fail(功能类似于 KMP 中的 next),
匹配则利用失败指针进行。
fail 指针实际上与 KMP 中的 next 相似,故 AC 自动机可看做 Trie 上的 KMP。
一般称 Trie 的结点为 AC 自动机的状态,Trie 中的边称为转移,fail 指针对应的结点叫做当前结点的失配转移
当我们在 Trie 树中匹配到某个深度为 i 的结点 u 时,它代表着某些串的前缀。
当 u 无法继续匹配下去后,我们希望和 KMP 里一样,找到另一些串,使得它们的前缀等于当前串的后缀且长度尽量长。
由于串的前缀可以用树中的某个结点 v 来表示,所以我们令 v 是 u 的 fail 指针,当 u 匹配失败后将匹配结点移动为 v。
显然 v 的深度小于 u,所以我们考虑从根结点 BFS构造 fail 指针,
求 fail 的过程也是自我匹配的过程,与 KMP 构造方式类似。
假设 u 的 fail 指针是 v,现在考虑构造 u 的字符 c 方向的儿子 w 的 fail 指针,
若 v 不存在 c 方向的边,则令 v = fail[v],直到存在后 v 就是 w 的 fail 指针
这里有一个优化就是如果 c 方向儿子 w 不存在,则可以直接令这个儿子是它的 fail 指针
fail[i]为与以i节点为结尾的串的后缀有最大公共长度的前缀的结尾编号。
fail树
每个点都是一个字符串的前缀,而且每个字符串的每个前缀在这棵树上都对应着一个点。
某个结点所对应的字符串肯定是其子结点所对应的字符串的后缀。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include<bits/stdc++.h> 2 using namespace std; 3 const int N=1e6+5; 4 queue<int>q; 5 struct node{ 6 int c[N][26],val[N],fail[N],cnt; 7 void ins(char *s){ 8 int len=strlen(s),now=0; 9 for(int i=0;i<len;i++){ 10 int v=s[i]-'a'; 11 if(!c[now][v]) c[now][v]=++cnt; 12 now=c[now][v]; 13 } 14 val[now]++; 15 } 16 void build(){ 17 for(int i=0;i<26;i++){ 18 if(c[0][i]) fail[c[0][i]]=0,q.push(c[0][i]); 19 } 20 while(!q.empty()){ 21 int u=q.front(); 22 q.pop(); 23 for(int i=0;i<26;i++){ 24 if(c[u][i]) fail[c[u][i]]=c[fail[u]][i],q.push(c[u][i]); 25 else c[u][i]=c[fail[u]][i]; 26 } 27 } 28 } 29 int query(char *s){ 30 int len=strlen(s); 31 int now=0,ans=0; 32 for(int i=0;i<len;i++){ 33 now=c[now][s[i]-'a']; 34 for(int t=now;t&&~val[t];t=fail[t]){ 35 ans+=val[t];val[t]=-1; 36 } 37 } 38 return ans; 39 } 40 }ac; 41 char p[N]; 42 int main(){ 43 int n; 44 scanf("%d",&n); 45 for(int i=1;i<=n;i++){ 46 scanf("%s",p); 47 ac.ins(p); 48 } 49 ac.build(); 50 scanf("%s",p); 51 printf("%d\n",ac.query(p)); 52 }
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include<bits/stdc++.h> 2 using namespace std; 3 const int N=1e6+5; 4 const int M=155; 5 struct nod { 6 int id,num; 7 } ans[N]; 8 bool cmp(nod a,nod b) { 9 if(a.num!=b.num) return a.num>b.num; 10 else return a.id<b.id; 11 } 12 struct node { 13 int c[N][26],fail[N],end[N],cnt; 14 void clear(int x) { 15 memset(c[x],0,sizeof(c[x])); 16 fail[x]=0; 17 end[x]=0; 18 } 19 void ins(char *s,int num) { 20 int len=strlen(s); 21 int now=0; 22 for(int i=0; i<len; i++) { 23 int v=s[i]-'a'; 24 if(!c[now][v]){ 25 c[now][v]=++cnt; 26 clear(cnt); 27 } 28 now=c[now][v]; 29 } 30 end[now]=num; 31 } 32 void build() { 33 queue<int>q; 34 for(int i=0; i<26; i++) { 35 if(c[0][i]) { 36 fail[c[0][i]]=0; 37 q.push(c[0][i]); 38 } 39 } 40 while(!q.empty()) { 41 int u=q.front(); 42 q.pop(); 43 for(int i=0; i<26; i++) { 44 if(c[u][i]) { 45 fail[c[u][i]]=c[fail[u]][i]; 46 q.push(c[u][i]); 47 } else c[u][i]=c[fail[u]][i]; 48 } 49 } 50 } 51 void query(char *s) { 52 int len=strlen(s); 53 int now=0; 54 for(int i=0; i<len; i++) { 55 now=c[now][s[i]-'a']; 56 for(int t=now; t; t=fail[t]) { 57 ans[end[t]].num++; 58 } 59 } 60 } 61 62 } ac; 63 64 char p[N],s[M][M]; 65 int main() { 66 int n; 67 while(~scanf("%d",&n)&&n) { 68 ac.cnt=0; 69 ac.clear(0); 70 for(int i=1; i<=n; i++) { 71 ans[i].id=i; 72 ans[i].num=0; 73 scanf("%s",s[i]); 74 ac.ins(s[i],i); 75 } 76 ac.fail[0]=0; 77 ac.build(); 78 scanf("%s",p); 79 ac.query(p); 80 sort(ans+1,ans+1+n,cmp); 81 printf("%d\n",ans[1].num); 82 printf("%s\n",s[ans[1].id]); 83 for(int i=2; i<=n; i++) { 84 if(ans[i].num==ans[i-1].num) printf("%s\n",s[ans[i].id]); 85 else break ; 86 } 87 } 88 }
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include<bits/stdc++.h> 2 using namespace std; 3 const int N=2000005; 4 queue<int>q; 5 struct node{ 6 int c[N][26],fail[N],cnt,ans[N],flag[N],judge[N],in[N],vis[N]; 7 void ins(char *s,int num){ 8 int len=strlen(s),now=1; 9 for(int i=0;i<len;i++){ 10 int v=s[i]-'a'; 11 if(!c[now][v]){ 12 c[now][v]=++cnt; 13 } 14 now=c[now][v]; 15 } 16 if(!flag[now]) flag[now]=num; 17 judge[num]=flag[now]; 18 } 19 void build(){ 20 for(int i=0;i<26;i++){ 21 c[0][i]=1; 22 } 23 q.push(1); 24 while(!q.empty()){ 25 int u=q.front();q.pop(); 26 //int fal=fail[u]; 27 for(int i=0;i<26;i++){ 28 int v=c[u][i]; 29 if(!v){ 30 c[u][i]=c[fail[u]][i]; 31 continue ; 32 } 33 fail[v]=c[fail[u]][i]; 34 in[fail[v]]++; 35 q.push(v); 36 } 37 } 38 } 39 void topu(){ 40 for(int i=1;i<=cnt;i++){ 41 if(!in[i]) q.push(i); 42 43 } 44 while(!q.empty()){ 45 int u=q.front(); 46 q.pop(); 47 vis[flag[u]]=ans[u]; 48 int v=fail[u]; 49 in[v]--; 50 ans[v]+=ans[u]; 51 if(!in[v]) q.push(v); 52 } 53 } 54 void query(char *s){ 55 int u=1,len=strlen(s); 56 for(int i=0;i<len;i++){ 57 u=c[u][s[i]-'a']; 58 ans[u]++; 59 } 60 } 61 }ac; 62 char s[N],t[N]; 63 int main(){ 64 int n; 65 scanf("%d",&n);ac.cnt=1; 66 for(int i=1;i<=n;i++){ 67 scanf("%s",s); 68 ac.ins(s,i); 69 } 70 ac.build(); 71 scanf("%s",t); 72 ac.query(t); 73 ac.topu(); 74 for(int i=1;i<=n;i++){ 75 printf("%d\n",ac.vis[ac.judge[i]]); 76 } 77 }
五、manacher
用来解决一类回文串问题,它可以求出以每个字符为中心的最长回文串的半径
首先在每个字符之间加入一个不在原串中出现的字符,比如井字符,这样统一了奇回文和偶回文串
令 pi 表示以 i 为中心的最长回文半径,考虑按顺序递推求解
首先考虑两个辅助变量 rmax 和 c,分别表示已求解的回文中心所拓展到的最右边界和对应的中心位置
计算 pi 时,令 i 关于 c 的对称点为 j = 2 × c i,则pi 的初始值可以为 min(pj, rmax i)
对于剩余的部分我们可以通过尝试不断令 pi + 1 来求匹配成功会导致 rmax 右移,
而 rmax 右移不超过 n次所以总时间复杂度 O(n)
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 #include<bits/stdc++.h> 2 using namespace std; 3 const int M=50000500; 4 char s1[M],s[M]; 5 int p[M]; 6 void work(){ 7 int len=strlen(s1); 8 for(int i=0;i<len;i++){ 9 s[i*2]='#'; 10 s[i*2+1]=s1[i]; 11 } 12 s[len*2]='#';s[len*2+1]='@'; 13 return ; 14 } 15 int manacher(int len){ 16 int rmax=0,ans=0,c=0;; 17 for(int i=0;i<len;i++){ 18 p[i]=i<rmax?min(p[2*c-i],rmax-i):1; 19 while(i-p[i]>=0&&i+p[i]<len&&s[i-p[i]]==s[i+p[i]]){ 20 ++p[i]; 21 } 22 if(i+p[i]>rmax){ 23 rmax=i+p[i]; 24 c=i; 25 } 26 ans=max(ans,p[i]); 27 } 28 return ans-1; 29 } 30 int main(){ 31 scanf("%s",s1);work(); 32 printf("%d\n",manacher(strlen(s))); 33 }