老周是岛上的道路管理员。岛上村庄最初修了不少弯弯绕绕的小路,有些路走着走着又绕回原点。老周想:能不能把所有村子连起来,同时又不留任何一条能把人绕晕的环路?这就是树。好几座这样的岛屿互不搭桥,岛内各有树状路网,合起来就是森林。为了省钱,他给每条路标上造价,选出一棵总造价最低的连接网,这是最小生成树。他还发现,n 个村子的最简网图可以数出 n^(n-2) 张,甚至能用一串门牌号完整存档,这就是 Cayley 公式和 Prufer 序列。最后,若限定每家最多两个孩子,便得到二叉树。这些概念从一座岛的路网出发,却构成了网络、搜索、组合优化和 AI 系统的数学底座。
树与森林是图论建模的起点;理解它的等价定义、生成树与编码方法,能帮你用图的语言解决网络设计、聚类和生成模型中的实际问题。
可视化演示
树是无环连通图,森林是无环图。本文详解树的多种等价定义、生成树与最小生成树的Kruskal与Prim算法、树的... 切换模式查看不同维度。
本节配有交互动效(进入视口后自动加载,约 3KB)。
树的等价定义:核心原理 动态展示关键流程与数据流动 交互控件调节参数实时渲染 f(x) = transform(x) 核心概念的可视化呈现
树的定义:无环连通
老周在岛上规划道路,要求很简单:每个村庄都能到达其他村庄,但不要出现绕圈的死循环。于是他只修“刚好够用”的路——这就是树的直觉。数学上,树是一个连通且无环的无向图。所谓连通,就是任意两个村庄之间都有路可走;所谓无环,就是你从任意一个村子出发,永远不可能沿着一条不重复的小路绕回自己。
把村庄看成顶点、小路看成边,树有一个极漂亮的性质:若树有 n 个顶点,则一定有 n-1 条边。这个性质反过来也成立:一个连通图只要边数等于顶点数减一,就必然是树。更进一步,在树中,任意两个顶点之间的路径存在且唯一。多修一条会成环,少修一条会断掉,不多不少正好 n-1。
这些等价定义就像老周的修路原则:只要满足“连通且无环”,无论先看边数、路径还是连通性,结论都是同一张树状路网。掌握了树的等价定义,后面所有树相关的算法都有了判断的依据。
- 顶点集合 V图中的村庄集合,|V| 表示村庄总数 n。
- 边集合 E图中的道路集合,|E| 表示道路总数 m。
- 边数公式树里的道路数 m 总比村庄数 n 少 1,相当于 n 个村子只用 n-1 座桥就能全部连通。
- 唯一路径符号∃! 表示“存在唯一”,即任意两个村庄之间的路径只有一条。
森林结构:无环图与连通分量
老周管辖的不只一座岛。群岛之间没有跨海大桥,每座岛内部各有自己的树状路网。这些互不相连的树放在一起,就是森林。数学上,森林是一个无环的图,它的每个连通分量都是一棵树。换句话说,森林就是若干棵树的并集。
因为每座岛内部都是一棵树,岛内边数等于该岛顶点数减一。把所有岛加在一起,总边数就等于总顶点数减去连通分量数。假设一个森林有 n 个顶点、c 个连通分量,那么边数 m 满足:
森林的结构让老周意识到:只要把每个岛内部修成树,整个群岛就是无环的。反过来,如果一张图没有环,但分成好几个互不相连的部分,它仍然叫做森林。判断森林时,我们既要看“有没有环”,也要数“有几个连通块”。
- 顶点总数 n整个群岛所有村庄的个数。
- 连通分量数 c群岛中互不连通的道路子网个数;每个岛是一个分量。
- 边数 m所有岛内道路的总和。
生成树与最小生成树
老周的岛上原有不少旧路,其中有环路,也有多余的歧路。他想在保留全部村庄连通的前提下,删掉一些路,使得最终道路网络是一棵树。这种包含原图所有顶点、又恰好是无环连通子图的树,就叫原图的生成树。一个有 n 个顶点的连通图,任取一条生成树,边数都是 n-1。
如果每条路还标了造价,问题就变成:在所有生成树中,选一棵总造价最小的树。这就是最小生成树(MST)。它可以写成:
求最小生成树有两大经典算法。Kruskal 算法像老周“先修便宜路”:把所有边按造价从低到高排序,每次选一条不会成环的最便宜边加入;Prim 算法则像“从村口向外扩散”:从任意一个村庄出发,每次选择连接已修路网和未修路网的最便宜边。两条路殊途同归,都能得到总造价最小的最简路网。
- 生成树集合$\mathcal{T}(G)$ 是图 G 的全部生成树的集合。
- 边权累加$\sum_{e\in T} w(e)$ 表示把 T 中每条路的造价加起来。
- 最小化$\min$ 表示在所有候选 T 里挑总造价最小的那棵。
Cayley公式:标号树计数
老周在整理图纸时发现:同样 4 个村庄,可以画出不止一种最简路网。为了统计到底有多少种不同方案,他给每个村庄编了号。我们把顶点编号过的树称为带标号树,编号不同就算不同的树。德国数学家 Cayley 给出了一个惊人的公式:n 个带标号顶点上共有 n^(n-2) 棵不同的树。
Cayley 公式让人惊讶:看似结构复杂的树,数量只由一个简单的幂函数决定。它一方面说明了带标号树比我们想象中更“规整”,另一方面也为随机生成树提供了理论支撑——只要随机生成一个 n-2 长度的序列,就能等概率对应一棵标号树。
- 标号树数量τ(n) 表示 n 个带标号顶点上树的总数。
- 底数 n每个顶点都有 n 种编号可选,排列自由。
- 指数 n-2真正需要“选择”的自由度是 n-2,而不是 n。
Prufer序列:树的编码
图纸太多,老周想找一个更省空间的存档方式。德国数学家 Prufer 发现:任意一棵 n 个带标号顶点的树,都可以唯一编码成一个长度为 n-2 的序列,序列里的每个数字都是 1 到 n 之间的顶点编号。反过来,任意一个这样的序列也都能唯一还原成一棵树。这种编码就叫 Prufer 序列。
Prufer 序列最漂亮的应用是推导度数:一个顶点在序列中出现的次数,加上 1,就是它在树中的度数。
从编码到解码,Prufer 序列给出了树与序列之间的一一对应。老周不再需要保存厚厚一叠图纸,只需记下一串数字,随时能重建整个路网。这既是组合计数的钥匙,也是图数据压缩与随机树生成的利器。
- 顶点度数deg(v) 是顶点 v 在树中的道路连接数。
- 出现次数occ(v) 是 v 在 Prufer 序列中出现的总次数。
- 关系式出现次数越多,说明该顶点越“繁忙”,连接分支也越多。
二叉树与有序树性质
后来岛上立了新规定:每个路口最多分出两条岔路,并且区分左支路和右支路。这种每个节点最多两个孩子的有序树,就是二叉树。二叉树在计算机里地位极高,因为它的层级结构可以直接映射到数组、栈和搜索逻辑。
二叉树有几个关键计数性质。如果把根节点看作第 0 层,那么第 i 层最多有 2^i 个节点;一棵高度为 h 的二叉树,最多能容纳:
还有一种特殊的满二叉树:每个内部节点都恰好有两个孩子。满二叉树中,叶子节点数 L 总比内部节点数 I 多 1。这就像家族谱系中,所有“分叉”的祖辈人数总和,总比最底层的“叶子”后辈人数少一个。
二叉树不是老周路网的全部,但它是有序树中最重要的一种特例。通过左孩子右兄弟等方式,任何有序森林都可以转换成二叉树,这也让树结构在计算机中有了统一、高效的存储方式。
- 叶子节点 L没有孩子的节点,好比族谱中最底层的后辈。
- 内部节点 I至少有一个孩子的节点,好比族谱中的分叉长辈。
- 关系因为每个内部节点恰好贡献一条“叶路径”,总数上叶子比内部节点多 1。
AI 应用场景
MST 聚类:无监督数据探索
阶段:数据探索与特征分析
解决的问题:传统 K-means 需要预先指定簇数,且对簇形敏感。MST 聚类把数据点看成带权完全图的顶点,边权是两两距离,先求出最小生成树,再剪断若干最长边,就能自然地把数据分成多个簇,无需人工猜测簇数。
具体案例:电商平台分析用户行为数据,点与点之间计算购买行为相似度,用 Kruskal 算法求 MST 后剪断高差异边,自动划分出高活跃、潜力型、沉睡型用户群。
数据中心网络规划
阶段:模型训练基础设施部署
解决的问题:GPU 集群节点之间需要高速互联,布线成本很高。直接用完全图连接会产生大量冗余光纤。用最小生成树为集群选择最省成本但又保证全连通的拓扑,能显著降低硬件与能耗成本。
具体案例:某算法团队搭建 32 卡训练集群,节点间光纤连接成本作为边权,用 Prim 算法规划骨干网,最终节省约 20% 布线费用。
Prufer 序列生成随机树
阶段:数据增强与模型验证
解决的问题:图神经网络的训练需要大量树形结构的样本。直接随机生成树无法保证均匀性,Prufer 序列提供了一种简洁方法:随机生成长度为 n-2、取值在 1 到 n 的序列,再唯一解码成一棵树。
具体案例:药物研发中枚举分子骨架,用 Prufer 序列生成大量树状小分子结构,作为分子性质预测模型的训练数据。
Python 演示
class DSU: def __init__(self, n): self.parent = list(range(n)) self.rank = [0] * n def find(self, x): while self.parent[x] != x: self.parent[x] = self.parent[self.parent[x]] x = self.parent[x] return x def union(self, a, b): ra = self.find(a) rb = self.find(b) if ra == rb: return False if self.rank[ra] < self.rank[rb]: ra, rb = rb, ra self.parent[rb] = ra if self.rank[ra] == self.rank[rb]: self.rank[ra] += 1 return True def kruskal(n, edges): edges = sorted(edges, key=lambda x: x[2]) dsu = DSU(n) mst = [] total = 0 for u, v, w in edges: if dsu.union(u, v): mst.append((u, v, w)) total += w return mst, total if __name__ == "__main__": edges = [ (0, 1, 5), (0, 2, 3), (1, 2, 1), (1, 3, 2), (2, 3, 4), (3, 4, 4), (2, 4, 6) ] mst, total = kruskal(5, edges) print("MST edges:", mst) print("Total weight:", total)
class DSU:并查集,用来快速判断两条路是否已经连通,避免成环。def find(self, x):查找顶点所在连通块,路径压缩让后续查询更快。def union(self, a, b):尝试合并两个连通块,合并成功说明这条边不会成环。edges = sorted(edges, key=lambda x: x[2])把全部边按边权从小到大排序,对应“先修便宜路”。for u, v, w in edges:从最便宜到最贵逐条尝试。if dsu.union(u, v):只有加入后仍然无环,才把它选进最小生成树。
常见误区
误区看到教材里画了很多二叉树,就以为图论中的树每个节点最多两个孩子,所有树都是二叉树。
正解图论中的“树”是度数不限、不区分左右孩子的无环连通图。二叉树额外要求每个节点最多两个孩子且有序,只是树的一种特例。就像岛上的岔路口可以是三岔、四岔,而二叉树规定只许两岔并且分主次。
为何常见:二叉树在编程课里出场率太高,画起来整齐,初学者容易把特例当全部。
误区只要 m=n-1,就觉得它一定是树,完全不管图连不连通。
正解m=n-1 只有和“连通”或“无环”搭配时才能推出树。例如一个三角形边数为 3、顶点数为 4,再加一个孤立点,边数 n-1 却不是树。必须额外检查全图是否被路网连成一个整体。
为何常见:树的边数公式太醒目,让人记住了结论却忽略了前提条件。
误区权值最小的生成树当然只有一棵,不然怎么叫“最小”。
正解当有多条边权值相同时,最小生成树可能不唯一。只要总权值最小,不同边的选取方案都是合法答案。比如两条同样便宜的路都能连接两片区域,选哪条总价都一样。
为何常见:算法执行时只输出一个具体解,掩盖了等权边造成的多解情况。
误区觉得 n^(n-2) 就是“所有树”的个数,不管顶点有没有编号。
正解Cayley 公式只适用于带标号树,即顶点有 1 到 n 的编号。两个编号不同但形状相同的树算作不同树。若只关心无标号的形状,树的数量没有这么简洁的公式。
为何常见:公式形式太简单,容易忽略“标号”这个关键前提。
误区既然序列是编码,感觉每个顶点应该出现且仅出现一次。
正解Prufer 序列长度是 n-2,元素允许重复。顶点出现次数恰恰反映它的度数:出现 k 次的顶点度数为 k+1。重复数字正是编码中表达度数差异的方式。
为何常见:人们熟悉排列“不重复”的印象,没注意 Prufer 序列本质是带重复的排列。
面试题
Q1. 一棵有 6 个顶点的树,边数是多少?
- A.5← 正确答案
- B.6
- C.7
- D.4
答案:A。树的边数等于顶点数减一,即 6-1=5。就像 6 个村庄的最简路网只需要 5 条路就能全部连通且不产生环路。
Q2. 关于树,下列说法正确的是?
- A.任意两点之间恰好有唯一一条路径← 正确答案
- B.树可能存在环
- C.树一定是有向图
- D.树必须包含完全图
答案:A。树是连通无环图,任意两村之间只有一条路。B 错在树无环;C 错在树通常指无向图;D 完全图含环,不是树。
Q3. 森林有 10 个顶点、3 个连通分量,它的边数是多少?
- A.9
- B.10
- C.7← 正确答案
- D.8
答案:C。森林边数 m=n-c=10-3=7。可以理解成三座岛各自有树状路网,总边数比总村庄数少一个连通分量数。
Q4. 树的等价定义包括?
- A.连通且无环← 正确答案
- B.连通且边数等于顶点数减一← 正确答案
- C.无环且边数等于顶点数减一← 正确答案
- D.任意两点之间至少两条不同路径
答案:ABC。前三个都是树的标准等价定义;D 恰恰相反,树要求任意两点间只有唯一路径。
Q5. Kruskal 算法每一步选择的是?
- A.当前未处理边中权值最小且加入后不成环的边← 正确答案
- B.权值最大的边
- C.从已选树中扩展最近点
- D.随机一条边
答案:A。Kruskal 把所有边按权值从小到大排序,用并查集判断是否成环。这就像修路时先修便宜又不会形成环路的路段。
Q6. 关于最小生成树,下列说法正确的是?
- A.可能不唯一← 正确答案
- B.一定唯一
- C.包含图中所有顶点← 正确答案
- D.总权值最小← 正确答案
答案:ACD。当存在权值相同的边时,MST 可能不唯一;但每棵 MST 都包含所有顶点,且总权值最小。B 错在等权边可能造成多解。
Q7. Cayley 公式给出 n 个带标号顶点的树有多少棵?
- A.n^(n-1)
- B.n^(n-2)← 正确答案
- C.2^(n-2)
- D.n(n-1)/2
答案:B。Cayley 公式说明 n 个标号点上的树有 n^(n-2) 棵,例如 4 个点有 4^2=16 棵。这相当于 n 个村庄能画出的最简路网图纸数。
Q8. 一棵带标号树有 7 个顶点,它的 Prufer 序列长度是多少?
- A.5← 正确答案
- B.6
- C.7
- D.8
答案:A。Prufer 序列长度为 n-2=7-2=5。序列相当于树的压缩存档,少两个数字也能完整还原一张路网图纸。
Q9. 关于 Prufer 序列的性质,正确的是?
- A.长度为 n-2,元素来自 1 到 n← 正确答案
- B.叶节点的编号在序列中出现 0 次← 正确答案
- C.序列可以唯一恢复一棵带标号树← 正确答案
- D.序列只适用于无标号树
答案:ABC。Prufer 序列是带标号树的编码,叶节点出现次数为 0,且与树一一对应。D 错,它处理的正是带标号树。
Q10. 满二叉树中,叶子结点数 L 与内部结点数 I 的关系是?
- A.L = I + 1← 正确答案
- B.L = I
- C.L = I - 1
- D.L = 2I
答案:A。满二叉树中每个内部结点恰好两个孩子,叶子数比内部结点数多 1。就像家族谱系里,最底层的叶子后辈总比中间的过渡长辈多一个。