绘制二叉树

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

按层数公式计算满二叉树画布坐标,递归绘制节点和斜边,并跳过被删除的整棵子树。

OJ: luogu

题目 ID: P1185

难度:普及+/提高

标签:二叉树递归模拟python

日期: 2026-07-16 18:17

题意

按固定 ASCII 规则绘制 m 层满二叉树,并删除指定节点、它的所有后代以及与父节点的连线。

思路

画布高度和宽度为:

height=3×2m2,width=2×height1 height=3\times2^{m-2},\qquad width=2\times height-1

根位于 (0,width//2)。除叶层外,第 level 层节点行号为 height-3*2^(m-level-1),叶层在最后一行。父子节点横向偏移恰好等于纵向行差,因此连线上的每一步同时改变一行一列。

用满二叉树堆编号表示节点:第 level 层第 index 个节点编号是 2^(level-1)+index-1。先递归把删除节点的整个子树加入集合;绘制时若孩子已删除,就连线和子树都跳过。

Python 知识

  • 二维画布必须写成 [[" "]*width for _ in range(height)],保证各行独立。
  • deleted 集合保存堆编号,判断某棵子树是否绘制。
  • 三元组 (child,direction,slash) 把左右孩子的对称绘制合并成同一循环。
  • 最后按固定宽度 "".join(row) 输出,保留题目要求的行尾空格。
  • /home/rainboy/mycode/hugo-blog/content/program_language/python/cpp_to_python_pitfalls.md:二维列表独立行。
  • /home/rainboy/mycode/hugo-blog/content/program_language/python/brute_force_validation.md:递归树状态。

代码

python
m, removal_count = map(int, input().split())
height = 3 * (1 << (m - 2))
width = 2 * height - 1
node_row = [0] * (m + 1)
for level in range(1, m):
    node_row[level] = height - 3 * (1 << (m - level - 1))
node_row[m] = height - 1

deleted = set()


def remove_subtree(node):
    if node >= 1 << m:
        return
    deleted.add(node)
    remove_subtree(node * 2)
    remove_subtree(node * 2 + 1)


for _ in range(removal_count):
    level, index = map(int, input().split())
    remove_subtree((1 << (level - 1)) + index - 1)

canvas = [[" "] * width for _ in range(height)]


def draw(node, level, row, column):
    if node in deleted:
        return
    canvas[row][column] = "o"
    if level == m:
        return

    child_row = node_row[level + 1]
    gap = child_row - row
    for child, direction, slash in ((node * 2, -1, "/"), (node * 2 + 1, 1, "\\")):
        if child in deleted:
            continue
        for step in range(1, gap):
            canvas[row + step][column + direction * step] = slash
        draw(child, level + 1, child_row, column + direction * gap)


draw(1, 1, 0, width // 2)
print("\n".join("".join(row) for row in canvas))
cpp
/**
 * P1185 绘制二叉树
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-27 00:00
 * update_at: 2026-07-27 00:00
 */

#include <bits/stdc++.h>
using namespace std;

const int MAXM = 11;

int m, k;
bool del[1 << MAXM]; // 标记被删除的结点(按完全二叉树编号)
char canvas[800][1600]; // 画布(宽 = 2*高-1)
int height, width;
int node_row[MAXM]; // 每层结点的行号

// 递归删除子树
void remove_sub(int u) {
    if (u >= (1 << m)) return;
    del[u] = true;
    remove_sub(u * 2);
    remove_sub(u * 2 + 1);
}

// 递归绘制
void draw(int u, int level, int row, int col) {
    if (del[u]) return;
    canvas[row][col] = 'o';
    if (level == m) return;
    int child_row = node_row[level + 1];
    int gap = child_row - row;
    // 左孩子
    if (!del[u * 2]) {
        for (int step = 1; step < gap; ++step)
            canvas[row + step][col - step] = '/';
        draw(u * 2, level + 1, child_row, col - gap);
    }
    // 右孩子
    if (!del[u * 2 + 1]) {
        for (int step = 1; step < gap; ++step)
            canvas[row + step][col + step] = '\\';
        draw(u * 2 + 1, level + 1, child_row, col + gap);
    }
}

int main() {
    scanf("%d%d", &m, &k);
    // 计算画布尺寸
    height = 3 * (1 << (m - 2));
    if (m == 1) height = 1;
    width = 2 * height - 1;
    // 计算每层结点的行号
    for (int i = 1; i < m; ++i)
        node_row[i] = height - 3 * (1 << (m - i - 1));
    node_row[m] = height - 1;
    // 读入并标记删除的结点
    for (int i = 1; i <= k; ++i) {
        int level, idx;
        scanf("%d%d", &level, &idx);
        int u = (1 << (level - 1)) + idx - 1;
        remove_sub(u);
    }
    // 初始化画布为空格
    for (int i = 0; i < height; ++i)
        for (int j = 0; j < width; ++j)
            canvas[i][j] = ' ';
    // 绘制
    draw(1, 1, 0, width / 2);
    // 输出
    for (int i = 0; i < height; ++i) {
        canvas[i][width] = '\0';
        printf("%s\n", canvas[i]);
    }
    return 0;
}

复杂度

画布大小为 O(4m)O(4^m) 个字符,绘制和输出均与画布大小同阶;递归深度 O(m)O(m)

总结

ASCII 绘树的难点是先推导稳定坐标。用堆编号管理删除、用行差同时决定横向偏移后,左右子树可以统一递归绘制。