树哈希

引入

如何 DFS 到某一节点后,利用无序的子树 Hash 值,使得同构的树 Hash 值相同,不同构的 Hash 值尽量不同,且可以 \(O(1)\) 地计算删除部分子树后的 Hash 值?

树结构哈希,简称树哈希,就是解决这类问题的方法。

定义

以某个结点为根的子树的 Hash 值,就是以它的所有儿子为根的子树的 Hash 值构成的多重集的 Hash 值,即

\[H(u)=f(\{H(v)|v \in son(u)\}) \]

其中 \(H(u)\) 表示以 \(u\) 为根的子树的 Hash 值,\(f\) 是多重集的 Hash 函数。

以树结构中使用的 Hash 函数为例:

\[H(u)=(c+\sum_{v \in son(u)}f(H(v))) \mod m \]

其中 \(c\) 为常数,一般使用 \(1\) 即可;\(m\) 为模数,一般使用 \(2^{64}\) 自然溢出或大素数;\(f\) 为整数到整数的映射,代码中优先考虑 xorshift。

xorshift

xorshift 是一个伪随机数生成算法,有 \(2^{32}-1,2^{64}-1\) 等周期。代码模版如下:

ull xorshift(ull x) {
    x ^= x << 13;
    x ^= x >> 7;
    x ^= x << 17;
    return x;
}

\(2^{31}\) 周期比较好用的有 \((1,3,10)\)\(2^{64}\) 周期比较好用的有 \((13,7,17), (5,11,14)\) 等。

模版

给定一棵以点 \(1\) 为根的树,你需要输出这棵树中有多少个互不同构的子树。

两棵有根树 \(T_1\)\(T_2\) 同构当且仅当他们的大小相等,且存在一个顶点排列 \(\sigma\) 使得在 \(T_1\)
\(i\)\(j\) 的祖先当且仅当在 \(T_2\)\(\sigma(i)\)\(\sigma(j)\) 的祖先。

代码如下,时间复杂度瓶颈在排序(去重)。

#include <iostream>
#include <algorithm>
using namespace std;
typedef unsigned long long ull;
const int N = 2e5 + 8;
int n, head[N], nxt[N << 1], to[N << 1], cnt;
ull hs[N], ppow[N];
ull xorshift(ull x) {
    x ^= x << 13;
    x ^= x >> 7;
    x ^= x << 17;
    return x;
}
void add(int u, int v) {
    nxt[++cnt] = head[u];
    to[cnt] = v;
    head[u] = cnt;
}
void dfs(int u, int f) {
    hs[u] = 1;
    for (int i = head[u], v; i; i = nxt[i]) {
        if ((v = to[i]) == f) continue;
        dfs(v, u);
        hs[u] += xorshift(hs[v]);
    }
}
int main() {
    cin >> n;
    for (int i = 1, a, b; i < n; i++) {
        cin >> a >> b;
        add(a, b);
        add(b, a);
    }
    dfs(1, 0);
    sort(hs + 1, hs + n + 1);
    cout << unique(hs + 1, hs + n + 1) - hs - 1;
    return 0;
}

例题

对称二叉树

  1. \(O(n)\) 解法:利用树哈希判断两棵子树是否对称。具体地,定义四种哈希值:\(h_1\) 是左子树优先的先序遍历,\(h_2\) 是右子树优先的先序遍历,\(h_3\) 是左子树优先的中序遍历,\(h_4\) 是右子树优先的中序遍历。先序与中序遍历可以确定唯一的一棵二叉树。以 \(h_1\) 为例,其左子树所有结点的权重为 \(p^{sz(r)}\)
#include <iostream>
using namespace std;
typedef unsigned long long ull;
const int N = 1e6 + 10, p = 1007;
int n, lc[N], rc[N], val[N], sz[N], ans;
ull ppow[N], h[4][N];
void dfs(int u) {
    if (lc[u]) dfs(lc[u]);
    if (rc[u]) dfs(rc[u]);
    int l = lc[u], r = rc[u], lsz = sz[l], rsz = sz[r];
    sz[u] = sz[l] + sz[r] + 1;
    if (h[0][l] == h[1][r] && h[2][l] == h[3][r]) ans = max(ans, sz[u]);
    h[0][u] = val[u] * ppow[lsz + rsz] + h[0][l] * ppow[rsz] + h[0][r]; // h[0]记录根左右
    h[1][u] = val[u] * ppow[lsz + rsz] + h[1][r] * ppow[lsz] + h[1][l]; // h[1]记录根右左
    h[2][u] = h[2][l] * ppow[rsz + 1] + val[u] * ppow[rsz] + h[2][r]; // h[2]记录左根右
    h[3][u] = h[3][r] * ppow[lsz + 1] + val[u] * ppow[lsz] + h[3][l]; // h[3]记录右根左
}
int main() {
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    ppow[0] = 1;
    for (int i = 1; i < N; i++) ppow[i] = ppow[i - 1] * p;
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> val[i];
    for (int i = 1; i <= n; i++) {
        cin >> lc[i] >> rc[i];
        lc[i] = max(lc[i], 0);
        rc[i] = max(rc[i], 0);
    }
    h[0][0] = h[1][0] = h[2][0] = h[3][0] = 1001;
    dfs(1);
    cout << ans;
    return 0;
}
  1. \(O(n \log n)\) 解法:暴力匹配,理论上来说应该是 \(O(n^2)\),然而实践出真知。

证明: 具体式子为:

\[\sum_{i=1}^{\log n} \sum_{j=1}^{2^{i-1}} (2^{\log n - i + 1} - 1) \]

去掉整棵树中第 \(i\) 层级以下的,化简:

\[\sum_{i=1}^{\log n} 2^{i-1}(2^{\log n-i+1}-1) \Longrightarrow \sum_{i=1}^{\log n} (2^{\log n}-2^{i-1}) \]

由满二叉树中 \(2^{\log n} = n + 1 \approx n\),进一步化简:

\[\sum_{i=1}^{\log n} (n-2^{i-1}) \Longrightarrow n \log n - \sum_{i=1}^{\log n}2^{i-1} \]

由等比数列求和公式得,\(\sum_{i=1}^{\log n}2^{i-1}=\frac{1-2^{\log n}}{1-2}=2^{\log n}-1\),化简:

\[n \log n - 2^{\log n} + 1 \Longrightarrow n \log n - n \Longrightarrow O(n \log n) \]

#include <iostream>
using namespace std;
const int N = 1e6 + 8;
int n, val[N], lc[N], rc[N], ans;
bool dfs(int l, int r) {
    if (val[l] != val[r])
        return false;
    else if (l == -1 && r == -1)
        return true;
    return dfs(lc[l], rc[r]) && dfs(lc[r], rc[l]);
}
int cal(int root) {
    if (root == -1)
        return 0;
    return cal(lc[root]) + cal(rc[root]) + 1;
}
int main() {
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> val[i];
    for (int i = 1; i <= n; i++)
        cin >> lc[i] >> rc[i];
    for (int i = 1; i <= n; i++)
        if (dfs(i, i))
            ans = max(ans, cal(i));
    cout << ans;
    return 0;
}

树同构

问题:给定两棵树 \(T_1,T_2\) ,若能够将他们的节点重新标号使得他们的结构相同,则称他们为同构树。现给定 \(m\) 棵无根树,对于每棵树求与其同构的树的最小编号。

用换根 DP 在 \(O(n)\) 的时间内求解以每个节点为根的哈希值。求所有哈希值中的最大和次大值作为树的特征。总时间复杂度为 \(O(mn)\),比较优秀。

#include <iostream>
#include <cstring>
using namespace std;
typedef unsigned long long ull;
const int N = 64;
int n, m, cnt, head[N], nxt[N << 1], to[N << 1];
ull h[N], fmx[N], smx[N];
ull xorshift(ull x) {
    return x ^= x << 13, x ^= x >> 7, x ^= x << 17;
}
void add(int u, int v) {
    nxt[++cnt] = head[u];
    to[cnt] = v;
    head[u] = cnt;
}
void dfs(int u, int f) {
    h[u] = 1;
    for (int i = head[u]; i; i = nxt[i]) {
        int v = to[i];
        if (v == f) continue;
        dfs(v, u);
        h[u] += xorshift(h[v]);
    }
}
void reroot(int u, int f) {
    for (int i = head[u]; i; i = nxt[i]) {
        int v = to[i];
        if (v == f) continue;
        ull hu = h[u];
        hu -= xorshift(h[v]);
        h[v] += xorshift(hu);
        reroot(v, u);
    }
}
int main() {
    cin >> m;
    for (int k = 1; k <= m; k++) {
        cnt = 0;
        memset(head, 0, sizeof(head));
        cin >> n;
        for (int u = 1, v; u <= n; u++) {
            cin >> v;
            if (v == 0) continue;
            add(u, v);
            add(v, u);
        }
        dfs(1, 0);
        reroot(1, 0);
        for (int i = 1; i <= n; i++) {
            if (h[i] > fmx[k]) {
                smx[k] = fmx[k];
                fmx[k] = h[i];
            } else if (h[i] > smx[k])
                smx[k] = h[i];
        }
        for (int i = 1; i <= k; i++) {
            if (fmx[i] == fmx[k] && smx[i] == smx[k]) {
                cout << i << '\n';
                break;
            }
        }
    }
    return 0;
}
posted @ 2026-02-17 21:41  zheyutao  阅读(15)  评论(0)    收藏  举报