后序计算每棵子树的正常表示和镜像表示,若二者相等则该子树对称,再用子树大小更新最大答案。
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.cpp 用 mirror_same(a, b) 直接递归比较左右两棵子树是否镜像一致,逻辑很直观。但这样会重复比较很多相同子结构,面对 10^6 规模不够稳。
镜像关系
这张图展示对称判断时要比较的对应关系:
graph TD A["u"] --> B["左子树"] A --> C["右子树"] B -.镜像对应.-> C
判断一棵子树是否对称,本质上是在问: 左子树和右子树在镜像意义下是否完全一致。 如果每次都真的深入比较整棵子树,会做很多重复工作。
更好的办法是先为每棵子树准备两种摘要:
- 正常表示:按“左、右”顺序描述这棵子树
- 镜像表示:按“右、左”顺序描述这棵子树
于是:
- 如果
normal[u] == mirror[u],说明以u为根的整棵子树对称
所以正式解做一次后序遍历即可:
- 先处理左右儿子
- 组合得到当前节点的正常表示和镜像表示
- 若两种表示相同,则这棵子树对称
- 用子树大小更新答案
代码里使用双哈希保存这两种表示,并用迭代后序遍历避免深递归爆栈。
代码
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;
}复杂度
每个节点只在后序过程中被处理常数次,所以总时间复杂度是
总结
这题的核心不是“如何一棵棵比较子树”,而是“如何给子树做摘要表示”。一旦能快速判断“原树摘要”和“镜像摘要”是否一致,最大对称子树就能在线性时间内找出来。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。





