哈希

update&勘误:不要写hs[i]=hs[i-1]*B+(s[i]-'a'),这样的话aaa的哈希值就一样了,更容易哈希冲突。很抱歉之前的写法有误。

由于当时本人没有意识到这个问题,所以代码写的全部是错误写法,在此说明,代码不作改动。


哈希是一个很神奇的东西。那么这一块呢,给咱同学做个说明。

一.字符串哈希

把一个字符串映射到一个整数的函数称作哈希函数,映射到的这个整数就是这个字符串的哈希值。

一般采用多项式哈希函数求字符串哈希值,即

屏幕截图 2025-07-08 132730

屏幕截图 2025-07-08 132853

当然毕竟哈希是把大范围映射到小范围里,难免会有哈希冲突,即两个不同的字符串的哈希值相等。双哈希是个很好的解决方法,也就是算两套哈希值,只有两个哈希值都一样才会被判定为是同一字符串。

例1.P3370

字符串哈希的模板题,可以将字符串多项式哈希后比较哈希值是否相等,单哈希即可通过。

点击查看代码
#include<bits/stdc++.h>
using namespace std;

const int N=2025;
const int T=1e4+4;
const int B=13331;
const int mod=998244353;
int t,n,a[T];
char s[N]; 

int main(){
	scanf("%d",&t);
	for(int i=1;i<=t;i++){
		scanf("%s",s+1);
		n=strlen(s+1);
		int HASH=0;
		for(int j=1;j<=n;j++){
			HASH=(HASH+(int)(s[j]))%mod;
			HASH=(HASH*B)%mod;
		}
		a[i]=HASH;
	}
	sort(a+1,a+t+1);
	int len=unique(a+1,a+t+1)-a-1;
	printf("%d",len);
	return 0;
} 

例2.ABC398F

当年这个题的赛时数据实在太水了,暴力都能打过去,不过后来加强了,遂讲其哈希之做法。

题目求的是最短的以原字符串为前缀的回文串,可以转化为求最长的原字符串的后缀回文子串,而回文串不管是正着求哈希还是倒着求哈希,哈希值都是固定的,我们就用这个性质判断是否回文。

单哈希就能过,当然双哈希更保险。

单哈希
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int B=79;
const int mod=998244353;
const int N=5e5+5;
int lst,hs1,hs2,n,pw[N];
char c[N];

signed main(){
	scanf("%s",c+1);
	n=strlen(c+1);
	pw[0]=1;
	for(int i=1;i<=n;i++){
		pw[i]=pw[i-1]*B%mod;
	}
	for(int i=n;i;i--){
		hs1=(hs1*B+(int)(c[i]))%mod;
		hs2=(hs2+(int)(c[i])*pw[n-i])%mod;
		//cout<<"i="<<i<<endl;
		//cout<<hs1<<" "<<hs2<<endl;
		if(hs1==hs2){
			lst=i;
		}
	}
	for(int i=1;i<lst;i++){
		cout<<c[i];
	}
	for(int i=n;i;i--){
		cout<<c[i];
	}
	return 0;
} 
双哈希
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int B1=79;
const int mod1=998244353;
const int B2=233;
const int mod2=1e9+7;
const int N=5e5+5;
int lst,hs1,hs2,hs3,hs4,n,pw1[N],pw2[N];
char c[N];

signed main(){
	scanf("%s",c+1);
	n=strlen(c+1);
	pw1[0]=pw2[0]=1;
	for(int i=1;i<=n;i++){
		pw1[i]=pw1[i-1]*B1%mod1;
		pw2[i]=pw2[i-1]*B2%mod2;
	}
	for(int i=n;i;i--){
		hs1=(hs1*B1+(int)(c[i]))%mod1;
		hs2=(hs2+(int)(c[i])*pw1[n-i])%mod1;
		hs3=(hs3*B2+(int)(c[i]))%mod2;
		hs4=(hs4+(int)(c[i])*pw2[n-i])%mod2;
		//cout<<"i="<<i<<endl;
		//cout<<hs1<<" "<<hs2<<endl;
		if(hs1==hs2&&hs3==hs4){
			lst=i;
		}
	}
	for(int i=1;i<lst;i++){
		cout<<c[i];
	}
	for(int i=n;i;i--){
		cout<<c[i];
	}
	return 0;
} 

----------------------------------------------------------并不华丽的分割线----------------------------------------------------------

很多字符串的算法也可以用哈希暴力解决,比如manache和KMP。

例3.P3375&P4824

本来是两个KMP的题目的,但是如果考场上忘了怎么写———哈希,启动!

KMP模板题可以用哈希做第一问,但第二问用哈希做就不太行,所以只放第一问核心代码。

核心代码

	for(int i=1;i<=n2;i++){//由于第二个串只需要完整哈希值,所以不用开数组 
		hs2=((hs2*B)%mod+(s2[i]-'A')%mod)%mod;
	}
	for(int i=1;i<=n1;i++){
		hs1[i]=((hs1[i-1]*B)%mod+(s1[i]-'A')%mod)%mod;
		if(hs1[i]-hs1[i-n2]*pw[n2]==hs2){//判断两串是否相等 
			printf("%d\n",i-n2+1);
		}
	}

P4824的一般做法是把原串按顺序丢进栈中,同时用KMP进行匹配,栈首匹配成功就将匹配部分弹出栈,最后剩下的就是答案。具体可参考前几篇题解

但是有一般就会有二般。

如果你一不小心忘了KMP怎么写时,今天的主角哈希就登场了。

同样是把字符串扔进栈里,不过用哈希代替KMP匹配,匹配成功就把匹配部分弹出,和刚才基本类似。

P4824完整代码
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;//自然溢出就相当于取模了,起码不会有负数 

const int N=1e6+6;//更正,其实是屑作者没判边界
const ull B=131;
int n1,n2,top;
ull hs2,pw[N],hst[N];
char s1[N],s2[N],st[N];

signed main(){
	scanf("%s%s",s1+1,s2+1);
	n1=strlen(s1+1),n2=strlen(s2+1);
	/*
	if(n1<n2){
		//要删除的串比原串还长,那就不会有要删的子串了 
		printf("%s",s1+1);
		return 0;
	}
	*/
	pw[0]=1;
	for(int i=1;i<=n2;i++){
		pw[i]=pw[i-1]*B;
		hs2=((hs2*B)+(s2[i]-'a'));
	}
	for(int i=1;i<=n1;i++){
		st[++top]=s1[i];//入栈 
		hst[top]=((hst[top-1]*B)+(s1[i]-'a'));
		if(top<n2){//如果栈里不足n2个元素是肯定不会匹配成功的,不判会RE
			continue;
		}
		if(hs2==hst[top]-hst[top-n2]*pw[n2]){//匹配成功 
			top-=n2;//就把这一段出栈,注意这里不会影响前面字符串,所以前面的哈希值不用改 
		}
	}
	for(int i=1;i<=top;i++){
		cout<<st[i];
	}
	return 0;
}

例4.P3805

本来是manache的经典板子题,但是如果你又一不小心把manache忘了 (那很不小心了)

那么哈希也能过掉这个题。

和马拉车有点类似,我们枚举中间点i,分奇数个字母的回文串和偶数个字母(此时i为偏左的那个点)的回文串两种情况讨论。

设len为回文串的半径(含端点),那么如果len+1回文len就回文,len-1不回文len就不回文,所以可以用O(nlogn)的时间复杂度解决。(可以过,就是时间很紧张)

但是,既然求最长回文串,那就没必要先二分,把当前的最优答案ans记录下来,并且判断以i为中心点、以ans为基础能否延展,可以的话就更新答案ans。

这样时间复杂度就差不多是O(n)了,虽然和manacher差点,但也足够AC了。

P3805哈希
#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;

const int N=1.1e7+1;
const ull B=131;
int n,ans;
ull hs1[N],hs2[N],pw[N];
char s[N];

int main(){
	scanf("%s",s+1);
	n=strlen(s+1);
	
	//for(int i=1;i<=n;i++){
	//	cout<<s[i];
	//}
	//cout<<endl;
	
	pw[0]=1;
	for(int i=1;i<=n;i++){
		pw[i]=pw[i-1]*B;
	}
	for(int i=1;i<=n;i++){
		hs1[i]=hs1[i-1]*B+(int)(s[i]);//正着求 
		hs2[n-i+1]=hs2[n-i+2]*B+(int)(s[n-i+1]);//倒着求 
	}
	for(int i=1;i<=n;i++){
		int len=(ans+1)>>1;//在最优答案的基础上判断能否更优 
		int l=i-len,r=i+len;
		//判断回文的方法同ABC398F 
		while(l>=1&&r<=n&&hs1[r]-hs1[l-1]*pw[r-l+1]==hs2[l]-hs2[r+1]*pw[r-l+1]){//奇 
			len++;ans=max(ans,2*len-1);l--;r++;//若可以就更新 
		}
		len=ans>>1;
		l=i-len,r=i+len+1;
		while(l>=1&&r<=n&&hs1[r]-hs1[l-1]*pw[r-l+1]==hs2[l]-hs2[r+1]*pw[r-l+1]){//偶 
			len++;ans=max(ans,2*len);l--;r++;
		}
	}
	printf("%d",ans);
	return 0;
}

值得一提的是,其实我们可以像马拉车一样在字符中间插入井号,这样就不用判断是奇数个还是偶数个字母的回文串了。

不过这个题显然不太行,数组占空间8.8e7,大抵会MLE。

例5.P3763

刚才我们反复提到哈希的一个性质:对于两个字符串s1,s2,如果他们的前i位相同,则前i-1位一定相同;如果前i位不同,则前i+1位不同。

这也就决定了哈希可以和二分搭配使用,比如这个题。

根据上述性质,我们可以三次二分,具体地说,每次二分求s1,s2的最大匹配长度l,则第l+1位一定失配。接着我们跳过l+1位,从s1,s2剩余未匹配部分接着找最大匹配长度,以此类推,直到s2匹配完成或已经进行了3次二分。

注意,如果是后一种情况的话,那三次二分完成后需要检验剩余部分是否相同。以及,多组数据一定记得清空+初始化。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;

const int N=1e5+5;
const int B=131;
int T,n1,n2,ans;
ull pw[N],hs1[N],hs2[N];
char s1[N],s2[N];

ull HASH(ull hs[],int l,int r){
	return hs[r]-hs[l-1]*pw[r-l+1];
}

bool check(int x){
	int l1=x,l2=1,r1=x+n2-1,r2=n2,lst=0;
	//lst:已经匹配了多少位 
	for(int t=1;t<=3;t++){
		int l=0,r=n2-lst,mid;//二分两串可以匹配的最大长度 
		//最差一个都匹配不上,最好剩下的都能匹配上 
		while(l<r){
			mid=(l+r+1)>>1;
			if(HASH(hs1,l1,l1+mid-1)==HASH(hs2,l2,l2+mid-1)){//能匹配上 
				l=mid;//就向后取mid,当然这个位置要保留 
			}
			else{//匹配不上 
				r=mid-1;
			}
		}
		//cout<<"l="<<l<<endl;
		l1=l1+l+1;l2=l2+l+1;
		lst+=l+1;
		//注意要跳过失配位置 
		if(l2>r2){//已经匹配完了 
			return 1;
		}
	}
	return (HASH(hs1,l1,r1)==HASH(hs2,l2,r2));
	//此时已经有3位失配,需要判断剩下的串是否相同 
}

int main(){
	scanf("%d",&T);
	pw[0]=1;
	while(T--){
		ans=0;//从0->100只需要一个清空 
		scanf("%s%s",s1+1,s2+1);
		n1=strlen(s1+1),n2=strlen(s2+1);
		//初始化 
		for(int i=1;i<=n2;i++){
			pw[i]=pw[i-1]*B;
		}
		for(int i=1;i<=n1;i++){
			hs1[i]=hs1[i-1]*B+s1[i];
		}
		for(int i=1;i<=n2;i++){
			hs2[i]=hs2[i-1]*B+s2[i];
		}
		for(int i=1;i+n2-1<=n1;i++){//枚举可能是基因开头位置的i 
			//cout<<"i="<<i<<endl;
			if(check(i)){
				ans++;
			}
		}
		printf("%d\n",ans);
	}
	return 0;
}

课后复习题:P4503

二.哈希表

哈希表是一种通过计算键的‌哈希值,并使用该哈希值作为‌数组的索引来存储和检索键值对的数据结构。常用的映射函数是将原数取模存储到数组中。

屏幕截图 2025-07-09 131837

但是哈希毕竟是将大范围的数映射到小范围内,难免会有冲突。这个时候,一般会采用常见的两种方法处理。

第一种是开放定址法,即向前找下一个空着的位置并存数,包括线性探测(一次只走一步)和二次探测(步长为平方)等。

屏幕截图 2025-07-09 132448

缺点是可能发生较连续的冲突

第二种是链表法,将多个冲突的数放在一个纵向链表里,如图。

屏幕截图 2025-07-09 132621

缺点是如果冲突集中查找复杂度会退化为O(n)。

例1.P11615

哈希表板子题。

介于这个题开unordered_map等其他数据结构会被卡,所以我们采用手写哈希表。

这里我用的是vector模拟哈希链表,而且开了pair,其中x表示映射前的数,y表示映射后的数。

每次查找x位时就先找到x所在的哈希表下标(这里是x%mod=mm),然后遍历mm里所有元素,找到了就收工,找不到说明x原来不在哈希表里,就在mm里新建一个x的位置。

这个题要求对2^64取模,但其实用ull自然溢出就约等于取模了。

点击查看代码
#include<bits/stdc++.h>
#define x first
#define y second
using namespace std;
typedef unsigned long long ull;

const int N=5e6+6;
const int mod=1e7+19;//某位大佬给的哈希表建议:约2~3倍元素个数N大小的质数 
ull n;

//题目给的快读,听说不用会TLE
//输入出现读入n及n对数后可继续输入的问题时,试试ctrl+Z 
char buf[1<<23],*p1=buf,*p2=buf;
#define gc() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
inline unsigned long long rd() {//读入一个 64 位无符号整数
	unsigned long long x=0;
	char ch=gc();
	while(!isdigit(ch))ch=gc();
	while(isdigit(ch)) x=x*10+(ch^48),ch=gc();
	return x;
}

vector<pair<ull,ull> > h[mod+5];

void CHANGE(ull x,ull y){//改变y值 
	ull mm=x%mod;
	for(ull i=0;i<h[mm].size();i++){
		if(h[mm][i].x==x){//查询、更改 
			h[mm][i].y=y;
			break;
		}
	}
}

ull FIND(ull x){//查找函数 
	ull mm=x%mod;
	for(ull i=0;i<h[mm].size();i++){
		if(h[mm][i].x==x){
			return h[mm][i].y;//找到直接返回 
		}
	}
	h[mm].push_back((pair<ull,ull>){x,0});//找不到则说明原来没有,新建一个空位
	//注意这里的大部分变量都得开ull 
	return 0;
}

int main(){
	n=rd();
	ull sum=0;
	for(ull i=1;i<=n;i++){
		ull x=rd();ull y=rd();
		ull ans=FIND(x);
		CHANGE(x,y);
		sum=sum+i*ans*1ll;
	}
	cout<<sum;
	return 0;
}

三.其他哈希

详情请见这个题单

包括但不限于哈希+线段树、树哈希。 作者是蒟蒻,所以这里不展开讲了

参考资料:

1.哈希题单
2.大佬的哈希文章
3.洛谷题解P4824
4.洛谷题解P3763
5.洛谷题解P11615

posted @ 2025-07-09 15:30  qwqSW  阅读(39)  评论(0)    收藏  举报