-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathSPBC.typst
More file actions
249 lines (193 loc) · 11.3 KB
/
SPBC.typst
File metadata and controls
249 lines (193 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
#import "@preview/fletcher:0.5.8" as fletcher: node, edge
#import "../_lib/kodama.typ": *
#show: kodama
#set text(font: ("Inria Sans", "HarmonyOS Sans SC"))
#show raw: set text(font: "JetBrains Mono")
#metadata((
title: [栈置换–二叉树同构], //
date: [November 06, 2024 --- November 08, 2024],
author: local("/kokic", text: "kokic"),
license: external("https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en", [CC BY-NC-SA 4.0]),
))
#subtree(
title: "栈置换",
taxon: "definition",
)[
栈置换 [stack permutation] 也称作栈混洗 [stack shuffle]. 给定一个非空栈 $A$ 和两个空栈 $B, S$, 每次只允许 #tex(`(i)`) 弹出 $A$ 并压入 $S$. #tex(`(ii)`) 弹出 $S$ 并压入 $B$. 最终 $A$ 的元素一定会全部进入 $B$. 这样的 $B$ 就称为 $A$ 的一个栈置换.
]
不难发现, $A$ 的所有栈置换其实就是 $A$ 的所有出栈可能. 熟知 $n$ 元素栈共有 $1/(n+1) binom(2n, n)$ 种出栈情况. 另一方面, $n$ 个节点能构成 $1/(n+1) binom(2n, n)$ 种二叉树, 两者的计算都可以在 Catalan 数的相关条目中找到. 这表明两者作为集合同构, 一个可以考虑的问题是, 如何实现该同构? 即, 具体地写出这个双射. 这方面的 #local("/bib/hille1982stack.md", text: [开创性工作]) 来自 R. F. Hille.
#subtree(
title: [Hille 编码].text,
taxon: "definition",
)[
1982 年, R. F. Hille 在 #local("/bib/hille1982stack.md", text: [Stack permutations and an order relation for binary trees]) 中提出了一种借助二进制序列构造二叉树的方法, 即 Hille 编码. 可整理为以下三条规则.
- `1` 表示添加当前树节点的左子树. 例如 `111` 表示的树为 `(⋅ (⋅ (⋅)))`.
- `01` 表示添加当前树节点的右子树. 例如 `10101` 表示的树为 `(⋅ _ (⋅ _ (⋅)))`.
- 而对于 `01` 前的 $k$ 个 `0`, 这些 `0` 用于表示将当前树节点回溯到前 $k$ 层. 例如 `11001` 表示的树为 `(⋅ (⋅) (⋅))`.
注意到每个树节点的双亲 [parent] 节点是唯一的, 因此回溯操作良定, 一次回溯就是将当前节点改为其双亲. 同时不难验证, 任给一个二叉树, 可以通过这三条规则得到其 Hille 编码.
]
可以据此定义 Hille 编码的解析规则, 用于将这样一段有效的二进制序列转换为构造一颗二叉树的若干操作. 我们使用一种类 BNF 文法来定义这个解析器, 仅供读者理解.
#auto-figure(auto-frame[
#align(center)[#table(
stroke: none,
align: (left),
column-gutter: 1em,
row-gutter: -0.5em,
columns: 3,
[`<lchild>`],
[`:=`],
[`"1"`],
[`<rchild>`],
[`:=`],
[`"01"`],
[`<up>`],
[`:=`],
[`"0"`],
[`<parser>`],
[`:=`],
[`(<lchild> | <rchild> | <up>)`#super[`*`]],
)]
])
由于我们的讨论不涉及具体元素, 不失一般性, 可以固定栈置换的入栈序列为 $123 dots.c n$. 同时这些数字也是二叉树节点的标签. 影响出栈序列的只有压入和弹出两个操作, 而构建二叉树允许的操作粗看起来要多一些. 因此首先需要通过一些技巧将二叉树构建操作的表示简化. 我们将栈的压入和弹出分别编码为 `1` 和 `0`, 并将栈置换对应的二进制序列称为栈编码. 对应的, 以 Hille 编码刻画二叉树的构建.
#subtree(title: "三节点二叉树", taxon: "example")[
以下写出三节点二叉树的所有 $5$ 种情况, 以展示这些树的二进制序列如何对应到栈置换.
此处二进制序列按栈弹空为标准, 补充了末尾的零.
#auto-figure(auto-frame[
#align(center)[
#let u = 0.4
#let gap = 3em
#let gaph = 1.5
#let cx = 1
#fletcher.diagram(
node-inset: 0pt,
node-stroke: 0.8pt,
crossing-fill: rgb(0, 0, 0, 0),
let y = gaph * 0,
node((0, y), radius: 0.4em),
edge(),
node((-u, y + u), radius: 0.4em),
edge(),
node((-2 * u, y + 2 * u), radius: 0.4em),
node((cx, y + u), [$arrow$ #h(gap) `111000` #h(gap) $arrow$ #h(gap) `321`], stroke: none),
let y = gaph * 1,
node((-u, y), radius: 0.4em),
edge(),
node((-2 * u, y + u), radius: 0.4em),
edge(),
node((-u, y + 2 * u), radius: 0.4em),
node((cx, y + u), [$arrow$ #h(gap) `110100` #h(gap) $arrow$ #h(gap) `231`], stroke: none),
let y = gaph * 2,
node((-u, y), radius: 0.4em),
edge(),
node((0, y + u), radius: 0.4em),
edge(),
node((-u, y + 2 * u), radius: 0.4em),
node((cx, y + u), [$arrow$ #h(gap) `101100` #h(gap) $arrow$ #h(gap) `132`], stroke: none),
let y = gaph * 3,
node((-2 * u, y), radius: 0.4em),
edge(),
node((-u, y + u), radius: 0.4em),
edge(),
node((0, y + 2 * u), radius: 0.4em),
node((cx, y + u), [$arrow$ #h(gap) `101010` #h(gap) $arrow$ #h(gap) `123`], stroke: none),
let y = gaph * 4,
node((-u, y), radius: 0.4em),
edge(),
node((-2 * u, y + u), radius: 0.4em),
edge((-u, y), (0, y + u)),
node((0, y + u), radius: 0.4em),
node((cx, y + u), [$arrow$ #h(gap) `110010` #h(gap) $arrow$ #h(gap) `213`], stroke: none),
)
]
])
]
这个过程的反向实际上并不平凡..如果只考虑 $n=3$ 也就是上图的情况, 敏锐的读者可以发现, 这些栈置换其实就是二叉树按照 Hille 编码逐步添加节点的顺序编号后的中序遍历序列, 如二叉树 `1101000100` 按添加顺序对节点编号, 然后做中序遍历得到的是 `2314`. 我们接下来将解释其中的不平凡之处, 以及这一巧合发生的具体条件. 首先是对原始文献的一个观察, #local("/bib/hille1982stack.md", text: [Hille 原始文献]) 当中提出的算法实际上存在错误. 将之改写成 Lean4 语言, 即
```lean
def encode : Tree → String
| .node l r => "1" ++ encode l ++ encode r
| _ => "0"
```
只要考虑下面这个例子即可发现, 将一棵二叉树转化为它的 Hille 编码并非是简单的中序遍历.
#import "../_lib/shared.typ": *
#import "../_lib/binary-tree.typ": *
#let enc = `encode`
#let splus = spaces(`++`)
$
#enc\(#tree4(0.4))
&eqq #`"1"` splus #enc\(#tree2(0.4)) splus #enc\(circle) \
&eqq #`"1"` splus #`"1"` splus #`"0"` splus 2 dot #enc \(circle) \ \
&eqq #`"110"` splus 2 dot #`"100"` \ \
&eqq #`"110100100"`
$
可以验证, 从 `110100100` 这个编码出发, 无法直接恢复原本的 $#tree4(0.25, baseline: 1.7em)$.
其正确的 Hille 编码应该为 `1101000100`. 另一方面, 如果将序列 `110100100` 解释为栈的压入弹出操作, 即栈编码, 则可以得到正确的栈置换 `2314`. 这就意味着, 当二叉树的节点个数 $n > 3$ 时, 存在二叉树使得其栈编码与 Hille 编码不同. 回忆二叉树和栈置换之间的双射关系, 这表明必然存在一套手续允许我们在两者之间互相转换, 下面我们将构造性地给出这个结果. 随后解释为何中序遍历在 $n$ 不大情况下能够频繁得到正确的 Hille 编码.
#subtree(
title: "栈编码与 Hille 编码的等价性",
taxon: "proposition",
)[
首先容易验证的是, 中序遍历总是能够给出二叉树 $b in B$ 到栈置换 $s in S$ 的映射, 而对于每一个栈置换 $s in S$, 都能够写出唯一的二进制序列, 即栈编码 $c in C$. 不考虑末尾连续的 `0`, 当栈编码 $c$ 与二叉树 $b$ 的 Hille 编码 $h in H$ 相同时, 对 $B$ 直接进行中序遍历便能得出正确的 Hille 编码.
#auto-figure(auto-frame[
#align(center, fletcher.diagram({
node((-1, -1), [$B$])
node((0, -1), [$S$])
node((-1, 0), [$H$])
node((0, 0), [$C$])
edge((-1, -1), (0, -1), "->")
edge((-1, -1), (-1, 0), "->")
edge((0, -1), (0, 0), "->")
edge((-1, 0), (0, 0), "->")
}))
])
#subtree(
taxon: "proof",
catalog: false,
)[
记 $B -> H$ 为 $f$, 根据 Hille 编码的定义, $f$ 是一个双射. 根据二叉树的性质, $g: B -> S$ 是中序遍历, 前文已经固定栈置换的入栈顺序为 $123 dots.c n$, 这样一来就固定了 $B$ 的层序遍历, 因此 $g$ 也是双射. 现在来看 $h: S -> C$, 这显然也是一个双射. 最后, 我们能够从 $h in H$ 当中恢复出 $c in C$ 的信息, 只要将 $h$ 序列视为栈编码, 并且忽略空栈的弹出, 这就意味着 $H -> C$ 是满射, 随后利用 $C -> S -> B -> H$, 这样就得到了 $H tilde.equiv C$.
]
现在, Hille 原文所使用的 `encode` 算法就是 $h compose g: B -> C$, 而预期的正确实现则是 $f$, 因此两者在结果上相差一个同构.
]
#subtree(
title: "相交数",
taxon: "proposition",
)[
所有 $n$ 节点二叉树的 Hille 编码构成的集合记为 $H_n$, 所有 $n$ 节点二叉树的栈编码构成的集合记为 $C_n$. 对于 $H_n inter C_n$ 的大小 $a_n eq.def |H_n inter C_n|$, 我们有如下刻画.
$
a_n spaces(=) sum_(k = 0)^(n-1) sum_(j = 0)^(n-k) 1/(j+1) binom(n-k-j, j) binom(k, j) binom(k+j+2, j)
$
记 $N = n+1$. 等式的证明可以通过分析一个半长为 $N$ 且不包含 `UUDD` 子序列的 Dyck 路径得到. 同时这也是从 $(0,0)$ 到 $(N,N)$ 不越过对角线且允许步长 $(1,k), (k,1), k gt.slant 1$ 的格路径的个数. 或者更简单地说, 是长度为 $N$ 的斜 Motzkin 路径的个数.
]
#subtree(
title: "相交率",
taxon: "corollary",
)[
记 Catalan 数为 $c_n$, 也即 $H_n$ 或 $C_n$ 的个数. 自然要问 $|H inter C_n|$ 在全体 $n$ 节点二叉树个数中所占的比例与 $n$ 的关系. 根据相交数与 Catalan 数的渐进估计 $a_n tilde sqrt(2+r)/(2sqrt(\pi)n^(3/2)r^n)$, $c_n tilde (4^n)/(sqrt(pi) n^(3/2))$ 可以知道 $limits(lim)_(n -> oo) a_n/c_n = 0$. 并且事实上只要二叉树的节点个数 $n$ 大于 $8$, $H_n$ 与 $C_n$ 相同的部分就会少于整体的一半.
]
#subtree(
title: "有效长度的上下界",
taxon: "exegesis",
)[
一个可供探究的问题是, 给定 $n$ 节点二叉树, 询问其 Hille 编码 $H_n$ 有效长度 $ell(H_n)$ 的范围. 这里的有效不外乎是指去除末尾连续的 `0`. 我们将对此问题给出确切的回答.
任意 $n$ 节点二叉树的 Hille 编码的有效长度满足下面的不等式
$ n spaces(lt.slant) ell_n spaces(lt.slant) max(0, 2n - 1, 3n - 4) $
下界 $n$ 的验证是容易的, 构造一棵 $n$ 节点二叉树最少也需要 $n$ 个 `1`.
由于三节点二叉树的讨论, 我们只需要验证 $n gt.slant 3$ 时 Hille 编码的有效长度至多是 $3n - 4$.
#subtree(taxon: "proof", catalog: false)[
注意到为使有效长度最大, 相应的二叉树中的右节点个数和回溯次数要尽可能多.
同时满足这两个要求意味着
#tex(`(i)`) 除根节点外至少有一个左结点. 如若不然, 这颗二叉树只会形如 `1010101`$dots.c$, 其长度为 $2n - 1$.
#tex(`(ii)`) 该二叉树必有一个位于根节点右侧的节点. 否则回溯贡献的长度便不是最大值 $n-2$. 如下图所示
$ #tree3(0.4) $
由此出发, 我们将剩余的所有 $n-3$ 个节点全部添加到树中唯一的左节点的右侧. 即
$ #tree-max(0.4) $
下面我们只需算出 $ell(M)$, 便可得到 $ell_n$ 的最大值. 不妨直接写出 $h(M)$
#let splus = spaces(`++`)
$
h(M)
&eqq #`"110101...00001"` \
&eqq #`"11"` splus (n-3) dot #`"01"` \
& #h(5em) splus (n-2) dot #`"0"` splus #`"01"` \
$
立刻看出 $ell(M) = 2 + 2(n-3) + (n-2) + 2 = 3n-4$.
]
反过来, 从树 $M$ 出发, 也可以验证任何使得节点总数不变的操作都不会增加其 Hille 编码的有效长度. 更进一步, 我们可以断言任何使得节点总数不变的操作都将严格减小其 Hille 编码的有效长度. 换言之, 使得 $ell(B) = 3n-4$ 的二叉树 $B$ 的结构是唯一的, 即 $M$.
]