介绍

两个树同构 $\iff$ 有且仅有一种重新编号方式,使得一棵树的节点重新编号后得到另外一棵树。

• 如果是有根树同构,那么还要额外加一个限制条件:树的节点重新编号,必须让 $r_1$ 映射到 $r_2$,其中 $r_1,r_2$ 为这两棵树的根。

哈希方法

树哈希的方法有很多种,这里提供一位黑红老哥的方法:

对于一棵以 $u$ 为根的子树,假设儿子是 $v_1,v_2,…,v_k$,定义子树的哈希

$$h(u) = 1 + \sum\limits_{i \in [1,k]} f(h(v_i))$$

其中 $h(v_i)$ 是 $v_i$ 对应子树的哈希,$f$ 为一个待定函数。

代码
mt19937 rng((uint32_t)chrono::steady_clock::now().time_since_epoch().count());
ull h[maxn], base = rng();
ull H(ull x){
	return x*x*x*19890535+19260817;
}
ull F(ull x){
	return H(x&((1ll<<32)-1))+H(x>>32);
}
void dfs2(int u, int p, int f) {
    h[u] = base;
    for (int e = head[u]; e; e = edges[e].nxt) {
        int v = edges[e].to;
        if (v == p || v == f) continue;
        dfs2(v, u, f);
        h[u] += F(h[v]);
    }
}

树同构的判断方式

判断两棵树是否同构,一般都是无根树的情况。而对于同一棵树来说,选定不同的节点作为根,得到的哈希值并不相同。

但如果两棵树同构,意味着它们的重心是相同的,所以只要分别找到两棵树的重心,然后让重心作为根,再求哈希值,比较是否相同即可。

• 需要注意的是重心可能有 $2$ 个,所以需要比较 $2$ 次,只要有一次对上了就说明同构。(如果同构,则说明第一棵树的任意一个重心 $a_1$ 的哈希值一定等于第二棵树的其中一个重心的哈希值)。

例题

例1 洛谷P5043 【模板】树同构

题意

给定 $M$ 棵树,根据同构关系将它们分为等价类。

其中,$n, M \leq 50$。

题解

求出每棵树的重心,然后暴力两两对比即可。

代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 55;
const int maxm = 1e5+5;

struct Edge {
    int from, to, nxt;
} edges[maxm<<1];
int head[maxm], ecnt = 1;
void addEdge(int u, int v) {
    Edge e = {u, v, head[u]};
    head[u] = ecnt;
    edges[ecnt++] = e;
}

int sz[maxm], cur_tr;
int M;
int rt[maxn], tr_sz[maxn];
vector<int> cent[maxn];
ull h[maxm], base = rng();
ull H(ull x){
	return x*x*x*19890535+19260817;
}
ull F(ull x){
	return H(x&((1ll<<32)-1))+H(x>>32);
}

void dfs1(int u, int p) {
    sz[u] = 1;
    int mx = 0;
    for (int e = head[u]; e; e = edges[e].nxt) {
        int v = edges[e].to;
        if (v == p) continue;
        dfs1(v, u);
        sz[u] += sz[v];
        mx = max(mx, sz[v]);
    }
    int cur_sz = tr_sz[cur_tr];
    mx = max(mx, cur_sz - sz[u]);
    if (mx <= cur_sz / 2) cent[cur_tr].push_back(u);
}

vector<ull> ha[maxn];
void dfs2(int u, int p) {
    h[u] = base;
    for (int e = head[u]; e; e = edges[e].nxt) {
        int v = edges[e].to;
        if (v == p) continue;
        dfs2(v, u);
        h[u] += F(h[v]);
    }
}


int main() {
    cin >> M;

    int id = 0;
    for (int j = 1; j <= M; j++) {
        int n; cin >> n;
        tr_sz[j] = n;
        for (int i = 1; i <= n; i++) {
            int p; cin >> p;
            if (p > 0) {
                p += id;
                addEdge(p, i+id);
                addEdge(i+id, p);
            } else {
                rt[j] = i + id;
            }
        }
        id += n;
    }

    for (int j = 1; j <= M; j++) {
        cur_tr = j;
        dfs1(rt[j], 0);
        for (int c : cent[j]) {
            dfs2(c, 0);
            ha[j].push_back(h[c]);
        }
    }

    for (int tj = 1; tj <= M; tj++) {
        for (int ti = 1; ti <= tj; ti++) {
            bool ok = 0;
            for (ull h1 : ha[tj]) {
                for (ull h2 : ha[ti]) {
                    if (h1 == h2) ok = 1;
                }
            }
            if (ok) {
                cout << ti << "\n";
                break;
            }
        }
    }
}

例2 CF1252F Regular Forestation

题意

给定一棵无根树,问是否存在一个点 $u$,使得将点 $u$ 移除后,得到的所有子树两两同构?

如果存在,输出 degree 最大的这样的 $u$。

• 要求 $u$ 的degree至少为 $2$。

其中,$n \leq 4000$。

题解

由于 $n \leq 4000$,所以先定 $1$ 为根,然后:

  1. 找出所有子树的重心。
  2. 找出对于所有 $u$,除了 $u$ 子树以外的部分的重心。
  3. 然后对于每个 $u$,check它的所有子树和它以外的部分是否同构。

第一步可以用树的重心提到的 $O(n)$ 方法来找,第二步好像没有什么好方法,只能暴力了。

第三步注意一下重心可能有两个,然后拿个 map 记录一下哈希值的 count,注意到一棵子树最多贡献一次哈希值count即可。

复杂度是 $O(n^2)$。

代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 8005;

struct Edge {
    int from, to, nxt;
} edges[maxn<<1];
int head[maxn], ecnt = 1;
void addEdge(int u, int v) {
    Edge e = {u, v, head[u]};
    head[u] = ecnt;
    edges[ecnt++] = e;
}

int sz[maxn], deg[maxn], son[maxn], par[maxn];
vector<int> cent[maxn<<1];  // cent[i]: centroids of subtree i, cent[i+n]: centroids of up[i]
ull h[maxn], base = rng();
ull H(ull x){
	return x*x*x*19890535+19260817;
}
ull F(ull x){
	return H(x&((1ll<<32)-1))+H(x>>32);
}

void dfs1(int u, int p) {
    par[u] = p;
    sz[u] = 1;
    int mx = 0;
    for (int e = head[u]; e; e = edges[e].nxt) {
        int v = edges[e].to;
        if (v == p) continue;
        dfs1(v, u);
        if (sz[v] > sz[son[u]]) {
            son[u] = v;
        }
        sz[u] += sz[v];
        mx = max(mx, sz[v]);
    }
    if (mx <= sz[u] / 2) cent[u].push_back(u);
    if (cent[son[u]].size()) {
        int k = cent[son[u]][0];
        while (k != u) {
            if (sz[son[k]] <= sz[u] / 2 && sz[u] - sz[k] <= sz[u] / 2) {
                cent[u].push_back(k);
            }
            if (cent[u].size() >= 2 || sz[son[k]] > sz[u] / 2) break;  // 再往上不会更优了
            k = par[k];
        }
    }
}

vector<ull> ha[maxn];
void dfs2(int u, int p, int f) {
    h[u] = base;
    for (int e = head[u]; e; e = edges[e].nxt) {
        int v = edges[e].to;
        if (v == p || v == f) continue;
        dfs2(v, u, f);
        h[u] += F(h[v]);
    }
}

int cursz, currt;  // 当前考虑部分的sz, root (如果是 i 外面的,currt 就是 i + n)
void dfs3(int u, int p, int f) {
    sz[u] = 1;
    int mx = 0;
    for (int e = head[u]; e; e = edges[e].nxt) {
        int v = edges[e].to;
        if (v == p || v == f) continue;
        dfs3(v, u, f);
        sz[u] += sz[v];
        mx = max(mx, sz[v]);
    }
    if (mx <= cursz / 2 && cursz - sz[u] <= cursz / 2) {
        cent[currt].push_back(u);
    }
}

void dfs4(int u, int p, int f) {
    h[u] = base;
    for (int e = head[u]; e; e = edges[e].nxt) {
        int v = edges[e].to;
        if (v == p || v == f) continue;
        dfs4(v, u, f);
        h[u] += F(h[v]);
    }
}

int n;
int ans = 0;
int main() {
    fastio;
    cin >> n;
    for (int i = 1; i < n; i++) {
        int u, v; cin >> u >> v;
        addEdge(u, v); addEdge(v, u);
        deg[u]++; deg[v]++;
    }
    dfs1(1, 0);

    for (int i = 1; i <= n; i++) {
        for (int c : cent[i]) {
            dfs2(c, 0, par[i]);
            ha[i].push_back(h[c]);
        }
    }

    // 现在处理所有子树外的部分
    for (int i = 1; i <= n; i++) {
        if (deg[i] < 2) continue;
        bool ok = 1;
        map<ull, int> mp;  // hash: count
        int s = sz[son[i]];
        if (n - sz[i] != s) ok = 0;
        for (int e = head[i]; e; e = edges[e].nxt) {
            int v = edges[e].to;
            if (v == par[i]) continue;
            if (sz[v] != s) ok = 0;
            if (ha[v].size() == 2 && ha[v][0] == ha[v][1]) {
                mp[ha[v][0]]++;
            } else {
                for (ull hv : ha[v]) {
                    mp[hv]++;
                }
            }
        }
        if (!ok) continue;
        
        // 现在说明 i 可能为最佳切割点
        // 寻找 i 外面子树的信息 (如果有)
        if (par[i]) {
            cursz = n - sz[i];
            currt = i + n;
            dfs3(par[i], 0, i);
        }

        for (int c : cent[i+n]) {
            dfs4(c, 0, i);
            ha[i+n].push_back(h[c]);
        }
        if (ha[i+n].size() == 2 && ha[i+n][0] == ha[i+n][1]) {
            mp[ha[i+n][0]]++;
        } else {
            for (ull hv : ha[i+n]) mp[hv]++;
        }
        for (auto ptr : mp) {
            if (ptr.second == deg[i]) {
                ans = max(ans, deg[i]);
            }
        }
    }
    cout << (ans ? ans : -1) << "\n";
}

参考链接

  1. https://zh.wikipedia.org/zh-hk/%E6%A0%91%E5%90%8C%E6%9E%84
  2. https://peehs-moorhsum.blog.uoj.ac/blog/7891
  3. https://uoj.ac/submission/580510