【6】哈希学习笔记
前言
哈希是一种常用的数据处理方法,可以牺牲极低的错误率来换取 \(O(1)\) 处理本来需要 \(O(n)\) 处理的东西,例如需要用 \(O(1)\) 来比较两个 \(O(n)\) 的东西。
基础哈希
数值哈希
对于一个较大的数 \(x\),我们考虑通过哈希将其降低为一个较小的值:
其中 \(mod\) 为大质数。
如果 \(has_x=has_y\),就可以粗略判断 \(x=y\)。
但是这样数字的冲突会很多,容易重复,所以我们考虑优化这个算法。
\(1\):双哈希
其中 \(mod_1,mod_2\) 为大质数且 \(\gcd(mod_1,mod_2)=1\)。
如果 \(has_{x1}=has_{y1}\) 并且 \(has_{x2}=has_{y2}\),那么判断 \(x=y\)。
这种做法可以扩展到 \(n\) 模数哈希,可以无限接近于正确,所以我们一般认为这个算法是正确的。
缺点是多个哈希值可能需要再次进行哈希,写起来比较麻烦。
\(2\):拉链法(知道有这个东西就行了)
STL哈希
STL 自带哈希表,名字叫 unordered_map。
定义操作与 map 一样:
unordered_map<int,int>a;
直接访问一个元素:(\(O(1)\))
a[i]=b;
printf("%d",a[i]);
清空:(\(O(1)\))
a.clear();
例题 \(1\) :
使用 unordered_map,记录每个数字是否出现过,如果出现过就不输出,如果没有出现过就标记出现过并输出。记得多测清空。
#include <bits/stdc++.h>
#include <map>
using namespace std;
unordered_map<int,bool>a;
int t;
int n,b;
int main()
{
scanf("%d",&t);
for(int sum=0;sum<t;sum++)
{
scanf("%d",&n);
a.clear();
for(int i=0;i<n;i++)
{
scanf("%d",&b);
if(a[b]==0)
{
a[b]=1;
printf("%d ",b);
}
}
printf("\n");
}
return 0;
}
例题 \(2\) :
unordered_map(或 map)也能记录字符串,把前面的类型名改掉即可。用 vector 记录每一个单词出现的位置,查询时输出即可。
这里实现的比较复杂。
#include <bits/stdc++.h>
using namespace std;
map<string , vector<int> >a;
long long n,l,m,book[20000];
char str[6000];
int main()
{
scanf("%lld",&n);
for(int i=1;i<=n;i++)
{
long long cnt=0;
int now=0;
char ch;
scanf("%lld",&l);
while(cnt<l)
{
ch=getchar();
if(!(ch>='a'&&ch<='z'))
{
if(now==0)
{
str[0]='\0';
now=0;
continue;
}
cnt++;
str[now]='\0';
a[str].push_back(i);
now=0;
}
else str[now++]=ch;
}
}
scanf("%lld",&m);
for(int i=1;i<=m;i++)
{
scanf("%s",str);
for(int j=0;j<a[str].size();j++)
{
if(!book[a[str][j]])printf("%d ",a[str][j]);
book[a[str][j]]=1;
}
printf("\n");
for(int j=1;j<=n;j++)
book[j]=0;
}
return 0;
}
数对哈希
对于数对 \((x,y)\),可以通过哈希将其变为一个变量:
其中 \(\inf\) 为一个极大的数,保证比任何 \(y\) 都大。这样就可以保证 \(has_{(x,y)}\) 在 \((x,y)\) 不同时值唯一,然后对于这个较大的数可以使用数值哈希。
数对哈希有许多用途,这里介绍几种常用的。
\(1\):双关键字排序(操作)
把第一关键字作为 \(x\),第二关键字作为 \(y\),直接按照哈希之后的值排序。由于 \(x\) 所占都权值一定远大于 \(y\),所以会优先按照第一关键字排序。
\(2\):图中直接访问边
图中一条边有两个属性:起点 \(u\) 和终点 \(v\),如果图中没有重边,那么这两个属性可以唯一确定一条边。可以把这两个值哈希下来,用数值哈希存储,最后可以直接访问一条边,维护一些信息。
区间哈希
对于一段区间 \([l,r]\),我们令这段区间的哈希值为如下式子:(其中 \(\%\) 表示取模)
其中 \(base\) 为自选的底数,\(mod\) 为大质数,且满足 \(\gcd(base,mod)=1\)。
如果 \(has_{[l1,r1]}=has_{[l2,r2]}\),则表示区间 \([l1,r1]\) 和 \([l2,r2]\) 完全相等。
这样的判断方法有极低的错误率,由于模数、底数自选,所以很难被卡掉,可以忽略不计。这一种哈希方法与元素的顺序有关,如果无关,则需要看下一部分的随机赋权哈希。
字符串哈希
类似区间哈希,对于一段字符串 \(s[l,r]\),我们令这段区间的哈希值为如下式子:(其中 \(\%\) 表示取模)
其中 \(s\) 为字符的键值,一般直接使用字符的 ASCII 码。\(base\) 为自选的底数,\(mod\) 为大质数,且满足 \(\gcd(base,mod)=1\)。
如果 \(has_{[l1,r1]}=has_{[l2,r2]}\),则表示字符串 \([l1,r1]\) 和 \([l2,r2]\) 完全相等。
例题 \(3\) :
字符串哈希模板题,不多赘述。
#include <bits/stdc++.h>
using namespace std;
long long n,hash1[100007],ans=0;
char str[10000];
int string_hash(char str[])
{
long long base=3319,l=strlen(str);
long long h=0;
for(int i=0;i<l;i++)
h=h*base+(long long)str[i],h%=(long long)10000000007;
return h;
}
int main()
{
scanf("%d",&n);
for(int i=0;i<n;i++)
{
scanf("%s",str);
hash1[i]=string_hash(str);
}
sort(hash1,hash1+n);
for(int i=0;i<n-1;i++)
if(hash1[i]!=hash1[i+1])ans++;
printf("%d",ans+1);
return 0;
}
例题 \(4\) :
考虑维护每个字符串的前缀和后缀,设字符串长度为 \(n\),枚举断点 \(i\),统计有多少字符串在 \([1,i-1]\) 和 \([i+1,n]\) 之间完全相等。由于没有两个相同的字符串,所以满足条件的这两个字符串有且仅有第 \(i\) 位不同,满足相似的定义。
哈希维护每个字符串的 \([1,i-1]\) 和 \([i+1,n]\),然后把这两个哈希值在进行哈希。具体而言,就是如下式子:
其中 \(\inf\) 为一个极大的数,大于每一个 \(has_{[i+1,n]}\)。这样的哈希方式就会使两个量合成一个量,从而方便比较。
然后统计每种量相同的有多少个,乘法原理统计即可。
#include <bits/stdc++.h>
using namespace std;
int n,l,s,ans=0;
unsigned long long z[40000][400],y[40000][400],w[40000];
char str[40000][400];
int main()
{
scanf("%d%d%d",&n,&l,&s);
for(int i=1;i<=n;i++)scanf("%s",str[i]+1);
for(int i=1;i<=n;i++)
for(int j=1;j<=l;j++)
z[i][j]=z[i][j-1]*2333+(unsigned long long)str[i][j];
for(int i=1;i<=n;i++)
for(int j=l;j>=1;j--)
y[i][j]=y[i][j+1]*2333+(unsigned long long)str[i][j];
for(int i=1;i<=l;i++)
{
for(int j=1;j<=n;j++)
w[j]=z[j][i-1]*300000000000+y[j][i+1];
sort(w+1,w+n+1);
int cnt=1;
for(int j=1;j<=n;j++)
if(w[j]!=w[j-1])ans+=cnt*(cnt-1)/2,cnt=1;
else cnt++;
if(cnt!=1)ans+=cnt*(cnt-1)/2;
}
printf("%d",ans);
return 0;
}
随机赋权哈希
集合哈希
集合哈希分为两种,和哈希和异或哈希,这里只讲异或哈希。
对于序列每一个元素值 \(a_{i}\),我们将其赋予一个随机权值 \(h_{a_{i}}\)。对于一段区间 \([l,r]\),则这一段区间的哈希值 \(has_{[l,r]}\) 为如下式子:
如果 \(has_{[l1,r1]}=has_{[l2,r2]}\),我们可以粗略判断 \([l1,r1]\) 和 \([l2,r2]\) 两段区间中包含的各种元素的数量相等。也就是说,把 \([l1,r1]\) 和 \([l2,r2]\) 两段区间看作两个集合,这两个集合完全相等。
例题 \(5\) :
P3560 [POI2013] LAN-Colorful Chain
考虑集合哈希。随机赋权,我们把符合条件的字串的哈希值求出,然后扫描每一个长度等于符合条件的字串的字串,比较两者哈希值是否相等,统计答案。
具体的,设符合条件的字串长度为 \(sum\),初始区间为 \([1,sum]\)。顺序遍历数组,每一次区间向后挪动一位时,减去原区间最前面的值,加上新区间最后面的值,保持区间长度不变。
#include <bits/stdc++.h>
using namespace std;
long long n,m,c[2000000],l[2000000],a[2000000],cnt=0,sum=0;
unsigned long long q[2000000],now=0,ans=0;
void has()
{
srand(33191024);
for(int i=1;i<=n;i++)q[a[i]]=rand();
for(int i=1;i<=m;i++)q[l[i]]=rand();
for(int i=1;i<=m;i++)ans+=q[l[i]]*c[i],sum+=c[i];
}
int main()
{
scanf("%lld%lld",&n,&m);
for(int i=1;i<=m;i++)scanf("%lld",&c[i]);
for(int i=1;i<=m;i++)scanf("%lld",&l[i]);
for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
has();
for(int i=1;i<=n;i++)
{
now+=q[a[i]];
if(i>sum)now-=q[a[i-sum]];
if(now==ans)cnt++;
}
printf("%lld\n",cnt);
return 0;
}
例题 \(6\) :
为了实现反击,整个图必须是一个环;为了实现连续穿梭,整个图必须每个点出度为 \(1\)。也就是说,整个图变成了一个每个点出度为 \(1\),没有其余节点的大环。也就是说,每个节点的入度也为 \(1\)。
显然,这个东西不好用图论来维护。由于是一张图,数据结构也不好做。最后,由于最终是比较两个图(当前图和目标图)是否在一部分上相同,考虑哈希。
由于操作修改的是某个点为终点的虫洞,我们维护入度。对于每一个终点为 \(i\) 的虫洞,赋随机权值 \(h_i\)。先将所有 \(h_i\) 加起来,得到目标图的权值。并加权求出整张图的哈希值。
对于每个点,维护一个 \(sum\) 数组,表示以这个点为终点的未被摧毁的虫洞的哈希值的和,并存储数组 \(s\) 表示初始时这个点为终点的未被摧毁的虫洞的哈希值的和。
对于操作 \(1\),由于保证该虫洞存在且未被摧毁,直接将哈希值减少 \(q_u\),表示 \(u\to v\) 这个虫洞被摧毁,然后将 \(sum_v\) 减少 \(q_u\) 表示 \(u\to v\) 这个虫洞以 \(v\) 为终点的虫洞被摧毁。
对于操作 \(2\),由于所有终点为 \(u\) 的虫洞被摧毁,直接将 \(sum_u\) 置为 \(0\),并将哈希值减少变化值。
对于操作 \(3\),由于保证该虫洞存在且被摧毁,同 \(1\) 得直接将哈希值增加 \(q_u\),将 \(sum_v\) 增加 \(q_u\)。
对于操作 \(4\),由于所有终点为 \(u\) 的虫洞被修复,直接将 \(sum_u\) 置为初始值 \(s\),并将哈希值增加变化值。
#include <bits/stdc++.h>
using namespace std;
int n,m,k,op,u,v;
unsigned long long q[600000],sum[600000],s[600000],now=0,ans=0;
void init()
{
srand(33191024);
for(int i=1;i<=n;i++)q[i]=(unsigned long long)rand();
}
int main()
{
scanf("%d%d",&n,&m);
init();
for(int i=1;i<=m;i++)scanf("%d%d",&u,&v),sum[v]+=q[u],s[v]=sum[v];
for(int i=1;i<=n;i++)ans+=q[i],now+=sum[i];
scanf("%d",&k);
for(int i=1;i<=k;i++)
{
scanf("%d",&op);
if(op==1)
{
scanf("%d%d",&u,&v);
now-=q[u],sum[v]-=q[u];
}
else if(op==2)
{
scanf("%d",&u);
now-=sum[u],sum[u]=0;
}
else if(op==3)
{
scanf("%d%d",&u,&v);
now+=q[u],sum[v]+=q[u];
}
else if(op==4)
{
scanf("%d",&u);
now+=(s[u]-sum[u]),sum[u]=s[u];
}
if(ans==now)printf("YES\n");
else printf("NO\n");
}
return 0;
}
后记
哈希题目的标志:需要 \(O(1)\) 比较两个 \(O(n)\) 的东西。

浙公网安备 33010602011771号