- 引例
\(N\)个正整数,\(M\)个询问,询问数字是否在\(N\) 个正整数中
建立hashtable[10010],hashtable[x] = true表示出现过,在读入时进行预处理
N个正整数,M个询问,询问数字在N个正整数中出现的次数
预处理hashtable[x]++
上面的共同点:将输入的数作为数组下标对这个数的性质进行统计
但是当规模过大,就不行,或者输入的是字符串,也不能用下标直接表示。所以引入了hash
散列(hash):将元素key通过一个函数H转为整数H(key),使得该整数为尽量唯一代表这个元素
- 当key为整数时:可以直接把key作为数组下标,或者用map函数实现映射,也可以自己设计映射函数
h(key)=key%p
,p是一个质数h(key)=a*key+b
当key不为整数时:
比如用一个整数表示二维坐标\((x,y),0\le{x},y\le{range}\) ,则可以设计映射函数为\(H[p]=x*range+y\) ,那么在定义域内,两个顶点的\(H[p]\) 不会相同,可以用这个整数唯一表示这个坐标。
!!一定记住可以设计一个这样的映射函数来表示。然后写一个函数,来进行转换!!!
int H(int x, int y);
Hash 表
hash表又叫散列表,由hash函数和链表结构构成。
hash本质是建立key到index的映射。
但是由于值域变简单,可能容易造成冲突,即不同的key经过hash之后映射到同一个index中。
解决冲突:开散列
- 以hash函数的值域建立邻接表。每个值是邻接表,然后把映射到相同值的不同的key都加在这条链表里面。
//值域0到cnt-1 vector<int> p[cnt]; //p[i]是一个邻接表
- 步骤
- 计算hash函数的值
- 定位到对应链表进行遍历比较
例子:
在一个长度为\(N\)的随机整数序列\(A\)中统计每个数出现次数
- 设计hash函数\(H(x)=x\%p+1\) ,\(p\)为最接近\(N\)的质数。所以值域缩小到了\(p\) , 那么定位到链表为\(H(A[i])\)
元组\(a_1,a_2,a_3,..,a_6\) 和元组\(a_2,a_3,...,a_6,a_1\) 是相等的。求\(N\)个元组中是否存在相同的元组。两个元组相同,当前仅当从某个角开始顺序针或者逆时针,能得到相同的元组
- 对于两片形状相同的雪花,六个角长度之和,长度之积都相等,则其hash值也相等。
- 令\(H(a_1,a_2,...,a_6)=(\sum{a+\prod{a}})\%P\) ,\(P\) 取最接近\(N\) 的质数为99991
- 枚举每一个元组,计算hash函数值,定位到对应的链表。
- 如果链表为空,说明现在还没有相同的元组,则执行插入(插入的是结点编号\(i\))
- 如果不为空,说明存在hash相同的元组,但是hash值相同,不代表元组相同,还要进一步遍历到那个位置,然后判断这两个元组是否相同。
const int maxn = 100010;
const int p = 99991; //取较大的质数
int n, snow[maxn][6];
vector<int> h[p]; //hash表
int H(int a[]){ //计算hash值
int sum = 0, mul = 1;
for(int i=0;i<6;i++){
sum = (sum + a[i]) % p;
mul = (long long)mul * a[i] % p;
}
return (sum+mul)%p;
}
for(int i=0;i<n;i++){
int val = H(snow[i]); //计算hash值,以hash值为邻接表的表头
for(unsigned int j=0;j<h[val].size();j++){ //链表不为空,则存在hash值相同的,进一步判断
int k = h[val][j];
if(equal(snow[k], snow[i])){ //判断是否相同
printf("Twin snowflakes found.\n");
return 0;
}
}
h[val].push_back(i); //链表为空,则插入
}
printf("No two snowflakes are alike.\n");
字符串hash
- 字符串Hash:将一个任意长度的字符串映射为一个非负整数,冲突概率几乎为0
步骤:
- 取一固定值P,把字符串看成P进制数,并分配一个大于0的数值,代表每个字符
- 对于小写字母构成的字符串,令\(a=1,b=2,...,z=26\)
- P一般取1331,13331
- 取一固定值M,求出该P进制数对M的余数,作为该字符串的Hash值
- \(M=2^{64}\) 即直接使用
unsigned long long
类型存储hash值,溢出时会自动对\(2^{64}\) 取模 - 避免低效的mod运算
- \(M=2^{64}\) 即直接使用
- 对字符串的各种操作,都可以对P进制数进行算术运算反映到Hash值上
操作
- \(S="abc"\) , 则\(H(S)=1*P^2+2*P+3\)
- 即:写个求hash值的函数
- \(S\)的Hash值\(H(S)\), 在\(S\)末尾添加一个字符\(c\) ,则\(H(S+c)=(H(S)*P+value[c])\%M\)
- 相当于把数左移一位,乘以\(P\) ,再加上字符的表示值,取模
- 已知\(S,S+T\)的hash值,则\(H(T)=(H(S+T)-H(S)*P^{len(T)})\%M\)
- 相当于把\(S\)左移\(len(T)\) ,后面补0,然后对齐以后做减法
可以在\(O(N)\)内预处理字符串的所有前缀hash值,从而在\(O(1)\)内查询任意子串hash值
设字符串为\(S\),区间为\([0,len-1]\),\(f[i]\) 表示\([0,i]\) 的hash值
unsigned long long f[maxn];
- \(value[s[0]]=s[0]-"a"+1\)
- \(f[0]=value[s[0]]\)
- \(f[i]=f[i-1]*P+value[s[i]]\)
- 可以得到任意区间[l,r]的hash值为: \(f[r]-f[l-1]*P^{len(r-l+1)}\)
当两个区间的hash相等,则可以认为两个子串相同
含有26个小写字母的字符串s,每次选择两个区间,q个询问两个区间中的子串是否相同
\(1\le{len(s),q}\le{10^6}\)
const int maxn = 1e6+10;
char s[maxn];
unsigned long long f[maxn]; //保存hash值
unsigned long long p[maxn]; //预处理,保存p的幂次的值
int s,q,l1,r1,l2,r2;
int main(){
cin>>s>>q;
int len = strlen(s);
f[0] = s[0]-'a'+1;
p[0] = 1;
for(int i=1;i<len;i++){
f[i] = f[i-1]*131+s[i]-'a'+1;
p[i] = p[i-1]*131; //131^i
}
for(int i=1;i<=q;i++){
cin>>l1>>r1>>l2>>r2;
if(f[r1]-f[l1-1]*p[r1-l1+1] == f[r2]-f[l2-1]*p[r2-l2+1]){
puts("yes");
}else{
puts("no");
}
}
}
给出\(N\)个只有小写字母的字符串,求不同的字符串的个数
set<string> s;
map<string, int> mp;
typedef unsigned long long ull; ull hashfunc(string str){ ull ans = 0; for(int i=0;i<str.length();i++){ ans = ans * p + str[i]-'a'; } return ans; } ans.push_back(hashfunc(str)); //对于重复的字符串,进行排序,再统计!!!!!!!重复元素的统计方法!!! for(int i=0;i<ans.size();i++){ if(i==0||ans[i]!=ans[i-1]) cnt++; }
输入两个长度不超过1000的字符串,求它们的最长公共子串的长度
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
const int maxn = 1e4+10;
char s1[maxn],s2[maxn];
ull p[maxn], f1[maxn],f2[maxn];
ull cal(int l, int r, ull f[]){ //返回hash值
return f[r] - f[l-1]*p[r-l+1];
}
struct node{
int l,r;
ull val;
};
vector<node> pr1,pr2; //保存hash值,边界
bool cmp(node x, node y){ //排序
int a = (x.r-x.l);
int b = (y.r-y.l);
if(a!=b) return a>b;
if(x.r != y.r) return x.r < y.r;
return x.l < y.l;
}
int main(){
scanf("%s%s",s1,s2);
int len1 = strlen(s1);
int len2 = strlen(s2);
//预处理
p[0] = 1; f1[0] = s1[0]; f2[0] = s2[0];
for(int i=1;i<len1;i++){
f1[i] = f1[i-1]*131+s1[i];
p[i] = p[i-1]*131;
}
for(int i=1;i<len2;i++){
f2[i] = f2[i-1]*131+s2[i];
p[i] = p[i-1]*131;
}
//保存两个字符串的所有子串的hash值
for(int i=0;i<len1;i++){
for(int j=i;j<len1;j++){
node tmp = {i,j,cal(i,j,f1)};
pr1.push_back(tmp);
}
}
for(int i=0;i<len2;i++){
for(int j=i;j<len2;j++){
node tmp = {i,j,cal(i,j,f2)};
pr2.push_back(tmp);
}
}
//从大到小排序
sort(pr1.begin(), pr1.end(), cmp);
//遍历两个数组,如果值相同,就保存,并break
int l,r;
for(int i=0;i<pr1.size();i++){
bool flag = false;
for(int j=0;j<pr2.size();j++){
if(pr1[i].val == pr2[j].val){
l = pr1[i].l;
r = pr1[i].r;
flag = true;
break;
}
}
if(flag) break;
}
//输出答案
for(int i=l;i<=r;i++){
printf("%c",s1[i]);
}
printf("\n");
}
//算法笔记中的代码
typedef long long ll;
const int mod = 1e9+7;
const int p = 1e7+19;
ll hashfunc(string str){
ll ans = 0;
for(int i=0;i<str.length();i++){
ans = (ans * p + str[i]-'a') % mod;
}
}