一、引入
哈希算法是通过一个哈希函数H,将一种数据(如字符串)转化为另一种数据(通常转化为整形数值),有些题可用map做,但数据一大就要用到字符串哈希
二、字符串哈希
寻找长度为n的主串S中的匹配串T(长度为m)出现的位置或次数属于字符串匹配问题。朴素算法(或称为暴力)就是枚举所有子串的起始位置,每枚举一次就要使用O(m)的时间,总共要O(nm)的时间。当然字符串匹配可以用KMP做,但这里介绍一下字符串哈希。
字符串哈希就是将每个字符串转化为一个数值,然后遍历主串,判断在主串起始位置为i长度为m的字符串的哈希值与匹配串的哈希值是否相等即可,每次判断为O(1)的时间。这样就可以转化为O(n)的时间完成判断。那么问题来了,怎么预处理哈希值呢?
我们选用两个互质常数base和mod,假设匹配串T=abcdefg……z(注意这里不是指T只有26位)那么哈希值为 H(T)=(a*base^(m-1)+b*base^(m-2)+c*base^(m-3)+……+z)%mod。相当于把每个字符串转换为一个base进制数,所以对于每道题我们取base时,要大于每一位上的值(避免重复),例如我们用的十进制数每一位都是小于10的。
例如字符串C="ABDB",则H(C)=‘A’+'B'*base+'D'*base^2+'B'*base^3(本人习惯直接取字符askII码值,也可以使‘A’=1)
那么怎么判断主串起始位置为i长度为m的字符串的哈希值与匹配串的哈希值是否相等呢?这里有个公式,若求字符串中第i位到第j位的哈希值(i<j),则这个值为H(j)-H(i-1)*base^(j-i+1)。有了这个公式,我们可以预处理一个数组H[i]表示字符串从第一位到第i位的哈希值和数组power[i]表示base^i。加上判断的时间,总时间为O(n+m)。
在计算时,我们可以使用无符号类型(通常本人习惯使用unsigned long long)的自然溢出,这样就可以不用%mod,包括减法也方便许多。
当然哈希会有可能重复,base值越大重复可能性越小,本人通常取131或233317。也可使用双哈希,即两个不同的mod
那么举个栗子玩玩:Power Strings(Poj2406)
【问题描述】
给定若干个长度小于等于1 000 000的字符串,询问每个字符串最多由多少个相同的子串重复连接而成。如:ababab最多由3个ab连接而成。
【输入格式】
若干行,每行一个字符串,遇“.”结束
【输出格式】
每行一个数,表示最多由多少子串连成
【样例输入】
abcd
aaaa
ababab
.
【样例输出】
1
4
3
【题目解析】
外循环枚举子串长度,如果整除总长度,那么内循环判断,代码如下:
#include <iostream>
#include <cstdio>
#include <cctype>
#include <climits>
using namespace std;
typedef unsigned long long ull;
const int N=1e6+5;
const ull prime=233317;//表示base
ull power[N]={1},h[N];
int main()
{
int l,i,j;
string a;
for(i=1;i<N;i++)
power[i]=power[i-1]*prime;//预处理
while(cin>>a)
{
if(a==".")
break;
l=a.size();
h[0]=ull(a[0]);
for(i=1;i<l;i++)
h[i]=h[i-1]*prime+ull(a[i]);//预处理从第一位到第i位的hash值,自然溢出不用取模
for(i=1;i<=l;i++)//枚举长度
{
if(l%i)
continue;
bool f=1;
for(j=i-1;j<l;j+=i)
if(h[j]-h[j-i]*power[i]!=h[i-1])//公式
{
f=0;//不成立
break;
}
if(f)//如果成立
{
cout<<l/i<<endl;
break;
}
}
}
return 0;
}
参考文献:黄新军等《信息学奥赛一本通·提高篇》