本周训练赛出了一道kmp模板题,但是由于长时间没有复习字符串处理算法,而且学习时也并没有彻底理解,只是大概明白了思路,所以导致比赛时迟迟没有做出这一题,最后现场拿出学校整理的材料现场重新学习才ac的这一题。趁这个机会整理一下常用的字符串处理算法以及模板。
字符串处理在比赛中一般都不是特别难(至少我遇到的没有),有的字符串处理会和dp放在一起出题,加大一些难度,而单纯的字符串处理其实还是比较好写。
一、strstr
strstr(str1,str2) 函数用于判断字符串str2是否是str1的子串。如果是,则该函数返回str2在str1中首次出现的地址;否则,返回NULL。据说strstr的效率和kmp差不多。
strstr没什么好说的,注意返回的是地址,如果要下标就用返回值减去数组首地址即可。
例题:http://acm.fzu.edu.cn/problem.php?pid=2128
分析:找出所有子串的位置,排序之后找到相邻两个子串的第二个字母与倒数第二个字母的距离,维护最大即可。
代码:
#include#include #include #include #define MAXN 1000010 using namespace std; char str[MAXN],tmp[105]; struct node{ int start,endn; }no[MAXN]; int cnt=0; bool cmp(node a,node b){ return a.start<b.start; } int main(){ while(~scanf("%s",str)){ int n; scanf("%d",&n); cnt=0; int len=strlen(str); int res=-1; while(n--){ scanf("%s",tmp); int pos=0; int ltmp=strlen(tmp); while(strstr(str+pos,tmp)!=NULL){ int ans=strstr(str+pos,tmp)-str; no[cnt].start=ans; no[cnt].endn=ans+ltmp-1; pos=no[cnt].endn; cnt++; } } no[cnt].start=no[cnt].endn=len; cnt++; sort(no,no+cnt,cmp); /*for(int i=0;i */ for(int i=0;i ;i++) res=no[i+1].endn-no[i].start-1>res?no[i+1].endn-no[i].start-1:res; if(res==-1) printf("%d\n",len); else printf("%d\n",res); } }1
二、字符串hash
具体的哈希总结会另开一专题,这里只贴一下字符串哈希常用的模板:
1.SDBMHash
unsigned int SDBMHash(char *str){ unsigned int hash = 0; while (*str){ // equivalent to: hash = 65599*hash + (*str++); hash = (*str++) + (hash << 6) + (hash << 16) - hash; } return (hash & 0x7FFFFFFF); }
2.BKDRHash
unsigned int BKDRHash(char *str){ unsigned int seed = 131; // 31 131 1313 13131 131313 etc.. unsigned int hash = 0; while (*str){ hash = hash * seed + (*str++); } return (hash & 0x7FFFFFFF); }
3.APHash
unsigned int APHash(char *str){ unsigned int hash = 0; int i; for (i=0; *str; i++){ if ((i & 1) == 0){ hash ^= ((hash << 7) ^ (*str++) ^ (hash >> 3)); } else{ hash ^= (~((hash << 11) ^ (*str++) ^ (hash >> 5))); } } return (hash & 0x7FFFFFFF); }
3.kmp
kmp是acm中最常用的字符串处理算法,虽然其效率可能不如Sunday,BM等算法,但是其地位是不容质疑的。
kmp算法实质是在暴力寻找的基础上添加了next数组,其思想就是先对于模式串进行预处理,然后利用已有的匹配信息,优化在查找时候的模式串移动位数。
举个例子:
比如主串是:ASDFVAGBASDFGABSDFASDABCBDSFB,模式串是ABCABD。
我们首先从左向右比较,发现第二位没有匹配,此时如果是暴力的思想,我们应该以主串的第二位为首重新进行匹配,但是其实我们没有必要这样做,因为我们可以在主串中找到下一个A开始比较即可。
而我们要做的就是如何利用已知信息去寻找这个next数组,也就是如何对查询过程进行优化。
由此,我们引出了对于一个字符窜的前后缀概念。前缀是指一个字符串除去最后一个字母并且包含首字母的子串,后缀是指一个字符串除去首字母并且包含尾字母的子串。
举个例子:
对于字符串:ABCDABD 来说:
- "A"的前缀和后缀都为空集,共有元素的长度为0;
- "AB"的前缀为[A],后缀为[B],共有元素的长度为0;
- "ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;
- "ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;
- "ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;
- "ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;
- "ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。
其实,我们也可以这样理解,next数组中存储的就是到这一位为止,有多少和从模式串的头算起相同的位数。
比如ABCDABD,next[6]==2,就是到第六位为止,对于这个子串存在一个两位的既存在于前缀中,也存在于后缀中,也就是在串中和模式串首重复的字串:AB。
而当我们在进行kmp时,模式串向后移动的距离就不简简单单是1位,而是“移动位数 = 已匹配的字符数 - 对应的部分匹配值”。
比如我们对于模式串ABCDABD来说,此时主串为ABCDABACDABCDABD,模式串的next数组前面已经得到,为0,0,0,0,1,2,0。首先我们匹配到第七位失配,就需要向后移动(已匹配的6位-最后一个匹配位所对应的next值2)=4位,这样就极大的提高了效率。
const int MAXN_T=1000010; const int MAXN_P=10010; char P[MAXN_P],T[MAXN_T]; int _next[MAXN_P]; void init_next(char *P){ int m=strlen(P+1); //数组下标从1开始 _next[1]=0; //next数组第一位是0 for(int k=0,q=2;q<=m;q++){ //q是模板字符串下标,k是最大前后缀长度 while(k>0 && P[k+1]!=P[q]) //递归求P串的各位最大相同前后缀长度 k=_next[k]; if(P[k+1]==P[q]) //如果两位相等,最大相同前后缀长度加1 k++; _next[q]=k; } }
int kmp(char *P,char *T){ int n=strlen(T+1),m=strlen(P+1); init_next(P); int sum=0; for(int i=1,q=0;i<=n;i++){ //i是T串下标,q是P串下标 while(q>0&&P[q+1]!=T[i]) //根据最大前后缀长度找应该向后移动多少位 q=_next[q]; if(P[q+1]==T[i]) q++; if(q==m){ //如果P串匹配到最后一位并且成功 sum++; //则计数加1 q=_next[q]; //可以继续查询T串中共出现P串多少次 } //如果只查询是否存在可以直接在这return } return sum; }
其余关于字符串处理的算法比如自动机,后缀数组,BM,Sunday,暂时还没有太深的了解,等到有了时间和机会,会再来补上!