第五周练习主要涉及string类、Kmp、manacher、字符串hash
拓展:BM、Sunday、
string是C++相对于C特有的类型,将字符串单独封装作为类使用,其基本的操作不再赘述。
以下内容参考《C++STL基础及应用》
插入
insert():第一个参数表明插入源串的位置,第二个参数表明要插入的字符串;
append():仅有一个输入参数,在源字符串尾部追加该字符串
size():无参数,返回字符串长度值,即多少个字符
替换
replace():三个输入参数,第一个用于指示从字符串什么位置开始改写,第二个用于指示从原字符串中删除多少个字符,第三个是替换字符串的值
查询
string::npos:string类的成员变量,一般与系统查询函数的返回值比较,若等于该值,表明没有符合查询条件的结果值
find:在一个字符串中查找指定的单个字符或字符组,找到返回首次匹配的开始位置,否则返回nops,一般为两参数,待查询字符串和起始位置(默认0)
find_first_of:在一个字符串中进行查找,返回第一个与指定字符串中任何字符匹配的字符位置,否则为npos,一般为两参数,待查询字符串和起始位置(默认0)
find_last_of:在一个字符串中进行查找,返回最后一个与指定字符串中任何字符匹配的字符位置,否则为npos,一般为两参数,待查询字符串和起始位置(默认0)
find_first_not_of:在一个字符串中进行查找,返回第一个与指定字符串中任何字符不匹配的字符位置,否则为npos,一般为两参数,待查询字符串和起始位置(默认0)
find_last_not_of:在一个字符串中进行查找,返回最后一个与指定字符串中任何字符不匹配的字符位置,否则为npos,一般为两参数,待查询字符串和起始位置(默认0)
rfind:对一个串从头到尾查找指定的单个字符或字符组,如果找到,返回首次匹配开始位置,否则返回npos,一般为两参数,待查询字符串和起始位置(默认串尾)
头文件<sstream>
istringstream:字符串输入流,提供读string功能
ostringstream:字符串输出流,提供写string功能
stringstream:字符串输入输出流,提供读写string功能
此三类配合>>和<<使用,方向代表存入的对象
KMP算法应用于查找B串在A串中的出现位置,也可以通过修改来求得两个字符串的最大公共序列,初次理解总觉晦涩难懂,查找多方资料并细细品读后才得以理解,本文尽力将KMP算法论述清楚
参考了该篇博客,其详尽自愧不如
还有mooc上的网课数据结构-浙江大学
每次匹配,正在匹配的模式串的部分分为三个板块
该处对称的定义:最大的相同前缀与后缀
正常来讲是这样,但是为了简化操作,只取前三块
匹配如图(前提:匹配失败)
首先是直观的理解,在KMP中,每次失配后,串移动的参照物为文本串,也就是每次都是移动模式串,即更改模式串的匹配位置,由图可知,每次失配后,为了减少再次匹配的次数,我们由对称进行移动,将左对称区间移动到右区间位置,中间的元素必定不能匹配,由上一张图可知,模式串中已匹配部分有三个板块,若按照暴力的思想右移一位,如图
可以看到,右移之后a与c匹配判断,b与d匹配判断,看下面的例子
A A C D A A F AACDAAF AACDAAF
A A C D A A AACDAA AACDAA
→
A A C D A A F AACDAAF AACDAAF
∗ A A C D A A *AACDAA ∗AACDAA
如果需要对称部分匹配成功,则C=A,D=A,模式串变为AAAAAA,显然与设定的条件矛盾
如果没有对称区间,那么就直接右移一位
设两个区间左端相隔m位
易证,右移一位,则串各元素全相等,右移两位,则奇数相等,偶数相等,由不完全归纳可得右移m位,则以m为模的对应位数元素相等
那么,接下来的问题就是如何进行移动操作,以及求对称长度的大小
如图
当s与p指向的元素相同时,两者都自增,当发生匹配失败时,p=match[p-1]+1,进行回溯,即将左对称移到右对称,或者说重新从左对称的最大下标的下一个开始匹配,这里的match的定义是
简单来说,match记录的是当模式串到下标j(是下标,也就是j+1个元素)时,满足下标0~j的字符串中有最大相等的前缀后缀的下标i
那么,在match已经构造好的前提下,代码如下
int KMP(char*text,char*pattern)
{
int n = strlen(string);
int m = strlen(pattern);
int s=0, p=0;
if ( n < m ) return -1;
while ( s<n && p<m ) {
if ( text[s]==pattern[p] )
{
s++;
p++;
}
else if(p>0)//p=0代表第一个元素不匹配
p = match[p-1]+1;
else s++;//第一个元素不匹配,则进行text的下一位匹配
}
return ( p==m )? (s-m) : -1;//判断p是否到达结尾,是,则说明已经找到,否则说明匹配失败
}
现在来讨论一下match的构造
我们用递归的方式来思考,对j-1来言,与其相等的字符下标为match[j-1],当我们讨论j的值时,有两种情况
代码如下
void BuildMatch(char*pattern)
{
memset(match,0,sizeof(match));//清空
int i=0,len=strlen(pattern);//数值初始化
match[0]=-1;//第一个为-1
for(int j=1;j<=m;j++)//求每一个的match值
{
i=match[j-1];
while(i>=0&&pattern[i]!=pattern[j])//递归过程,找到第一个前缀的后一个元素与pattern[j]相等
i=match[i];//减小长度,有DP的意思
if(i<0)
match[j]=-1;//如果找不到
else
match[j]=match[j-1]+1;
}
}
字符串哈希可以简单理解为将字符串对应成整数来处理,用函数的思想(数学中的函数)将每个字符串对应到存储空间的各个位置上,即通过哈希函数尽量创造出一个一一对应的数组,方便进行后续操作
对于给定的字符串str,由字母组成,设置一个字母T对应映射 i d x ( T ) = T − ′ a ′ + 1 ( i d x ( T ) = ( i n t ) T ) idx(T)=T-'a'+1(idx(T)=(int)T) idx(T)=T−′a′+1(idx(T)=(int)T)
h a s h [ i ] = h a s h [ i − 1 ] × p + i d ( s t r [ i ] ) hash[i]=hash[i-1]×p+id(str[i]) hash[i]=hash[i−1]×p+id(str[i])
hash[i]设置为unsighed long long,相当于默认对 2 64 − 1 2^{64}-1 264−1取模
h a s h [ i ] = h a s h [ i − 1 ] × p + i d ( s t r [ i ] ) % m o d hash[i]=hash[i-1]×p+id(str[i])\%mod hash[i]=hash[i−1]×p+id(str[i])%mod
其中p、mod为质数,mod>p,当mod、p很大时,冲突概率低
将一个字符用不同mod进行两次哈希,哈希结果为一个二元组
h a s h 1 [ i ] = h a s h [ i − 1 ] × p + i d ( s t r [ i ] ) % m o d 1 hash_1[i]=hash[i-1]×p+id(str[i])\%mod_1 hash1[i]=hash[i−1]×p+id(str[i])%mod1
h a s h 2 [ i ] = h a s h [ i − 1 ] × p + i d ( s t r [ i ] ) % m o d 2 hash_2[i]=hash[i-1]×p+id(str[i])\%mod_2 hash2[i]=hash[i−1]×p+id(str[i])%mod2
结果为 < h a s h 1 [ n ] , h a s h 2 [ n ] >
由于进行了两次哈希,并且用的mod不同,冲突概率极低
已知一个字符串str的 h a s h [ i ] , 1 ≤ i ≤ n hash[i],1≤i≤n hash[i],1≤i≤n,其子串 s t r l … s t r r ( 1 ≤ l ≤ r ≤ n ) str_l\dots str_r(1≤l≤r≤n) strl…strr(1≤l≤r≤n)的哈希值为
h a s h [ r ] − h a s h [ l − 1 ] × p r − l + 1 hash[r]-hash[l-1]×p^{r-l+1} hash[r]−hash[l−1]×pr−l+1
又因为需要每次取模
( h a s h [ r ] − h a s h [ l − 1 ] × p r − l + 1 ) % m o d (hash[r]-hash[l-1]×p^{r-l+1})\%mod (hash[r]−hash[l−1]×pr−l+1)%mod
再加上考虑负数的情况