树哈希小记
树哈希
感觉没什么好说的,类似树形 DP 一样求出一个点的子树的哈希值,用以判断树是否同构,哈希函数很多,这里给出一种实现。
UOJ板子
const int N=1000006;
int n,siz[N],ans;
vector<int>edge[N];
map<ull,bool>vis;
ull hsh[N];
ull poland(ull x){
x^=x>>7;x^=x<<13;x^=x>>17;
return x;
}
void dfsp(int u,int f){
hsh[u]=1;siz[u]=1;
for(auto v:edge[u]){
if(v==f)continue;
dfsp(v,u);
siz[u]+=siz[v];
hsh[u]+=poland(hsh[v]);
}
hsh[u]*=siz[u];
if(vis.find(hsh[u])==vis.end())
{++ans;vis[hsh[u]]=1;}
}
int main(){
int n=read();
for(int i=1;i<n;++i){
int u=read(),v=read();
edge[u].push_back(v);
edge[v].push_back(u);
}
dfsp(1,0);
printf("%d\n",ans);
return 0;
}
例题
LG5043 【模板】树同构([BJOI2015]树的同构)
无根树树同构,以每个点为根求一边树哈希判断即可。
LG4323 [JSOI2016]独特的树叶
JYY有两棵树 \(A\) 和 \(B\) :树 \(A\) 有 \(N\) 个点,编号为 \(1\) 到 \(N\) ;树 \(B\) 有\(N+1\) 个节点,编号为 \(1\) 到\(N+1\)
JYY 知道树 \(B\) 恰好是由树 \(A\) 加上一个叶节点,然后将节点的编号打乱后得到的。他想知道,这个多余的叶子到底是树 \(B\) 中的哪一个叶节点呢?
求出 \(A\) 树以每个节点为根的哈希值,和 \(B\) 树去掉一个叶子节点的哈希值,如果这两棵树中有哈希值相等,则可以得出对应的叶子节点。
直接做是 \(O(n^2)\) 的,由于树哈希的过程其实就是树形 DP,所以实际上可以换根做到 \(O(n)\)。
这里的哈希函数是,其中 \(S\) 是常数。
然后用 set 就可以对比了。
具体见代码吧
typedef long long ull;
const ull S=114514;
struct Edge{
int enxt[M],head[N],to[M],ent,n,deg[N];
ull dp[N],siz[N],has[N];
inline void addline(int u,int v){
to[++ent]=v;
enxt[ent]=head[u];
head[u]=ent;
++deg[v];
}
inline void addedge(int u,int v){
addline(u,v);
addline(v,u);
}
inline void clear(){
memset(head,0,sizeof(head));
ent=0;
}
inline void build(){
for(int i=1;i<n;++i){
int u=read(),v=read();
addedge(u,v);
}
}
inline void dfsp(int u,int f){
dp[u]=siz[u]=1ull;
for(int i=head[u];i;i=enxt[i]){
int v=to[i];
if(v==f)continue;
dfsp(v,u);
siz[u]+=siz[v];
dp[u]^=(dp[v]*S+siz[v]);
}
}
inline void dfsg(int u,int f){//换根dp
if(!f)has[u]=dp[u];
else has[u]=dp[u]^((has[f]^(dp[u]*S+siz[u]))*S+n-siz[u]);
for(int i=head[u];i;i=enxt[i]){
int v=to[i];
if(v==f)continue;
dfsg(v,u);
}
}
}e1,e2;
int n,siz[N];
set<ull>se;
int main(){
n=read();
e1.n=n;e1.build();
e1.dfsp(1,0);e1.dfsg(1,0);
//e1.debug();
for(int i=1;i<=n;++i)
se.insert(e1.has[i]);
e2.n=n+1;e2.build();
/*for(int i=1;i<=e2.n;++i){
if(e2.deg[i]!=1){//注意从第二颗树的非叶子节点开始dp,这样可以才能保证下面计算的正确性
e2.dfsp(i,0);e2.dfsg(i,0);
break;
}
}*/
e2.dfsp(1,0);e2.dfsg(1,0);
for(int i=1;i<=e2.n;++i){
if(e2.deg[i]==1){
if(se.count(e2.has[e2.to[e2.head[i]]]^(e2.dp[i]*S+1)))
return printf("%d\n",i),0;
}
}
return 0;
}
// 179ms / 12.19MB / 1.91KB C++14 (GCC 9) O2
CF718D Andrew and Chemistry
给你一个有 \(n\) 个点的树。当每一个点的度不超过 \(4\) 时这棵树是合法的。现在让你再添加一个点,在树仍然合法的情况下,一共有多少种树。
当两棵树同构时视作同一种。
根据 LG4323 的思路,我们可以使用换根 DP 求出以每个点为根的哈希值,然后对度数小于 \(4\) 的点求不同种的哈希值数量即可。
但是似乎没有用到度数为 \(4\) 这一性质。
使用记搜,如果当前节点已经访问过就直接返回原来的值。
至于哈希的过程,我们考虑用 \(\text{vector}\) 来存储。
每个 \(\text{vector}\) 所存储的都是其儿子的哈希值,就可以不断递归向上合并。
见代码吧。
const int N=100005,M=200005;
map<int,int>dp[N];
map<vector<int>,int>vis;int tot;
set<int>se;
struct Edge{
int enxt[M],head[N],to[M],ent,n,deg[N];
inline void addline(int u,int v){
to[++ent]=v;
enxt[ent]=head[u];
head[u]=ent;
++deg[v];
}
inline void addedge(int u,int v){
addline(u,v);
addline(v,u);
}
inline void clear(){
memset(head,0,sizeof(head));
ent=0;
}
inline void build(){
for(int i=1;i<n;++i){
int u=read(),v=read();
addedge(u,v);
}
}
int dfs(int u,int f){
if(dp[u].find(f)!=dp[u].end())return dp[u][f];//因为每个点的度数<=4,则总的状态数小于 n*4*2,其中2代表不同方向
vector<int>tmp;
for(int i=head[u];i;i=enxt[i]){
int v=to[i];
if(v==f)continue;
tmp.push_back(dfs(v,u));
}
sort(tmp.begin(),tmp.end());
if(vis.find(tmp)==vis.end())vis[tmp]=++tot;
return dp[u][f]=vis[tmp];
}
void solve(){
for(int i=1;i<=n;++i){
if(deg[i]<4)se.insert(dfs(i,0));
//printf("%d %d\n",deg[i],dfs(i,0));
}
}
}e1;
int n;
int main(){
e1.n=n=read();
e1.build();e1.solve();
printf("%d\n",(int)se.size());
return 0;
}
//orz
//本质是求以不同的点为根进行树哈希得到的不同形态数,因为这题状态比较少,故可以采取上方这种很“暴力”的方法,换根也是可以的。
//可能换根写起来也没复杂多少,但是这种方法挺有新意的。
本文作者:BigSmall_En
本文链接:https://www.cnblogs.com/BigSmall-En/p/16754418.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步