【6】哈希学习笔记
前言
哈希是一种常用的数据处理方法,可以牺牲极低的错误率来换取 处理本来需要 处理的东西,例如需要用 来比较两个 的东西。
基础哈希
数值哈希
对于一个较大的数 ,我们考虑通过哈希将其降低为一个较小的值:
其中 为大质数。
如果 ,就可以粗略判断 。
但是这样数字的冲突会很多,容易重复,所以我们考虑优化这个算法。
:双哈希
其中 为大质数且 。
如果 并且 ,那么判断 。
这种做法可以扩展到 模数哈希,可以无限接近于正确,所以我们一般认为这个算法是正确的。
缺点是多个哈希值可能需要再次进行哈希,写起来比较麻烦。
:拉链法(知道有这个东西就行了)
STL哈希
STL 自带哈希表,名字叫 unordered_map
。
定义操作与 map
一样:
unordered_map<int,int>a;
直接访问一个元素:()
a[i]=b;
printf("%d",a[i]);
清空:()
a.clear();
例题 :
使用 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;
}
例题 :
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;
}
数对哈希
对于数对 ,可以通过哈希将其变为一个变量:
其中 为一个极大的数,保证比任何 都大。这样就可以保证 在 不同时值唯一,然后对于这个较大的数可以使用数值哈希。
数对哈希有许多用途,这里介绍几种常用的。
:双关键字排序(操作)
把第一关键字作为 ,第二关键字作为 ,直接按照哈希之后的值排序。由于 所占都权值一定远大于 ,所以会优先按照第一关键字排序。
:图中直接访问边
图中一条边有两个属性:起点 和终点 ,如果图中没有重边,那么这两个属性可以唯一确定一条边。可以把这两个值哈希下来,用数值哈希存储,最后可以直接访问一条边,维护一些信息。
区间哈希
对于一段区间 ,我们令这段区间的哈希值为如下式子:(其中 表示取模)
其中 为自选的底数, 为大质数,且满足 。
如果 ,则表示区间 和 完全相等。
这样的判断方法有极低的错误率,由于模数、底数自选,所以很难被卡掉,可以忽略不计。这一种哈希方法与元素的顺序有关,如果无关,则需要看下一部分的随机赋权哈希。
字符串哈希
类似区间哈希,对于一段字符串 ,我们令这段区间的哈希值为如下式子:(其中 表示取模)
其中 为字符的键值,一般直接使用字符的 ASCII 码。 为自选的底数, 为大质数,且满足 。
如果 ,则表示字符串 和 完全相等。
例题 :
字符串哈希模板题,不多赘述。
#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;
}
例题 :
考虑维护每个字符串的前缀和后缀,设字符串长度为 ,枚举断点 ,统计有多少字符串在 和 之间完全相等。由于没有两个相同的字符串,所以满足条件的这两个字符串有且仅有第 位不同,满足相似的定义。
哈希维护每个字符串的 和 ,然后把这两个哈希值在进行哈希。具体而言,就是如下式子:
其中 为一个极大的数,大于每一个 。这样的哈希方式就会使两个量合成一个量,从而方便比较。
然后统计每种量相同的有多少个,乘法原理统计即可。
#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;
}
随机赋权哈希
集合哈希
集合哈希分为两种,和哈希和异或哈希,这里只讲异或哈希。
对于序列每一个元素值 ,我们将其赋予一个随机权值 。对于一段区间 ,则这一段区间的哈希值 为如下式子:
如果 ,我们可以粗略判断 和 两段区间中包含的各种元素的数量相等。也就是说,把 和 两段区间看作两个集合,这两个集合完全相等。
例题 :
P3560 [POI2013] LAN-Colorful Chain
考虑集合哈希。随机赋权,我们把符合条件的字串的哈希值求出,然后扫描每一个长度等于符合条件的字串的字串,比较两者哈希值是否相等,统计答案。
具体的,设符合条件的字串长度为 ,初始区间为 。顺序遍历数组,每一次区间向后挪动一位时,减去原区间最前面的值,加上新区间最后面的值,保持区间长度不变。
#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;
}
例题 :
为了实现反击,整个图必须是一个环;为了实现连续穿梭,整个图必须每个点出度为 。也就是说,整个图变成了一个每个点出度为 ,没有其余节点的大环。也就是说,每个节点的入度也为 。
显然,这个东西不好用图论来维护。由于是一张图,数据结构也不好做。最后,由于最终是比较两个图(当前图和目标图)是否在一部分上相同,考虑哈希。
由于操作修改的是某个点为终点的虫洞,我们维护入度。对于每一个终点为 的虫洞,赋随机权值 。先将所有 加起来,得到目标图的权值。并加权求出整张图的哈希值。
对于每个点,维护一个 数组,表示以这个点为终点的未被摧毁的虫洞的哈希值的和,并存储数组 表示初始时这个点为终点的未被摧毁的虫洞的哈希值的和。
对于操作 ,由于保证该虫洞存在且未被摧毁,直接将哈希值减少 ,表示 这个虫洞被摧毁,然后将 减少 表示 这个虫洞以 为终点的虫洞被摧毁。
对于操作 ,由于所有终点为 的虫洞被摧毁,直接将 置为 ,并将哈希值减少变化值。
对于操作 ,由于保证该虫洞存在且被摧毁,同 得直接将哈希值增加 ,将 增加 。
对于操作 ,由于所有终点为 的虫洞被修复,直接将 置为初始值 ,并将哈希值增加变化值。
#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;
}
后记
哈希题目的标志:需要 比较两个 的东西。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!