【6】哈希学习笔记

前言

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

基础哈希

数值哈希

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

\[has_x=x\%mod \]

其中 \(mod\) 为大质数。

如果 \(has_x=has_y\),就可以粗略判断 \(x=y\)

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

\(1\):双哈希

\[has_{x1}=x_1\%mod_1,has_{x2}=x_2\%mod_2 \]

其中 \(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\)

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\times \inf+y \]

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

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

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

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

\(2\):图中直接访问边

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

区间哈希

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

\[has_{[l,r]}=\sum_{i=l}^ra_i\times base^{r-l}\%mod \]

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

如果 \(has_{[l1,r1]}=has_{[l2,r2]}\),则表示区间 \([l1,r1]\)\([l2,r2]\) 完全相等。

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

字符串哈希

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

\[has_{[l,r]}=\sum_{i=l}^rs_i\times base^{r-l}\%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,i-1]\)\([i+1,n]\) 之间完全相等。由于没有两个相同的字符串,所以满足条件的这两个字符串有且仅有第 \(i\) 位不同,满足相似的定义。

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

\[has_{[1,i-1]}\times \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;
}

随机赋权哈希

集合哈希

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

对于序列每一个元素值 \(a_{i}\),我们将其赋予一个随机权值 \(h_{a_{i}}\)。对于一段区间 \([l,r]\),则这一段区间的哈希值 \(has_{[l,r]}\) 为如下式子:

\[has_{[l,r]}=\sum_{i=l}^{r}h_{a_{i}} \]

如果 \(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\) 的虫洞,赋随机权值 \(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)\) 的东西。

哈希题单

posted @ 2025-02-08 14:11  w9095  阅读(93)  评论(0)    收藏  举报