多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

力扣双周赛 76 D 题 Maximum Score of a Node Sequence:枚举边 + 邻接点前三大预处理的 Go 解法剖析

力扣双周赛 76 D 题 Maximum Score of a Node Sequence:枚举边 + 邻接点前三大预处理的 Go 解法剖析 科学计算【免费下载链接】codeforces-go算法竞赛模板库 by 灵茶山艾府 项目地址https://gitcode.com/GitHub_Trending/co/codeforces-go点击查看免费下载本篇文章以 codeforces-go 仓库中 双周赛 76 D 题题解 为主体完整讲解力扣 2242「节点序列的最高得分」Maximum Score of a Node Sequence的经典做法枚举每条边作为序列中间的桥配合每个节点只保留分数前三大的邻居的预处理技巧将暴力枚举的复杂度从 $O(m \cdot \deg^2)$ 降到 $O(m)$并给出 Python / Go / C / Java 四种语言的完整可运行实现。读完本文你将掌握这一类序列由相邻边拼接而成、需要从邻居中选最优候选点的图题通用套路也能学会如何利用仓库中 d_test.go 与 d.txt 驱动数据对拍验证。一、题目回顾与核心观察题目要求在一个无向图节点数为 $n$边数为 $m$中找一条由 4 个互不相同节点组成的路径 $a-x-y-b$使得四个节点的scores之和最大若不存在这样的路径返回 $-1$。直接暴力枚举 $x$ 与 $y$ 的所有可能组合即枚举所有边再在 $x$、$y$ 的邻居中枚举 $a$、$b$复杂度为 $O(m \cdot \deg(x) \cdot \deg(y))$在最坏情况下如稠密图不可接受。题解给出了四个递进式提示是本做法的灵魂提示 1设序列为 $a-x-y-b$$-$ 表示边枚举edges中的每条边作为序列中的 $x-y$。这样一来问题转化为固定中间边 $(x,y)$在 $x$ 的邻居中选 $a$在 $y$ 的邻居中选 $b$。提示 2与 $x$ 相邻的点中选择分数最大、且不同于 $y$ 和 $b$ 的点作为 $a$与 $y$ 相邻的点中选择分数最大、且不同于 $x$ 和 $a$ 的点作为 $b$。提示 3由于 $a$ 只需要与 $y$、$b$ 不同最多排除两个候选每个节点仅需保留分数最大的三个邻居$a$ 必定落在这三个点中。同理 $b$ 也只需考虑 $y$ 的前三大邻居。提示 4因此在枚举edges之前先对所有节点预处理出分数前三大邻居即可。这一保留 Top-k 个候选的思路来源于选择候选时排除条件最多只有两个排除 $x/y$ 与排除另一个端点Top-3 足以保证最优候选不被排除掉。二、算法步骤与正确性论证2.1 建图邻居按分数存储对每条边 $(x, y)$在g[x]中记录(分数, 邻居节点号)形式的邻居信息g[y]同理。这一建图方式在仓库的 d.go 实现 中体现为type nb struct{ to, s int } g : make([][]nb, len(scores)) for _, e : range edges { x, y : e[0], e[1] g[x] append(g[x], nb{y, scores[y]}) g[y] append(g[y], nb{x, scores[x]}) }2.2 预处理每个节点只保留分数前三大的邻居对每个节点的邻居列表按分数降序排序截断到前 3 个。对应 d.gofor i, vs : range g { sort.Slice(vs, func(i, j int) bool { return vs[i].s vs[j].s }) if len(vs) 3 { vs vs[:3] } g[i] vs }为什么 3 个足够固定中间边 $(x, y)$ 后选择 $a$ 的排除条件是$a \neq y$ 且 $a \neq b$其中 $b$ 至多是一个具体节点。也就是说$x$ 的邻居中被排除的至多 2 个$y$ 与 $b$因此按分数排序后前 3 大邻居中必然存在合法的 $a$若有邻居。$b$ 同理。因此截断到 Top-3 不丢最优解却能把后续枚举量压缩到常数。2.3 枚举每条边并检查合法性对每条边 $(x, y)$从g[x]的前 3 个候选中挑 $a$从g[y]的前 3 个候选中挑 $b$两两组合至多 $3 \times 3 9$ 对校验 $a \neq y$、$b \neq x$、$a \neq b$即四个节点互不相同更新答案。对应 d.goans : -1 for _, e : range edges { x, y : e[0], e[1] for _, p : range g[x] { for _, q : range g[y] { if p.to ! y q.to ! x p.to ! q.to { ans max(ans, p.sscores[x]scores[y]q.s) } } } } return ans注意这里每次组合都用scores[x] scores[y]补齐中间两个节点p.s、q.s分别是 $a$、$b$ 的分数ans初始化为 $-1$若始终没有合法组合则返回 $-1$正确覆盖不存在合法路径的情形。2.4 复杂度分析建图$O(m)$预处理每个节点的邻居排序总代价为 $O(\sum \deg(v) \log \deg(v)) \le O(m \log m)$若使用 C 的nth_element题解 C 版本或求前三大可做到 $O(m)$枚举边每条边只检查常数对最多 9 对$O(m)$。整体复杂度为 $O(m \log m)$排序实现或 $O(m)$nth_element实现空间 $O(n m)$远优于暴力枚举。三、四种语言完整实现原题解全量呈现题解 d.md 给出了四种语言的等价实现核心逻辑完全一致差异仅在排序/取前三大与配对枚举的写法上。以下全部保留可直接提交Python3nlargest取前三大class Solution: def maximumScore(self, scores: List[int], edges: List[List[int]]) - int: g [[] for _ in range(len(scores))] for x, y in edges: g[x].append((scores[y], y)) g[y].append((scores[x], x)) for i, vs in enumerate(g): g[i] nlargest(3, vs) # 下面这一段可以简写成一行为了可读性这里就不写了 ans -1 for x, y in edges: for (score_a, a), (score_b, b) in product(g[x], g[y]): if y ! a ! b ! x: ans max(ans, score_a scores[x] scores[y] score_b) return ans其中y ! a ! b ! x是链式比较等价于(y ! a) and (a ! b) and (b ! x)与 Go 版的三个条件一一对应。Go排序 截断即仓库 d.go 的解法func maximumScore(scores []int, edges [][]int) int { type nb struct{ to, s int } g : make([][]nb, len(scores)) for _, e : range edges { x, y : e[0], e[1] g[x] append(g[x], nb{y, scores[y]}) g[y] append(g[y], nb{x, scores[x]}) } for i, vs : range g { sort.Slice(vs, func(i, j int) bool { return vs[i].s vs[j].s }) if len(vs) 3 { vs vs[:3] } g[i] vs } ans : -1 for _, e : range edges { x, y : e[0], e[1] for _, p : range g[x] { for _, q : range g[y] { if p.to ! y q.to ! x p.to ! q.to { ans max(ans, p.sscores[x]scores[y]q.s) } } } } return ans } func max(a, b int) int { if b a { return b }; return a }Cnth_element线性求前三大class Solution { public: int maximumScore(vectorint scores, vectorvectorint edges) { int n scores.size(); vectorvectorpairint, int g(n); for (auto e : edges) { int x e[0], y e[1]; g[x].emplace_back(-scores[y], y); g[y].emplace_back(-scores[x], x); } for (auto vs : g) if (vs.size() 3) { nth_element(vs.begin(), vs.begin() 3, vs.end()); vs.resize(3); } int ans -1; for (auto e : edges) { int x e[0], y e[1]; for (auto [score_a, a] : g[x]) for (auto [score_b, b] : g[y]) if (a ! y b ! x a ! b) ans max(ans, -score_a scores[x] scores[y] - score_b); } return ans; } };这里用-scores[y]存储负分数使nth_element按从小到大排前 3 个即分数最大的 3 个从而避免自定义比较器。JavaCollections.sortsubList截断class Solution { public int maximumScore(int[] scores, int[][] edges) { var n scores.length; Listint[][] g new ArrayList[n]; for (var i 0; i n; i) g[i] new ArrayList(); for (var e : edges) { int x e[0], y e[1]; g[x].add(new int[]{scores[y], y}); g[y].add(new int[]{scores[x], x}); } for (var i 0; i n; i) if (g[i].size() 3) { Collections.sort(g[i], (a, b) - (b[0] - a[0])); g[i] new ArrayList(g[i].subList(0, 3)); } var ans -1; for (var e : edges) { int x e[0], y e[1]; for (var p : g[x]) { var a p[1]; for (var q : g[y]) { var b q[1]; if (a ! y b ! x a ! b) ans Math.max(ans, p[0] scores[x] scores[y] q[0]); } } } return ans; } }四种实现的判定条件完全等价$a \neq y$、$b \neq x$、$a \neq b$即序列四节点互不相同。四、仓库实测测试驱动与用例格式该题在仓库中配备了完整的本地测试链路可通过数据文件驱动对拍验证。4.1 测试入口leetcode/biweekly/76/d/d_test.go 是 copypasta 模板自动生成的测试文件核心只有一句话func Test_d(t *testing.T) { targetCaseNum : 0 // -1 if err : testutil.RunLeetCodeFuncWithFile(t, maximumScore, d.txt, targetCaseNum); err ! nil { t.Fatal(err) } }它调用 testutil/leetcode.go 中的RunLeetCodeFuncWithFile从同目录下的d.txt读取测试数据。该框架通过反射reflect.TypeOf(f)解析待测函数maximumScore的入参个数与返回值个数按每fNumIn fNumOut行一组数据切分用例并逐条用t.Run执行、比对输出targetCaseNum为 0 表示跑全部用例为负数表示从末尾倒数定位单个用例。测试失败时会打印输入与期望/实际输出便于快速定位错误。4.2 测试数据格式leetcode/biweekly/76/d/d.txt 采用每 3 行一组的文本格式第 1 行是scores数组第 2 行是edges数组第 3 行是期望答案[5,2,9,8,4] [[0,1],[1,2],[2,3],[0,2],[1,3],[2,4]] 24 [9,20,6,4,11,12] [[0,3],[5,3],[2,4],[1,3]] -1两个用例恰好覆盖了本题的两类关键场景存在合法路径scores [5,2,9,8,4]6 条边组成的图中最优序列取到 24选取分数最大的 9、8、4 与一个 5 或 2 组成路径答案 24不存在合法路径图由[0,3]、[5,3]、[2,4]、[1,3]四条边构成节点 0、1、2、5 都只与 3 相连4 只与 2 相连无法形成 4 个节点的路径正确答案为 $-1$验证了初始化ans -1并保持返回的分支正确性。可以推断d.txt中留空的分隔行仅用于排版trimSpaceAndEmptyLine见 testutil/helper.go在读取时会自动剔除空行与首尾空白因此格式对空行不敏感。4.3 运行方式在仓库根目录执行go test ./leetcode/biweekly/76/d/ -v即可运行该题的全部测试。targetCaseNum改为1或-1时框架只验证指定用例单用例通过后会自动继续跑全部用例见 leetcode.go 中targetCaseNum 0时递归全量测试的逻辑。这套题解 源码 数据文件三位一体的结构是仓库中每个力扣题目的标准组织方式同一模式也适用于weekly、season等目录。五、方法论总结与举一反三本题解法的价值不止于这一道题它沉淀了一个可复用的图论枚举范式固定桥枚举当目标结构路径、环、三元组等可以被一条边唯一确定时先枚举边把问题降维成从两侧邻居中各取一个点Top-k 候选剪枝候选点需要排除的禁用点数量有限本题至多 2 个时只需保留分数最大的 $k 禁用数 1$ 个候选即可保证最优解不丢失将内层枚举从 $O(\deg^2)$ 压缩到常数去重条件前置在枚举时用简单的节点号判等a ! y b ! x a ! b完成四节点互异校验比事后去重更直接高效。如果读者需要进一步研究该仓库中其他图论与枚举相关模板如拓扑、最短路、树上路径统计等可在 copypasta 目录 下的 graph、graph_tree、graph_flow 等源文件中找到大量可复用实现而本题的题解原文与四语言实现则完整保留在 leetcode/biweekly/76/d/d.md配合 d.go 与 d_test.go 即可就地验证、按需修改后提交。赞分享科学计算【免费下载链接】codeforces-go算法竞赛模板库 by 灵茶山艾府 项目地址https://gitcode.com/GitHub_Trending/co/codeforces-go点击查看免费下载相关推荐codeforces-go 力扣双周赛 158 题解精讲Maximize Subarray GCD Score 的暴力枚举与 LogTrick 双解法codeforces go 力扣双周赛 158 题解精讲Maximize Subarray GCD Score 的暴力枚举与 LogTrick 双解法 导读科学计算枚举木板对与双哈希表力扣双周赛 188「最宽栅栏」O(n²) 题解剖析codeforces-go 实战枚举木板对与双哈希表力扣双周赛 188「最宽栅栏」O n² 题解剖析codeforces go 实战 导读 本文围绕 codeforces go 仓库中科学计算力扣双周赛 172 题解Maximum Score After Binary Swaps——最大堆 / 最小堆维护「动态前 k 大」的两种遍历方向力扣双周赛 172 题解Maximum Score After Binary Swaps——最大堆 / 最小堆维护「动态前 k 大」的两种遍历方向 本篇技术指科学计算上一篇FanControl控制器识别问题深度解决方案从诊断到预防的完整指南下一篇终极指南FanControl风扇控制软件从零到精通打造完美静音散热系统创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表