Hash学习笔记
由老师的课件整理而成
目录
- 前言
- 整数hash
2.1 hash冲突
2.2 如果 \(7\) 和 \(19260824\) 都出现了 - 字符串hash
3.1 与顺序有关的hash
3.2 与顺序无关的hash
3.3 与顺序和字母都无关的hash - 例题
- 推荐习题
1.前言
今天学 \(hash\) 直接学蒙,
于是写篇博客来整理一下思绪。
2. 整数hash
作用: 在 \(O(1)\) 时间内判断整数是否出现。
设 \(h(x)\) \(=\) \(x\) % \(P\) ,保证 \(p\) 是个质数且 \(p\) 不是很大。
设 \(F\) 数组
\(F[ h(x) ]\) 来判断 x 是否出现。
2.1 hash冲突
有时候,我们会遇到hash冲突的情况,也就是 \(x\) \(!= y\) 但是 \(h(x) = h(y)\)
如:想判断 \(7\) 是否出现,但是 \(7\) 没有出现,\(19260824\) 出现了,所以 \(F[h(7)]=1\),但实际上 \(7\) 没有出现。
如何解决?
设一 \(G\) 数组。
当 \(x\) 插入的时候,将 \(F[h(x)]\) 置为 \(1\), 同时,我们将 \(G[h(x)]\) 置为 \(x\) 就可以了。
2.2 如果 7 和 19260824 都出现了
第一种方法:
对于 \(x\),希望将其加入到 \(h(x)\) 位置,但是可能 \(h(x)\) 已经被占据,这个时候,
我们构造一个数列 \(S\) ,依次检查 \(S\) 的每一个位置,如果位置上没有被占据,就占据这个位置。
那么 $S = $ { $ h(x) , h(x)+1, h(x)+2, h(x)+3, .. $}
所以 \(S_ 7 =\) { \(h(7), h(7)+1, h(7)+2, h(7)+3, ..\) }
假设 \(19260824\) 已经加入了,也就是说 \(h(7)\) 已经被占据,
那么检查 \(h(7)+1\) 是否已经被占据,如果是,则检查下一个,直到找到一个 \(h(7)+u\) 没有被占据。
找到后,就使 \(F[h(7)+u] = 1\), \(G[h(7)+u] = 7\) 。
代码实现:
void hash(int x)
{
pos=x%p;
while(F[pos]) pos++;
F[pos]=1,G[pos]=x;
}
int judge(int x)
{
pos=x%p;
while(F[pos]&&G[pos]!=x) ++pos;
return F[pos];
}
码风巨丑,勿喷
如果插入二元组 \((x, y)\),
已知 \(F\) 有方程 \(F(x) = F(\sqrt{x})\) \(+F(\sqrt{x}+1)\)。
我们转移需要利用到 \(F\) 的值,但是这个值是离散的,比如我要算 \(F(10^ {12})\),实际上需要用的
\(F\) 值只有 \(10^ 5\),这个时候就可以考虑使用 hash 来存下 \(F\) 的值。
相当于二元组 \((x, F(x))\)。
代码实现:
void hash(x,y)
{
pos=x%p;
while(F[pos]) pos++;
F[pos]=x,G[pos]=y;
}
int judge(int x)
{
pos=x%p;
while(F[pos]&&F[pos]!=x) ++pos;
return G[pos];
}
int f(int x)
{
int t=judge(x);
if(t) return t;
fx = ...//省略
hash(x,fx)
return fx
}
第二种方法:
内置链表,在每一个节点内置一个链表 \((vector)\)。
会快一些,但是难写一些。
代码实现:
void hash(int x,int y)
{
pos=x%p;
v[pos].push_back((x, y));
}
int judge(int x,int y)
{
pos=x%p;
for(int i=0;i<v[pos].size();i++)
if(v[pos][i].first==x)
return v[pos][i].second;
return -1;
}
链表开的空间一般是 \(100\) 倍,如果存 \(10^ 5\) 个数,则数组开到 \(10^ 7\), 否则没有办法保证时间复杂度了。
3. 字符串 hash
3.1 与顺序有关的 hash
需要判断字符 \(S\) 是否出现过(字符必须完全一样)。
第一种做法
可得: \(h(S) = (S[0] + S[1]\) \(×\) \(p + S[2]\) \(×\) \(p^ 2 + S[3] × p^ 3 + ... )\) % \(p_ 2\)
所以判断 \(S\) 是否出现即为判断 \(h(S)\) 是否被占据。
一般数组开到 $min(10^ 7, $ 数据量\(^ 2)\) 。
此做法一般赌脸,脸白便能过。
第二种做法 \((double\) \(hash)\)
几乎不可能被卡,除非你的 \(p_ 2, q_ 2\) 被出题人知道了,或者 \(p_ 2\) 或者 \(q_ 2\) 等于 \(19260817\)。
\(h1(S) = (S[0] + S[1] × p + S[2] × p^ 2 + S[3] × p^ 3 + .. )\) % \(p2\)
\(h2(S) = (S[0] + S[1] × q + S[2] × q^ 2 + S[3] × q^ 3 + .. )\) % \(q2\)
则判断 \(S\) 是否出现即为判断二元组 \((h1(S), h2(S))\) 是否出现。
3.2 与顺序无关的 hash
定义字符串 \(S\) , \(T\) 相似,
相似指当且仅当 \(S\) , \(T\) 内每个出现的字符的出现次数相同。
则 \(S\) 和 \(T\) 相似即为 \(h(S) = h(T)\)。
3.3 与顺序和字母都无关的 hash
定义字符串 \(S\) , \(T\) 相似当且仅当 \(S\), \(T\) 内字符的出现次数集合相同。
例如 \(abab\) 和 \(yyxx\) 相似,因此出现次数集合都为 { \(2,2\) }
4.例题
P3823 [NOI2017] 蚯蚓排队
我们可以在 \(hash\) 表内存 \(F\) , \(G\) ,\(F\) 意义同上,\(G\) 表示出现次数,刚插入时 \(G=1\),之后每次插入的时候,
做一遍查找,如果存在,\(G+1\) ,否则继续插入。
操作1:
因为每次合并最多产生 \(k-1\) 个向后 \(k\) 数字串,
于是我们可以暴力合并,把新产生的向后 \(k\) 数字串存到 \(hash\) 表内。
时间复杂度 : \(O(nk)\)
操作2:
对于分离操作,最多减少 \(k-1\) 个向后 \(k\) 数字串。
我们暴力删除,把减少的向后 \(k\) 数字串在 \(hash\) 中删除(实际上就是让 G-1)。
时间复杂度: \(O(Ck)\)
操作3:
直接在 \(hash\) 表内查询。
优化:
首先把所有的操作都读进来,然后把所有操作需要用到的向后 \(k\) 数字串都
在 \(hash\) 表内保存一个位置,则 \(1, 2\) 操作如果插入或者删除的串 \(hash\) 表内没有,则可以直接跳过。
#include<bits/stdc++.h>
#define ll unsigned long long
using namespace std;
const int N=200000+5,mod=998244353,Mod=19820826,K=55,NK=N*K*2,bb=13;
ll n,t,tot=0;
ll a[N],st[Mod],then[NK],val[NK],cnt[NK],s[K*4],pre[N],suf[N];
char S[10000005];
void hash(ll x,ll y)
{
for(ll i=st[x%Mod];i;i=then[i])
if(val[i]==x)
{
cnt[i]+=y;
return ;
}
then[++tot]=st[x%Mod];
st[x%Mod]=tot;
val[tot]=x;
cnt[tot]=y;
return ;
}
ll qq(ll x)
{
ll res=0;
for(ll i=st[x%Mod];i;i=then[i])
if(val[i]==x)
return cnt[i];
return 0;
}
void cz(ll x,ll y,ll val)
{
ll l=50,r=51;
for(ll i=x;i&&l>=1;i=pre[i],l--)
s[l]=a[i];
for(ll i=y;i&&r<=100;i=suf[i],r++)
s[r]=a[i];
l++;
r--;
for(ll i=l;i<=50;i++)
{
ll ab=0;
for(ll j=i;j<=50;j++)
ab=ab*bb+s[j];
for(ll j=51;j<=r&&j-i+1<=50;j++)
{
ab=ab*bb+s[j];
hash(ab,val);
}
}
return ;
}
int main()
{
ios::sync_with_stdio(false);
cin>>n>>t;
for(ll i=1;i<=n;i++)
{
cin>>a[i];
hash(a[i],1);
}
while(t--)
{
ll op,x,y;
cin>>op;
if(op==1)
{
cin>>x>>y;
cz(x,y,1);
pre[y]=x;
suf[x]=y;
}
if(op==2)
{
cin>>x;
cz(x,suf[x],-1);
pre[suf[x]]=0;
suf[x]=0;
}
if(op==3)
{
ll x;
cin>>S>>x;
y=strlen(S);
ll ans,ab=0,abab=1;
for(ll i=0;i<x;i++)
{
ab=ab*bb+(S[i]-48);
abab*=bb;
}
ans=qq(ab);
for(ll i=x;i<y;i++)
{
ab=ab*bb-(S[i-x]-48)*abab+(S[i]-48);
ans=1ll*ans*qq(ab)%mod;
}
cout<<ans<<endl;
}
}
return 0;
}
5.推荐习题
P3370 【模板】字符串哈希
P7469 [NOI Online 2021 提高组] 积木小赛
P3538 [POI2012]OKR-A Horrible Poem