前置概念
Key : 我们提供的一个要进行哈希的数字
\(f(x)\):即为哈希函数,将key扔到这个函数里面,可以得到Value,最核心的构造哈希表的东西
Hash地址:hash出来的值在哈希表中的存储位置
进入正题
字符串hash
例题1:【模板】KMP
现有T组数据,每次给定两个字符串\(s1\text{和}s2\),求\(s1\text{在}s2\)中出现了几次。
首先考虑的当然是KMP了(逃
但是由于我们讲的是字符串hash,那就考虑怎么用字符串hash求解;
考虑每次枚举每一个子串的hash值,但是复杂度.....\(O(nm)\)
所以介绍一个优化技巧:滚动hash
滚动hash
滚动hash的诞生就是为了避免在\(O(m)\)的时间复杂度内计算一个长度为m的字符串的hash值:
我们选取两个合适的互质常数(虽然不知道为什么互质)b和h,对于字符串c,我们搞一个hash函数:
\(hash(c)=(c_1b^{m-1}+c_2b^{m-2}+.....+c_mb^0)mod h\)
这个hash函数的构造过程是以递推实现的,设
\(hash(c,k)\)为前k个字符构成的子串的hash值,有
\(hash(c,k)=hash(c,k-1)\times b+c_{k}\)
为方便理解,设\(c="ABCD"\)且\(A=1,B=2....\)则
\(hash(c,2)=1\times b+2\)
\(hash(c,3)=1 \times b^2+2 \times b +3\)
\(hash(c,4)=1\times b^3+2 \times b^2+3\times b+4\)
对于c的子串\(c'=c_{k+1}c_{k+2}....c_{k+n}\),有:
\(hash(c')=hash(c,k+n)-hash(c,k)\times b^n\)
很像前缀和是不是?
也很像b进制转十进制是不是?
某位老师说过,探究新知的最好方法就是特值代入法,所以如果大家拿上面的那个例子来稍微做一下运算,就能很好地理解滚动hash这个优化方法了。
举个例子:
如果我们想求上面那个例子的子串\("CD"\)的hash值,那么根据这个公式,就是:
\(hash("CD")=hash(4)-hash(2)\times b^2\)
而\(hash(2)\times b^2 = 1\times b^3+2\times b^2\),
所以,原式\(=3\times b+4\)
这很像我们有一个b进制数1234要转成十进制,而上面所做的就是把1234中的12给杀掉,只留下34,再转成十进制就OK了
所以,如果我们预处理出\(b^n\),就可以做到在\(O(1)\)的时间复杂度内get到任意子串的hash值,所以上面那道例题的时间复杂度就成功地降到了\(O(n+m)\)。
但是有些细心的同学会发现,如果某两个子串的hash值撞车了怎么办呢?那么可以考虑double_hash,也就是将一个hash值取模两次,书本上说:可以将h分别取\(10^9+7\)和\(10^9+9\),因为他们是一对“孪生质数”,虽然我也不知道这是什么意思
(提醒:要开成unsigned long long,据说是为了自然溢出,省去取模运算)
哈希表
大概就是这样子一个东西。
那这个东西有什么用呢?
假设我们要将中国每个人的身份证号映射到每个人的头上
如果有一个人的身份证号xxxxxx19621011XXXX
这是一个18位数!!!!(难道你要弄一个数组存??)
经过计算,\(1390000000/10^4=13900\),即至少有13900人的身份证后四位是一样的
所以我们可以将所有身份证后四位相同的人装到一个桶里面,这个桶的编号就是这个人身份证的后四位,这就是哈希表,主要目的就是为了解决哈希冲突,即F(key)的数值发生重复的情况。
如上面的那个身份证号,我们可以考虑:
故,哈希表就是将\(F(key)\)作为key的哈希地址的一种数据结构。
哈希的某些方法
直接定址法 :地址集合 和 关键字集合大小相同
数字分析法 :根据需要hash的 关键字的特点选择合适hash算法,尽量寻找每个关键字的 不同点
平方取中法:取关键字平方之后的中间极为作为哈希地址,一个数平方之后中间几位数字与数的每一位都相关,取得位数由表长决定。比如:表长为512,=2^9,可以取平方之后中间9位二进制数作为哈希地址。
折叠法:关键字位数很多,而且关键字中每一位上的数字分布大致均匀的时候,可以采用折叠法得到哈希地址,
除留取余法:除P取余,可以选P为质数,或者不含有小于20的质因子的合数
随机数法:通常关键字不等的时候采用此法构造哈希函数较恰当。
但是这些东西貌似都是形式上的,具体怎么操作还是得靠实现
哈希表的实现
听课的同学里面有多少人写过图/最短路等算法呢?
图的存储有两种方法:
-
邻接矩阵
-
邻接表
在这里我们用邻接表来实现。
void add(int a,int b,int c){ dt[cnt].from=a; dt[cnt].to=b; dt[cnt].value=c; dt[cnt].next=head[a]; head[a]=cnt++; }
这是邻接表。
void add(int a,int b){ dt[cnt].end=b; dt[cnt].next=head[a]; head[a]=cnt++; }
这是哈希表。
很像有木有???
在这里\(a,b\)是我们用double_hash取出来的,取两个不同的模数,两个\(F(key)\)决定一个字符串。
唯一不同的是head数组的下标是\(key1\)。
其实要不要这么做随你。
如果我们要遍历一个哈希表?
同样,
for(int i=head[x];i;i=dt[i].next){ ....... }
跟遍历邻接表一模一样。
hash表中hash函数的确定
如果是一个数的话,上面讲过。(好像用离散化就行了)
如果是一个字符串的话,用前面的滚动hash就可以了。
分两种情况:
如果你不想用double_hash:
那你也不需要把\(key1\)作为head的下标了。
那就直接unsigned ll乱搞吧,自然溢出
如果你要用double_hash:
那你需要把\(key1\)作为head的下标。
这时候你不能ull了,,那就弄那个什么孪生质数取模吧。
b记得开小一点,最好算一算。
例题2:图书管理
图书馆要搞一个系统出来,支持两种操作:
add(s):表示新加入一本书名为s的书。
find(s):表示查询是否存在一本书名为s的书。
对于每个find操作,输出一行yes或no。书名与指令之间有空格隔开,书名可能有一大堆空格,对于相同字母但大小写不同的书名,我们认为它是不同的。
【样例输入】
4
add Inside C#
find Effective Java
add Effective Java
fine Effective Java
【样例输出】
no
yes
【题目分析】
这题是哈希表的一个变式,判断一个字符串是否已经出现
可以用滚动hash搞哈希表,采用double_hash
伪代码(不知道算不算):
void add(int a,int b){ ..... } int find(int a,int b){ for(int i=head[a];i;i=next[i]){ if(value[i]==b)true; } false; } int main(){ while(n--){ cin>>order; gets(s); for(i=0;i<len;i++){ key1=(key1*b1+s[i])%mod1; key2=(key2*b2+s[i])%mod2; } if(add)add(key1,key2); else{ if(find(key1,key2))yes; else no; } } }
这题还算简单。
例题3 [LuoguP3498&POI2010]Beads
Jbc买了一串车挂饰装扮自己,上有n个数字。它想要把挂饰扔进发动机里切成\(k\)串。如果有n mod k !=0,则最后一段小于k的可以直接舍去。而且如果有子串\((1,2,3)\)或\((3,2,1)\),Jbc就会认为这两个子串是一样的。Jbc想要多样的挂饰,所以Jbc想要找到一个合适的\(k\),使得它能得到不同的子串最多。
例如:这一串挂饰是:\((1,1,1,2,2,2,3,3,3,1,2,3,3,1,2,2,1,3,3,2,1)\),
\(k=1\)的时候,我们得到3个不同的子串: $(1),(2),(3) $
\(k=2\)的时候,我们得到6个不同的子串: $(1,1),(1,2),(2,2),(3,3),(3,1),(2,3) $
\(k=3\)的时候,我们得到5个不同的子串: \((1,1,1),(2,2,2),(3,3,3),(1,2,3),(3,1,2)\)
\(k=4\)的时候,我们得到5个不同的子串: \((1,1,1,2),(2,2,3,3),(3,1,2,3),(3,1,2,2),(1,3,3,2)\)
【输入格式】
第一行一个整数n,第二行接n个数字。
【输出格式】
第一行2个正整数,表示能获得的最大不同子串个数以及能获得最大值的k的个数。第二行输出所有的k。
【数据范围】
\(n\le 200000\)
\(1\le a_i\le n\)
【样例输入】
21
1 1 1 2 2 2 3 3 3 1 2 3 3 1 2 2 1 3 3 2 1
【样例输出】
6 1
2
【题目分析】
考虑最暴力的方法:
枚举k,枚举每一个子串,从前往后、从后往前各扫一遍。
所以我们就碰到了和字符串hash一样的问题:
枚举每一个数复杂度有点高啊啊啊啊啊
为了避免在\(O(k)\)的复杂度内枚举每一个子串,我们采用滚动hash(好像跟前面引述滚动hash的时候有点像)
预处理出正着跑的hash值以及反着跑的hash值。
枚举每一个子串,将正的hash值和反的hash值乘起来。
然后再扔到set里,因为我们知道set的特性:如果set里面有两个相同的数就会自动删除。
最后再弄一个小根堆,如果当前k能够获得当前最大值,就扔进小根堆里,否则将这个小根堆清空,再扔k。
然后呢?
没有然后了。
#include<bits/stdc++.h> #define ull unsigned long long using namespace std; ull n,a[1010101],power[1010101]; ull hash[1010101],hashback[1010101],ans=0; set<ull>ba; priority_queue<ull,vector<ull>,greater<ull> >gas; const ull b=1926; ull dash(ull i){ ba.clear(); for(ull j=1;j+i-1<=n;j+=i){ ull cas1=hash[j+i-1]-hash[j-1]*power[i]; ull cas2=hashback[j]-hashback[j+i]*power[i]; ba.insert(cas1*cas2); } return (ull)ba.size(); } int main(){ cin>>n; for(ull i=1;i<=n;i++){ cin>>a[i]; } power[0]=1; for(ull i=1;i<1000000;i++) power[i]=power[i-1]*b; for(ull i=1;i<=n;i++) hash[i]=hash[i-1]*b+a[i]; for(ull i=n;i>=1;i--) hashback[i]=hashback[i+1]*b+a[i]; /* for(ull i=1;i<=n;i++) cout<<hash[i]<<" "; cout<<endl; for(ull i=n;i;i--) cout<<hashback[i]<<" "; cout<<endl; cout<<hash[3]-hash[1]*power[2]<<" "<<b*b+b+1<<endl; cout<<hashback[n-2]-hashback[n+1]*power[3]<<endl;*/ for(ull i=1;i<=n;i++){ ull cnt=dash(i); if(cnt>ans){ ans=cnt; while(!gas.empty())gas.pop(); } if(cnt==ans)gas.push(i); } cout<<ans<<" "<<gas.size()<<endl; for(;!gas.empty();){ cout<<gas.top()<<" "; gas.pop(); } }
讲完了
祝大家身体健康
参考:信息学奥赛一本通 提高篇
来源:https://www.cnblogs.com/ironwheel/p/10510409.html