字符串 hash 讲解及模板
字符串 hash 讲解
1、Hash是什么
- Hash就是一个像函数一样的东西,你放进去一个值,它给你输出来一个值。输出的值就是Hash值。一般Hash值会比原来的值更好储存(更小)或比较。
- Hash函数具有抗碰撞性、单向性、雪崩效应等
- 有单关键字Hash和多关键字Hash等
2、字符串Hash解读
-
字符串Hash:把字符串转换成一个整数的函数;而且要尽量做到使字符串对应唯一的Hash值。Hash主要返回一个值,这个值应当包含对象的特征
-
字符串Hash主要用于快速判断字符串是否相同
-
主要思路
选取恰当的进制,可以把字符串中的字符看成一个大数字中的每一位数字,不过比较字符串和比较 大数字的复杂度并没有什么区别(高精数的比较也是O(n)的),但只要把它对一个数取模,然后认为取模后的结果相等原数就相等,那么就可以在一定的错误率的基础上O(1)进行判断了。 -
如何选取进制?
-
首先不要把任意字符对应到数字0,比如假如把a对应到数字0,那么将不能只从Hash结果上区分ab和b(虽然可以额外判断字符串长度,但不把任意字符对应到数字0更加省事且没有任何副作用),因为00,0000,000000都对应数值0,失去了长度信息
-
对于字符串而言我们可以把字符串看成一个26进制数
-
如果包含其他特殊符号,可看成|S|进制数,其中|S|为字符集大小
-
关于进制的选择实际上非常*,大于所有字符对应的数字的最大值,不要含有模数的质因子(那还模什么),比如一个字符集是a到z的题目,选择27、233、19260817 都是可以的。
-
如何选取模数
-
绝大多数情况下,不要选择一个10^9级别的数,因为这样随机数据都会有Hash冲突,根据生日悖论,随便找上109‾‾‾‾√个串就有大概率出现至少一对Hash 值相等的串(参见BZOJ 3098 Hash Killer II)。
-
稳妥的办法是选择两个10^9级别的质数,只有模这两个数都相等才判断相等,但常数略大,代码相对难写,目前暂时没有办法卡掉这种写法(除了卡时间让它超时)(参见BZOJ 3099 Hash Killer III)。
-
如果能背过或在考场上找出一个10^18级别的质数(Miller-Rabin),也相对靠谱,主要用于前一种担心会超时,后一种担心被卡。
-
偷懒的写法就是直接使用unsigned long long,【自然溢出 】,不手动进行取模,它溢出时会自动对2^64,进行取模,如果出题人比较良心,这种做法也不会被卡,但这个是完全可以卡的,卡的方法参见BZOJ 3097 Hash Killer I。
-
冲突了如何解决?
-
实在不行放大/换一个mod(一个合理的质数)就行
-
如果还是不放心可以开mod个vector或者set进行冲突处理
-
冲突还是严重?利用多关键字Hash
map< pair<ULL,ULL> , set<ULL> >
map< pair<ULL,ULL> ,vector<ULL> >
3、小结:
-
Hash用于判断相等是O(1)的,不仅是对于字符串,对于序列,其他对象也是如此
-
字符串Hash非常好写,可以代替kmp,有时候可以代替SAM,SA,ACAM等字符串相关的高级数据结构,且效率和常数一般要优秀的多
-
自然溢出unsigned long long会提高效率,但会需要map或者离散化进行后续处理复杂度会变成logn 模数最好取较大的质数
4、可以用hash处理的字符串问题:
1、kmp问题
- 给两个字符串S1,S2,求S2是否是S1的子串,并求S2在S1中出现的次数
- 把S2Hash出来,在S1里找所有长度为|S2|的子串,Hash比较。
- 效率 O(|S1|)
2、AC自动机
- 给N个单词串,和一个文章串,求每个单词串是否是文章串的子串,并求每个单词在文章中出现的次数。
- 把每一个单词hash成整数,再把文章的每一个子串hash成整数,接下来只需要进行整数上的查找即可。
- 复杂度:O(|A|2+|S|)
- 用AC自动机可以做到O(|A|+|S|)的复杂度,|S|是单词串总长,|A|是文章长度
3、后缀数组
-
给两个字符串S1,S2,求它们的最长公共子串的长度
-
将S1的每一个子串都hash成一个整数,将S2的每一个子串都hash成一个整数两堆整数,相同的配对,并且找到所表示的字符串长度最大的即可。
-
复杂度:O(|S1|2+|S2|2)
-
用后缀数组可以优化到O(|S|log|S|)
4、马拉车
- 给一个字符串S,求S的最长回文子串。
- 先求子串长度位奇数的,再求偶数的。枚举回文子串的中心位置,然后二分子串的长度,直到找到一个该位置的最长回文子串,不断维护长度最大值即可。
- 复杂度:O(|S|log|S|)
- 用manacher可以做到O(|S|)的复杂度
5、扩展kmp
- 给一个字符串S,求S的每个后缀与S的最长公共前缀
- 枚举每一个后缀的起始位置,二分长度,求出每个后缀与S的最长公共前缀。
- 复杂度:O(|S|log|S|)
- 用extend-kmp可以做到O(|S|)的复杂度
模板一:自然溢出
typedef unsigned long long ULL;
typedef long long LL;
unordered_map<ULL, bool> mp;
const int mx = 1000005;
ULL H[mx], P[mx];
ULL PA = 131;//进制数
int str1[mx], str2[mx];
void Hash(int str[], int len)
{
H[0] = len;
P[0] = 1;
for(int i = 1; i <= len; i++)
{
H[i] = H[i - 1] * PA + str[i] + 1;
P[i] = P[i - 1] * PA;
}
}
ULL code (int l, int r)//获取[L,R]的hash值
{
return H[r] - H[l - 1] * P[r - l + 1];
}
模板二:单模数
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef unsigned long long ull;
ull base=131;
ull a[10010];
char s[10010];
int n,ans=1;
ull mod=19260817;
ull hashs(char s[])
{
int len=strlen(s);
ull ans=0;
for (int i=0;i<len;i++)
ans=(ans*base+(ull)s[i])%mod;
return ans;
}
main()
{
scanf("%d",&n);
for (int i=1;i<=n;i++)
{
scanf("%s",s);
a[i]=hashs(s);
}
sort(a+1,a+n+1);
for (int i=2;i<=n;i++)
if (a[i]!=a[i-1])
ans++;
printf("%d\n",ans);
}
模板三:双模数
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef unsigned long long ull;
ull base=131;
struct data
{
ull x,y;
}a[10010];
char s[10010];
int n,ans=1;
ull mod1=19260817;
ull mod2=19660813;
ull hash1(char s[])
{
int len=strlen(s);
ull ans=0;
for (int i=0;i<len;i++)
ans=(ans*base+(ull)s[i])%mod1;
return ans;
}
ull hash2(char s[])
{
int len=strlen(s);
ull ans=0;
for (int i=0;i<len;i++)
ans=(ans*base+(ull)s[i])%mod2;
return ans;
}
bool comp(data a,data b)
{
return a.x<b.x;
}
main()
{
scanf("%d",&n);
for (int i=1;i<=n;i++)
{
scanf("%s",s);
a[i].x=hash1(s);
a[i].y=hash2(s);
}
sort(a+1,a+n+1,comp);
for (int i=2;i<=n;i++)
if (a[i].x!=a[i-1].x || a[i-1].y!=a[i].y)
ans++;
printf("%d\n",ans);
}
模板四:只用一个10^18质数
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef unsigned long long ull;
ull base=131;
ull a[10010];
char s[10010];
int n,ans=1;
ull mod=212370440130137957ll;
ull hashs(char s[])
{
int len=strlen(s);
ull ans=0;
for (int i=0;i<len;i++)
ans=(ans*base+(ull)s[i])%mod;
return ans;
}
main()
{
scanf("%d",&n);
for (int i=1;i<=n;i++)
{
scanf("%s",s);
a[i]=hashs(s);
}
sort(a+1,a+n+1);
for (int i=2;i<=n;i++)
if (a[i]!=a[i-1])
ans++;
printf("%d\n",ans);
}
例题一:HDU 1711 Number Sequence
Given two sequences of numbers : a[1], a[2], … , a[N], and b[1], b[2], … , b[M] (1 <= M <= 10000, 1 <= N <= 1000000). Your task is to find a number K which make a[K] = b[1], a[K + 1] = b[2], … , a[K + M - 1] = b[M]. If there are more than one K exist, output the smallest one.
Input
The first line of input is a number T which indicate the number of cases. Each case contains three lines. The first line is two numbers N and M (1 <= M <= 10000, 1 <= N <= 1000000). The second line contains N integers which indicate a[1], a[2], … , a[N]. The third line contains M integers which indicate b[1], b[2], … , b[M]. All integers are in the range of [-1000000, 1000000].
Output
For each test case, you should output one line which only contain K described above. If no such K exists, output -1 instead.
Sample Input
2
13 5
1 2 1 2 3 1 2 3 1 3 2 1 2
1 2 3 1 3
13 5
1 2 1 2 3 1 2 3 1 3 2 1 2
1 2 3 2 1
Sample Output
6
-1
题意:
给出串a,串b,问串b在串a中第一次出现的位置
思路:
求出b的哈希值,在串a中枚举串b长度的串,求哈希值,比较
AC代码
#include <iostream>
#include <stdio.h>
#include <string>
#include <string.h>
using namespace std;
typedef unsigned long long ULL;
typedef long long LL;
const int mx = 1000005;
ULL H[mx], P[mx];
ULL PA = 131;
int str1[mx], str2[mx];
void Hash(int str[], int len)
{
H[0] = len;
P[0] = 1;
for(int i = 1; i <= len; i++)
{
H[i] = H[i - 1] * PA + str[i] + 1;
P[i] = P[i - 1] * PA;
}
}
ULL code (int l, int r)
{
return H[r] - H[l - 1] * P[r - l + 1];
}
int main()
{
int t, n, m;
scanf("%d", &t);
while(t--)
{
int flag = 0, cur;
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++)
scanf("%d", &str1[i]);
for(int i = 1; i <= m; i++)
scanf("%d", &str2[i]);
Hash(str2, m);
ULL a2 = code(1, m), a1;
Hash(str1, n);
for(int i = 1; i + m - 1 <= n; i++)
{
a1 = code(i, i + m - 1);
if(a1 == a2)
{
printf("%d\n", i);
flag = 1;
break ;
}
}
if(flag == 0)
printf("-1\n");
}
return 0;
}