)
教程文档【免费下载链接】LogicStack-LeetCode公众号「宫水三叶的刷题日记」刷穿 LeetCode 系列文章源码项目地址https://gitcode.com/gh_mirrors/lo/LogicStack-LeetCode点击查看免费下载导读本篇围绕 LogicStack-LeetCode 仓库中 851. 喧闹和富有中等 的完整题解展开讲解如何把「谁比谁更有钱」这一偏序关系抽象为有向无环图DAG并借助拓扑排序在图上按「由富到穷」的确定性顺序完成「最安静且不比他穷的人」的传递求解。读完本文你将掌握「建图 入度统计 队列拓扑序 随拓扑序同步做状态传递」这一可复用的解题范式理解邻接矩阵与邻接表链式前向星两种存图方式在稠密图场景下的取舍并能将同一套路迁移到仓库中 207、802、1976 等系列拓扑排序题目。一、题目回顾理解「富有」与「安静」两个维度1.1 问题描述有一组n个人作为实验对象从0到n - 1编号其中每个人都有不同数目的钱以及不同程度的安静值quietness。为了方便起见编号为x的人简称为person x。给定数组richer其中richer[i] [ai, bi]表示person ai比person bi更有钱给定整数数组quiet其中quiet[i]是person i的安静值安静值越小表示越安静。题目保证richer所给出的数据逻辑自洽在person x比person y更有钱的同时不会出现person y比person x更有钱的情况即不会出现相互矛盾的「更有钱」关系。需要返回整数数组answer其中answer[x] y的前提是在所有拥有的钱肯定不少于person x的人中person y是最安静的人即quiet[y]最小。1.2 两个示例的完整推演示例 1输入richer [[1,0],[2,1],[3,1],[3,7],[4,3],[5,3],[6,3]], quiet [3,2,5,4,6,1,7,0] 输出[5,5,2,5,4,5,6,7]原题解给出的推演逻辑answer[0] 5person 5比person 3更有钱person 3比person 1更有钱person 1比person 0更有钱因此钱不少于person 0的人集合中包含了5 → 3 → 1 → 0这一条链上的全部人。虽然全局最安静的是person 7quiet[7] 0但无法确认person 7是否比person 0更有钱所以不能选它在这条链上最安静的是person 5quiet[5] 1故answer[0] 5。answer[7] 7在所有拥有的钱肯定不少于person 7的人中可能包括person 3、4、5、6以及7自己最安静的是person 7本身quiet[7] 0。其余答案可同理推得。示例 2输入richer [], quiet [0] 输出[0]当只有一个人、没有任何「更有钱」关系时钱不少于person 0的只有他自己所以answer[0] 0。1.3 数据范围与关键提示原文档给出的约束如下n quiet.length且1 n 5000 quiet[i] n且quiet的所有值互不相同保证安静值可作为唯一比较依据0 richer.length n * (n - 1) / 20 ai, bi nai ! biricher中所有数对互不相同richer的观察在逻辑上一致。其中两点对算法选择影响最大richer.length的上界是n*(n-1)/2即最坏情况下接近完全图——这是典型的稠密图为邻接矩阵存图提供了直接依据quiet互不相同——意味着「谁最安静」的比较结果唯一确定不存在并列简化了更新逻辑。二、模型抽象把「更有钱」变成一张有向无环图2.1 偏序关系 → 有向边原题解给出的建图方式是对于每组richer[i] (ai, bi)添加一条从a指向b的有向边有钱指向没钱。这样「有向边」就精确表达了「边的起点比终点更有钱」这一偏序关系。2.2 为什么一定是 DAG题面中的「richer逻辑自洽」翻译成图论语言就是该图中不存在环即构造出的图是一个 DAG有向无环图。这一点是整个解法的基石正因为图无环才能跑出完整的拓扑序而拓扑序恰好保证了「处理到某个点时所有比它更有钱的点都已经被处理完毕」从而保证状态传递不会漏算。关于「有向无环图必然存在拓扑序」这一基本事实及其反证法证明仓库中 802. 找到最终的安全状态中等 一文有完整论述可作为前置知识补充阅读。2.3 在图上重新定义答案有了 DAG 之后answer[x]的含义可以重新表述为在从「比x更有钱」的节点出发能到达x的所有节点含x自身中选取quiet值最小的那个节点。换句话说我们要在 DAG 上为每个节点求「所有前驱含自身中 quiet 值最小的节点」。由于 DAG 上节点的偏序是传递的这个答案天然具有可沿边传递的性质这正是拓扑排序可以顺带解决的原因。三、核心算法入度统计 拓扑排序 同步状态传递3.1 算法主流程原文档把求解过程拆成三步第一步建图 统计入度。遍历richer对每组(a, b)添加a → b的有向边同时in[b]统计每个节点的入度。第二步初始化答案并入队零入度节点。起始时每个ans[i] i每个人至少「钱不少于自己」且在最坏情况下若不存在任何比自己更有钱的人最安静者就是自己。随后把所有入度为0的节点入队——入度为 0 意味着「没有人比他更有钱」是图中的「最富层级」。第三步跑拓扑排序边出队边更新。每次从队首弹出节点t遍历t的所有邻点u即所有被t直接或间接影响到的更穷节点做两件事状态传递利用t → u的关系尝试用ans[t]更新ans[u]。因为存在t指向u的边说明t比u更有钱那么「钱不少于u的集合」一定包含「钱不少于t的集合」加上u自己。若满足quiet[ans[t]] quiet[ans[u]]说明ans[t]比ans[u]更安静用ans[t]覆盖ans[u]入度递减删除节点t对 DAG 的影响等价于「所有邻点的入度减一」若某个邻点更新后入度变为0则将其入队。3.2 为什么拓扑序天然保证答案正确关键在于拓扑序的偏序保证当节点u出队/被处理时所有指向u的节点即所有比u更有钱的人必然都已经被处理完毕。因此ans[u]在第一次被更新时就已经综合了所有比u有钱的人的信息后续不会被遗漏——每一次ans[t]对ans[u]的更新都建立在ans[t]已经是完整正确结果的基础上形成了一种沿 DAG 拓扑序推进的「链式 DP」效果。3.3 稠密图场景两种存图方式的选择原文档特别强调本题是稠密图点数为n边数为m当m与n^2为同一数量级定义为稠密图。数据约束中richer.length n*(n-1)/2恰好印证了这一点。因此在代码实现上原文档给出了两种等价写法P1邻接矩阵——用int[n][n]的w数组存边w[a][b] 1表示存在a → b的边。适合稠密图遍历出边时直接扫描整行O(n)P2邻接表链式前向星——用he / e / ne三个数组模拟链表存储出边遍历出边复杂度只与该节点的出度相关。两种方式的求解逻辑完全一致区别仅在「出边遍历」的实现上。仓库中 207. 课程表中等 的 C 版本也使用了同款链式前向星写法可作为对照。四、完整代码实现邻接矩阵 邻接表双版本4.1 P1邻接矩阵版本class Solution { public int[] loudAndRich(int[][] richer, int[] quiet) { int n quiet.length; int[][] w new int[n][n]; int[] in new int[n]; for (int[] r : richer) { int a r[0], b r[1]; w[a][b] 1; in[b]; } DequeInteger d new ArrayDeque(); int[] ans new int[n]; for (int i 0; i n; i) { ans[i] i; if (in[i] 0) d.addLast(i); } while (!d.isEmpty()) { int t d.pollFirst(); for (int u 0; u n; u) { if (w[t][u] 1) { if (quiet[ans[t]] quiet[ans[u]]) ans[u] ans[t]; if (--in[u] 0) d.addLast(u); } } } return ans; } }关键点说明ans[i] i的初始化对应「没有任何人比我更有钱时最安静的人是我自己」w[a][b] 1建立a → b的边in[b]同步维护入度出队节点t的邻点u同时完成「答案传递」和「入度递减」两步顺序无关紧要但都要在遍历中完成若richer为空如示例 2入度全为 0所有节点初始即入队答案保持ans[i] i结果自然正确。4.2 P2邻接表链式前向星版本class Solution { int N 510, M N * N 10; int[] he new int[N], e new int[M], ne new int[M]; int idx; void add(int a, int b) { e[idx] b; ne[idx] he[a]; he[a] idx; idx; } public int[] loudAndRich(int[][] richer, int[] quiet) { int n quiet.length; int[] in new int[n]; Arrays.fill(he, -1); for (int[] r : richer) { int a r[0], b r[1]; add(a, b); in[b]; } DequeInteger d new ArrayDeque(); int[] ans new int[n]; for (int i 0; i n; i) { ans[i] i; if (in[i] 0) d.addLast(i); } while (!d.isEmpty()) { int t d.pollFirst(); for (int i he[t]; i ! -1; i ne[i]) { int u e[i]; if (quiet[ans[t]] quiet[ans[u]]) ans[u] ans[t]; if (--in[u] 0) d.addLast(u); } } return ans; } }实现细节he数组记录每个节点的第一条出边在e数组中的下标-1表示无边故需Arrays.fill(he, -1)add(a, b)以头插法把新边插入链表idx是边数组的游标数组容量N 510、M N*N 10根据题目数据范围n 500、richer.length n*(n-1)/2预留足以容纳最坏情况下的全部边遍历出边时从he[t]出发沿ne链走逐条处理邻点u e[i]。两种写法在仓库原文档中位于 851. 喧闹和富有中等.md 的 P1/P2 代码块可直接复制到 LeetCode 提交。4.3 复杂度分析原文档给出的复杂度结论时间复杂度令n为人数点数、m为richer长度边数。建图复杂度为O(m)拓扑排序过程每条边、每个节点各被处理一次复杂度为O(m n)。整体复杂度为O(m n)邻接矩阵版本因需要扫描整行实际为O(n²)量级但在m ~ n²的稠密图下二者同阶空间复杂度邻接表存图O(m)加上入度数组与答案数组整体为O(m n)邻接矩阵版本为O(n²)。五、正确性进一步论证5.1 传递更新的完备性要证明算法正确需要说明两点答案一定在「前驱集合 ∪ 自身」中ans[x]的候选人是所有钱不少于x的人而「钱不少于x」恰是「存在一条从该人到x的路径或该人就是x」。这是由边方向「有钱指向没钱」和偏序的传递性共同保证的拓扑序保证不漏不重由于是 DAG拓扑排序可以完整处理所有节点。当u被处理时其所有前驱均已出队ans[u]已经综合了全部前驱的候选信息而每个前驱的ans又在其出队时就是完整正确的。归纳地看ans[t]更新ans[u]的每一次赋值都是「当前已知道的最优」且后续不会再被更优值覆盖——因为更优值只会来自更富的人而更富的人一定先于u被处理。5.2 与「反向图 拓扑排序」思路的关联原文档在题解中引用了一篇前置讲义其中详细说明了「拓扑排序的基本流程」以及「反向图 拓扑排序做法的正确性证明」。本题不需要反向图因为正向的偏序方向恰好与答案传递方向一致但仓库中 802. 找到最终的安全状态中等 是「反向图 拓扑排序」的典型应用它将出度为 0 的安全节点作为拓扑起点反向递推标记所有安全节点。两题对照阅读可以完整理解拓扑排序在「沿依赖链传递信息」这一大类问题中的两种方向用法。六、边界情况与易错点richer为空所有节点入度为 0直接全部入队答案恒为ans[i] i示例 2 即如此。n 1只有一个节点无任何边返回[0]。quiet互不相同比较quiet[ans[t]] quiet[ans[u]]不会出现相等情况无需处理平局如果题目允许相等则可约定取编号更小者只需把改为并调整比较目标。ans[i]的初始化不能省若某节点没有任何「更富」前驱入度为 0它永远不会被任何ans[t]更新必须靠初始值兜底保证答案就是它自己。入度递减与答案更新的顺序两步都要在遍历出边时执行二者互不干扰但入度减到 0 的判定必须用递减后的新值。七、仓库中的相关题解串联一个模板多种变形本题的「建图 入度 队列拓扑序」模板在仓库的「图论 拓扑排序」分类下有多道变形应用见 Index/图论 拓扑排序.md题目难度拓扑排序的用途207. 课程表中等判断 DAG出队节点数是否等于n用于检测环802. 找到最终的安全状态中等反向图 拓扑排序标记无法进入环的安全节点851. 喧闹和富有中等沿拓扑序做答案传递本文954. 二倍数对数组中等按值排序后线性构造避免建图1976. 到达目的地的方案数中等最短路 拓扑序计数2049. 统计最高分的节点数目中等树的遍历统计子树规模2127. 参加会议的最多员工数困难基环树 拓扑排序剥环2876. 有向图访问计数困难内向基环树 拓扑排序剑指 Offer II 114. 外星文字典困难字符间偏序建图 拓扑排序输出字典序剑指 Offer II 115. 重建序列中等拓扑序唯一性判定对比可见207只用拓扑排序的「出队计数」能力判环不做任何附加计算851则把拓扑序当作「保证依赖信息就绪」的执行顺序在出队过程中顺带完成答案的链式传递是「拓扑排序 DP 传递」的典型结合802展示了边方向需要翻转时的处理思路。把这几篇放在一起读就能形成对拓扑排序问题「建图方向选择、环检测、状态传递、反向图」四类考点的完整认知。八、小结LeetCode 851「喧闹和富有」是一道非常典型的「偏序关系 拓扑排序」综合题其核心方法论可提炼为识别偏序把「A 比 B 更有钱」这类比较关系抽象为有向边构造 DAG选择建图方式根据m与n²的数量关系判断稠密/稀疏决定用邻接矩阵还是邻接表跑拓扑序并同步传递初始化答案为自己零入度节点入队出队时一边递减邻点入度、一边用quiet[ans[t]]尝试优化ans[u]复杂度建图O(m)拓扑排序O(m n)整体O(m n)邻接表实现空间O(m n)。这套「拓扑排序 沿拓扑序做链式状态传递」的范式在 LogicStack-LeetCode 仓库的 图论 拓扑排序 分类下被反复使用是解决「依赖关系可达性 聚合查询」类问题的高频套路值得熟练掌握。赞分享教程文档【免费下载链接】LogicStack-LeetCode公众号「宫水三叶的刷题日记」刷穿 LeetCode 系列文章源码项目地址https://gitcode.com/gh_mirrors/lo/LogicStack-LeetCode点击查看免费下载相关推荐AlgoNote 算法题解LeetCode 0851「喧闹和富有」——用拓扑排序求解 DAG 上的最安静富人AlgoNote 算法题解LeetCode 0851「喧闹和富有」——用拓扑排序求解 DAG 上的最安静富人 本篇题解聚焦于 LeetCode 0851「喧闹教程文档知识库LogicStack-LeetCode 题解精讲LeetCode 15. 三数之和排序 双指针LogicStack LeetCode 题解精讲LeetCode 15. 三数之和排序 双指针 本指南以「宫水三叶的刷题日记」刷穿 LeetCode教程文档排序专题刷题指南LogicStack-LeetCode 排序算法题解精讲排序专题刷题指南LogicStack LeetCode 排序算法题解精讲 本文以「宫水三叶的刷题日记」刷穿 LeetCode 系列仓库中的 排序专题索引 ht教程文档上一篇免费本地OCR字幕提取完整指南用 video-subtitle-extractor 把视频硬字幕3分钟变成srt文件下一篇科研人的中文文献管理神器Jasminum插件3步告别手打元数据创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考