【6】哈希学习笔记

前言

哈希是一种常用的数据处理方法,可以牺牲极低的错误率来换取 O(1) 处理本来需要 O(n) 处理的东西,例如需要用 O(1) 来比较两个 O(n) 的东西。

基础哈希

数值哈希

对于一个较大的数 x,我们考虑通过哈希将其降低为一个较小的值:

hasx=x%mod

其中 mod 为大质数。

如果 hasx=hasy,就可以粗略判断 x=y

但是这样数字的冲突会很多,容易重复,所以我们考虑优化这个算法。

1:双哈希

hasx1=x1%mod1,hasx2=x2%mod2

其中 mod1,mod2 为大质数且 gcd(mod1,mod2)=1

如果 hasx1=hasy1 并且 hasx2=hasy2,那么判断 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

P4305 [JLOI2011] 不重复数字

使用 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

P3879 [TJOI2010] 阅读理解

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),可以通过哈希将其变为一个变量:

has(x,y)=x×inf+y

其中 inf 为一个极大的数,保证比任何 y 都大。这样就可以保证 has(x,y)(x,y) 不同时值唯一,然后对于这个较大的数可以使用数值哈希。

数对哈希有许多用途,这里介绍几种常用的。

1:双关键字排序(操作)

把第一关键字作为 x,第二关键字作为 y,直接按照哈希之后的值排序。由于 x 所占都权值一定远大于 y,所以会优先按照第一关键字排序。

2:图中直接访问边

图中一条边有两个属性:起点 u 和终点 v,如果图中没有重边,那么这两个属性可以唯一确定一条边。可以把这两个值哈希下来,用数值哈希存储,最后可以直接访问一条边,维护一些信息。

区间哈希

对于一段区间 [l,r],我们令这段区间的哈希值为如下式子:(其中 % 表示取模)

has[l,r]=i=lrai×baserl%mod

其中 base 为自选的底数,mod 为大质数,且满足 gcd(base,mod)=1

如果 has[l1,r1]=has[l2,r2],则表示区间 [l1,r1][l2,r2] 完全相等。

这样的判断方法有极低的错误率,由于模数、底数自选,所以很难被卡掉,可以忽略不计。这一种哈希方法与元素的顺序有关,如果无关,则需要看下一部分的随机赋权哈希。

字符串哈希

类似区间哈希,对于一段字符串 s[l,r],我们令这段区间的哈希值为如下式子:(其中 % 表示取模)

has[l,r]=i=lrsi×baserl%mod

其中 s 为字符的键值,一般直接使用字符的 ASCII 码。base 为自选的底数,mod 为大质数,且满足 gcd(base,mod)=1

如果 has[l1,r1]=has[l2,r2],则表示字符串 [l1,r1][l2,r2] 完全相等。

例题 3

P3370 【模板】字符串哈希

字符串哈希模板题,不多赘述。

#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

P4503 [CTSC2014] 企鹅 QQ

考虑维护每个字符串的前缀和后缀,设字符串长度为 n,枚举断点 i,统计有多少字符串在 [1,i1][i+1,n] 之间完全相等。由于没有两个相同的字符串,所以满足条件的这两个字符串有且仅有第 i 位不同,满足相似的定义。

哈希维护每个字符串的 [1,i1][i+1,n],然后把这两个哈希值在进行哈希。具体而言,就是如下式子:

has[1,i1]×inf+has[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;
}

随机赋权哈希

集合哈希

集合哈希分为两种,和哈希和异或哈希,这里只讲异或哈希。

对于序列每一个元素值 ai,我们将其赋予一个随机权值 hai。对于一段区间 [l,r],则这一段区间的哈希值 has[l,r] 为如下式子:

has[l,r]=i=lrhai

如果 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

P8819 [CSP-S 2022] 星战

为了实现反击,整个图必须是一个环;为了实现连续穿梭,整个图必须每个点出度为 1。也就是说,整个图变成了一个每个点出度为 1,没有其余节点的大环。也就是说,每个节点的入度也为 1

显然,这个东西不好用图论来维护。由于是一张图,数据结构也不好做。最后,由于最终是比较两个图(当前图和目标图)是否在一部分上相同,考虑哈希。

由于操作修改的是某个点为终点的虫洞,我们维护入度。对于每一个终点为 i 的虫洞,赋随机权值 hi。先将所有 hi 加起来,得到目标图的权值。并加权求出整张图的哈希值。

对于每个点,维护一个 sum 数组,表示以这个点为终点的未被摧毁的虫洞的哈希值的和,并存储数组 s 表示初始时这个点为终点的未被摧毁的虫洞的哈希值的和。

对于操作 1,由于保证该虫洞存在且未被摧毁,直接将哈希值减少 qu,表示 uv 这个虫洞被摧毁,然后将 sumv 减少 qu 表示 uv 这个虫洞以 v 为终点的虫洞被摧毁。

对于操作 2,由于所有终点为 u 的虫洞被摧毁,直接将 sumu 置为 0,并将哈希值减少变化值。

对于操作 3,由于保证该虫洞存在且被摧毁,同 1 得直接将哈希值增加 qu,将 sumv 增加 qu

对于操作 4,由于所有终点为 u 的虫洞被修复,直接将 sumu 置为初始值 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) 的东西。

哈希题单

posted @   w9095  阅读(16)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!
点击右上角即可分享
微信分享提示