假如你是班长,手里握着一张“谁认识谁”的名单。你发现一个问题:小张认识小李,小李认识小王,可小张和小王之间却从没打过照面。作为热心肠的班长,你决定做点什么——要么让每个人先认识自己(自反),要么把单向认识变成双向好友(对称),要么干脆牵线搭桥,让“朋友的朋友”成为“新朋友”(传递)。 这三种“补全关系网”的操作,在数学上恰好对应三种关系闭包:自反闭包、对称闭包和传递闭包。闭包的概念说白了就是:在原关系的基础上,用最少的补充让关系满足某个性质。就像你补全名单时不会瞎加人,只加那些“必须加”的关系。 更妙的是,传递闭包有一个高效的算法——Warshall算法,它就像一个“层层引荐”的社交机制,用三重循环就能快速算出所有“间接认识”的关系。这个算法是图论中可达性分析、社交网络推荐、编译原理中变量依赖分析的地基。
关系闭包是离散数学分析到算法实现的桥梁,掌握它意味着你能把“关系”变成“可计算的程序”,在可达性分析、数据依赖检测、等价类划分等 AI 底层系统设计中直接落地。
可视化演示
关系闭包是包含原关系且满足特定性质的最小关系,包括自反闭包、对称闭包与传递闭包。本文详解三种闭包的计算方法、传... 切换模式查看不同维度。
本节配有交互动效(进入视口后自动加载,约 3KB)。
关系闭包的基:核心原理 动态展示关键流程与数据流动 交互控件调节参数实时渲染 f(x) = transform(x) 核心概念的可视化呈现
闭包概念解析
班级群里有一份“好友名单”,小张和小李互相关注,小李和小王互相关注。可是你发现小王和小张虽然通过小李“间接认识”,却不在彼此的关注列表里。作为班长,你想让这份名单变得更“完善”——但你不打算乱点鸳鸯谱,只修补那些非补不可的关系。这个“最小补全”思想,就是闭包的灵魂。
在离散数学中,给定集合 A 上的关系 R,闭包是在 R 的基础上添加最少的元素,使新关系满足某个指定性质(自反、对称或传递)。闭包记作 r(R)(自反闭包)、s(R)(对称闭包)或 t(R)(传递闭包)。它必须满足三个条件:包含原关系 R;满足目标性质;在所有满足前两条的关系中元素最少。
用矩阵的眼光看,关系 R 对应一个 0-1 矩阵 M,闭包运算就是对矩阵做定向修补。自反闭包像把矩阵对角线的 0 全部改成 1;对称闭包像把矩阵沿着对角线“镜像翻转”补齐;传递闭包则是最复杂的——它要找出所有“间接路径”并直接标成 1。
闭包的定义虽然抽象,但抓住“最小补全”四个字就抓住了牛鼻子。后续要学的三种闭包,本质是三个不同方向的“补全作业”:一个补对角线,一个补反向,一个补路径。
- 原关系 R这是初始的人际网络,是补全的出发点。
- 补齐最少关系 P为了让性质成立所必须要加的“桥梁关系”。
- 闭包结果原关系与补充关系的并集,记为 C(R)。
自反闭包求解
你翻看名单,发现很多人“不认识自己”——这当然不合常理。你决定强制执行一条规则:每个人都得加自己为好友。这是最简单的补全,只需要给名单上每个名字加一条“自己指向自己”的记录。自反闭包干的就是这件事。
自反闭包的定义是:在 R 中补充所有形如 (a, a) 的有序对,使得每个元素都与自身相关。数学上,只需要求并上恒等关系 I_A(即对角线上的全 1 关系)。对应到矩阵操作,就是把邻接矩阵的主对角线全部置为 1,其他位置原样保留。
自反闭包就是“全员加自环”的操作,简单直接,但它是很多算法的地基——比如在带权图中,我们常把 dist[i][i] = 0 当成自反闭包的一种表现。理解了自反闭包,你就掌握了闭包家族中最朴素的成员。
- 恒等关系 I_A所有
(a, a)组成的集合,在矩阵上就是对角线。 - 并运算把 R 和 I_A 合并,重复元素只保留一份。
- 矩阵形式M 的对角线元素全部改为 1。
对称闭包求解
名单上有一条“小张 → 小李”的单向关注,可小李并没有回关小张。这不太公平——你决定让每一条好友关系都变成双向的。凡是名单上有“甲 → 乙”而没有“乙 → 甲”,就补上后者。这就是对称闭包。
对称闭包在数学上的操作极为优雅:把 R 和它的逆关系 R^{-1} 并起来。R^{-1} 就是把每条有序对的两个元素对调。在矩阵的视角下,这相当于把邻接矩阵和它的转置矩阵做按位或(OR)运算——只要原矩阵或者转置矩阵中有一个位置是 1,结果就是 1,这样矩阵自动变得“对称”。
对称闭包是“镜像补全”的艺术。在无向图建模中,我们常把有向边“对称化”来得到无向关系。值得一提的是,自反闭包和对称闭包的计算都很省事,但传递闭包才是真正的“重头戏”——它的计算量比前两者高一个量级,也引出了最经典的算法。
- 逆关系 R^{-1}把每条有序对 (a,b) 交换成 (b,a)。
- 并运算原来的单向边和补上的反向边合在一起。
- 矩阵转置视角M 与转置 Mᵀ 做逻辑或,得到对称矩阵。
传递闭包原理
群里流传着一条消息:小张认识小李,小李认识小王,于是小张可以通过小李“间接认识”小王。但名单上并没有“小张 → 小王”这条直接记录。为了让名单如实反映“认识链”,你需要把这种“二步可达”的关系也补进去。更进一步,三步行、四步行……所有通过任意长度路径可达的关系都要补上。这就是传递闭包的任务。
传递闭包 t(R) 是最小的传递关系,包含 R 中所有“通过有限步路径可达”的有序对。用数学语言说,t(R) 等于 R 的所有正整数次幂的并集。所谓 R^2 就是“走两步能到”的关系,R^3 是“走三步能到”的关系……一直并下去,直到所有间接关系都被覆盖。
理论上讲,传递闭包就是“路径可达性”的数学表达。但这个定义给的是无穷并集,如果真按这个公式用矩阵连乘去算,复杂度高达 O(n⁴),极不划算。我们需要更聪明的办法——这就是 Warshall 算法的用武之地。
- 关系的复合 R²走两步能到达的路径,通过中间节点衔接。
- R^k 的含义恰好走 k 步能到达的关系(允许重复经过节点)。
- 无穷并集把所有长度的可达关系都并进去。
- 有限终止当集合大小为 n 时,超过 n 步的路径必然重复经过节点,可以截断。
Warshall算法
你拿着名单苦思冥想:怎么高效地算出所有人的“间接关系链”?聪明的你想到一个策略——让节点逐个当“中间人”。第一轮,只允许第 1 号人物当中介;假如甲认识 1 号、1 号认识乙,就补上“甲 → 乙”。第二轮,允许 1、2 号当中介……每一轮新累积的关系,下一轮还能继续“发酵”。这其实就是 Warshall 算法的核心思想。
Warshall 算法是一个基于动态规划的三重循环。设布尔矩阵 M[i][j] 表示 i 到 j 是否可达,外层循环变量 k 代表“当前允许使用前 k 个节点作为中转点”,内层两个循环枚举所有起点 i 和终点 j。核心递推式是:
Warshall 算法的精妙之处在于“滚动更新”:每一轮新补上的边,在下一轮立刻可以当中转边使用,信息在矩阵中像涟漪一样扩散。只需 O(n³) 次布尔运算即可完成,比朴素的矩阵连乘快了整整一个数量级。在实际编程中,每一轮 m[i][j] |= (m[i][k] && m[k][j]) 就是算法的全部——极简而深邃。
- 初始化M⁰ 就是原关系的邻接矩阵。
- 外层循环 k逐个加入中转点,k 从 1 到 n。
- 内层枚举 i、j检查所有有序对。
- 递推关系经 k 中转或原本可达,两者任一成立即可达。
- 算法完成经过 n 轮迭代,Mⁿ 就是传递闭包矩阵。
闭包性质分析
拍完三种闭包以后,你开始琢磨它们的脾气:闭包之间有没有先后顺序?多套几层会不会变出更多新鲜东西?经过反复测试,你总结出三条规律——单调性、幂等性和复合顺序。
首先是单调性:如果原关系 R 包含在 S 中,那么 R 的闭包也包含在 S 的闭包中。这很好理解:熟人圈大的人,闭包扩充后的圈子也一定大。其次是幂等性:对闭包再取闭包,不会得到更多新关系。一旦关系已经自反(或对称、传递),再补一次是白费功夫——朋友的朋友补完了,就不会再有新朋友。最后是复合顺序规律:多种闭包叠加时,顺序会让结果变大或变小。
这些性质让闭包运算成为关系代数中“干净利落”的操作:它们像数学意义上的“封装”——不论怎么组合,最终都会收敛到某个固定的“完备形态”。在设计算法时,你可以放心地对同一个关系反复调用闭包运算而不必担心副作用。
- 单调性原关系越大,闭包结果也越大。
- 自反闭包幂等自反闭包的自反闭包还是它自己。
- 传递闭包幂等走过一遍的可达关系再走一遍不会新增。
- 复合顺序先对称后传递的范围 ≥ 先传递后对称,自反性不受影响。
应用场景实践
闭包不只是教科书上的抽象玩具,它在真实工程中是一个“隐形工具”。最经典的应用是图的可达性分析:要判断一个节点能否到达另一个节点,直接求图的传递闭包即可。社交软件的“你可能认识的人”推荐、编译器检测变量间的依赖链、数据库查询优化中的依赖关系推导,背后都是传递闭包在出力。
另一个重要的应用是等价关系的构造。给一个不是等价的关系(比如有向图中的“互相可达”),通过传递闭包和对称闭包的组合,可以诱导出一个等价关系,其等价类正是图中的强连通分量。这在大规模图计算中非常有用——把互相可达的节点归为一组,从而压缩图的结构。
在 AI 领域的依赖分析中,这一招同样锋利——比如在深度学习编译器中,张量之间的计算依赖关系被建模成图,传递闭包用来判断两个算子能否并行执行。闭包不再是纸上谈兵,而是自动微分、算子调度背后的“关系引擎”。
- 互达关系两个节点互相可达,当且仅当它们双向都在传递闭包中。
- 等价类互达关系划分出的强连通分量。
- 工程落地Warshall 算法跑一遍,强连通分量直接得出。
例题与矩阵运算
理论讲了一箩筐,不如亲手算一遍。已知集合 A = {1, 2, 3} 上的关系 R = {(1,2), (2,3)},我们依次求三种闭包。首先自反闭包:补上 (1,1)、(2,2)、(3,3),得到 r(R) = {(1,2), (2,3), (1,1), (2,2), (3,3)}。其次对称闭包:补上反向边 (2,1)、(3,2),得到 s(R) = {(1,2), (2,3), (2,1), (3,2)}。
最后是传递闭包,用 Warshall 算法推演:初始邻接矩阵只有两个 1;第一轮 k=1 无变化;第二轮 k=2 时,发现 1→2 且 2→3,于是补上 (1,3);第三轮 k=3 时,1→3 已经存在,但 3→?没有更多输出,最终 t(R) = {(1,2), (2,3), (1,3)}。三次运算三种结果,各司其职。
通过手算你可以清晰感知三种闭包的“分子手术”:自反在补环,对称在补镜像,传递在补跳转链。如果数据量再大些,手算就不现实了——下一节我们用 Python 把这套流程自动化。
- 自反闭包矩阵把主对角线上的 0 改成 1。
- 对称闭包矩阵把矩阵转置后与自己按位或。
- K=2 轮更新第 2 行第 3 列为 1,且第 1 行第 2 列为 1,故置第 1 行第 3 列为 1。
- 最终传递闭包所有可达关系计入。
AI 应用场景
场景一:社交网络“可能认识的人”推荐
阶段:特征工程 / 图数据分析
解决的问题:传统做法只做一阶好友推荐(朋友的朋友),覆盖不全。利用传递闭包,可以找到所有“多跳可达”的潜在好友,让推荐覆盖整个社交链——同时通过对称闭包保证推荐关系是双向的。AI 模型把闭包后的邻接矩阵作为图特征输入GraphSAGE 等 GNN,推荐效果显著提升。
具体案例:在领英的“人脉推荐”中,系统对二度人脉(传递闭包长度为 2 的关系)和三度人脉分别计算权重。工程师用 Warshall 算法预处理关系矩阵,把“二度可达”标记出来,再输入排序模型,冷启动用户的推荐命中率提高了 20%。
场景二:AI编译器的算子依赖分析
阶段:模型编译优化 / 算子调度
解决的问题:深度学习编译器中,需要判断两个算子在计算图中是否存在间接依赖,以决定能否并行执行。传统方法对每个算子对做 DFS 遍历,复杂度高。用传递闭包预处理依赖图,O(1) 时间即可回答任意算子间的依赖关系。
具体案例:TVM 或 MLIR 的调度器中,编译器先把计算图转为关系矩阵,运行 Warshall 算法得到可达矩阵,再对无依赖关系的算子执行算子融合。闭包运算让融合决策从 O(N²) 次 DFS 下降到 O(N³) 预处理 + O(1) 查询。
场景三:知识图谱的推理补全
阶段:知识推理 / 图嵌入预处理
解决的问题:知识图谱中大量的隐式关系(如 A 的父亲是 B,B 的父亲是 C,则 A 的祖父是 C)需要被显式补全。用传递闭包对关系矩阵做推理,可以批量“补全”隐含的三元组,为后续的向量化表示提供稠密关系输入。
具体案例:在构建医疗知识图谱时,用 Warshall 算法对“治疗关系”求传递闭包,推导出药物→症状的间接治疗路径,再结合图神经网络做关系预测。闭包预处理后,推理召回率提升 15%。
Python 演示
# -*- coding: utf-8 -*-"""使用 Warshall 算法计算传递闭包,并演示三种闭包的求法。""" def reflexive_closure(matrix): """自反闭包:对角线置 1。""" n = len(matrix) result = [row[:] for row in matrix] for i in range(n): result[i][i] = 1 return result def symmetric_closure(matrix): """对称闭包:矩阵与其转置做逻辑或。""" n = len(matrix) result = [row[:] for row in matrix] for i in range(n): for j in range(n): if matrix[i][j] == 1: result[j][i] = 1 return result def transitive_closure_warshall(matrix): """Warshall 算法求解传递闭包,O(n^3)。""" n = len(matrix) result = [row[:] for row in matrix] for k in range(n): for i in range(n): if result[i][k] == 1: for j in range(n): result[i][j] = result[i][j] or result[k][j] return result def print_matrix(name, matrix): print(f"{name}:") for row in matrix: print(" ".join(map(str, row))) print() if __name__ == "__main__": R = [[0, 1, 0, 0], [0, 0, 1, 0], [1, 0, 0, 1], [0, 0, 0, 0]] print("原文关系 R (1-indexed 节点):") print_matrix("R", R) print_matrix("r(R) 自反闭包", reflexive_closure(R)) print_matrix("s(R) 对称闭包", symmetric_closure(R)) T = transitive_closure_warshall(R) print_matrix("t(R) 传递闭包", T) print("可达性验证: 节点1(索引0)能否到达节点4(索引3)?", "是" if T[0][3] == 1 else "否")
def transitive_closure_warshall(matrix):定义 Warshall 算法的核心函数,矩阵是 0/1 邻接矩阵。for k in range(n):外层循环逐个引入中间节点 k,这是“允许 k 当中转人”的轮次。if result[i][k] == 1:剪枝优化:如果 i 到 k 不可达,跳过整个内层循环。result[i][j] = result[i][j] or result[k][j]核心递推:i 到 j 可达,要么原本可达,要么经 k 中转可达。reflexive_closure` 和 `symmetric_closure分别补对角线、补反向边,代码直白对应定义。
常见误区
误区“传递闭包就是把所有节点两两之间都连上边,反正要传递嘛。”很多初学者觉得闭包就是“全连接”。
正解闭包是“最小补全”,只补那些由既有关系推导得出的关系。如果原关系中两个节点之间没有路径,闭包不会凭空造一条边。好比补全朋友圈名单:只有“通过朋友链真的能联系上”的人才该加进来,不是把全班人都加一遍。判定标准是“是否有路径可达”,而非“是否有关系好看”。
为何常见:因为教材反复强调“最小”却很少给反例,而矩阵中 0 变 1 的过程看起来像“越来越满”,于是被误读为全连。
误区“矩阵更新必须用原始矩阵做判断,否则会污染结果,用刚刚更新的边可能产生错误。”很多初学者对动态规划不熟,担心就地更新会“作弊”。
正解Warshall 算法的精妙之处恰恰在于允许就地更新。当 k 轮加入节点 k 时,即使 i→k 或 k→j 的边是本轮刚添的,也完全可以立即使用——因为“借助更小编号节点到达 k”的组合,在 k 轮之前已经完备了。这就像滚雪球:新补的边立刻有资格当中转边,反而加速收敛,且不会产生逻辑错误。
为何常见:受普通矩阵乘法思维影响,人们习惯“旧矩阵乘旧矩阵”,忽略了动态规划中阶段间状态传递的本质。
误区“反正最后都是求闭包,先做哪个再做哪个结果都一样吧?”初学者容易默认运算交换律成立。
正解三种闭包不是可交换的!比如 s(t(R))(先传递再对称)和 t(s(R))(先对称再传递)结果可能不同。前者是对可达关系做双向化,得到的是“可达且互达”的强连通关系;后者把每条边对称化再传递,可能把两个原本独立的子图通过反向边连通起来。性质分析一节中的公式精确刻画了这些包含关系——决策前必须想清楚先补哪个。
为何常见:实数加法满足交换律的直觉迁移到关系运算上,忽视了关系的方向性和复合规则。
误区“Warshall 算法就是图算法,和 AI 有什么关系?我只会在刷题时用到它。”许多初学者把闭包锁死在离散数学课本里。
正解关系闭包是一种通用的“依赖传播”建模工具,广泛用于数据库(函数依赖的闭包推导)、编译器(数据依赖分析)、知识图谱(推理补全)以及 AI 的图学习(多层邻居聚合本质上是 k 跳可达的近似)。当你遇到“传递性推导”问题——A 影响 B、B 影响 C,问 A 是否影响 C——就在用它。
为何常见:教材以图为例讲 Warshall,缺少跨领域案例,学生难以迁移抽象的关系运算思想。
面试题
Q1. 给定集合 A={1,2,3},关系 R={(1,2)},则自反闭包 r(R) 是?
- A.{(1,2),(1,1),(2,2),(3,3)}← 正确答案
- B.{(1,2),(2,1)}
- C.{(1,2),(1,1),(2,2)}
- D.{(1,2),(1,1),(2,2),(3,3),(2,1)}
答案:A。自反闭包要求每个元素都与自身相关,因此要补上 (1,1)、(2,2)、(3,3),再加上原关系 (1,2)。选项 B 是对称闭包,C 漏了 (3,3),D 是自反闭包和对称闭包的混合。类比“补全好友名单”:每个人必须加上自己,3 号也不能漏。
Q2. 对称闭包 s(R) 等价于哪个矩阵运算?
- A.矩阵与其转置做逻辑或← 正确答案
- B.矩阵对角线置 1
- C.矩阵自乘
- D.矩阵转置
答案:A。对称闭包是 R ∪ R⁻¹,矩阵形式上即为 M 与 Mᵀ 按位或。对角线置 1 是自反闭包;矩阵自乘是求二步可达关系,属于传递闭包的步骤;单纯转置只得到逆关系,没有保留原关系。这就像补全双向关注:原关注加上反向关注,二者取并集。
Q3. Warshall 算法的核心递推式中,M^(k)[i][j] 的含义是?
- A.只允许经过前 k 个节点中转,i 能否到达 j← 正确答案
- B.恰好经过 k 条边,i 能否到达 j
- C.i 和 j 之间的距离小于 k
- D.i 通过第 k 个节点直达 j
答案:A。Warshall 是以“允许使用的中间节点”来划分阶段的,M^(k) 表示路径的中间节点编号全部不超过 k。它不是限定边数(B),也不是距离(C),更不是要求必须经过 k(D)。类比引荐规则:第 k 轮允许前 k 号人当中间人,而不是要求每个人都必须出场。
Q4. 对任意关系 R,以下哪个等式一定成立?
- A.t(t(R)) = t(R)← 正确答案
- B.s(s(R)) = R
- C.r(r(R)) = R
- D.t(R ∪ S) = t(R) ∪ t(S)
答案:A。传递闭包是幂等的:已经传递的关系再取传递闭包不会新增任何边。s(s(R)) = s(R) 而非 R,因为 R 本身不一定对称;r(r(R)) = r(R) 而非 R;并集的传递闭包与各自闭包的并集通常不相等(跨关系的传递路径会丢失)。就像压缩饼干再压不会更小,闭包是单调幂等的“收敛”操作。
Q5. 下列关于闭包性质的描述,正确的有?
- A.自反闭包和对称闭包满足交换律:s(r(R)) = r(s(R))← 正确答案
- B.传递闭包的对称闭包,结果是强连通关系← 正确答案
- C.r(R) 是 A×A 的子集← 正确答案
- D.若 R 已经是对称的,则 s(R) = R← 正确答案
答案:ABCD。s 和 r 互不影响(一个管对角线、一个管反向边),所以 A 正确。t(R) 反映可达性,再取对称即“互相可达”,正是强连通关系,B 正确。闭包是 A×A 的子集,C 是定义的直接推论。已经对称的关系取对称闭包不新增元素,D 是幂等性的体现。
Q6. 已知有向图有 4 个节点,邻接矩阵的 Warshall 计算中,第二轮(k=1,0-indexed)共补了 3 条新边,关于这些新边,下列说法正确的是?
- A.这些新边在后续轮次中可以作为中转边使用← 正确答案
- B.这些新边必须等所有轮次结束后才能参与计算
- C.这些新边不能和旧边共同构成路径
- D.这些新边是矩阵转置得到的
答案:A。Warshall 是滚动更新(就地更新)算法,新补的边立即允许作为后续轮次的中转边,这正是它比朴素矩阵连乘高效的原因。B 错在把算法误解为需要快照;C 错在路径可以混合新旧边;D 错在把对称闭包的转置操作误解到传递闭包上。这就像雪球越滚越大,新成员也有资格当引荐人。
Q7. 关于关系 R 的三种闭包,下列说法正确的有?
- A.若 R 是传递的,则 t(R) = R← 正确答案
- B.对任意 R,s(R) 一定是对称的← 正确答案
- C.对任意 R,t(R) 中不会出现长度为偶数的环
- D.r(s(R)) 中的记号表示先求 s(R) 再求 r← 正确答案
答案:ABD。传递关系取传递闭包不新增元素,A 是幂等性。对称闭包定义就是补全成对称关系,B 正确。C 错误:传递闭包完全可能包含偶数长度的环(比如双向边构成二环)。D 是复合运算的阅读顺序,先内后外。A 类比“已经是朋友的朋友圈,补全后不变”。
Q8. 集合 A 有 n 个元素,用 Warshall 算法求传递闭包的时间复杂度是?
- A.O(n³)← 正确答案
- B.O(n²)
- C.O(n⁴)
- D.O(n log n)
答案:A。Warshall 是三重循环,每层最多 n 次迭代,总复杂度 O(n³)。对比:朴素矩阵连乘法需 O(n⁴),Floyd 算法同是 O(n³) 但用于带权最短路。n² 不够覆盖所有 ij 对,n⁴ 是暴力连续乘法的复杂度。三重循环就像“三个人接力传消息”,n³ 次检查必不可少。
Q9. 在 AI 编译器中,用传递闭包分析算子依赖关系,以下说法正确的有?
- A.传递闭包可以回答“两个算子之间是否存在间接依赖”← 正确答案
- B.传递闭包可以直接判断两个算子是否等价
- C.闭包预处理后,依赖查询可做到 O(1) 时间← 正确答案
- D.传递闭包可以代替图的最短路算法
答案:AC。可达矩阵回答“是否存在路径”,正是间接依赖判断,A 正确。等价性需要额外信息(如算子语义),B 错误。预计算闭包矩阵后,查表即可回答依赖关系,C 正确。最短路需要边权,传递闭包只关心是否可达,D 混淆了目标和信息量。
Q10. 给定 R={(1,2),(2,1)},下列说法中错误的是?
- A.R 是对称的
- B.t(R) 包含环 (1,1) 和 (2,2)
- C.r(R) 比 s(R) 多两个元素← 正确答案
- D.R 不是传递的
答案:C。R 显然对称,A 正确。因为 1→2 且 2→1,传递闭包补上 (1,1) 和 (2,2),B 正确。r(R) 补 (1,1)、(2,2) 共 2 个元素;s(R) 已是自身,补 0 个,所以 r(R) 恰好比 s(R) 多 2 个元素,C 说反了。R 缺自环,按定义不是传递的,D 正确。这个例子展示三种闭包各自独立地补不同的“缺”。