FBI树

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

递归分治:按子串区间直接构造 FBI 树并后序输出,根类型由子串含 0/1 情况决定。

OJ: roj

题目 ID: 1365

难度:入门

标签:递归分治二叉树后序遍历

创建: 2026-09-30 07:15

更新: 2026-10-12 15:28

形式化题目

给定长度为 2N2^N 的 01 串(0⩽N⩽100\leqslant N\leqslant 10)。按规则构造 FBI 树:根结点类型由整串决定(全 0 为 B,全 1 为 I,否则为 F);长度大于 1 时从中间分成左右等长子串,分别递归构造左右子树。输出该树的后序遍历序列。

输入:第一行整数 NN,第二行 01 串。
输出:一行字符串,即后序遍历序列。

样例输入: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 保证同一子串只算一次):

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: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++ 版(同一算法):

cpp
/**
 * 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 版:

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++ 版(同一算法):

cpp
/**
 * 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 里几乎是「一行一个递归」;解法二把题面的构造过程如实落到结点上,读题面与读代码能一一对上。

总结

  1. 子串 ↔ 子树是分治题的常见建模:想清楚「哪一段对应哪棵子树」,剩下的只是递归的返回值选字符串还是选结点编号。
  2. 解法一的 @cache 与解法二的结点表,本质都是避免重复算同一段——前者按子串内容去重,后者按区间位置去重。
  3. 结点用 Node{type, lch, rch} 聚合、编号即下标,是这本仓库建二叉树的标准写法(见《树的存储》)。