[NOIP 2018 普及组] 对称二叉树

GitHub跳转原题关系图返回列表

后序计算每棵子树的正常表示和镜像表示,若二者相等则该子树对称,再用子树大小更新最大答案。

OJ: luogu

题目 ID: P5018

难度:普及+/提高

标签:二叉树树形结构思维

日期: 2026-06-19 21:24

题意

给出一棵有点权的二叉树,要求找出其中节点数最多的一棵对称子树。

这里“对称”指的是:把整棵子树的左右儿子全部交换后,结构仍然对应,且对应节点权值也相等。

思路

最直接的办法是枚举每个节点,暴力比较它的左子树和右子树是否镜像相同。

先看一个可以直接验证想法的朴素解:

cpp
#include <bits/stdc++.h>
using namespace std;

static vector<int> value_arr;
static vector<int> left_son;
static vector<int> right_son;
static vector<int> subtree_size;

int calc_size(int u) {
    if (u == -1) {
        return 0;
    }
    subtree_size[u] = calc_size(left_son[u]) + calc_size(right_son[u]) + 1;
    return subtree_size[u];
}

bool mirror_same(int a, int b) {
    if (a == -1 && b == -1) {
        return true;
    }
    if (a == -1 || b == -1) {
        return false;
    }
    if (value_arr[a] != value_arr[b]) {
        return false;
    }
    return mirror_same(left_son[a], right_son[b]) &&
           mirror_same(right_son[a], left_son[b]);
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;

    value_arr.assign(n + 1, 0);
    left_son.assign(n + 1, -1);
    right_son.assign(n + 1, -1);
    subtree_size.assign(n + 1, 0);

    for (int i = 1; i <= n; ++i) {
        cin >> value_arr[i];
    }
    for (int i = 1; i <= n; ++i) {
        cin >> left_son[i] >> right_son[i];
    }

    calc_size(1);

    int ans = 1;
    for (int i = 1; i <= n; ++i) {
        if (mirror_same(left_son[i], right_son[i])) {
            ans = max(ans, subtree_size[i]);
        }
    }
    cout << ans << '\n';
    return 0;
}

brute.cppmirror_same(a, b) 直接递归比较左右两棵子树是否镜像一致,逻辑很直观。但这样会重复比较很多相同子结构,面对 10^6 规模不够稳。

镜像关系

这张图展示对称判断时要比较的对应关系:

graph TD
  A["u"] --> B["左子树"]
  A --> C["右子树"]
  B -.镜像对应.-> C

判断一棵子树是否对称,本质上是在问: 左子树和右子树在镜像意义下是否完全一致。 如果每次都真的深入比较整棵子树,会做很多重复工作。

更好的办法是先为每棵子树准备两种摘要:

  • 正常表示:按“左、右”顺序描述这棵子树
  • 镜像表示:按“右、左”顺序描述这棵子树

于是:

  • 如果 normal[u] == mirror[u],说明以 u 为根的整棵子树对称

所以正式解做一次后序遍历即可:

  1. 先处理左右儿子
  2. 组合得到当前节点的正常表示和镜像表示
  3. 若两种表示相同,则这棵子树对称
  4. 用子树大小更新答案

代码里使用双哈希保存这两种表示,并用迭代后序遍历避免深递归爆栈。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

using ull = unsigned long long;

struct HashValue {
    ull a;
    ull b;
};

struct Frame {
    int u;
    int state;
};

static const ull NULL_A = 1469598103934665603ULL;
static const ull NULL_B = 1099511628211ULL;

ull splitmix64(ull x) {
    x += 0x9e3779b97f4a7c15ULL;
    x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9ULL;
    x = (x ^ (x >> 27)) * 0x94d049bb133111ebULL;
    return x ^ (x >> 31);
}

HashValue combine_hash(int value, const HashValue &left_hash,
                       const HashValue &right_hash) {
    ull x = splitmix64((ull)(value + 1007) ^
                       (left_hash.a * 0x9e3779b97f4a7c15ULL) ^
                       (right_hash.a * 0xc2b2ae3d27d4eb4fULL));
    ull y = splitmix64((ull)(value + 2003) ^
                       (left_hash.b * 0x94d049bb133111ebULL) ^
                       (right_hash.b * 0xbf58476d1ce4e5b9ULL));
    return {x, y};
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;

    vector<int> value(n + 1);
    vector<int> left_son(n + 1), right_son(n + 1);
    for (int i = 1; i <= n; ++i) {
        cin >> value[i];
    }
    for (int i = 1; i <= n; ++i) {
        cin >> left_son[i] >> right_son[i];
    }

    vector<int> subtree_size(n + 1, 0);
    vector<HashValue> normal_hash(n + 1, {0, 0});
    vector<HashValue> mirror_hash(n + 1, {0, 0});
    vector<int> order;
    order.reserve(n);

    vector<Frame> st;
    st.reserve(n * 2);
    st.push_back({1, 0});

    while (!st.empty()) {
        Frame cur = st.back();
        st.pop_back();

        int u = cur.u;
        if (u == -1) {
            continue;
        }

        if (cur.state == 0) {
            st.push_back({u, 1});
            st.push_back({right_son[u], 0});
            st.push_back({left_son[u], 0});
        } else {
            order.push_back(u);
        }
    }

    int ans = 1;
    for (int u : order) {
        HashValue left_norm = (left_son[u] == -1 ? HashValue{NULL_A, NULL_B}
                                                 : normal_hash[left_son[u]]);
        HashValue right_norm = (right_son[u] == -1 ? HashValue{NULL_A, NULL_B}
                                                   : normal_hash[right_son[u]]);
        HashValue left_mirror =
            (left_son[u] == -1 ? HashValue{NULL_A, NULL_B}
                               : mirror_hash[left_son[u]]);
        HashValue right_mirror =
            (right_son[u] == -1 ? HashValue{NULL_A, NULL_B}
                                : mirror_hash[right_son[u]]);

        int left_size = (left_son[u] == -1 ? 0 : subtree_size[left_son[u]]);
        int right_size = (right_son[u] == -1 ? 0 : subtree_size[right_son[u]]);
        subtree_size[u] = left_size + right_size + 1;

        normal_hash[u] = combine_hash(value[u], left_norm, right_norm);
        mirror_hash[u] = combine_hash(value[u], right_mirror, left_mirror);

        if (normal_hash[u].a == mirror_hash[u].a &&
            normal_hash[u].b == mirror_hash[u].b) {
            ans = max(ans, subtree_size[u]);
        }
    }

    cout << ans << '\n';
    return 0;
}

复杂度

每个节点只在后序过程中被处理常数次,所以总时间复杂度是 O(n)O(n),空间复杂度也是 O(n)O(n)

总结

这题的核心不是“如何一棵棵比较子树”,而是“如何给子树做摘要表示”。一旦能快速判断“原树摘要”和“镜像摘要”是否一致,最大对称子树就能在线性时间内找出来。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析