ruye07

导航

二叉树专题:从遍历、重建到应用

二叉树专题:从遍历、重建到应用

覆盖洛谷 8 道经典二叉树题目,从遍历互求到树上DP,一篇文章讲透。

题单:P1827 / P1030 / P1229 / P1305 / P5076 / P1364 / P3884 / P1185


一、基础知识速览

1.1 三种遍历的定义

以下面这棵树为例:

        A
       / \
      B   C
     / \   \
    D   E   F
遍历方式 访问顺序 该树输出
前序(先根) → 左子树 → 右子树 A B D E C F
中序(中根) 左子树 → → 右子树 D B E A C F
后序(后根) 左子树 → 右子树 → D E B F C A

1.2 递归模板

三种遍历的递归实现,只有 print(root) 的位置不同:

def preorder(u):
    if not u: return
    print(u.val)       # 根
    preorder(u.left)   # 左
    preorder(u.right)  # 右

def inorder(u):
    if not u: return
    inorder(u.left)
    print(u.val)       # 根在中间
    inorder(u.right)

def postorder(u):
    if not u: return
    postorder(u.left)
    postorder(u.right)
    print(u.val)       # 根在最后

1.3 核心性质

给定中序 + 另一种遍历 → 唯一确定二叉树

给定前序 + 后序 → 不能唯一确定(后文展开)

原因:中序能区分左右子树,而前序/后序只能确定根。


二、遍历互求(三道核心题)


2.1 P1827 前序 + 中序 → 后序

题目:给定前序遍历和中序遍历,输出后序遍历。节点最多 26 个。

样例输入:
ABEDFCHG
CBADEFGH

样例输出:
AEFDBHGC

对应二叉树:
        C
       / \
      B   G
     / \  /
    A   D H
       / \
      E   F

思路

前序首字符 = 根
中序中根的位置 = 左右子树的分界点
前序: C B A D E F G H
中序: A B E D F C H G

   root='C', 在中序的 idx=5
   左子树: in="ABEDF"(5个), pre="BADEF"(根后5个)
   右子树: in="HG"(2个), pre="GH"(剩余2个)
       ↓ 递归
   后序 = 左后序 + 右后序 + 'C'
def build(in_o, pre_o):
    if not pre_o:
        return ''
    root = pre_o[0]
    idx = in_o.index(root)

    left_in  = in_o[:idx]
    right_in = in_o[idx + 1:]
    left_pre  = pre_o[1 : 1 + idx]
    right_pre = pre_o[1 + idx:]

    return (build(left_in, left_pre) +
            build(right_in, right_pre) +
            root)

# 输入
in_o = input().strip()   # ⚠️ strip() 不能省,样例第二行末尾有空格
pre_o = input().strip()
print(build(in_o, pre_o))

易错点

  • input().strip() — 洛谷部分测试数据行末带空格
  • 切片 pre[1 : 1+idx] 取左子树,正好 idx 个节点

2.2 P1030 中序 + 后序 → 先序

题目:给定中序和后序遍历,输出前序遍历。(NOIP 2001 普及组)

样例输入:
BADC
BDCA

样例输出:
ABCD

与 P1827 的区别:后序的最后一个字符是根,其余完全对称。

中序: B A D C
后序: B D C A

   root = post[-1] = 'A', 在中序的 idx=1
   左子树: in="B", post="B"
   右子树: in="DC", post="DC"
       ↓ 递归
   先序 = 'A' + 左先序 + 右先序  ← 注意拼接顺序变了
def build(in_o, post_o):
    if not post_o:
        return ''
    root = post_o[-1]                     # 后序最后一个 = 根
    idx = in_o.index(root)

    left_in  = in_o[:idx]
    right_in = in_o[idx + 1:]
    left_post  = post_o[:idx]             # 后序前 idx 个
    right_post = post_o[idx:-1]           # 中间部分(不含根)

    return root + build(left_in, left_post) + build(right_in, right_post)
    #        ↑ 先序:根在前

对照表

P1827(前+中→后) P1030(中+后→先)
根的位置 pre[0] post[-1]
拼接顺序 左后序 + 右后序 + 根 根 + 左先序 + 右先序
核心思路 完全一致 完全一致

2.3 P1229 前序 + 后序 → 中序计数

题目:给定前序和后序遍历,求可能的中序遍历总数

样例输入:
abc
cba

样例输出:
4

关键结论:前序+后序无法唯一确定二叉树的原因是——当某个节点只有一个子节点时,无法区分它是左孩子还是右孩子。

    A        A
   /    vs    \
  B            B

前序都是: A B
后序都是: B A
但中序可以是: B A 或 A B

算法:统计有多少个节点只有一个子节点(即该节点的前序中,子节点紧跟在它后面,且在后序中子节点紧贴在它前面),记为 cnt,答案 = $2^{cnt}$。

pre  = "abc"
post = "cba"

① 整段 pre="abc", post="cba"
   根='a', 左子根=pre[1]='b', 右子根=post[-2]='b'
   'b'=='b' → 只有一个孩子,cnt++

② 子段 pre="bc", post="cb"  
   根='b', 左子根=pre[1]='c', 右子根=post[-2]='c'
   'c'=='c' → 只有一个孩子,cnt++

cnt=2, 答案=2²=4
def count(pre, post):
    if not pre:
        return 0
    if len(pre) == 1:
        return 0
    # pre[1] = 左子树的根(如果有左孩子)
    # post[-2] = 右子树的根(如果有右孩子)
    cnt = 0
    if pre[1] == post[-2]:    # 只有一个孩子
        cnt = 1

    root = pre[0]
    # 找到左子树的根在 post 中的位置来切分
    left_root = pre[1]
    idx = post.index(left_root)

    left_pre  = pre[1 : 2 + idx]
    left_post = post[:idx + 1]
    right_pre = pre[2 + idx:]
    right_post = post[idx + 1 : -1]

    return cnt + count(left_pre, left_post) + count(right_pre, right_post)

pre = input().strip()
post = input().strip()
print(2 ** count(pre, post))

总结

已知条件 结果 原因
前序 + 中序 唯一二叉树 中序区分左右
后序 + 中序 唯一二叉树 同上
前序 + 后序 不唯一 单子节点方向不确定
层序 + 中序 唯一二叉树 层序首个=根,中序定左右

三、二叉树的构建与遍历(P1305)

题目:给定每个节点的左右孩子(* 表示空),输出前序遍历。第一行读入的为根。

样例输入:
6
abc
bdi
cj*
d**
i**
j**

样例输出:
abdicj

对应二叉树:
        a
       / \
      b   c
     / \  /
    d   i j

思路:用字典存每个节点的左右孩子,然后从根开始递归前序遍历。

n = int(input())
tree = {}
root = None

for _ in range(n):
    line = input().split()
    if len(line) == 1:
        # 可能是 "abc" 三个字符挤在一起
        p, l, r = line[0][0], line[0][1], line[0][2]
    else:
        p, l, r = line
    if root is None:
        root = p
    tree[p] = (l if l != '*' else None, r if r != '*' else None)

def preorder(u):
    if u is None:
        return ''
    l, r = tree.get(u, (None, None))
    return u + preorder(l) + preorder(r)

print(preorder(root))

注意:输入格式不统一,有的数据是空格分隔,有的三个字符连在一起,需要容错处理。


四、二叉搜索树操作(P5076)

题目:维护一个集合,支持 5 种操作(查询排名、按排名查数、前驱、后继、插入)。q ≤ 10⁴。

样例输入:
7
5 1
5 3
5 5
1 3
2 2
3 3
4 3

样例输出:
2        ← 操作1: 3的排名, 集合={1,3,5}, 小于3的有1个, 排名=2
3        ← 操作2: 排名为2的数=3
1        ← 操作3: 3的前驱=1
5        ← 操作4: 3的后继=5

操作过程: 插入1 → 插入3 → 插入5 → 查3的排名 → 查第2小 → 3的前驱 → 3的后继

思路一:手写 BST

class Node:
    __slots__ = ('val', 'left', 'right', 'size', 'cnt')
    def __init__(self, val):
        self.val = val
        self.left = None
        self.right = None
        self.size = 1   # 子树节点数
        self.cnt = 1    # 该值出现次数

def insert(root, x):
    if root is None:
        return Node(x)
    if x < root.val:
        root.left = insert(root.left, x)
    elif x > root.val:
        root.right = insert(root.right, x)
    else:
        root.cnt += 1
    root.size = (root.left.size if root.left else 0) + \
                root.cnt + \
                (root.right.size if root.right else 0)
    return root

def rank(root, x):
    """查询 x 的排名(小于 x 的数的个数 + 1)"""
    if root is None:
        return 1
    if x <= root.val:
        return rank(root.left, x)
    else:
        left_size = root.left.size if root.left else 0
        return left_size + root.cnt + rank(root.right, x)

def kth(root, k):
    """查询排名为 k 的数"""
    left_size = root.left.size if root.left else 0
    if k <= left_size:
        return kth(root.left, k)
    elif k <= left_size + root.cnt:
        return root.val
    else:
        return kth(root.right, k - left_size - root.cnt)

def predecessor(root, x):
    """前驱:小于 x 的最大值"""
    if root is None:
        return -2147483647
    if root.val >= x:
        return predecessor(root.left, x)
    else:
        # 当前根小于 x,答案可能是根,也可能在右子树
        return max(root.val, predecessor(root.right, x))

def successor(root, x):
    """后继:大于 x 的最小值"""
    if root is None:
        return 2147483647
    if root.val <= x:
        return successor(root.right, x)
    else:
        return min(root.val, successor(root.left, x))

思路二:Python bisect 硬搞(比赛偷懒版)

由于 q ≤ 10⁴,O(q²) 能过(插入时 O(n) 挪动),直接维护有序列表即可:

import bisect

a = []
for _ in range(q):
    op, x = map(int, input().split())
    if op == 1:    # 查排名
        print(bisect.bisect_left(a, x) + 1)
    elif op == 2:  # 按排名查
        print(a[x - 1])
    elif op == 3:  # 前驱
        idx = bisect.bisect_left(a, x) - 1
        print(a[idx] if idx >= 0 else -2147483647)
    elif op == 4:  # 后继
        idx = bisect.bisect_right(a, x)
        print(a[idx] if idx < len(a) else 2147483647)
    elif op == 5:  # 插入
        bisect.insort(a, x)

推荐:比赛用 bisect,手写 BST 是理解「在递归中利用子树大小做决策」的最佳练习。

BST 决策逻辑总结

        [根5, size=6]
        /           \
   [左子树]      [右子树]
   size=3        size=2
   值<5          值>5

查排名(7):7>5 → 跳过左子树(3个)+根(1个) → 在右子树查 rank=rank-4
查第k大:k<=左size → 去左;k<=左size+1 → 答案=根;否则去右

五、树的性质与DP


5.1 P1364 医院设置(树的重心)

题目:二叉树每个节点有居民人数 w,选一个节点建医院,使所有人到医院的距离之和最小。相邻节点距离=1。

样例输入:
5
13 2 3
4 0 0
12 4 5
20 0 0
40 0 0

样例输出:
81

树结构:          1(w=13)
                /      \
            2(w=4)    3(w=12)
                      /      \
                  4(w=20)  5(w=40)

医院建在3: 4×2 + 13 + 20 + 40 = 81  (最优)
医院建在1: 4 + 12 + 2×20 + 2×40 = 136

思路一:暴力枚举(n ≤ 100,O(n²) 稳过)

以每个节点为医院候选,BFS/DFS 计算总距离:

from collections import deque

n = int(input())
w = [0] * (n + 1)
g = [[] for _ in range(n + 1)]

for i in range(1, n + 1):
    weight, l, r = map(int, input().split())
    w[i] = weight
    if l:
        g[i].append(l)
        g[l].append(i)
    if r:
        g[i].append(r)
        g[r].append(i)

ans = float('inf')
for i in range(1, n + 1):
    # BFS 计算以 i 为医院的总距离
    dist = [-1] * (n + 1)
    dist[i] = 0
    q = deque([i])
    total = 0
    while q:
        u = q.popleft()
        total += w[u] * dist[u]
        for v in g[u]:
            if dist[v] == -1:
                dist[v] = dist[u] + 1
                q.append(v)
    ans = min(ans, total)
print(ans)

思路二:树形 DP(换根法,O(n))

第一次 DFS:以 1 为根,计算子树总人数 sz[u] 和以 1 为医院的总距离 dp[1]

第二次 DFS:换根,dp[v] = dp[u] - sz[v] + (total_sum - sz[v])

# 篇幅原因只给出核心转移方程
# dp[v] = dp[u] + total_w - 2 * sz[v]

直观理解:把医院从 u 移到邻居 v 时,

  • v 子树内的居民少走 1 步(减 sz[v])
  • v 子树外的居民多走 1 步(加 total_w - sz[v])

5.2 P3884 二叉树问题(深度、宽度、距离)

题目:给定以 1 为根的二叉树,求:

  1. 深度:最大层数
  2. 宽度:同一层最多节点数
  3. 距离:dist(u, v) = 2×(到 LCA 的向上边数) + (LCA 到另一节点的向下边数)
样例输入:
10
1 2
1 3
2 4
2 5
3 6
3 7
5 8
5 9
6 10
8 6

样例输出:
4    ← 深度
4    ← 宽度(第2层和第3层各有4个节点)
8    ← dist(8,6)

对应二叉树:
        1
       / \
      2   3
     /\   /\
    4  5 6  7
      /\ |
     8 9 10

距离定义的直观理解

   1          ← LCA
  / \
 2   3
 |   |
 5   6        dist(8, 6) = ?
 |
 8

路径 8→5→2→1→3→6
     ←向上→  ←向下→
      3条     2条
距离 = 2×3 + 2 = 8

实现

n = int(input())
g = [[] for _ in range(n + 1)]
parent = [0] * (n + 1)
depth = [0] * (n + 1)
width = [0] * (n + 1)   # width[d] = 第 d 层的节点数

for _ in range(n - 1):
    u, v = map(int, input().split())
    g[u].append(v)
    parent[v] = u

x, y = map(int, input().split())
# 如果最后一行是空格分隔的 (题目用例是两行)
# 需要容错处理

# DFS 统计深度和宽度
max_depth = 0
def dfs(u, d):
    global max_depth
    max_depth = max(max_depth, d)
    depth[u] = d
    width[d] += 1
    for v in g[u]:
        dfs(v, d + 1)

dfs(1, 0)

# 求 LCA:向上标记法(n≤100 够用)
def lca(a, b):
    visited = set()
    while a:
        visited.add(a)
        a = parent[a]
    while b:
        if b in visited:
            return b
        b = parent[b]
    return 1

anc = lca(x, y)
up = depth[x] - depth[anc]      # 向上的边
down = depth[y] - depth[anc]    # 向下的边
dist = 2 * up + down

print(max_depth + 1)            # 深度 = 最大层号+1(取决于根算第几层)
print(max(width))
print(dist)

输入最后一行是两个整数 x, y,注意读入方式。


六、模拟与绘制(P1185)

题目:绘制一棵去掉若干节点的满二叉树。m 层,删除 n 个节点。

# 样例1: m=2, n=0(不删除任何节点)
输入:
2 0
输出:
  o
 / \
o   o

# 样例2: m=4, n=0(完整4层满二叉树)
输入:
4 0
输出:
           o
          / \
         /   \
        /     \
       /       \
      /         \
     o           o
    / \         / \
   /   \       /   \
  o     o     o     o
 / \   / \   / \   / \
o   o o   o o   o o   o

# 样例3: m=4, 删除(3,2) (4,1) (3,4)
输入:
4 3
3 2
4 1
3 4
输出:
           o
          / \
         /   \
        /     \
       /       \
      /         \
     o           o
    /           /
   /           /
  o           o
   \         / \
    o       o   o

核心难点:坐标计算和字符摆放。

6.1 满二叉树的绘制规律

m=3 的满二叉树:
      o          ← 第1层, 根在列中心
     / \
    o   o        ← 第2层
   / \ / \
  o  o o  o     ← 第3层(叶子),同父间3空格,异父间1空格

坐标推算

对于 m 层的满二叉树:

  • 叶子层(第 m 层)节点间距固定:同父 3 空格 + 节点 o,异父 1 空格
  • 每个叶子节点占据宽度 = 1(字符 'o')+ 右侧空格
  • 从叶子层逐层向上推父节点的列坐标:col[parent] = (col[left] + col[right]) // 2

6.2 实现思路

# 第1步:确定叶子层每个节点的列坐标
# 第2步:从下往上推父节点坐标
# 第3步:在字符矩阵中放置 'o' 和 '/' '\'
# 第4步:根据删除列表挖掉节点及子树

m, n = map(int, input().split())
deleted = set()
for _ in range(n):
    layer, pos = map(int, input().split())
    # 标记该节点及其子树为删除
    ...

这道题考察的是坐标映射 + 模拟,而非传统数据结构。建议先手算小样例理解坐标关系,再编码。


七、题目能力矩阵

题号 核心考点 难度 关键技巧
P1305 树的存储与遍历 ★☆☆ 邻接表 + 递归
P1827 遍历互求 ★★☆ 分治 + 切片
P1030 遍历互求(变体) ★★☆ 与 P1827 对称
P1229 遍历唯一性 ★★★ 单子节点判定 + 计数
P5076 BST 操作 ★★★ size 维护 + 递归决策
P1364 树的重心 ★★★ 换根DP / 暴力BFS
P3884 树性质 + LCA ★★★ 深度/宽度/路径分解
P1185 绘制模拟 ★★★★ 坐标映射 + 格式化

八、总结

             遍历:前中后,根的位置决定一切
             重建:中序定左右,另一遍历定根
             计数:单子节点是歧义的来源(2^cnt)
             操作:BST 维护 size,递归做决策
             DP:  换根转移 = 总人数 - 2×子树人数
             距离:LCA 分解 = 2×向上 + 向下
             绘制:叶子定位 → 倒推父坐标 → 排版

整理时间:2026-05-18 · 题源:洛谷

posted on 2026-05-18 18:40  ruye07  阅读(71)  评论(0)    收藏  举报