递归分治:按子串区间直接构造 FBI 树并后序输出,根类型由子串含 0/1 情况决定。
OJ: roj
题目 ID: 1365
难度:入门
标签:递归分治二叉树后序遍历
创建: 2026-09-30 07:15
更新: 2026-10-12 15:28
形式化题目
给定长度为
输入:第一行整数
输出:一行字符串,即后序遍历序列。
样例输入:3 / 10001011,样例输出:IBFBBBFIBFIIIFF。
解法总览
两种解法共用同一条分治规则:一段 01 子串就是一棵 FBI 子树,根类型看整段含 0 还是 1,串长大于 1 就从中间对半切。差别只在「后序序列是直接拼出来,还是先把树存下来再走一遍」。
| 解法一:子串递归直接拼(原实现) | 解法二:先建树再后序(正式主解) | |
|---|---|---|
| 做法 | 递归返回左右两半的后序串,拼上根类型 | new_node() 建出 Node{type, lch, rch},再 postorder(root) 收集 |
| 中间结构 | 递归返回的临时字符串(Python 用 @cache 按子串去重) |
结点表 O(L),可复用 |
| 时间 | 每个结点扫一遍自己的子串定类型,O(L log L) | 同左,另有 O(L) 的建树与遍历 |
| 代码 | direct.py / direct.cpp |
main.py / main.cpp |
正式主解是 main.py / main.cpp。
解法一:子串递归直接拼
思路
子串区间与 FBI 子树一一对应。递归处理左右两半,返回后序序列,再拼接根结点类型即可。长度 1 时直接返回该字符对应的 B/I。
代码
Python 版(原实现,@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)。
# FBI 树:子串区间递归,直接算出后序序列,不显式建结点。
# 一段 01 串 bits 就对应一棵 FBI 子树:全 0 为 B,全 1 为 I,既有 0 又有 1 为 F,
# 串长大于 1 时从中间切成等长两半,后序 = 左半 + 右半 + 根。
# 如何使用:
# 1. read_input() # 读入 N 和 01 串 s
# 2. postorder(s) # 返回串 s 构造出的 FBI 树的后序遍历序列
# 3. solve() # 装配:读入 -> 递归 -> 输出
import sys
from functools import cache
s = "" # 输入的 01 串,串长 2^N
def read_input() -> None:
"""读入 N 和 01 串。"""
global s
data = sys.stdin.buffer.read().decode().split()
s = data[1] if int(data[0]) else "" # N == 0 时按空串处理
def node_kind(bits: str) -> str:
"""判断一段 01 串对应的 FBI 结点类型:全 0 为 B,全 1 为 I,否则为 F。"""
return "B" if "1" not in bits else "I" if "0" not in bits else "F"
@cache
def postorder(bits: str) -> str:
"""返回 bits 所构造 FBI 树的后序遍历序列。"""
mid = len(bits) // 2
# 长度大于 1 时先递归左右子树,再拼上根;长度为 1 时直接返回自身类型。
return postorder(bits[:mid]) + postorder(bits[mid:]) + node_kind(bits) if mid else node_kind(bits)
def solve() -> None:
read_input()
print(postorder(s))
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:07
* update_at: 2026-10-05 12:07
*/
// direct.cpp:原方法——子串递归直接拼出后序序列,不显式建结点树(对比新方法见 main.cpp)。
#include <iostream>
#include <string>
using namespace std;
typedef long long ll;
const int MAXN = 1 << 10; // N <= 10,最大长度 1024
char s[MAXN + 5]; // 输入的 01 串
// 判断子串 s[l..r-1] 的结点类型:全 0 为 B,全 1 为 I,否则为 F
char node_kind(int l, int r) {
bool has0 = false, has1 = false;
for (int i = l; i < r; ++i) {
if (s[i] == '0') has0 = true;
else has1 = true;
if (has0 && has1) return 'F';
}
if (has0) return 'B';
return 'I';
}
// 递归返回子串 s[l..r-1] 对应 FBI 树的后序遍历序列
string postorder(int l, int r) {
if (r - l == 1) {
return string(1, node_kind(l, r));
}
int mid = (l + r) >> 1;
return postorder(l, mid) + postorder(mid, r) + node_kind(l, r);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n >> s;
int len = 1 << n; // 2^N
cout << postorder(0, len) << '\n';
return 0;
}复杂度
- 时间:每个结点都要扫自己的子串判断类型,所有子串长度之和为 O(L log L)(树是完全二叉的,每层共 L 个字符,共 log L + 1 层)。
- 空间:递归栈 O(log L),加上返回的字符串与缓存 O(L log L)。
解法二:先建树再后序
思路
规则完全一样,区别在于先把树存下来:build(l, r) 对子串开一个结点、算好类型,串长大于 1 时从 mid = (l + r) / 2 切开,把左右孩子接上;建完再从根做一遍 postorder(u),左、右、根依次收集类型。
结点的类型、左儿子、右儿子收进同一个 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
# 新方法:按题面规则构造 FBI 树,再后序遍历输出结点类型
#(原方法「子串递归直接拼后序」见 direct.py)。
# 树的存储:一个结点的类型、左儿子、右儿子收进 Node,编号即下标。
# 如何使用:
# 1. read_input() # 读入 n 和 01 串
# 2. root = build(0, 2**n) # 一段 01 串就建一棵 FBI 子树,返回根编号
# 3. postorder(root) # 左 -> 右 -> 根 收集类型,solve() 负责装配输出
import sys
class Node:
"""编号 u 的结点:类型 + 左右孩子(0 表示叶子/空)。"""
def __init__(self) -> None:
self.type = "" # 结点类型:全 0 为 B,全 1 为 I,既有 0 又有 1 为 F
self.lch = 0 # 左儿子编号,0 表示叶子
self.rch = 0 # 右儿子编号,0 表示叶子
node: list[Node] = [] # node[u]:编号 u 的结点,下标 0 是占位
node_cnt = 0 # 已分配的结点个数
s = "" # 输入的 01 串,子串 s[l:r] 就是一棵待建的子树
out: list[str] = [] # 后序序列,按访问顺序收集类型
def new_node(kind: str) -> int:
"""动态化静态开点:分配一个新结点并返回它的编号。"""
global node_cnt
node_cnt += 1
node.append(Node())
node[node_cnt].type = kind
return node_cnt
def read_input() -> None:
"""读入 n 和 01 串(串长固定为 2^n)。"""
global s, node
data = iter(sys.stdin.read().split())
n = int(next(data))
s = next(data)
assert len(s) == 1 << n # 题面保证串长恰好 2^n
node = [Node()] # 下标 0 占位,结点编号从 1 开始
def node_kind(l: int, r: int) -> str:
"""判断子串 s[l:r] 的类型:全 0 为 B,全 1 为 I,否则为 F。"""
has0 = has1 = False
for ch in s[l:r]:
if ch == "0":
has0 = True
else:
has1 = True
if has0 and has1:
return "F" # 0 和 1 都出现了,不用再看后面
return "B" if has0 else "I"
def build(l: int, r: int) -> int:
"""递归构造子串 s[l:r] 对应的 FBI 子树,返回根编号。"""
u = new_node(node_kind(l, r)) # 根的类型由整段串决定
if r - l > 1: # 串长为 1 时是叶子,不再往下分
mid = (l + r) // 2 # 从中间切成等长的左右两半
node[u].lch = build(l, mid)
node[u].rch = build(mid, r)
return u
def postorder(u: int) -> None:
"""后序遍历:左子树 -> 右子树 -> 根,把类型依次写进 out。"""
if u == 0:
return
postorder(node[u].lch)
postorder(node[u].rch)
out.append(node[u].type)
def solve() -> None:
read_input()
postorder(build(0, len(s)))
print("".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:07
* update_at: 2026-10-12 15:09
*/
// main.cpp:按题面规则构造 FBI 树(动态化静态建结点),再后序遍历输出结点类型。
//
// 一段 01 串就对应一棵 FBI 子树:全 0 为 B,全 1 为 I,既有 0 又有 1 为 F;
// 串长大于 1 时从中间切成等长两半,递归建左右子树。
// 结点的类型、左儿子、右儿子聚合成 struct Node,node[u] 就是编号 u 的结点,
// 后序走一遍(左 -> 右 -> 根)就是答案。
#include <iostream>
#include <string>
using namespace std;
typedef long long ll;
const int maxn = 2050; // N ≤ 10,串长 2^10 = 1024,FBI 树共 2*1024-1 = 2047 个结点
struct Node {
char type; // 结点类型:全 0 为 B,全 1 为 I,既有 0 又有 1 为 F
int lch; // 左儿子编号,叶子为 0
int rch; // 右儿子编号,叶子为 0
};
Node node[maxn]; // node[u]:编号 u 的结点(编号按建树顺序分配)
int node_cnt; // 已分配的结点个数
string s; // 输入的 01 串,子串 s[l..r-1] 就是一棵待建的子树
// 动态化静态开点:静态数组 + 分配指针 node_cnt,返回新结点的编号
int new_node(char type) {
node_cnt++;
node[node_cnt].type = type;
node[node_cnt].lch = 0;
node[node_cnt].rch = 0;
return node_cnt;
}
// 判断子串 s[l..r-1] 的结点类型:全 0 为 B,全 1 为 I,否则为 F
char node_kind(int l, int r) {
bool has0 = false, has1 = false;
for (int i = l; i < r; i++) {
if (s[i] == '0') {
has0 = true;
} else {
has1 = true;
}
if (has0 && has1) {
return 'F'; // 0 和 1 都出现了,不用再看后面
}
}
if (has0) {
return 'B';
}
return 'I';
}
// 递归构造子串 s[l..r-1] 对应的 FBI 子树,返回根编号
int build(int l, int r) {
int u = new_node(node_kind(l, r)); // 根的类型由整段串决定
if (r - l > 1) { // 串长为 1 时是叶子,不再往下分
int mid = (l + r) / 2; // 从中间切成等长的左右两半
node[u].lch = build(l, mid);
node[u].rch = build(mid, r);
}
return u;
}
// 后序遍历:左子树 -> 右子树 -> 根,依次输出结点类型
void postorder(int u) {
if (u == 0) {
return;
}
postorder(node[u].lch);
postorder(node[u].rch);
cout << node[u].type;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n >> s;
int len = 1 << n; // 串长 2^N
postorder(build(0, len));
cout << '\n';
return 0;
}复杂度
- 时间:定类型 O(L log L)(与解法一相同),建树与后序遍历各 O(L)。
- 空间:结点表 O(L),递归栈 O(log L)。
复杂度对比
| 解法一 | 解法二 | |
|---|---|---|
| 时间 | O(L log L) | O(L log L) |
| 空间 | 字符串拼接 + 缓存 | 结点表 O(L) |
| 能否复用树结构 | 不能 | 能 |
| 适合场景 | 只求一次后序、写法最短 | 后续还要在树上做别的事 |
两法同阶。解法一最短、Python 里几乎是「一行一个递归」;解法二把题面的构造过程如实落到结点上,读题面与读代码能一一对上。
总结
- 子串 ↔ 子树是分治题的常见建模:想清楚「哪一段对应哪棵子树」,剩下的只是递归的返回值选字符串还是选结点编号。
- 解法一的
@cache与解法二的结点表,本质都是避免重复算同一段——前者按子串内容去重,后者按区间位置去重。 - 结点用
Node{type, lch, rch}聚合、编号即下标,是这本仓库建二叉树的标准写法(见《树的存储》)。
