我們有時需要判斷一些樹是否同構。這時,選擇恰當的Hash方式來將樹映射成一個便於儲存的Hash值(一般是 32 位或 64 位整數)是一個優秀的方案。
樹Hash定義在有根樹上。判斷無根樹同構的時候,可以比較重心為根的Hash值或者比較每個點為根的Hash值。
樹哈希有很多種哈希方式,下面介紹其中一種:
$f_x$表示$x$為根的子樹的Hash值,$son_x$表示$x$的兒子結點集合,$size_y$表示$y$為根的子樹規模,$prime(i)$表示第$i$個素數,則
$$
f_x = 1 + \sum_{y\in son_x}{f_y \times prime(size_y)}
$$
注意到我們求得的是子樹的Hash值,也就是說只有當根一樣時同構的兩棵子樹 hash 值才相同。如果數據范圍較小,我們可以暴力求出以每個點為根時的Hash值,也可以通過up and down樹形dp的方式,遍歷樹兩遍求出以每個點為根時的Hash值,排序后比較。
如果數據范圍較大,我們可以通過找重心的方式來優化復雜度。(一棵樹的重心最多只有兩個,分別比較即可)
例題1:洛谷P5043 [模板]樹同構
判斷無根樹同構,通過兩遍dfs樹形dp,求出每個點為根時的Hash值,排序后比較即可。

#include <cstdio> #include <cstring> #include <vector> #include <algorithm> using std::vector; using std::sort; const int N = 60; int f[N], g[N], siz[N]; int n; struct Edge { int nex, to; } edge[N<<1]; int head[N], tot; vector<int> hs[N]; bool isprime[1000]; int prime[N]; void init(int i) { tot = 1; memset(head, 0, sizeof(head)); hs[i].clear(); } void add_edge(int u, int v) { edge[tot].to = v; edge[tot].nex = head[u]; head[u] = tot++; } void get_prime(int MAX) { int x = 0; memset(isprime, true, sizeof(isprime)); for (int i = 2; i < MAX; i++) { if (x > 55) break; if (isprime[i]) prime[x++] = i; for (int j = 0; j < x; j++) { if (i * prime[j] >= MAX) break; isprime[i * prime[j]] = 0; if (i % prime[j] == 0) break; } } } void dfs1(int x, int fa) { siz[x] = f[x] = 1; for (int i = head[x]; i; i = edge[i].nex) { int y = edge[i].to; if (y == fa) continue; dfs1(y, x); f[x] += f[y] * prime[siz[y]]; siz[x] += siz[y]; } } void dfs2(int x, int fa, int fa_f) { g[x] = f[x] + fa_f * prime[n-siz[x]]; fa_f *= prime[n-siz[x]]; for (int i = head[x]; i; i = edge[i].nex) { int y = edge[i].to; if (y == fa) continue; dfs2(y, x, fa_f + f[x] - f[y] * prime[siz[y]]); } } bool Equal(int x, int y) { if (hs[x].size() != hs[y].size()) return false; for (int i = 0; i < hs[x].size(); i++) { if (hs[x][i] != hs[y][i]) return false; } return true; } int main() { get_prime(1000); int m; while (~scanf("%d", &m)) { for (int i = 1; i <= m; i++) { init(i); scanf("%d", &n); for (int j = 1, x; j <= n; j++) { scanf("%d", &x); if (x) add_edge(x, j), add_edge(j, x); } dfs1(1, 0); dfs2(1, 0, 0); for (int j = 1; j <= n; j++) hs[i].push_back(g[j]); sort(hs[i].begin(), hs[i].end()); } puts("1"); for (int i = 2; i <= m; i++) { for (int j = 1; j <= i; j++) { if (Equal(i, j)) { printf("%d\n", j); break; } } } } return 0; }
貼一個樹重心為根求Hash值進行比較的代碼,用了其他Hash方法:

inline void DFS(re int x,re int fa){ size[x]=1; re int i,y,res=0; for(i=h[x];i;i=e[i].next){ y=e[i].to;if(y==fa)continue; DFS(y,x); size[x]+=size[y]; res=max(res,size[y]); } res=max(res,n-size[x]);d[x]=res; maxl=min(maxl,res); } inline int Solve(re int x,re int fa){ re int i,y,res=2333; re vector<int > t; for(i=h[x];i;i=e[i].next){ y=e[i].to;if(y==fa)continue; t.push_back(Solve(y,x)); } sort(t.begin(),t.end()); for(i=0;i<t.size();++i)res=((res*Mul)^t[i])%Mod; return res; } int main(void){ re int i,j,x; scanf("%d",&m); memset(ans,INF,sizeof ans); for(i=1;i<=m;++i){ scanf("%d",&n); cnt=0;memset(h,0,sizeof h);tot=0; for(j=1;j<=n;++j){scanf("%d",&x);if(x){AddEdge(j,x);AddEdge(x,j);}} maxl=INF;DFS(1,0); for(j=1;j<=n;++j){if(d[j]==maxl)rt[++tot]=j;} for(j=1;j<=tot;++j)ans[i]=min(ans[i],Solve(rt[j],0)); for(j=1;j<=i;++j)if(ans[j]==ans[i]){printf("%d\n",j);break;} } return 0; }
處理出樹A每個點為根時的Hash值,放進set里;
處理出樹B每個點為根時的Hash值,對於葉子結點,計算其父親結點為根,去掉此葉子結點后的Hash值(也就是減去第一個素數2),在set中查找是否存在此值,是則說明此葉子結點為多余的葉子。

#include <cstdio> #include <cstring> #include <unordered_set> using std::unordered_set; const int N = 100010; bool isprime[2000010]; int prime[N]; int in[N], near[N]; struct Edge { int nex, to; }; struct Tree { int f[N], g[N], siz[N], head[N]; int n, tot; Edge edge[N<<1]; void init(int nn) { n = nn; tot = 1; memset(head, 0, sizeof(head)); } void add_edge(int u, int v) { edge[tot].to = v; edge[tot].nex = head[u]; head[u] = tot++; } void dfs1(int x, int fa) { siz[x] = f[x] = 1; for (int i = head[x]; i; i = edge[i].nex) { int y = edge[i].to; if (y == fa) continue; dfs1(y, x); f[x] += f[y] * prime[siz[y]]; siz[x] += siz[y]; } } void dfs2(int x, int fa, int fa_f) { g[x] = f[x] + fa_f * prime[n-siz[x]]; fa_f *= prime[n-siz[x]]; for (int i = head[x]; i; i = edge[i].nex) { int y = edge[i].to; if (y == fa) continue; dfs2(y, x, fa_f + f[x] - f[y] * prime[siz[y]]); } } } tree1, tree2; void get_prime(int MAX) { int x = 1; memset(isprime, true, sizeof(isprime)); for (int i = 2; i < MAX; i++) { if (x > N - 5) break; if (isprime[i]) prime[x++] = i; for (int j = 1; j < x; j++) { if (i * prime[j] >= MAX) break; isprime[i * prime[j]] = 0; if (i % prime[j] == 0) break; } } } int main() { get_prime(2000010); int n; while (~scanf("%d", &n)) { tree1.init(n); tree2.init(n + 1); memset(in, 0, sizeof(in)); unordered_set<int> se; for (int i = 0, u, v; i < n - 1; i++) { scanf("%d %d", &u, &v); tree1.add_edge(u, v); tree1.add_edge(v, u); } for (int i = 0, u, v; i < n; i++) { scanf("%d %d", &u, &v); tree2.add_edge(u, v); tree2.add_edge(v, u); ++in[u], ++in[v]; near[u] = v, near[v] = u; } tree1.dfs1(1, 0); tree1.dfs2(1, 0, 0); for (int i = 1; i <= n; i++) se.insert(tree1.g[i]); tree2.dfs1(1, 0); tree2.dfs2(1, 0, 0); for (int i = 1; i <= n + 1; i++) { if (in[i] != 1) continue; if (se.count(tree2.g[near[i]] - 2)) { printf("%d\n", i); break; } } } return 0; }