先序首字符定根、中序中根的位置切分左右子树,递归求出每棵子树的叶子数即结点长度,再按先序输出「字符 × 长度」。
OJ: roj
题目 ID: 1366
难度:普及-
标签:二叉树树的遍历递归记忆化哈希表python
创建: 2026-09-30 07:15
更新: 2026-10-12 15:28
形式化题目
输入两行字符串,分别是一棵二叉树(字符互不重复)的先序和中序序列。按凹入表示法输出这棵树:按先序,每个结点占一行,该行由它的字符重复「结点长度」次,行数等于结点数;叶结点长度为 ABCDEFG 与 CBDAFEG,样例输出依次为 AAAA、BB、C、D、EE、F、G。
解法总览
两种解法共用同一条观察:先序首字符是根,它在中序里的位置把左右子树切开。差别在「长度(子树叶子数)什么时候算」——递归直接输出时现算,建树时顺手算进结点里。
| 解法一:区间递归直接输出(原实现) | 解法二:先建树再输出(正式主解) | |
|---|---|---|
| 做法 | walk() 按先序往下走,走到一个子树就先现算它的叶子数,输出「字符 × 长度」 |
build() 建出 Node{val, lch, rch, len},长度在建树时算好,walk(root) 只负责输出 |
| 长度来源 | 单独一个 length() 递归(Python 加 @cache 记忆化) |
node[u].len,叶子 1、非叶左右之和 |
| 时间 | C++ 版每结点现算 O(子树大小),链状最坏 O(n²);Python 版记忆化后 O(n) | pos 表 O(1) 定位,建树与输出都是 O(n) |
| 代码 | direct.py / direct.cpp |
main.py / main.cpp |
正式主解是 main.py / main.cpp。
解法一:区间递归直接输出
思路
先序首字符是根,它在中序序列中的位置把左右子树切开,递归下去就能还原整棵树;空子树叶子数记
代码
Python 版(原实现,length() 用 @cache 记忆化,每个区间只算一次):
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 07:15
# update_at: 2026-10-12 15:05
# direct.py:原方法——区间/子串递归直接产出,不显式建结点树(对比新方法见 main.py)。
# 二叉树的凹入表示法:先序 + 中序区间递归,不显式建结点。
# 先序 [pl, pl+n) / 中序 [il, il+n) 就是一棵子树,先序首字符是根,
# 根在中序中的位置把左右子树切开;结点「长度」= 子树叶子数。
# 如何使用:
# 1. read_input() # 读入先序、中序,建好 pos 表(字符 -> 中序下标)
# 2. walk(0, 0, len) # 按先序把「字符 × 长度」逐行写进 out
# 3. solve() # 装配:读入 -> 递归 -> 输出
import sys
from functools import cache
pre = "" # 先序遍历序列
ino = "" # 中序遍历序列
pos: dict[str, int] = {} # pos[c] = 字符 c 在中序中的下标
out: list[str] = [] # 输出的每一行
def read_input() -> None:
"""读入先序、中序序列,建好中序下标表。"""
global pre, ino, pos
data = iter(sys.stdin.buffer.read().split())
pre = next(data).decode()
ino = next(data).decode()
pos = {c: i for i, c in enumerate(ino)} # 字符互不重复,中序下标就是唯一切分点
@cache
def length(pl: int, il: int, n: int) -> int:
"""先序 [pl, pl+n) / 中序 [il, il+n) 这棵子树的根的长度。
长度等于子树里的叶子数:叶为 1,否则左右之和;空子树记 0,
于是「只有一个孩子」不必分情况,缺失的那侧自然贡献 0。
"""
if n <= 1:
return n # n == 0 是空子树,n == 1 是叶
left_n = pos[pre[pl]] - il # 根在中序的位置 → 左子树大小
return length(pl + 1, il, left_n) + length(pl + 1 + left_n, pos[pre[pl]] + 1, n - 1 - left_n)
def walk(pl: int, il: int, n: int) -> None:
"""按先序产出整棵子树:先根自己的那一行,再左子树、右子树。"""
if n == 0:
return
left_n = pos[pre[pl]] - il
out.append(pre[pl] * length(pl, il, n)) # 该结点字母重复「长度」次
walk(pl + 1, il, left_n)
walk(pl + 1 + left_n, pos[pre[pl]] + 1, n - 1 - left_n)
def solve() -> None:
read_input()
walk(0, 0, len(pre))
print('\n'.join(out))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-05 12:08
* update_at: 2026-10-05 12:08
*/
// direct.cpp:原方法——先序/中序区间递归直接输出,不显式建结点树(对比新方法见 main.cpp)。
#include <cstdio>
typedef long long ll;
const int MAXN = 100005; // 结点数上限(题面未给,取足够大)
char buf[MAXN]; // 读入缓冲
int pre[MAXN]; // 先序序列,存字符编码
int ino[MAXN]; // 中序序列,存字符编码
int pos[256]; // pos[字符编码] = 该字符在中序序列中的下标,用于 O(1) 定位切分点
int n; // 结点个数
// 返回先序 [pl, pl+cnt) 与中序 [il, il+cnt) 这棵子树的叶子数。
// 题面中结点的「长度」就等于子树的叶子数:叶为 1,非叶为左右之和。
// 空子树(cnt == 0)返回 0,于是只有一个孩子时缺失的那侧自然贡献 0。
int leaf_count(int pl, int il, int cnt) {
if (cnt <= 1) {
return cnt;
}
int left_cnt = pos[pre[pl]] - il; // 先序首字符是根,它在中序中的位置给出左子树大小
return leaf_count(pl + 1, il, left_cnt)
+ leaf_count(pl + 1 + left_cnt, il + left_cnt + 1, cnt - 1 - left_cnt);
}
// 按先序输出这棵子树:先输出根那一行(字符重复「叶子数」次),再递归左右子树。
void walk(int pl, int il, int cnt) {
if (cnt == 0) {
return;
}
int line_cnt = leaf_count(pl, il, cnt); // 该结点字符要重复的次数
for (int i = 0; i < line_cnt; i++) {
putchar(pre[pl]);
}
putchar('\n');
int left_cnt = pos[pre[pl]] - il; // 左子树大小
walk(pl + 1, il, left_cnt);
walk(pl + 1 + left_cnt, il + left_cnt + 1, cnt - 1 - left_cnt);
}
int main() {
scanf("%s", buf);
n = 0;
while (buf[n] != '\0') {
pre[n] = buf[n];
n++;
}
scanf("%s", buf);
for (int i = 0; i < n; i++) {
ino[i] = buf[i];
pos[ino[i]] = i;
}
walk(0, 0, n);
return 0;
}复杂度
- 时间:Python 版每个
(先序起点, 中序起点, 区间长)只算一次,O(n);C++ 版没有记忆化,每输出一个结点就重算一次它的子树叶子数,链状最坏 O(n²)、平衡时 O(n log n),实测数据可过。 - 空间:递归栈 O(树高)。
解法二:先建树再输出
思路
观察相同,区别在于先序 + 中序还原这棵树时就把长度算好:build(pl, il, cnt) 用先序首字符定根、pos[] O(1) 定位切分点,递归接上左右孩子;叶子长度置 1,非叶取左右之和——长度与结点同时诞生,后面 walk(root) 只需按先序输出「字符 × len」,一次遍历,不必回头重算。
结点的字符、左儿子、右儿子、长度收进同一个 Node,编号即下标(静态数组 + node_cnt 动态开点)。
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 07:15
# update_at: 2026-10-12 15:25
# 新方法:先序 + 中序还原二叉树,再按凹入表示法输出(结点长度 = 子树叶子数)
#(原方法「区间递归直接输出」见 direct.py)。
# 树的存储:一个结点的字符、左右孩子、长度收进 Node,编号即下标;
# 长度在建树时顺手算好:叶子为 1,非叶为左右之和(空子树为 0)。
# 如何使用:
# 1. read_input() # 读入先序、中序,建好中序下标表 pos
# 2. root = build(0, 0, n) # 先序首字符定根,中序位置切分,递归建树
# 3. walk(root) # 先序输出「字符 × 长度」,solve() 负责装配输出
import sys
class Node:
"""编号 u 的结点:字符 + 左右孩子 + 长度(子树叶子数)。"""
def __init__(self) -> None:
self.val = "" # 结点字符
self.lch = 0 # 左儿子编号,0 表示空
self.rch = 0 # 右儿子编号,0 表示空
self.len = 0 # 结点长度 = 子树叶子数:叶为 1,非叶为左右之和,空子树为 0
node: list[Node] = [] # node[u]:编号 u 的结点,下标 0 是占位
node_cnt = 0 # 已分配的结点个数
pre = "" # 先序遍历序列
ino = "" # 中序遍历序列,只用来建 pos 表
pos: dict[str, int] = {} # pos[c] = 字符 c 在中序中的下标(字符互不重复,可 O(1) 定位)
out: list[str] = [] # 凹入输出的每一行
def new_node(ch: str) -> int:
"""动态化静态开点:分配一个新结点并返回它的编号。"""
global node_cnt
node_cnt += 1
node.append(Node())
node[node_cnt].val = ch
return node_cnt
def read_input() -> None:
"""读入先序、中序序列,建好中序下标表。"""
global pre, ino, pos, node
data = iter(sys.stdin.read().split())
pre = next(data)
ino = next(data)
pos = {ch: i for i, ch in enumerate(ino)}
node = [Node()] # 下标 0 占位,结点编号从 1 开始
def build(pl: int, il: int, cnt: int) -> int:
"""用先序 pre[pl:pl+cnt] 与中序 ino[il:il+cnt] 建树,返回根编号;空子树返回 0。"""
if cnt == 0:
return 0
u = new_node(pre[pl]) # 先序首字符就是根
left_cnt = pos[pre[pl]] - il # 根在中序中的位置给出左子树大小
node[u].lch = build(pl + 1, il, left_cnt)
node[u].rch = build(pl + 1 + left_cnt, il + left_cnt + 1, cnt - 1 - left_cnt)
# 长度 = 子树叶子数:叶为 1,非叶为左右之和
node[u].len = 1 if node[u].lch == 0 and node[u].rch == 0 else (
node[node[u].lch].len + node[node[u].rch].len
)
return u
def walk(u: int) -> None:
"""按先序输出凹入表示:先根那一行(字符重复 len 次),再递归左右子树。"""
if u == 0:
return
out.append(node[u].val * node[u].len)
walk(node[u].lch)
walk(node[u].rch)
def solve() -> None:
read_input()
walk(build(0, 0, len(pre)))
print("\n".join(out))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-05 12:08
* update_at: 2026-10-12 15:09
*/
// main.cpp:先序 + 中序还原二叉树,按凹入表示法输出(结点长度 = 子树叶子数)。
//
// 先序首字符是根,它在中序里的位置把左右子树切开,递归建树;
// 结点的值、左儿子、右儿子、长度聚合成 struct Node,node[u] 就是编号 u 的结点:
// 叶子长度为 1,非叶长度等于左右子树长度之和(空子树记 0),
// 建树时顺手算好 node[u].len,最后按先序输出「字符 × 长度」。
#include <cstdio>
#include <cstring>
typedef long long ll;
const int maxn = 100005; // 题面未给串长上限(字母互不重复),按原实现开足够大
struct Node {
char val; // 结点的字符
int lch; // 左儿子编号,0 表示空
int rch; // 右儿子编号,0 表示空
int len; // 结点长度 = 子树叶子数:叶为 1,非叶为左右之和,空子树为 0
};
Node node[maxn]; // node[u]:编号 u 的结点(编号按建树顺序分配)
int node_cnt; // 已分配的结点个数
char pre[maxn]; // 先序遍历序列
char ino[maxn]; // 中序遍历序列,只用来建 pos 表
int pos[256]; // pos[c] = 字符 c 在中序中的下标,用于 O(1) 定位切分点
int n; // 结点个数
// 动态化静态开点:静态数组 + 分配指针 node_cnt,返回新结点的编号
int new_node(char val) {
node_cnt++;
node[node_cnt].val = val;
node[node_cnt].lch = 0;
node[node_cnt].rch = 0;
node[node_cnt].len = 0;
return node_cnt;
}
// 用先序 [pl, pl+cnt) 与中序 [il, il+cnt) 建树,返回根编号;空子树返回 0
int build(int pl, int il, int cnt) {
if (cnt == 0) {
return 0;
}
int u = new_node(pre[pl]); // 先序首字符就是根
int left_cnt = pos[pre[pl]] - il; // 根在中序中的位置给出左子树大小
node[u].lch = build(pl + 1, il, left_cnt);
node[u].rch = build(pl + 1 + left_cnt, il + left_cnt + 1, cnt - 1 - left_cnt);
// 长度 = 子树叶子数:叶为 1,非叶为左右之和
if (node[u].lch == 0 && node[u].rch == 0) {
node[u].len = 1;
} else {
node[u].len = node[node[u].lch].len + node[node[u].rch].len;
}
return u;
}
// 按先序输出凹入表示:先根那一行(字符重复 len 次),再递归左右子树
void walk(int u) {
if (u == 0) {
return;
}
for (int i = 0; i < node[u].len; i++) {
putchar(node[u].val);
}
putchar('\n');
walk(node[u].lch);
walk(node[u].rch);
}
int main() {
scanf("%s %s", pre, ino);
n = strlen(pre);
for (int i = 0; i < n; i++) {
pos[ino[i]] = i; // 字符互不重复,中序下标就是唯一的切分点
}
walk(build(0, 0, n));
return 0;
}复杂度
- 时间:建树 O(n)(pos 表让切分是 O(1)),输出 O(n),合计 O(n)。
- 空间:结点表 O(n),递归栈 O(树高)。
复杂度对比
| 解法一 | 解法二 | |
|---|---|---|
| 时间 | Python 记忆化 O(n);C++ 无记忆化最坏 O(n²) | 两种语言都 O(n) |
| 空间 | 递归栈 + 缓存 | 结点表 O(n) |
| 能否复用树结构 | 不能 | 能 |
| 适合场景 | 只求一次凹入输出 | 后续还要在树上做别的事(改遍历序、统计) |
解法一少一层结构、写起来短;解法二把「长度 = 子树叶子数」这条定义直接物化进结点,任何语言都能稳定 O(n),也是后续树题的标准起点。
总结
- 先序 + 中序还原二叉树的套路:首字符定根、中序位置切分,
pos[]表让切分从 O(n) 降到 O(1)。 - 「递归返回值」和「结点字段」是存同一个信息的两种方式:
length()现算 vsnode[u].len预存,差别在于会不会被重复调用——这正是 C++ 版解法一最坏 O(n²)、解法二稳定 O(n) 的原因。 - 结点用
Node聚合、编号即下标,是这本仓库建二叉树的标准写法(见《树的存储》)。