前言

如果我们需要观察程序运行过程中,某一个变量、某一个序列的变化情况,你可以在修改的地方打断点 debug,或者直接在需要的地方输出就行了。

但是对于一些树形结构,我们不好将其直观地呈现出来,常常只是输出每一个结点的值,但是这就丢失了结点之间的连边情况。有时候不得不手动画图。

所以我们经常累死。

于是,为了让我们活着,我想到了一种轻量级的,在终端直观呈现树形结构的方法。

正文

经典例子

回顾如下场景:

  • Windows 下命令行中,我们使用 tree 来观察目录结构。

比如,在某一目录下,使用 tree /A /F 的输出如下:

+---.vscode
| launch.json
|
+---blog-prettier
| LICENSE
| README.md
|
+---web server
| | checkstatues.log
| | client.html
| | data.txt
| | gen-key.py
| | main_service.log
| | script-obfsed.js
| | test.html
| |
| \---fetch-new-url
| | README.md
| |
| \---docs
| test
|
\---test
a.html
b.html
index.html
script.js
style.css

这种经典的方法显然可以运用到我们的调试中。

分析

二叉树

我们不妨来考虑简单的二叉树,例如线段树、Treap、Splay 等平衡树。

我们考虑一种最简单的递归过程,仅在参数中传递输出的前缀。简单码出以下代码:

void output(int x, string pre) {
cout << pre << "-" << x << ": " << tr[x].val << endl;
if (!x) return;
output(tr[x].son[1], pre + " |");
output(tr[x].son[0], pre + " |");
} void output() {
output(root, ">");
}

这里先输出再 return 是为了让输出的二叉树更好看,不然遇到一个孩子不知道是左儿子还是右儿子。

将右儿子作为第一个儿子输出,是为了符合二叉查找树。

可能的输出:一棵不断插入的 Splay
>-1: 1
> |-0: 0
> |-0: 0
>-2: 1
> |-1: 1
> | |-0: 0
> | |-0: 0
> |-0: 0
>-3: 4
> |-0: 0
> |-1: 1
> | |-0: 0
> | |-2: 1
> | | |-0: 0
> | | |-0: 0
>-4: 5
> |-0: 0
> |-3: 4
> | |-0: 0
> | |-1: 1
> | | |-0: 0
> | | |-2: 1
> | | | |-0: 0
> | | | |-0: 0
>-5: 1
> |-3: 4
> | |-4: 5
> | | |-0: 0
> | | |-0: 0
> | |-2: 1
> | | |-1: 1
> | | | |-0: 0
> | | | |-0: 0
> | | |-0: 0
> |-0: 0
>-6: 4
> |-3: 4
> | |-4: 5
> | | |-0: 0
> | | |-0: 0
> | |-0: 0
> |-5: 1
> | |-1: 1
> | | |-0: 0
> | | |-2: 1
> | | | |-0: 0
> | | | |-0: 0
> | |-0: 0

这对于考场上调试来说已经足够了,仅需将头逆时针旋转 \(45^\circ\) 就能看到一棵完美的二叉树了。你可以在每个结点之后输出更多的信息。

但是,我们怎样达到更完美的效果呢,比如第二个孩子之前不输出树杈、第二个孩子后输出空行(多个第二个孩子仅输出一个空行)等等。

我们仅需多记录是否是第一个孩子即可。

void output(int x, string pre, bool firstSon) {
cout << pre << (firstSon ? "+" : "\\") << "---" << x << ": " << tr[x].val << endl;
if (!x) return;
pre += firstSon ? "|" : " ";
output(tr[x].son[1], pre + " ", true);
output(tr[x].son[0], pre + " ", false);
if (firstSon) cout << pre << endl;
} void output() {
output(root, "", false);
}

效果见文末。

多叉树

多叉树就只能是 LCT 了吧,还有什么扭曲的树你必须要打印出来的?

虽然好像打印出来还是不方便调试……

我们加以改进,由于有了虚实链之分,我们在空节点不直接 return,而是输出一条边。然后把是否是第一个孩子,变成是否是最后一个孩子。

代码:

vector<int> edge[N];

void output(int x, string pre, bool lastSon, bool real) {
cout << pre << (!lastSon ? "+" : "\\") << "---";
if (x) cout << x << ": " << tr[x].val << endl;
else cout << "null" << endl;
pre += !lastSon ? (real ? "|" : "`") : " ";
if (x && (tr[x].son[0] || tr[x].son[1] || edge[x].size())) {
pushdown(x);
output(tr[x].son[1], pre + " ", false, true);
output(tr[x].son[0], pre + " ", edge[x].empty(), false);
for (int y : edge[x])
output(y, pre + " ", y == edge[x].back(), false);
}
if (!lastSon) cout << pre << endl;
} void output(int n) {
for (int i = 1; i <= n; ++i)
edge[i].clear();
for (int i = 1; i <= n; ++i)
if (isRoot(i))
edge[tr[i].fa].emplace_back(i);
cout << "==== LCT forest ====" << endl;
for (int i = 1; i <= n; ++i)
if (!tr[i].fa)
output(i, "", true, false);
cout << "====================" << endl;
}

效果见文末。

代码

二叉树
void output(int x, string pre, bool firstSon) {
cout << pre << (firstSon ? "+" : "\\") << "---" << x << ": " << tr[x].val << endl;
if (!x) return;
pre += firstSon ? "|" : " ";
output(tr[x].son[1], pre + " ", true);
output(tr[x].son[0], pre + " ", false);
if (firstSon) cout << pre << endl;
} void output() {
output(root, "", false);
}
多叉树 LCT
vector<int> edge[N];

void output(int x, string pre, bool lastSon, bool real) {
cout << pre << (!lastSon ? "+" : "\\") << "---";
if (x) cout << x << ": " << tr[x].val << endl;
else cout << "null" << endl;
pre += !lastSon ? (real ? "|" : "`") : " ";
if (x && (tr[x].son[0] || tr[x].son[1] || edge[x].size())) {
pushdown(x);
output(tr[x].son[1], pre + " ", false, true);
output(tr[x].son[0], pre + " ", edge[x].empty(), false);
for (int y : edge[x])
output(y, pre + " ", y == edge[x].back(), false);
}
if (!lastSon) cout << pre << endl;
} void output(int n) {
for (int i = 1; i <= n; ++i)
edge[i].clear();
for (int i = 1; i <= n; ++i)
if (isRoot(i))
edge[tr[i].fa].emplace_back(i);
cout << "==== LCT forest ====" << endl;
for (int i = 1; i <= n; ++i)
if (!tr[i].fa)
output(i, "", true, false);
cout << "====================" << endl;
}

输出效果

可能的输出:一棵不断插入的 Splay
\---1: 1
+---0: 0
\---0: 0
\---2: 1
+---1: 1
| +---0: 0
| \---0: 0
|
\---0: 0
\---3: 4
+---0: 0
\---1: 1
+---0: 0
\---2: 1
+---0: 0
\---0: 0
\---4: 5
+---0: 0
\---3: 4
+---0: 0
\---1: 1
+---0: 0
\---2: 1
+---0: 0
\---0: 0
\---5: 1
+---3: 4
| +---4: 5
| | +---0: 0
| | \---0: 0
| |
| \---2: 1
| +---1: 1
| | +---0: 0
| | \---0: 0
| |
| \---0: 0
|
\---0: 0
\---6: 4
+---3: 4
| +---4: 5
| | +---0: 0
| | \---0: 0
| |
| \---0: 0
|
\---5: 1
+---1: 1
| +---0: 0
| \---2: 1
| +---0: 0
| \---0: 0
|
\---0: 0
可能的输出:一棵带有左右边界的不断插入的 Treap
\---2: inf
+---0: 0
\---1: -inf
+---3: 1
| +---0: 0
| \---0: 0
|
\---0: 0
\---2: inf
+---0: 0
\---1: -inf
+---3: 1
| +---0: 0
| \---0: 0
|
\---0: 0
\---2: inf
+---0: 0
\---1: -inf
+---3: 1
| +---4: 4
| | +---0: 0
| | \---0: 0
| |
| \---0: 0
|
\---0: 0
\---2: inf
+---0: 0
\---1: -inf
+---3: 1
| +---5: 5
| | +---0: 0
| | \---4: 4
| | +---0: 0
| | \---0: 0
| |
| \---0: 0
|
\---0: 0
\---2: inf
+---0: 0
\---1: -inf
+---3: 1
| +---5: 5
| | +---0: 0
| | \---4: 4
| | +---0: 0
| | \---0: 0
| |
| \---0: 0
|
\---0: 0
\---2: inf
+---0: 0
\---1: -inf
+---3: 1
| +---5: 5
| | +---0: 0
| | \---4: 4
| | +---0: 0
| | \---0: 0
| |
| \---0: 0
|
\---0: 0
可能的输出:一棵不断插入的无旋 Treap
\---1: 1
+---0: 0
\---0: 0
\---1: 1
+---0: 0
\---2: 1
+---0: 0
\---0: 0
\---3: 4
+---0: 0
\---1: 1
+---0: 0
\---2: 1
+---0: 0
\---0: 0
\---3: 4
+---4: 5
| +---0: 0
| \---0: 0
|
\---1: 1
+---0: 0
\---2: 1
+---0: 0
\---0: 0
\---5: 1
+---3: 4
| +---4: 5
| | +---0: 0
| | \---0: 0
| |
| \---1: 1
| +---0: 0
| \---2: 1
| +---0: 0
| \---0: 0
|
\---0: 0
\---5: 1
+---6: 4
| +---3: 4
| | +---4: 5
| | | +---0: 0
| | | \---0: 0
| | |
| | \---0: 0
| |
| \---1: 1
| +---0: 0
| \---2: 1
| +---0: 0
| \---0: 0
|
\---0: 0
可能的输出:一棵动态开点线段树
\---[1, 5]: 1
+---[1, 3]: 0
\---[4, 5]: 1
+---[4, 4]: 0
\---[5, 5]: 1
\---[1, 5]: 6
+---[1, 3]: 0
\---[4, 5]: 6
+---[4, 4]: 0
\---[5, 5]: 6
\---[1, 5]: 10
+---[1, 3]: 0
\---[4, 5]: 10
+---[4, 4]: 4
\---[5, 5]: 6
\---[1, 5]: 12
+---[1, 3]: 2
| +---[1, 2]: 0
| \---[3, 3]: 2
|
\---[4, 5]: 10
+---[4, 4]: 4
\---[5, 5]: 6
\---[1, 5]: 15
+---[1, 3]: 5
| +---[1, 2]: 3 (with lazy = 3)
| | +---[1, 1]: 0
| | \---[2, 2]: 0
| |
| \---[3, 3]: 2
|
\---[4, 5]: 10
+---[4, 4]: 4
\---[5, 5]: 6
\---[1, 5]: 15
+---[1, 3]: 5
| +---[1, 2]: 3 (with lazy = 3)
| | +---[1, 1]: 0
| | \---[2, 2]: 0
| |
| \---[3, 3]: 2
|
\---[4, 5]: 10
+---[4, 4]: 4
\---[5, 5]: 6
\---[1, 5]: 19
+---[1, 3]: 5
| +---[1, 2]: 3 (with lazy = 3)
| | +---[1, 1]: 0
| | \---[2, 2]: 0
| |
| \---[3, 3]: 2
|
\---[4, 5]: 14
+---[4, 4]: 6
\---[5, 5]: 8
\---[1, 5]: 19
+---[1, 3]: 5
| +---[1, 2]: 3 (with lazy = 3)
| | +---[1, 1]: 0
| | \---[2, 2]: 0
| |
| \---[3, 3]: 2
|
\---[4, 5]: 14
+---[4, 4]: 6
\---[5, 5]: 8
\---[1, 5]: 24 (with lazy = 1)
+---[1, 3]: 5
| +---[1, 2]: 3 (with lazy = 3)
| | +---[1, 1]: 0
| | \---[2, 2]: 0
| |
| \---[3, 3]: 2
|
\---[4, 5]: 14
+---[4, 4]: 6
\---[5, 5]: 8
\---[1, 5]: 24 (with lazy = 1)
+---[1, 3]: 5
| +---[1, 2]: 3 (with lazy = 3)
| | +---[1, 1]: 0
| | \---[2, 2]: 0
| |
| \---[3, 3]: 2
|
\---[4, 5]: 14
+---[4, 4]: 6
\---[5, 5]: 8
可能的输出:一棵树状数组

这玩意你还要调试?

可能的输出:左偏树森林
==== 左偏树 1 ====
\---5: <4, 2>
+---3: <4, 3>
| +---4: <5, 2>
| | +---0: <0, 0>
| | \---7: <9, 4>
| | +---0: <0, 0>
| | \---0: <0, 0>
| |
| \---0: <0, 0>
|
\---0: <0, 0> ==== 左偏树 2 ====
\---1: <3, 3>
+---6: <8, 4>
| +---0: <0, 0>
| \---2: <9, 3>
| +---0: <0, 0>
| \---0: <0, 0>
|
\---0: <0, 0> ==== 左偏树 1 ====
\---5: <4, 2>
+---3: <4, 3>
| +---4: <5, 2>
| | +---0: <0, 0>
| | \---7: <9, 4>
| | +---0: <0, 0>
| | \---0: <0, 0>
| |
| \---0: <0, 0>
|
\---1: <5, 3>
+---6: <8, 4>
| +---0: <0, 0>
| \---2: <9, 3>
| +---0: <0, 0>
| \---0: <0, 0>
|
\---0: <0, 0> ==== 左偏树 1 ====
\---3: <4, 3>
+---4: <5, 2>
| +---0: <0, 0>
| \---7: <9, 4>
| +---0: <0, 0>
| \---0: <0, 0>
|
\---1: <5, 3>
+---6: <8, 4>
| +---0: <0, 0>
| \---2: <9, 3>
| +---0: <0, 0>
| \---0: <0, 0>
|
\---0: <0, 0> ==== 左偏树 1 ====
\---4: <5, 2>
+---1: <5, 3>
| +---6: <10, 4>
| | +---0: <0, 0>
| | \---2: <9, 3>
| | +---0: <0, 0>
| | \---0: <0, 0>
| |
| \---7: <11, 4>
| +---0: <0, 0>
| \---0: <0, 0>
|
\---0: <0, 0> ==== 左偏树 1 ====
\---1: <5, 3>
+---6: <10, 4>
| +---0: <0, 0>
| \---2: <9, 3>
| +---0: <0, 0>
| \---0: <0, 0>
|
\---7: <11, 4>
+---0: <0, 0>
\---0: <0, 0> ==== 左偏树 1 ====
\---6: <10, 4>
+---2: <11, 3>
| +---0: <0, 0>
| \---7: <11, 4>
| +---0: <0, 0>
| \---0: <0, 0>
|
\---0: <0, 0> ==== 左偏树 1 ====
\---2: <11, 3>
+---0: <0, 0>
\---7: <11, 4>
+---0: <0, 0>
\---0: <0, 0> ==== 左偏树 1 ====
\---7: <11, 4>
+---0: <0, 0>
\---0: <0, 0> ==== 左偏树 1 ====
\---0: <0, 0>
可能的输出:Link Cut Tree
==== LCT forest ====
\---1: 114
\---2: 514
\---3: 19
\---4: 19
\---5: 810
==================== link 1 and 2 success ==== LCT forest ====
\---2: 514
+---null
|
+---null
`
\---1: 114
\---3: 19
\---4: 19
\---5: 810
==================== cut 1 and 2 success ==== LCT forest ====
\---1: 114
\---2: 514
\---3: 19
\---4: 19
\---5: 810
==================== link 1 and 2 success ==== LCT forest ====
\---2: 514
+---null
|
+---null
`
\---1: 114
\---3: 19
\---4: 19
\---5: 810
==================== link 2 and 3 success ==== LCT forest ====
\---3: 19
+---null
|
+---null
`
\---2: 514
+---null
|
+---null
`
\---1: 114
\---4: 19
\---5: 810
==================== cut 1 and 3 failed ==== LCT forest ====
\---1: 114
+---2: 514
| +---3: 19
| |
| \---null
|
\---null
\---4: 19
\---5: 810
==================== link 1 and 3 failed ==== LCT forest ====
\---1: 114
+---3: 19
| +---null
| |
| \---2: 514
|
\---null
\---4: 19
\---5: 810
==================== link 4 and 5 success ==== LCT forest ====
\---1: 114
+---3: 19
| +---null
| |
| \---2: 514
|
\---null
\---5: 810
+---null
|
+---null
`
\---4: 19
==================== link 2 and 5 success ==== LCT forest ====
\---5: 810
+---null
|
+---null
`
+---2: 514
` +---1: 114
` |
` +---null
` `
` \---3: 19
`
\---4: 19
==================== modify value 5 to 233333 success ==== LCT forest ====
\---5: 233333
+---null
|
+---null
`
+---2: 514
` +---1: 114
` |
` +---null
` `
` \---3: 19
`
\---4: 19
==================== access 3 success ==== LCT forest ====
\---5: 233333
+---2: 514
| +---3: 19
| |
| +---null
| `
| \---1: 114
|
+---null
`
\---4: 19
==================== split 2 ~ 4 success ==== LCT forest ====
\---4: 19
+---null
|
\---5: 233333
+---null
|
\---2: 514
+---null
|
+---null
`
+---1: 114
`
\---3: 19
==================== split 2 ~ 5 success ==== LCT forest ====
\---5: 233333
+---null
|
+---2: 514
` +---null
` |
` +---null
` `
` +---1: 114
` `
` \---3: 19
`
\---4: 19
====================

一种调试 线段树 / Treap / Splay / 左偏树 / LCT 等树形结构的技巧的更多相关文章

  1. 【BZOJ 1367】 1367: [Baltic2004]sequence (可并堆-左偏树)

    1367: [Baltic2004]sequence Description Input Output 一个整数R Sample Input 7 9 4 8 20 14 15 18 Sample Ou ...

  2. 洛谷.3273.[SCOI2011]棘手的操作(左偏树)

    题目链接 还是80分,不是很懂. /* 七个操作(用左偏树)(t2表示第二棵子树): 1.合并:直接合并(需要将一个t2中原有的根节点删掉) 2.单点加:把这个点从它的堆里删了,加了再插入回去(有负数 ...

  3. HDU 1512 Monkey King(左偏树+并查集)

    [题目链接] http://acm.hdu.edu.cn/showproblem.php?pid=1512 [题目大意] 现在有 一群互不认识的猴子,每个猴子有一个能力值,每次选择两个猴子,挑出他们所 ...

  4. 左偏树 / 非旋转treap学习笔记

    背景 非旋转treap真的好久没有用过了... 左偏树由于之前学的时候没有写学习笔记, 学得也并不牢固. 所以打算写这么一篇学习笔记, 讲讲左偏树和非旋转treap. 左偏树 定义 左偏树(Lefti ...

  5. 【BZOJ 2333 】[SCOI2011]棘手的操作(离线+线段树|可并堆-左偏树)

    2333: [SCOI2011]棘手的操作 Description 有N个节点,标号从1到N,这N个节点一开始相互不连通.第i个节点的初始权值为a[i],接下来有如下一些操作: U x y: 加一条边 ...

  6. 洛谷 P3377 【模板】左偏树(可并堆)

    洛谷 P3377 [模板]左偏树(可并堆) 题目描述 如题,一开始有N个小根堆,每个堆包含且仅包含一个数.接下来需要支持两种操作: 操作1: 1 x y 将第x个数和第y个数所在的小根堆合并(若第x或 ...

  7. 『左偏树 Leftist Tree』

    新增一道例题 左偏树 Leftist Tree 这是一个由堆(优先队列)推广而来的神奇数据结构,我们先来了解一下它. 简单的来说,左偏树可以实现一般堆的所有功能,如查询最值,删除堆顶元素,加入新元素等 ...

  8. 洛谷P4331 [BOI2004] Sequence 数字序列 [左偏树]

    题目传送门 数字序列 题目描述 给定一个整数序列 a1​,a2​,⋅⋅⋅,an​ ,求出一个递增序列 b1​<b2​<⋅⋅⋅<bn​ ,使得序列 ai​ 和 bi​ 的各项之差的绝对 ...

  9. BZOJ 1455 罗马游戏 ——左偏树

    [题目分析] 左偏树的模板题目,大概就是尽量维护树的深度保持平衡,以及尽可能的快速合并的一种堆. 感觉和启发式合并基本相同. 其实并没有快很多. 本人的左偏树代码自带大常数,借鉴请慎重 [代码] #i ...

  10. 【BZOJ-1455】罗马游戏 可并堆 (左偏树)

    1455: 罗马游戏 Time Limit: 5 Sec  Memory Limit: 64 MBSubmit: 1355  Solved: 561[Submit][Status][Discuss] ...

随机推荐

  1. 深度解读GaussDB逻辑解码技术原理

    本文分享自华为云社区<[GaussTech技术专栏]GaussDB逻辑解码技术原理>,作者:GaussDB 数据库. 1.背景 随着国内各大行业数字化改造步伐的加快,异构数据库数据同步的需 ...

  2. 【总结】线性dp的几种重要模型

    当前点定义 \(f[i]\) :走到第 \(i\) 个点的方案数 / 最值. \(f[i][j]\) :走到第 \(i\) 个点,选了 \(j\) 个的答案. 依据题目的限制个数可以继续添加维数,也可 ...

  3. 【转载】《扩散模型是实时游戏引擎(Diffusion Models Are Real-Time Game Engines)》的论文,向我们展示了世界上第一个完全由神经模型驱动的游戏引擎,GameNGen。这也是历史上首次,AI能在不借助其他游戏引擎的情况下,为玩家生成实时游戏了,并且在单个TPU上速度可以达到每秒20帧

    地址: https://www.youtube.com/watch?v=VniPJII6ak0 8月29号,谷歌DeepMind发布了一篇名为<扩散模型是实时游戏引擎(Diffusion Mod ...

  4. Mysql数据库笔记整理

    数据库-理论基础 1.什么是数据库? 数据:描述事物的符号记录,可以是数字.文字.图形.图像.声音.语言等,数据有多种形式,它们都可以经过数字化后存入计算机. 数据库:存储数据的仓库,是长期存放在计算 ...

  5. 2023NOIP A层联测26 T2 competition

    2023NOIP A层联测26 T2 competition tjm 的做法,很抽象. 考场思路 考虑每道题被做过多少次肯定不现实,那么考虑每一道题有多少次没有做出来. 假设某一次可以做出来题 \(x ...

  6. python操作sqlite的小例子

    照着菜鸟教程 学习python操作sqlite ubuntu 安装 sudo apte-get install sqlite3 找到了 sqlite3/bionic-updates,bionic-se ...

  7. Solr Facet技术的应用与研究

      http://tech.meituan.com/solr-facet.html 问题背景 在 <搜索引擎关键字智能提示的一种实现> 一文中介绍过,美团的CRM系统负责管理销售人员的门店 ...

  8. Java的多线程编程模型5--从AtomicInteger开始(自增长实现)

    AtomicInteger,一个提供原子操作的Integer的类.在Java语言中,++i和i++操作并不是线程安全的,在使用的时候,不可避免的会用到synchronized关键字.而AtomicIn ...

  9. Java方法参数太多怎么办—Part 1—自定义类型

    本文由 ImportNew - 王村平 翻译自 dzone.如需转载本文,请先参见文章末尾处的转载要求. 本文是这个系列的第一篇文章,介绍了采用自定义类型处理参数过多的问题.如果你也希望参与类似的系列 ...

  10. golang之json.RawMessage

    RawMessage 具体来讲是 json 库中定义的一个类型.它实现了 Marshaler 接口以及 Unmarshaler 接口,以此来支持序列化的能力.注意上面我们引用 官方 doc 的说明. ...