树哈希 & 浅学

介绍

树哈希是哈希的一种废话,是一种将一颗有跟无标号的树映射为一个数的方法。
可以类比字符串哈希。
我们观察一下字符串哈希:

\[Hash_x=G(F(Hash_{x-1})+c_x) \]

看不懂?换成更常见的式子:

\[Hash_x=(Hash_{x-1}*base+c_x)\bmod p \]

其中 \(base\) 表示进制, \(c\) 表示字符串,\(p\) 表示模数。
对应的 \(F()\) 表示哈希函数,\(G()\) 则是取模的函数。
在树哈希中,我们可以采取相同的操作:

\[Hash_x=G\left[\sum_{y\in son_x}F(Hash_y)\right] \]

但是,就止步于此了吗?
树哈希的哈希函数当然可以像字符串哈希那样搞一个进制,然后乘起来,但那太容易被卡了(第一个例题就会卡你这个),所以我们要重新设计哈希函数。

xor-shift

我们要使哈希函数不被卡,那必须使用我们自己都不知道能算出来什么的哈希函数。
我们大胆想一下,什么运算足够"随机"到我们都不知道会算出什么呢?
那就是按位异或(xor)看似无厘头却又自成逻辑,十分符合我们的需求。
当然只有异或显然还是不够的,于是我们又引入了随机数,我们在程序开头随机提取一个随机数当做类似进制的东西,对一个数进行按位异或,再左移右移瞎搞之后,哈希函数映射出来的东西终于不是人类所能理解的了。
这种使用按位异或的方法叫做 xor-shift

例题

树哈希\(^{UOJ763}\)

一道板题,用set维护字数个数即可。

#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;
const int NUM=1e6+10;

set<ull> s;
int h[NUM],to[NUM<<1],nxt[NUM<<1],tot;
ull C,f[NUM],val;
int n;
void addedge(int x,int y){
	to[++tot]=y;
	nxt[tot]=h[x];
	h[x]=tot;
}

//哈希函数,这里用的Cissia的名字
ull Cissia(ull x){
	x^=val,x^=(x>>1),x^=(x<<5),x^=(x<<6);//瞎搞的左移右移,这里用的Cissia的身高。
	x+=237051146237051146ull;
	return x;
}

void dfs(int x,int fa){
	f[x]=0;
	for(int i=h[x];i;i=nxt[i]){
		int y=to[i];
		if(y==fa) continue;
		dfs(y,x);
		f[x]+=Cissia(f[y]);
	}
	f[x]+=C;
	s.insert(f[x]);
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	srand(237051146);//种子用的Cissia的主色调色号。
	cin>>n;
	C=rand(),val=rand();
	for(int i=1,x,y;i<n;++i){
		cin>>x>>y;
		addedge(x,y);
		addedge(y,x);
	}
	dfs(1,0);
	cout<<s.size();
	return 0;
}

【模板】树同构([BJOI2015] 树的同构)\(^{luoguP5043}\)

我们可以使用换根DP求出以每个节点为跟的哈希值,然后你取最大值、最小值、平均值或中位数来表示这棵树,然后用map维护一下分类即可。

#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;
const int NUM=1e6+10;
mt19937 rnd(237051146);

set<ull> s;
int h[NUM],to[NUM<<1],nxt[NUM<<1],tot;
ull C,f[NUM],val,f2[NUM];
int n;
void addedge(int x,int y){
	to[++tot]=y;
	nxt[tot]=h[x];
	h[x]=tot;
}

ull Cissia(ull x){
	x^=val,x^=(x>>1),x^=(x<<6),x^=(x<<5);
	x+=237051146237051146ull;
	return x;
} 

void dfs(int x,int fa){
	f[x]=0;
	for(int i=h[x];i;i=nxt[i]){
		int y=to[i];
		if(y==fa) continue;
		dfs(y,x);
		f[x]+=Cissia(f[y]);
	}
	f[x]+=C;
	s.insert(f[x]);
}

void dfs2(int x,int fa){
	for(int i=h[x];i;i=nxt[i]){
		int y=to[i];
		if(y==fa) continue;
		f2[y]=f[y]+Cissia(f2[x]-Cissia(f[y]));
		dfs2(y,x);
	}
}

map<ull,int> mp;

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	C=rnd(),val=rnd();
	cin>>n;
	for(int i=1,x;i<=n;++i){
		cin>>x;
		for(int j=1,y;j<=x;++j){
			cin>>y;
			if(y==0) continue;
			addedge(y,j);
			addedge(j,y);
		}
		dfs(1,0);
		f2[1]=f[1];
		dfs2(1,0);
		ull res=0;
		for(int j=1;j<=x;++j){
			res=max(res,f2[j]);
		}
		if(!mp.count(res)){
			cout<<i<<'\n';
			mp[res]=i;
		}else cout<<mp[res]<<'\n'; 
		memset(h,0,sizeof(h));
		memset(nxt,0,sizeof(nxt));
		memset(to,0,sizeof(to));
		tot=0;
	}
	return 0;
}

Symmetree\(^{CF1800G}\)

也可以说是一道水题,虽然这道题在洛谷上是紫题,CF上评级也才2200。
对于每一棵树跑一边树哈希,然后开始根节点检查是否对称。
这里需要注意,若剩下一个子节点,就需要继续检查这个剩下的子节点,当然因为我太蒻了,题解写的比我这简单得多。
具体细节请看代码:

#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;
const int NUM=2e5+10;
mt19937_64 rnd(237051146);

int h[NUM],to[NUM<<1],nxt[NUM<<1],tot;
ull C,f[NUM],val,f2[NUM];
int n;
void addedge(int x,int y){
	to[++tot]=y;
	nxt[tot]=h[x];
	h[x]=tot;
}

ull Cissia(ull x){
	x^=val,x^=(x>>1),x^=(x<<6),x^=(x<<5);
	x+=237051146237051146ull;
	return x;
} 

void clear(){
	memset(h,0,sizeof(h));
	memset(nxt,0,sizeof(nxt));
	memset(to,0,sizeof(to));
	tot=0;
}

void dfs(int x,int fa){
	f[x]=0;
	for(int i=h[x];i;i=nxt[i]){
		int y=to[i];
		if(y==fa) continue;
		dfs(y,x);
		f[x]+=Cissia(f[y]);
	}
	f[x]+=C;
}
bool check(int x,int fa){
	set<ull> s;
	map<ull,int> mp;
	for(int i=h[x];i;i=nxt[i]){
		int y=to[i];
		if(y==fa) continue;
		if(mp[f[y]]!=0){
			s.erase(f[y]);
			mp[f[y]]=0;
		}else{
			s.insert(f[y]);
			mp[f[y]]=y;
		}
	}
	if(s.size()>=2) return 0;
	if(s.size()==1) return check(mp[*s.begin()],x);
	return 1;
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	C=rnd(),val=rnd();
	int t;
	cin>>t;
	while(t--){
		cin>>n;
		for(int i=1,x,y;i<n;++i){
			cin>>x>>y;
			addedge(x,y);
			addedge(y,x);
		}
		dfs(1,0);
		cout<<(check(1,0)?"YES\n":"NO\n");
		clear();
	}
	return 0;
}

独特的树叶\(^{luoguP4323}\)

我们可以使用换根DP求出所有A树和B树所有节点为跟时的哈希值。
很容易发现在A树上shift一下然后在B树上减去那个常数,判一下是不是有相等就可以了。
如果不理解可以想一下你求树哈希的过程。
具体细节请看代码:

#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;
const int NUM=1e6+10;
mt19937 rnd(237051146);
set<ull> s; 
int h[NUM],to[NUM<<1],nxt[NUM<<1],tot;
ull C,f[NUM],val,f2[NUM];
int n;
void addedge(int x,int y){
	to[++tot]=y;
	nxt[tot]=h[x];
	h[x]=tot;
}

ull Cissia(ull x){
	x^=val,x^=(x>>1),x^=(x<<6),x^=(x<<5);
	x+=237051146237051146ull;
	return x;
} 

void dfs(int x,int fa){
	f[x]=0;
	for(int i=h[x];i;i=nxt[i]){
		int y=to[i];
		if(y==fa) continue;
		dfs(y,x);
		f[x]+=Cissia(f[y]);
	}
	f[x]+=C;
}

void dfs2(int x,int fa){
	for(int i=h[x];i;i=nxt[i]){
		int y=to[i];
		if(y==fa) continue;
		f2[y]=f[y]+Cissia(f2[x]-Cissia(f[y]));
		dfs2(y,x);
	}
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	C=rnd(),val=rnd();
	cin>>n;
	for(int i=1,x,y;i<n;++i){
		cin>>x>>y;
		addedge(x,y);
		addedge(y,x);
	}
	dfs(1,0);
	f2[1]=f[1];
	dfs2(1,0);
	for(int i=1;i<=n;++i) s.insert(Cissia(f2[i]));
	memset(h,0,sizeof(h));
	memset(nxt,0,sizeof(nxt));
	memset(to,0,sizeof(to));tot=0;
	for(int i=1,x,y;i<=n;++i){
		cin>>x>>y;
		addedge(x,y);
		addedge(y,x);
	}
	dfs(1,0);
	f2[1]=f[1];
	dfs2(1,0);
	for(int i=1;i<=n;++i){
		if(s.find(f2[i]-C)!=s.end()){
			cout<<i;
			return 0;
		}
	}
	return 0;
}

最后

马蜂奇特,不喜勿喷。

posted @ 2026-07-27 21:32  LZYXT  阅读(2)  评论(0)    收藏  举报