1.字符串匹配
寻找字符串S中字符串T出现的位置或次数的问题属于字符串匹配问题
1.哈希算法
S的长度为n,T的长度为m。朴素方法,很容易想到,时间复杂度为O(nm).
下面介绍哈希算法
对于每个起始位置,我们不是O(m)直接比较字符串是否匹配,而是O(l)的比较长度为m的字符串的哈希值与T的哈希值是否相等。虽然哈希值相等字符串未必相等,但是若哈希值随机分布,不同字符串哈希值相等的概率很低,可以当做几乎不会发生。
如果采用O(m)的算法计算长度为m的字符串子串的哈希值,复杂度仍为O(nm)。这里采用滚动哈希的优化技巧。选取两个合适的互素的常数h和b(l<b<h),假设字符串C=c1c2c3……cm,定义哈希函数H(C)=(c1*b^(m-1)+c2*b^(m-2)+……cm*b^0)modh。其中b是奇数,相当于把字符串看成k进制数。利用S[k,k+m-1]的哈希值,就可以计算S[k+1,k+m]的哈希值.H(S[k+1,k+m])=(H(S[k,k+m-1]*b-sk*b^m+s[k+m])modh.
于是,不断这样计算开始位置右移移位后的字符串子串的哈希值。就可以在O(n)的时间内得到所有位置对应的哈希值,从而可以在O(n+m)时间内完成字符串匹配。在实现时,采用64位无符号整数计算哈希值,并取h=2^64,通过自然溢出省去模运算。
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <map>
#include <queue>
#include <string>
#include <set>
#include <vector>
#include<cmath>
#include<bitset>
using namespace std;
//字符串匹配算法模板
typedef unsigned long long ull;
const ull B=100000007;//哈希的基数
//a是否在b中出现
bool contain(string a,string b)
{
int al=a.length(),bl=b.length();
if(al>bl) return false;
//计算B的al次方
ull t=1;
for(int i=0;i<al;i++) t*=B;
//计算a和b长度为al的前缀对应的哈希值
ull ah=0,bh=0;
for(int i=0;i<al;i++) ah=ah*B+a[i];
for(int i=0;i<al;i++) bh=bh*B+b[i];
//对b不断右移一位,更新哈希值并判断
for(int i=0;i+al<=bl;i++){
if(ah==bh) return true;//b从位置i开始长度为al的字符串等于a
if(i+al<bl){
bh=bh*B-b[i]*t+b[i+al];
}
}
return false;
}
//模板
int main()
{
string s1,s2;
cin>>s1>>s2;
if(contain(s1,s2)){
cout<<1<<endl;
}
else{
cout<<0<<endl;
}
return 0;
}
注:这种利用滚动哈希的字符串匹配的算法叫做Rabin-Karp算法。原算法在哈希值相等时,还要用传统的字符串比较算法来判断字符串是否相等。程序设计竞赛中,通常只需比较哈希值,不再做朴素的检查。
不光是右移一位,对于左移一位、左端或右端加长一位或是缩短一位的情况,也能类似处理。
例如,假设要求S的后缀与T的前缀相等的最大长度,可以采用上述类似方法在O(n+m)内求解。
typedef unsigned long long ull;
const ull B=100000007;//哈希的基数
//a的后缀和b的前缀相等的最大长度
int overlap(string a,string b)
{
int al=a.length(),bl=b.length();
int ans=0;
ull ah=0,bh=0,t=1;
for(int i=1;i<=min(al,bl);i++){
ah=ah+a[al-i]*t;//a的长度为i的后缀的哈希值
bh=bh*B+b[i-1];//b的长度为i的前缀的哈希值
if(ah==bh) ans=i;
t*=B;
}
return ans;
}