哈希问题的一类技巧

浅谈处理哈希问题的一类方法-线段树维护哈希

前言

一个初三蒟蒻粗浅的认知和总结,dalao不喜勿喷。分享的题也大都很水,仅是代表浅层理解。

简介/概括

哈希是一种常用的算法。我们在oi中使用哈希的主要目的是将难以直接处理、维护、比较的对象映射到范围小/便于维护/便于处理/便于比较的对象上。

映射的方式是哈希函数 $hash(x)$,设计哈希函数时会考虑对象的特性,并尽可能避免冲突。

通常,我们时常会把研究对象映射到整数上,这样就会出现溢出的问题,解决方法无非就两种:

1.自然溢出

就是不管它,让它自行溢出,根据溢出值来比较。仅限于long long类型

2.双哈希

同时设计两种不同哈希函数进行维护。可大幅提升准确率,减少冲突概率。

字符串哈希

哈希里面比较常见的一种,就是把一个字符串看成一个 $n$ 进制数,把字符串弄成一个类似键值的东西。

对字符串作哈希后,可以进行高效的比较、取子串等操作,有时还会用线段树去维护它(一般带修)。

总而言之,字符串哈希是处理字符串问题的一种常见思路。

CF580E Kefa and Watch

题意描述

维护一种由数字构成字符数组,可以执行以下操作:

1.格式:l r c,将l~r都赋值为$c$

2.格式: l r d,询问l~r是否有长度为$d$的循环节

solve

使用线段树去维护这个字符串哈希值。

关于怎样判断一个字符串 $s$ 有长度为 $d$ 的循环节,可以证明字符串 $s$ 有长度为 $d$ 的循环节等价于 $s[1...n-d]=s[d+1...n]$ ($s[l,r]$ 表示 $s$ 从 $l$ 到$r$ 的子串)。

使用线段树维护子串的哈希值,就可以完成以上比较了。

code


#include<iostream>
#include<cstdio>
#include<string>
#define int long long
#define N 1000007
using namespace std;
struct Sigement{
	string s;
	int mod,pw[100007],seed,note[100007];
	struct Tr{
		int tag,value;
	}tree[500007];
	void init(){
		int len=s.size();
		pw[0]=1;
		note[0]=1;
		for(int i=1;i<len+4;i++){
			pw[i]=pw[i-1]*seed%mod;
			note[i]=note[i-1]+pw[i];
			note[i]%=mod;
		}
	}
	void pushup(int l,int r,int pos){
		int mid=(l+r)/2;
		tree[pos].value=tree[pos*2].value*pw[r-mid]%mod+tree[pos*2+1].value;
		tree[pos].value%=mod;
	}
	void pushdown(int k,int l,int r,int pos){
		int mid=(l+r)/2;
		tree[pos*2].tag=k;
		tree[pos*2].value=(k+'0')*note[mid-l]%mod;
		tree[pos*2+1].tag=k;
		tree[pos*2+1].value=(k+'0')*note[r-mid-1]%mod;
		tree[pos].tag=-1;
	}
	void build(int l,int r,int cnt){
		tree[cnt].tag=-1;
		if(l==r){
			tree[cnt].value=s[l];
			return ;
		}
		int mid=(l+r)/2;
		build(l,mid,cnt*2);
		build(mid+1,r,cnt*2+1);
		pushup(l,r,cnt);
	}
	void update(int l,int r,int L,int R,int k,int cnt){
		if(L<=l&&r<=R){
			tree[cnt].tag=k;
			tree[cnt].value=(k+'0')*note[r-l]%mod;
			return ;
		}
		int mid=(l+r)/2;
		if(tree[cnt].tag!=-1){
			pushdown(tree[cnt].tag,l,r,cnt);
		}
		if(L<=mid){
			update(l,mid,L,R,k,cnt*2);
		}
		if(R>mid){
			update(mid+1,r,L,R,k,cnt*2+1);
		}
		pushup(l,r,cnt);
	}
	int query(int l,int r,int L,int R,int cnt){
		if(L<=l&&r<=R){
			return tree[cnt].value;
		}
		int mid=(l+r)/2,ans=0;
		if(tree[cnt].tag!=-1){
			pushdown(tree[cnt].tag,l,r,cnt);
		}
		if(L<=mid){
			ans+=query(l,mid,L,R,cnt*2);
		}
		if(R>mid){
			ans=ans*pw[min(r,R)-mid]%mod+query(mid+1,r,L,R,cnt*2+1);
			ans%=mod;
		}
		return ans;
	}
}tr1,tr2;
signed main(){
	int n,m,k;
	cin>>n>>m>>k;
	cin>>tr1.s;
	tr1.seed=128;
	tr1.mod=998244353;
	tr1.init();
	tr1.build(0,n-1,1);
	tr2.s=tr1.s;
	tr2.seed=131;
	tr2.mod=1e9+7;
	tr2.init();
	tr2.build(0,n-1,1);
	for(int i=1;i<=m+k;i++){
		int op;
		cin>>op;
		if(op==1){
			int l,r,c;
			cin>>l>>r>>c;
			tr1.update(0,n-1,l-1,r-1,c,1);
			tr2.update(0,n-1,l-1,r-1,c,1);
		}else{
			int l,r,d;
			cin>>l>>r>>d;
			l--;
			r--;
			if(r-l+1==d){
				cout<<"YES\n";
			}
			else if(tr1.query(0,n-1,l+d,r,1)==tr1.query(0,n-1,l,r-d,1)&&tr2.query(0,n-1,l+d,r,1)==tr2.query(0,n-1,l,r-d,1)){
				cout<<"YES\n";
			}else cout<<"NO\n";
		}
	}
	return 0;
}

P2757 [国家集训队] 等差子序列

题意描述

给一个数组,问它里面存不存在等差数列。

solve

这题比较抽象,它卡常。

首先,显然存在等差子序列等价于存在长度为 3 的等差子序列。

这大大降低了问题的难度。因为很容易可以想到枚举中间项,看看存不存在一个 $k$,使得在当前数两边分别存在等于 $a_i+k$ 与 $a_i-k$ 的数。

时间复杂度 $O(n^2)$。

接下来就是要优化上述过程。正难则反,我们可以考虑什么情况下无解。显然假如对于一个 $a_i$,$a_i-k$ 与 $a_i+k$ 都在它的左边,这就是无解的。

其实我们可以整一个桶,把便利过的 $a_i$ 都标记一下,再沿当前位置对折,看看重合的两个数是否都相等,若是,对于这个中项无解,否则有解。

这么说可能比较抽象,其实就是看以 $a_i$ 为中心,长度为 $2k-1$ 的 01 串是否是回文串。( $k$ 指最大的合法的 $k$ )

我们可以用权值线段树去维护这个字符串哈希,同时维护正序倒序哈希,就可以判回文了。

code

#include<bits/stdc++.h>
using namespace std;
int t,n,a[500005];
int mod=998244353,PW=2;
int hash1[2000005],hash2[2000005],pw[500005];
void pushup(int now,int l,int r){
	int mid=(l+r)/2;
	hash1[now]=(hash1[now*2]*1LL*pw[r-mid]%mod+hash1[now*2+1])%mod;
	hash2[now]=(hash2[now*2]+hash2[now*2+1]*1LL*pw[mid-l+1]%mod)%mod;
}
void add(int now,int l,int r,int x){
	if(l==r){
		hash1[now]=hash2[now]=1;
	}else{
		int mid=(l+r)/2;
		if(x<=mid) add(now*2,l,mid,x);
		if(x>mid) add(now*2+1,mid+1,r,x);
		pushup(now,l,r);
	}
}
int query1(int now,int l,int r,int L,int R){ 
	if(L<=l&&r<=R){
		return hash1[now];
	}else{
		int mid=(l+r)/2;
		int ans=0;
		if(R<=mid) return query1(now*2,l,mid,L,R);
		if(L>mid) return query1(now*2+1,mid+1,r,L,R);
		ans+=query1(now*2,l,mid,L,R);
		ans=(ans*1LL*pw[min(r,R)-mid]%mod+query1(now*2+1,mid+1,r,L,R))%mod;
		return ans;
	}
}
int query2(int now,int l,int r,int L,int R){
	if(L<=l&&r<=R){
		return hash2[now];
	}else{
		int mid=(l+r)/2;
		int ans=0;
		if(R<=mid) return query2(now*2,l,mid,L,R);
		if(L>mid) return query2(now*2+1,mid+1,r,L,R);
		ans+=query2(now*2+1,mid+1,r,L,R);
		ans=(ans*1LL*pw[mid-max(l,L)+1]%mod+query2(now*2,l,mid,L,R))%mod;
		return ans;
	}
}
void build(int x,int l,int r){
	if(l==r){
		hash1[x]=0;
		hash2[x]=0;
		return ;
	}
	int mid=(l+r)/2;
	build(x*2,l,mid);
	build(x*2+1,mid+1,r);
	pushup(x,l,r);
}
int main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cin>>t;
	pw[0]=1;
	for(int i=1;i<=500002;i++){
		pw[i]=pw[i-1]*1LL*PW%mod;
	}
	while(t--){
		cin>>n;
		build(1,1,n);
		for(int i=1;i<=n;i++){
			cin>>a[i];
		}
		int flg=0;
		for(int i=1;i<=n;i++){
			add(1,1,n,a[i]);
			int len=min(n-a[i]+1,a[i]);
			if(query1(1,1,n,a[i]-len+1,a[i])!=query2(1,1,n,a[i],a[i]+len-1)){
				flg=1;
				break;
			}
		}
		if(flg){
			cout<<"Y\n";
		}else cout<<"N\n";
	}
	return 0;
} 

其它类型的哈希

有时还会去用哈希维护一个难以直接维护、操作、比较的数列或集合等。这时就需要根据这个维护对象的特质构建哈希函数,有时还会套上数据结构。真是挺恶心的。

蒟蒻目前常见到的两种套路(dalao勿喷):

1.考虑顺序的数列

比如有个有序数列 $a$ , $|a|=n$,比较两个数列,我们可以使用以下哈希函数:

$$hash(a)=\sum_{i=1}{n}baseia_i$$

2.无序性集合

有个集合 $A$,判断两个集合是否相同时可以使用以下哈希函数:

$$hash(A)=\sum_{x \in A}base^x$$

P6688 可重集

这道题和上道题差不多难,就是不卡常好许多。

就是把每个数当做幂数,这样区间的 hash 值就和数字顺序无关了。

如对于数列 $x$,哈希方式为

$$ hash(x)=\sum_{i=1}nbase\mod p$$

日常线段树维护一下。

维护两个量,一个是哈希值,因为要区间加上 $k$,再维护最小值。

code

#include<iostream>
#include<cstdio>
using namespace std;
int n,q,a[1000007];
const int mod=1e9+7,base=2;
int pw[1000007],sum[4000007],minn[4000007];
void pushup(int now){
	minn[now]=min(minn[now*2],minn[now*2+1]);
	sum[now]=(sum[now*2]+sum[now*2+1])%mod;
}
void build(int now,int l,int r){
	if(l==r){
		sum[now]=pw[a[l]];
		minn[now]=a[l];
		return ;
	}
	int mid=(l+r)/2;
	build(now*2,l,mid);
	build(now*2+1,mid+1,r);
	pushup(now);
}
void upd(int now,int l,int r,int x,int y){
	if(l==r){
		sum[now]=pw[y];
		minn[now]=y;
		return ;
	}
	int mid=(l+r)/2;
	if(x<=mid) upd(now*2,l,mid,x,y);
	else upd(now*2+1,mid+1,r,x,y);
	pushup(now);
}
long long Qs(int now,int l,int r,int L,int R){
	if(L<=l&&r<=R){
		return sum[now];
	}
	int mid=(l+r)/2;
	long long ans=0;
	if(L<=mid) ans=(ans+Qs(now*2,l,mid,L,R))%mod;
	if(R>mid) ans=(ans+Qs(now*2+1,mid+1,r,L,R))%mod;
	return ans;
}
int Qm(int now,int l,int r,int L,int R){
	if(L<=l&&r<=R){
		return minn[now];
	}
	int mid=(l+r)/2;
	int ans=0x3f3f3f3f;
	if(L<=mid) ans=min(ans,Qm(now*2,l,mid,L,R));
	if(R>mid) ans=min(ans,Qm(now*2+1,mid+1,r,L,R));
	return ans;
}
void pri(int now,int l,int r){
	if(l==r){
		cout<<minn[now]<<" ";
		return ;
	}
	int mid=(l+r)/2;
	pri(now*2,l,mid);
	pri(now*2+1,mid+1,r);
}
int main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	pw[0]=1;
	for(int i=1;i<=1000000;i++){
		pw[i]=pw[i-1]*1LL*base%mod;
	}
	cin>>n>>q;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	build(1,1,n);
	while(q--){
		int op;
		cin>>op;
		if(op==0){
			int x,y;
			cin>>x>>y;
			upd(1,1,n,x,y);
		}else{
			int l1,r1,l2,r2;
			cin>>l1>>r1>>l2>>r2;
			long long ans1,ans2,fi1,fi2;
			ans1=Qs(1,1,n,l1,r1);
			ans2=Qs(1,1,n,l2,r2);
			fi1=Qm(1,1,n,l1,r1);
			fi2=Qm(1,1,n,l2,r2);
			if(fi1>fi2){
				swap(ans1,ans2);
				swap(fi1,fi2);
			}
			if(ans1*1LL*pw[fi2-fi1]%mod==ans2){
				cout<<"YES\n";
			}else cout<<"NO\n";
		}
	}
	return 0;
}

Thanks For Your Reading!

by 积雨云 (xjhoi)

posted @ 2025-10-01 19:37  积雨云xjh  阅读(26)  评论(0)    收藏  举报