常用字符串算法

简介

字符串的处理几乎无处不在,常用的字符串算法有KMP、扩展KMP、Trie树、AC自动机、Manacher、哈希、SA、SAM等。

Knuth-Morris-Pratt 算法

给你两个字符串AB,询问B串是否是A串的子串(A串是否包含B串)。

可以枚举从A串的什么位置起开始与B匹配,然后验证是否匹配。假如A串长度为n,B串长度为m,那么这种方法的复杂度是O (mn)的。

而KMP算法能够在线性复杂度内求出一个串在另一个串的所有匹配位置。

KMP的核心思想在于构建一个前缀数组(失配数组),对于模式串P,假设字符串下标从1开始,数组F满足F[i]=max{j|j<i且P[1..j]=P[i-j+1..i},即P的长度为j的前缀等于P的长度为i的前缀的长度为j的后缀。这说明F[i]所求的是串P[1..i]的一个最长的前缀P[1..j],这个前缀也是它的后缀。当这个前缀找不到的时候,F[i]设为0。

例如P=ababc,则F[4]=2,因为ab(P[1..2])是abab(P[1..4])的最长的前缀,又是它的后缀ab(P[3..4])。

有了F数组,就可以匹配两个字符串,主串T与模式串P。

先求出P的F数组,然后维护两个下标i和j,表示当前模式串P的前j个字符与主串T在位置i的前j个字符匹配,即P[1..j]=T[i-j+1..i]。

当算法在尝试对T[i+1]和P[j+1]匹配时,发现它们字符不同,那么就用F数组进行跳跃,使j=F[j]。

KMP算法高效的原因在于充分利用了匹配中的已有信息。关键的一步在于T[i+1]!=P[j+1]时j=F[j]。

因为我已知P[1..j]=T[i-j+1..i]而P[1..F[j]]=P[j-F[j]+1..j],因此P[1..F[j]]=T[i-F[j]+1..i],所以我们可以直接从F[j]位置开始继续匹配。

在实际实现中,字符串的下标从0开始,而我们仍然定义F数组的下标从1开始,对代码做一些简单的调整即可。

 1 const int maxn=1111111;
 2 char P[maxn];
 3 char T[maxn];
 4 int f[maxn];
 5
 6 void getFail(char P[],int f[]){
 7     int i=0,j=-1;
 8     int len=strlen(P);
 9     f[0]=-1;
10     while (i<len){
11         if (j==-1||P[i]==P[j]){
12             i++,j++;
13             f[i]=j;
14         }
15         else{
16             j=f[j];
17         }
18     }
19 }
20
21 void KMP(char T[],char P[],int f[]){
22     int i=0,j=0;
23     int n=strlen(T);
24     int m=strlen(P);
25     getFail(P,f);
26     while(i<n){
27         if(j==-1||T[i]==P[j]){
28             i++,j++;
29         }
30         else{
31             j=f[j];
32         }
33         if(j==m){
34             // TO DO:
35             //ans++;
36             j=f[j];
37         }
38     }
39 }

KMP

一些练习题

POJ 3461 Oulipo
计算单词W在整篇文章T中出现的次数。
KMP最基本的应用,统计出现次数,套模板即可。

1         if(j==m){
2             // TO DO:
3             ans++;
4             j=f[j];
5         }

POJ 2752 Seek the Name, Seek the Fame
找到一个S的子串作为前缀-后缀字符串。所谓前缀-后缀字符串即S的子串不仅是S的前缀又是S的后缀。
子串s[ 1 -> f[n] ] 是最长的子串,既是是s的前缀又是s的后缀,同理1 -> f[ f[n] ] 是次短的...依次递归。

1         while (f[n]>0){
2             stk.push(f[n]);
3             n=f[n];
4         }

POJ 2406 Power Strings
输出最大的n使得s由a重复n次而成。
当 n%(n-f[n])==0时,n-f[n] 是s最短的循环节。

1         if (n%(n-f[n])==0){
2             printf("%d\n",n/(n-f[n]));
3         }
4         else{
5             printf("1\n");
6         }

POJ 1961 Period
对每个前缀i,若能由某些字符重复k次形成,输出最大的k。
与上题类似,枚举i,若i%(i-f[i])==0 则最短循环节为i-f[i],k为i/(i-f[i])

1         for (int i=2;i<=n;i++){
2             if (f[i]>0&&i%(i-f[i])==0){
3                 printf("%d %d\n",i,i/(i-f[i]));
4             }
5         }

HDU 3336 Count the string
求出s有多少个子串是它本身的前缀。
DP公式如下。

1         for (int i=1;i<=n;i++){
2             dp[i]=dp[f[i]]+1;
3             ans=(ans+dp[i])%10007;
4         }

HDU 3746 Cyclic Nacklace
至少要在字符串s后面补几个字符才能凑成一个循环。
若本身已经有循环节,则答案为0。

1         if (f[n]>0&&n%(n-f[n])==0) printf("0\n");
2         else printf("%d\n",n-f[n]-n%(n-f[n]));

HDU 2087 剪花布条
给定T和P,为T中能分出几块P。
只匹配一次的KMP。当匹配成功时将j置为0即可。

1         if(j==m){
2             // TO DO:
3             ans++;
4             j=0;
5         }

HDU 2594 Simpsons’ Hidden Talents
求a的最长前缀是b的后缀。
将两串拼接成s,a在前b在后,则问题转化为求一个串的前缀是后缀。
注意s的前缀不一定是a的前缀也不一定是b的后缀,所以当f[n]>na或f[n]>nb时我们要忽略子串s[ 1->f[n] ]。

 1         while (f[m]>n1||f[m]>n2){
 2             m=f[m];
 3         }
 4         if (f[m]>0){
 5             for (int i=0;i<f[m];i++){
 6                 printf("%c",s1[i]);
 7             }
 8             printf(" %d\n",f[m]);
 9         }
10         else{
11             printf("0\n");
12         }

hdu 4763 Theme Section
求出满足EAEBE格式的最长子串E的长度。
由最长前缀后缀推广而来。
首先由大到小枚举前缀后缀,对于每个前缀后缀f[x],在字符串中间寻找f[i]=f[x],若找到则输出答案,否则继续枚举。

 1         int x=n;
 2         bool flag=false;
 3         while (f[x]>(n/3)) x=f[x];
 4         while (f[x]>0){
 5             flag=false;
 6             for (int i=f[x]*2;i<=n-f[x];i++){
 7                 if (f[i]==f[x]){
 8                     flag=true;
 9                     break;
10                 }
11             }
12             if (flag) break;
13         }
14         if (!flag) printf("0\n");
15         else printf("%d\n",f[x]);

扩展 KMP 算法

给定主串S和模板串T,扩展KMP问题要求解的就是extend[1..|S|],其中extend[i]表示S[i..|S|]与T的最长公共前缀,即S中的每个后缀与T的最长公共前缀的长度。

对比KMP算法,发现当extend[i]=|T|时,T恰好在S中出现。因此该算法是KMP算法的进一步扩展,称为扩展KMP算法。

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cstring>
 4 using namespace std;
 5 const int MM=100005;
 6 int next[MM],extand[MM];
 7 char S[MM],T[MM];
 8 void GetNext(const char *T){
 9      int len=strlen(T),a=0;
10      next[0]=len;
11      while(a<len-1 && T[a]==T[a+1]) a++;
12      next[1]=a;
13      a=1;
14      for(int k=2;k<len;k++){
15          int p=a+next[a]-1,L=next[k-a];
16          if( (k-1)+L >= p){
17              int j = (p-k+1)>0 ? (p-k+1) : 0;
18              while(k+j<len && T[k+j]==T[j]) j++;
19              next[k]=j;
20              a=k;
21          }
22          else
23              next[k]=L;
24      }
25 }
26 void GetExtand(const char *S,const char *T){
27      GetNext(T);
28      int slen=strlen(S),tlen=strlen(T),a=0;
29      int MinLen = slen < tlen ? slen : tlen;
30      while(a<MinLen && S[a]==T[a]) a++;
31      extand[0]=a;
32      a=0;
33      for(int k=1;k<slen;k++){
34          int p=a+extand[a]-1, L=next[k-a];
35          if( (k-1)+L >= p){
36              int j= (p-k+1) > 0 ? (p-k+1) : 0;
37              while(k+j<slen && j<tlen && S[k+j]==T[j]) j++;
38              extand[k]=j;
39              a=k;
40          }
41          else
42              extand[k]=L;
43      }
44 }
45 int main(){
46     while(scanf("%s%s",S,T)==2){
47          GetExtand(S,T);
48          for(int i=0;i<strlen(T);i++)
49              printf("%d ",next[i]);
50          puts("");
51          for(int i=0;i<strlen(S);i++)
52              printf("%d ",extand[i]);
53          puts("");
54     }
55     return 0;
56 }

扩展KMP

一些练习题

HDU 4333 Revolving Digits
读入数字串P,T由两个P拼接而成。
则T从0到n的每个长度为n的后缀即为一种数字排列。
对于T的后缀i,设其与原数字P的最长公共前缀长度为L。
若L>=n,说明此后缀表示的数与原数字相等。
若L<n,则令 T[i+extand[i]] 与 P[extand[i]] 比较大小即可得出两数的大小。
对于类似123123形式的重复串,排列三次以后又回到了123123的形式,所以答案必须除以循环节。
用KMP的找到最小循环节个数n/(n-f[n])

 1         scanf("%s",P);
 2         strcpy(T,P);
 3         strcat(T,P);
 4         GetExtand(T,P);
 5         int n=strlen(P);
 6         int cnt1=0,cnt2=0,cnt3=0;
 7         for (int i=0;i<n;i++){
 8             if (extand[i]>=n) cnt2++;
 9             else if (T[i+extand[i]]<P[extand[i]]) cnt1++;
10             else cnt3++;
11         }
12         getFail(P,f);
13         int tol=1;
14         if (n%(n-f[n])==0) tol=n/(n-f[n]);
15         printf("Case %d: %d %d %d\n",++Cas,cnt1/tol,cnt2/tol,cnt3/tol);

HDU 4300 Clairewd’s message
给一个密文到明文的映射表
给一个串,前面为密文,后面为明文,密文一定是完整的,但明文不完整或没有
将这个串补全。
令原串为T,将原串全部翻译为P。
可以发现原串T的后缀i是P的前缀。
从(n+1)/2开始枚举T的后缀,对于每个后缀i,若i+extand[i]>=n则从T:0~i-1为密文,P:i~n-1为明文。

1         GetExtand(T,P);
2         int ret=len;
3         for (int i=(len+1)/2;i<len;i++){
4             if (extand[i]+i>=len){
5                 ret=i;
6                 break;
7             }
8         }  

Trie 树

Aho-Corasick 自动机

Manacher 算法

字符串哈希

后缀数组

后缀自动机

常用字符串算法

时间: 2024-10-02 07:54:31

常用字符串算法的相关文章

常用MD5算法代码

常用的MD5算法代码日期: 2014年8月4日作者: 铁锚 MD5,全称为 Message Digest Algorithm 5(消息摘要算法第五版).详情请参考 维基百科:MD5 MD5加密后是一个字节数组, 但我们一般是取其十六进制的字符串表示法,当然,十六进制数字符串是区分大小写,在 mysql数据库,Java,和JavaScript语言中,一般是使用小写的字符串来表示, 而在 Oracle数据库官方提供的包中,返回的是大写字符串,这算是一个坑,如果你想要执行多次 md5,可能需要转换为小

javascript常用经典算法实例详解

javascript常用经典算法实例详解 这篇文章主要介绍了javascript常用算法,结合实例形式较为详细的分析总结了JavaScript中常见的各种排序算法以及堆.栈.链表等数据结构的相关实现与使用技巧,需要的朋友可以参考下 本文实例讲述了javascript常用算法.分享给大家供大家参考,具体如下: 入门级算法-线性查找-时间复杂度O(n)--相当于算法界中的HelloWorld ? 1 2 3 4 5 6 7 8 9 10 //线性搜索(入门HelloWorld) //A为数组,x为要

常用的算法思想总结

对于计算机科学而言,算法是一个非常重要的概念.它是程序设计的灵魂,是将实际问题同解决该问题的计算机程序建立起联系的桥梁.接下来,我们来看看一些常用的算法思想. (一)穷举法思想 穷举法,又称为强力法.它是一种最为直接,实现最为简单,同时又最为耗时的一种解决实际问题的算法思想. 基本思想:在可能的解空间中穷举出每一种可能的解,并对每一个可能解进行判断,从中得到问题的答案. 使用穷举法思想解决实际问题,最关键的步骤是划定问题的解空间,并在该解空间中一一枚举每一个可能的解.这里有两点需要注意,一是解空

DotNet常用排序算法总结

数据结构和算法对一个程序来说是至关重要的,现在介绍一下几种算法,在项目中较为常用的算法有:冒泡排序,简单选择排序,直接插入排序,希尔排序,堆排序,归并排序,快速排序等7中算法. 现在介绍选择排序算法,希尔排序算法,快速排序算法. (1).选择排序算法:通过n-i次关键字间的比较,从n-i+1个记录中选择出关键字最小的记录,并和第i(1大于等于i小于等于n)个记录交换. (2).希尔排序:先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组.所有距离为d1的倍数的记录放在同一个组中.先在各

字符串算法之 AC自己主动机

近期一直在学习字符串之类的算法,感觉BF算法,尽管非常easy理解,可是easy超时,全部就想学习其它的一些字符串算法来提高一下,近期学习了一下AC自己主动机.尽管感觉有所收获,可是还是有些朦胧的感觉,在此总结一下,希望大家不吝赐教. 一.AC自己主动机的原理: Aho-Corasick automaton.该算法在1975年产生于贝尔实验室,是著名的多模匹配算法之中的一个. 一个常见的样例就是给出N个单词,在给出一段包括m个字符的文章,让你找出有多少个单词在这文章中出现过,.要搞懂AC自己主动

Java常用排序算法+程序员必须掌握的8大排序算法+二分法查找法

Java 常用排序算法/程序员必须掌握的 8大排序算法 本文由网络资料整理转载而来,如有问题,欢迎指正! 分类: 1)插入排序(直接插入排序.希尔排序) 2)交换排序(冒泡排序.快速排序) 3)选择排序(直接选择排序.堆排序) 4)归并排序 5)分配排序(基数排序) 所需辅助空间最多:归并排序 所需辅助空间最少:堆排序 平均速度最快:快速排序 不稳定:快速排序,希尔排序,堆排序. 先来看看 8种排序之间的关系: 1.直接插入排序 (1)基本思想:在要排序的一组数中,假设前面(n-1)[n>=2]

常用推荐系统算法总结

一,常用推荐系统算法总结 1.Itemcf (基于商品的协同过滤) 这个算法是cf中的一种,也是当今很多大型网站都在采用的核心算法之一.对于商城网站(以Amazon为代表,当然也包括京东那种具有搞笑特色的推荐系统在内),影视类推荐,图书类推荐,音乐类推荐系统来说,item的增长速度远不如user的增长速度,而且item之间的相似性远不如user之间的相似性那么敏感,所以可以在离线系统中将item的相似度矩阵计算好,以供线上可以近乎即时地进行推荐.因为这种方法靠的是item之间的相关性进行推荐,所

JS常用字符串处理方法总结

1.indexOf()方法,从前往后查找字符串位置,大小写敏感,从0开始计数.同理,lastIndexOf() 方法从后往前,两个方法对于相同的检索条件输出的结果是一样的 例如: <script type="text/javascript"> var str="Hello World!" document.write(str.indexOf("Hello"))//输出0 document.write(str.indexOf("

常用排序算法比较与分析

一.常用排序算法简述 下面主要从排序算法的基本概念.原理出发,分别从算法的时间复杂度.空间复杂度.算法的稳定性和速度等方面进行分析比较.依据待排序的问题大小(记录数量 n)的不同,排序过程中需要的存储器空间也不同,由此将排序算法分为两大类:[内排序].[外排序]. 内排序:指排序时数据元素全部存放在计算机的随机存储器RAM中. 外排序:待排序记录的数量很大,以致内存一次不能容纳全部记录,在排序过程中还需要对外存进行访问的排序过程. 先了解一下常见排序算法的分类关系(见图1-1) 图1-1 常见排