news 2026/9/29 1:50:40

集合划分与覆盖:离散数学基础如何驱动算法与工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
集合划分与覆盖:离散数学基础如何驱动算法与工程实践

如果你翻开任何一本离散数学教材,翻到“3-9 集合的划分和覆盖”这一节,第一眼看到的就是两个很朴素的定义:划分是把一个集合拆成若干个互不相交的非空子集,覆盖是用若干个非空子集把全集盖住、子集之间允许重叠。光看字面,这节内容确实简单到有点不起眼。但我在带离散数学课程的这些年里越来越觉得,这一小节是整个“集合论与关系”章节里最容易被低估的部分——它不仅直接支撑着等价关系的商集理论,也是后续图论、计数原理、算法分析乃至工程里做数据分区、聚类评估、选址优化时绕不开的数学底座。这篇文章我把这一节里里外外拆开来讲,告诉你定义背后的逻辑、定理怎么理解、工程里怎么用、以及考试里最容易被绕进去的几个坑。

1. 先看定义:划分和覆盖到底有什么区别

1.1 划分:非空、两两不相交、并集等于全集

设 A 是一个非空集合,π = {A₁, A₂, ..., Aₙ} 是一组由 A 的子集构成的集合族。如果这组子集满足下面三个条件,就称 π 是 A 的一个划分:

  • 每个 Aᵢ 都是非空集合;
  • 任意两个不同的块不相交,即 Aᵢ ∩ Aⱼ = ∅(i ≠ j);
  • 所有块的并集恰好等于 A,即 A₁ ∪ A₂ ∪ ... ∪ Aₙ = A。

这里的每个 Aᵢ 叫做划分的一个块,也叫划分块或者类。三个条件缺一个都不行:允许空块,那么空块对“分割”没有任何意义,还要占掉一个位置;允许两个块相交,那么交集里的元素到底算谁的,分类语义就模糊了;并集不把 A 完全盖住,就说明有元素漏掉了,这当然不能算把集合“分完”。

举个例子。A = {1, 2, 3, 4},π₁ = {{1, 2}, {3, 4}} 是一个划分,因为它只有两个块,块内非空,块间没有交集,并起来正好是 {1, 2, 3, 4}。π₂ = {{1, 2}, {2, 3}, {4}} 就不是划分,因为前两块相交,共用了元素 2。π₃ = {{1}, {2, 3}} 也不是划分,因为并集是 {1, 2, 3},漏掉了 4。

生活化的理解很简单:划分就是给全班同学分组,每个人只能在一个组,且每个人必须被分到某个组,不允许有空的组名。这个类比后面整节课都用得上。

1.2 覆盖:只管盖满,不要求块与块之间干净

覆盖的定义比划分松得多。还是设 A 是非空集合,S = {S₁, S₂, ..., Sₘ} 是 A 的一组非空子集。如果 S₁ ∪ S₂ ∪ ... ∪ Sₘ = A,就称 S 是 A 的一个覆盖。

注意,覆盖只要求两条:子集非空,并集等于 A。它不要求 Sᵢ 彼此不相交,也就是说同一元素可以同时出现在多个子集里。回到分组的类比:覆盖相当于布置多张任务海报,一个学生可以出现在好几张海报上,只要每张海报上至少有一个学生、并且所有学生至少在某一张海报上露脸就行。

所以有下面这条直白的关系:任何一个划分一定是覆盖,但覆盖不一定是划分。只要把划分定义里的“两两互不相交”这个条件去掉,划分就退化成覆盖了。而一个覆盖如果想“升级”成划分,就需要额外满足“两两不相交”这一约束。

1.3 一张对照表看清两个概念的界限

比较维度划分覆盖
子集是否非空必须非空必须非空
子集之间是否相交两两互不相交允许相交
并集是否等于全集必须等于 A必须等于 A
本质语义把元素唯一归类用多个块覆盖全部元素
对应应用分类、分区、等价类覆盖问题、选址、冗余策略

很多初学者学完定义之后会犯一个惯性错误:总想给覆盖也加上“互不相交”当默认条件。实际上完全不用,覆盖一旦满足互不相交,它就同时是一个划分了。判断一个覆盖是不是划分,就只看这一条新增条件,这是最经济的判断路径。

2. 划分背后的数学原理:为什么等价关系说了算

2.1 等价关系与划分是一一对应的

如果只看教材,划分这一节的定位多半是“等价关系的继续”。前面你已经学了等价关系的三个性质:自反、对称、传递。那么等价关系和划分之间是什么关系?核心定理可以这样说:集合 A 上的任意一个等价关系 R,都可以唯一确定 A 的一个划分;反过来,A 的任意一个划分也能唯一确定 A 上的一个等价关系。

前面半句话的构造方式就是等价类。给定关系 R,对每个 x ∈ A,记 [x]ᵣ = {y ∈ A | xRy} 为 x 的等价类。这些等价类天然满足划分的三条性质:因为自反性保证 x ∈ [x]ᵣ,所以等价类非空;因为如果两个等价类有公共元素 z,那么由 xRz 和 yRz 结合对称性与传递性可以推得 xRy,从而这两个等价类其实是同一个等价类,所以不同的等价类一定不相交;又因为每个元素都属于它自己的等价类,所以所有等价类的并集就是 A。于是 A/R = {[x]ᵣ | x ∈ A} 就是一个划分,它的块就是各个等价类。

反过来也一样。如果给定了 A 的一个划分 π,定义一个关系 R:xRy 当且仅当 x 和 y 在同一个划分块里。自反、对称、传递都显然成立,所以 R 是等价关系。这个双向构造是最标准的证明思路,考场上如果在证明题里遇到“由等价关系构造划分”或者反过来,照着上面这套逻辑写基本不会丢分。

2.2 商集:用“块”作为元素看世界

等价类构成的划分在数学上有一个专门的记号 A/R,叫做 A 关于 R 的商集。商集里的元素不是 A 里的单个元素,而是 A 的等价类——你可以理解为“把每个划分块打包成一个新的抽象元素”。

这就是分类问题的数学雏形。比如在编程语言里,判断“两个引用是否指向同一个对象”本质上是在对象集合上定义一个等价关系,然后对象集合按这个关系划分成一个个等价类,每个类就是一个“对象身份”。在数据库设计里,按某个字段的值给记录分组,等价类就是同一组记录的集合。商集 A/R 实际上把一个“大集合”压缩成了“块级别的小集合”,后续在块上做映射、做统计,都建立在划分已经给出的前提下。

所以在学划分时,不要只把它当成一个孤立定义。看到“划分”两个字,同时要在脑子里出现三条信息:块非空、块不相交、并集为全集,并且把它和等价关系、商集串在同一条知识链上。这样后面学到同态映射、群论、格论时会轻松很多。

2.3 一个集合到底有多少种划分:贝尔数

划分还有一个很自然的计数问题:一个 n 元集合一共有多少种不同的划分?这个数叫做贝尔数,记作 Bₙ。它的前几项是 B₁ = 1,B₂ = 2,B₃ = 5,B₄ = 15,B₅ = 52。

贝尔数可以用第二类斯特林数拆开来看。第二类斯特林数 S(n, k) 表示把 n 个不同元素划分成 k 个非空、不可区分的块有多少种方案。那么贝尔数就是把它对所有可能的 k 求和:

Bₙ = S(n, 1) + S(n, 2) + ... + S(n, n)

而 S(n, k) 本身有递推公式:

S(n, k) = S(n - 1, k - 1) + k · S(n - 1, k)

这个递推的直观解释是:看第 n 个元素,它要么自己单独成为一个新块,那剩下的 n - 1 个元素需要分成 k - 1 块,也就是 S(n - 1, k - 1);它要么被放进已有 k 个块的某一个里,而 n - 1 个元素已经分好了 k 块,它可以选择进入这 k 个块中的任意一个,所以贡献是 k · S(n - 1, k)。这个“看最后一个元素怎么处理”的思路在排列组合里特别常用,后面学集合划分的计数题基本都靠它。

贝尔数还有一个漂亮的贝尔三角形构造,本质和帕斯卡三角形类似。第一行写 1,从第二行开始:每行第一个数是上一行最后一个数,后面的每个数是它左上方的数加上同一行左边的数:

1 1 2 2 3 5 5 7 10 15 15 20 27 37 52

每行的最后一个数恰好就是下一个贝尔数。上课时我经常让学生自己把贝尔三角形多写几行,比死记前几项有效得多——算着算着你就理解这种结构为什么能把递推关系表达得这么整齐。

3. 实战场景:集合覆盖问题与贪心算法的落地

3.1 集合覆盖问题:一个非常现实的优化模型

划分和覆盖在应用层面完全不同:划分解决“怎么把资源不重叠地分配”,覆盖解决“怎么用最少的资源覆盖全部需求”。后者在运筹学里有一个著名的模型叫集合覆盖问题(Set Cover Problem)。

问题定义很简单:给定全集 U 和若干个子集组成的集合族 S,要求选出最少个数的子集,使它们的并集等于 U。这个模型能套到非常多真实场景里:

  • 城市消防站选址:每个候选站点能覆盖周边一片区域,问最少建几个站能覆盖全部居民区;
  • 基站部署:每个基站有服务半径,问如何选择站点使所有地区都有信号;
  • 文章关键词提取:每个关键词能覆盖若干主题标签,想用少量关键词覆盖常见主题;
  • 病毒基因检测:每份样本可以覆盖一组毒株,用最少的检测样本覆盖所有关注毒株。

这类问题有个不太友好的性质:它是 NP 难问题。意思是一旦问题规模变大,想在有限时间内找到数学上最优的解几乎不现实。但这不代表实际工程里我们只能干瞪眼——绝大多数场景下,我们只需要一个足够好的近似解。而求解集合覆盖最经典、最实用的方法就是贪心算法。

3.2 贪心算法的直观逻辑与 Python 实现

贪心算法的策略一句话就能说清:每一轮都选取当前能覆盖最多“尚未被覆盖元素”的子集,直到所有元素都被覆盖为止。这个策略不保证找到最优解,但实现极其简单,而且实际效果通常很好。

比如 U = {1, 2, 3, 4, 5, 6},子集族是 S₁ = {1, 2, 3},S₂ = {2, 4, 5},S₃ = {4, 5, 6},S₄ = {1, 6}。最优解是 S₁ ∪ S₃,两个子集就覆盖了全集。贪心算法先看所有子集的覆盖量,S₁ 覆盖 3 个未覆盖元素,S₂ 也覆盖 3 个,S₃ 覆盖 3 个,S₄ 覆盖 2 个,先挑 S₁;此时未覆盖集合变成 {4, 5, 6},下一轮 S₃ 能覆盖 3 个,于是选 S₃。结果也是两个子集,表现完美。

用 Python 写起来很短,核心循环就是“每轮取当前覆盖未覆盖元素最多的子集”:

def greedy_set_cover(U, subsets): uncovered = set(U) chosen = [] while uncovered: # 选出与当前未覆盖元素交集最大的子集 best = max(subsets, key=lambda s: len(s & uncovered)) chosen.append(best) uncovered -= best return chosen U = {1, 2, 3, 4, 5, 6} subsets = [ {1, 2, 3}, {2, 4, 5}, {4, 5, 6}, {1, 6} ] result = greedy_set_cover(U, subsets) print(result) # 输出: [{1, 2, 3}, {4, 5, 6}]

实际工程中需要注意一个细节:被选中的子集要用集合去重,因为同一元素一旦被覆盖,即使后续子集还包含它,也不会计入新的覆盖贡献。uncovered -= best这一步就是在做这个操作。另一个细节是子集列表里可能有重复项,用集合类型存储能天然去重;如果子集本身带有成本信息,简单的“覆盖最多”贪心就不一定最优了,需要改成“单位成本覆盖数最大”的加权贪心,这是以后深化时要考虑的。

3.3 贪心算法为什么可以用:近似比的直白解释

有人会问:贪心算法不保证最优,那它到底有多差?能不能接受?这个问题的回答是:贪心算法得到的覆盖大小,最多不会超过最优解的 ln n 倍,这里 n 是全集元素个数。

证明思路不复杂。设最优解需要 OPT 个子集,那么在贪心算法运行的任意一步,如果还剩 r 个元素没被覆盖,这 r 个元素一定可以被 OPT 个子集全部覆盖。根据抽屉原理,这 OPT 个子集中至少有一个子集能覆盖 r / OPT 个当前未覆盖元素。贪心算法每轮选的子集不会比这个差,所以每轮至少能把未覆盖元素数变成 r × (1 - 1/OPT)。

从初始 n 个未覆盖元素开始,经过 t 轮之后剩余未覆盖元素不超过 n × (1 - 1/OPT)^t。递归展开后可以得到:贪心算法选出的子集数不会超过 OPT × ln n。这个结论放在工程实践里,基本意思是:问题规模别太离谱,贪心解离最优解的差距通常是可控的;而它换来的时间优势是巨大的——最优解可能要在指数时间里挣扎,贪心只要线性扫若干轮就出结果了。

我当年做一个快递自提点选址需求时,城市区域被划分成上百个网格,候选点位也有几百个,用贪心跑出来一个覆盖方案,再拿整数规划去验证,发现差距不到 5%。从那之后我对这类问题就形成了一条经验:先上贪心拿基线结果,再根据业务容忍度和数据规模决定要不要投入更复杂的算法。盲目追求最优解,在很多场景里是纯粹的浪费。

4. 手把手实操:判断、构造并枚举一个集合的所有划分

4.1 手工判断一个集合族是否构成划分的三步法

拿到一个“集合族是否构成某集合的划分”这种题目,别急着心算,按三步走,每一步都很机械,不容易出错:

第一步,看是否为空块。遍历所有子集,任何一个子集为空,直接判否。因为划分定义第一条就是“块非空”。

第二步,看两两是否相交。任意两个子集如果存在公共元素,直接判否。这步要特别警惕子集个数比较多时的遗漏,建议先在草稿上把子集按顺序编号,然后像冒泡排序那样两两比较一遍。

第三步,看并集是否等于全集。把第二步中确认互不相交的若干块做并集,和给定全集比较。并集不等于全集,说明有元素漏掉了,判否。

三步全过,才是一个合法划分。反过来,如果你在判断“覆盖”,只需要执行第一和第三步,第二步可以完全省略。这种“多一步压缩成少一步”的判断链路,其实也体现了定义之间的层级关系。

4.2 用 Python 快速校验划分和覆盖

手工判断适合小规模题目,一旦子集数量多,人肉枚举很容易漏判。写个简单的 Python 函数就能把判断自动化,而且代码本身也能帮助理解定义:

def is_partition(U, blocks): # 条件1:所有块非空 if any(len(b) == 0 for b in blocks): return False # 条件2:两两交集为空 for i in range(len(blocks)): for j in range(i + 1, len(blocks)): if blocks[i] & blocks[j]: return False # 条件3:并集等于全集 return set().union(*blocks) == set(U) def is_cover(U, blocks): # 非空条件保留,相交条件删去 if any(len(b) == 0 for b in blocks): return False return set().union(*blocks) == set(U) print(is_partition({1, 2, 3}, [{1, 2}, {3}])) # True print(is_partition({1, 2, 3}, [{1, 2}, {2, 3}])) # False,相交了 print(is_partition({1, 2, 3}, [{1}, {2}])) # False,并集少了元素 print(is_cover({1, 2, 3}, [{1, 2}, {2, 3}])) # True,尽管相交但盖满了

set().union(*blocks)这个写法值得说明一下,它等价于从空集合出发,把 blocks 里的所有集合依次求并集。这样写比blocks[0] | blocks[1] | ...这种写法的好处是,不需要关心 blocks 到底有多少个元素。如果 blocks 恰好为空,这个表达式会返回空集合,不会抛异常,所以在某些边界情况下代码更稳健。

4.3 枚举一个集合的所有划分,体会贝尔数的增长

判断一个集合族是不是划分只是基本功,另一个常见需求是“把一个集合的所有划分都列出来”。手工枚举 n = 3 时还能靠脑补,n = 4 时就已经有 15 种,n = 5 直接到 52 种,完全不可能靠逐个猜。用递归写一个枚举器,能让这种计数问题变成一条命令的事:

def all_partitions(elements): if not elements: return [[]] first, rest = elements[0], elements[1:] result = [] for partition in all_partitions(rest): # 把 first 放入已有某个块 for i in range(len(partition)): new_blocks = [block.copy() for block in partition] new_blocks[i].add(first) result.append(new_blocks) # 把 first 单独作为一个新块 result.append([{first}] + partition) return result for p in all_partitions([1, 2, 3]): print(p)

输出正好是 5 种:

[{1, 2, 3}] [{1, 2}, {3}] [{1, 3}, {2}] [{1}, {2, 3}] [{1}, {2}, {3}]

递归思路和斯特林数递推完全一致:处理第一个元素,要么把它并入已经生成的某个划分块,要么让它独自成块。理解了这一点,你在写代码时也就顺手把第二类斯特林数的递推公式过了一遍。这也是我这些年教课的一个体会:数学里的递推关系和程序里的递归函数往往是同一个东西,只是表现形态不同,能打通两者的人,学算法的速度真的会快很多。

实际使用这个函数时还有个注意点:n 一旦超过 8 或 9,划分数量会爆炸式增长,所有划分全列出来会占大量内存,这时应该改用生成器(yield)按需迭代,而不是用列表一次性收集全部结果。很多初学者在这里吃过亏,我也是在跑 n = 10 时内存直接爆掉才记住的教训。

5. 应用盘点与避坑指南:从数据库分表到考试易错点

5.1 计算机系统里的划分与覆盖

划分和覆盖的概念在计算机系统里几乎无处不在。数据库水平分表是最典型的一个:用户表按 user_id 哈希到 16 张分表里,每行记录只会落在唯一一张表,这 16 张表就构成全量用户集合的一个划分。一致性哈希也是类似思路,数据按哈希值映射到环上的某个区间,每个节点负责一段连续区间,这个区间切分就是一个划分。

而在分布式计算里,覆盖思想同样常见。比如任务调度中,一个大规模任务被拆成若干子任务,每个子任务处理一部分数据,这些子任务的数据范围之间可能有重叠,以保证计算结果可以相互校验或冗余恢复,这就是覆盖而非划分的应用。另一个直观例子是微服务中的多副本:同一个用户的请求被发送到多个实例上处理,每个实例覆盖全部用户群体,副本之间并非划分关系,而是多重覆盖关系。

聚类算法也为划分提供了很好的注脚。K-means 做硬聚类时,每个样本点最终被分到唯一一个簇,所有簇构成样本集合的一个划分;而模糊 C 均值这类软聚类算法允许同一个样本以不同的隶属度属于多个簇,这时样本集就不再是严格划分,而是更接近覆盖。当你看到某个系统文档里说“分区”“分桶”“切分”时,脑子里应该立刻映射到划分;看到“覆盖范围”“副本覆盖”“覆盖组”时,就应该联想到覆盖。这种“概念到术语”的映射,学多了会形成一种直觉,读技术文档都会顺畅不少。

5.2 五个最典型的认知误区

第一个误区是“划分块之间只要有元素重复就不行”这句话本身没错,但有人会把“子集族整体重复”也算错。比如 {{1, 2}, {1, 2}},这显然不是划分,因为同样的块出现了两次,其实质等同于两个相同集合相交。在数学集合族的话语体系里,重复的集合不产生新的覆盖能力,写代码时要注意去重。

第二个误区是“空集也可以是划分的一部分”。划分定义的三个条件里明确要求每个块非空,所以 {{∅, {1, 2}}} 不是集合 {1, 2} 的划分。空块唯一合法的特例是对空集本身做划分时,空集合族可以视作空集的一个划分,但绝大多数初等教材不展开讨论这种边界,考试默认全集非空。

第三个误区是“划分块的数量应该尽可能多”。划分块数没有强制要求,{ {1, 2, 3} } 和 { {1}, {2}, {3} } 都是集合 {1, 2, 3} 的合法划分,前者叫最粗划分,后者叫最细划分。考试里如果问“是不是划分”,只要满足定义三条就成立,块数是多还是少完全不影响合法性。

第四个误区是“覆盖里的子集一定是划分里的块”。覆盖比划分宽松,一个覆盖经过调整可以变成划分,但原封不动的覆盖子集并不自动等于划分块。比如覆盖 {{1, 2}, {2, 3}} 里,子集 {1, 2} 和 {2, 3} 都不能直接作为某个划分块,因为它们相交。

第五个误区是“等价类的个数就是块的个数”。严格来说,A/R 中不同元素的等价类如果重合,只算一个等价类。判断等价类是否相同时,要看是否存在公共元素,而不是看代表元素是否长得一样。这个问题几乎每届考试都会有人丢分,值得单独记一笔。

5.3 一道典型题的完整解法复盘

最后用一道典型的考试题型把整个流程串一遍。设 A = {1, 2, 3, 4},R 是由“同模 2”定义的等价关系,即 xRy 当且仅当 x ≡ y mod 2,写出 A 关于 R 的商集。

先找等价类。1 的等价类包含所有与 1 同奇偶的数:1 和 3;2 的等价类包含所有与 2 同奇偶的数:2 和 4。于是 A/R = { {1, 3}, {2, 4} },商集包含两个块。这个结果同时也是 A 的一个划分,块非空、块间无交集、并集正好是 A。

题目如果继续问“写出所有由该等价类划分诱导出的等价关系”,其实就是在考划分与等价关系的互推:同一个划分只能诱导出一个关系,所以结果是唯一的。很多学生会在这一步自作聪明地说还能诱导出好几个,这就是没有理解一一对应关系。我建议每次做到这类题,都把“等价类 → 划分 → 商集”这条链路在草稿上画一遍,用不了一分钟,但能避免绝大多数低级错误。

我自己在教这一节时,最少不了的一句提醒是:遇到划分的题,不要凭感觉,回到定义一条条核对;遇到覆盖的题,先问自己“相交允不允许”,如果题目没说不允许,那就默认允许。这两句话看起来简单,但能把很多模棱两可的题目变成机械判断,正确率会高很多。这套从定义出发、用三步判断、再落到具体场景的思考方式,也正是这一节在整门离散数学里真正想训练你的东西。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 1:50:19

大鱼营销揭秘:市面上靠谱的知名谷歌SEO公司推荐

在全球化数字营销浪潮中,谷歌 SEO 成为中国企业开拓海外市场、实现品牌破圈的核心手段。以下为你推荐几家靠谱的知名谷歌 SEO 公司,首推深圳大鱼营销有限公司。深圳大鱼营销有限公司服务体系完备大鱼营销围绕外贸企业出海获客全流程需求,构建…

作者头像 李华
网站建设 2026/9/29 1:50:16

储能电芯从结构到选型:循环寿命与安全边界深度解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 1:50:14

javaWeb简易购物车:Session存储、Servlet+JSP实现与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 1:48:31

台达B3伺服RS-485通讯调试全链路排错指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 1:48:21

LLC环路设计避坑指南:K因子法参数计算与相位裕度优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 1:46:58

Stisla后台模板实战:基于Bootstrap 4的快速开发与二次改造

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华