C++实现斐波那契堆:原理、代码与在图算法中的应用
1. 项目概述为什么是斐波那契堆如果你写过Dijkstra最短路径算法或者用过一些需要高性能优先队列的库你大概率接触过二叉堆。它简单、高效是入门数据结构的必修课。但当你需要频繁进行“降低某个元素的优先级”这个操作时二叉堆的O(log n)时间就显得笨拙了。这时一个听起来很数学、实现起来有点“妖”的数据结构就该登场了——斐波那契堆。斐波那契堆并不是一个日常开发中高频使用的数据结构但它却是算法理论中的一个明珠。它的核心价值在于其摊还时间复杂度上的优越性插入操作是O(1)合并两个堆是O(1)而降低关键字和删除最小元素虽然最坏情况是O(log n)但其摊还代价也是O(1)和O(log n)。这使得它在需要大量插入和降低关键字操作的图算法中如带负权边的Dijkstra算法、Prim最小生成树算法拥有理论上的最佳性能。我用C来实现它不仅仅是为了复现一个教科书上的结构。更重要的是通过亲手实现这个相对复杂的数据结构我们能深入理解“摊还分析”这一重要的算法分析思想掌握如何用指针和链表来组织一个“松散”但高效的结构并对C的面向对象、内存管理进行一次综合演练。你会发现它比红黑树或AVL树更有趣因为它牺牲了每次操作的最坏情况性能换来了大多数操作的平均高效这种设计哲学本身就值得玩味。2. 核心设计思路与结构拆解斐波那契堆的设计充满了“懒惰”和“优化滞后”的智慧。它不像二叉堆那样时刻保持严格的树形结构而是允许堆由多个子树在斐波那契堆中称为“根树”组成形成一个“根链表”。只有当必要的时候例如执行删除最小元素操作时它才会进行一轮“合并”操作来整理结构。2.1 节点结构设计一个斐波那契堆的节点是核心它需要比二叉堆节点携带更多的信息。在C中我们通常用一个结构体或类来定义。关键字段包括键值 (key)节点存储的数值用于比较优先级。度数 (degree)该节点的子节点数目。父指针 (parent)和子指针 (child)用于构成树形结构。左兄弟指针 (left)和右兄弟指针 (right)这是实现“根链表”和“子节点环形双向链表”的关键。所有根节点通过左右指针连成一个环形双向链表同一个父节点的所有子节点也通过左右指针连成一个环形双向链表。这种设计使得节点的插入、删除、链表合并都能在O(1)时间内完成。标记 (marked)这是一个用于优化“降低关键字”操作的关键布尔标志。它记录了该节点自成为另一个节点的子节点后是否已经失去过一个子节点。如果是那么在它失去第二个子节点时就需要进行“级联切断”操作以防止树变得过深。template typename T struct FibonacciNode { T key; // 键值 int degree; // 度数子节点数 FibonacciNodeT* parent; FibonacciNodeT* child; FibonacciNodeT* left; FibonacciNodeT* right; bool marked; // 标记位用于降低关键字操作 // 构造函数 FibonacciNode(T k) : key(k), degree(0), parent(nullptr), child(nullptr), left(this), right(this), marked(false) {} };使用环形双向链表是斐波那契堆实现O(1)插入和合并的秘诀。新节点可以轻松插入到根链表的任何位置而合并两个堆就是将它们两个根链表连接起来。2.2 堆的整体管理堆本身需要一个管理类它只需保存少量信息最小节点指针 (min_node)指向根链表中具有最小键值的节点。这是获取堆最小值的O(1)操作的基础。节点数量 (node_num)堆中节点的总数。template typename T class FibonacciHeap { private: FibonacciNodeT* min_node_; // 指向最小键值节点 int node_num_; // 堆中节点总数 // 一系列内部操作函数如合并、链接、切断等 void _link(FibonacciNodeT* y, FibonacciNodeT* x); void _consolidate(); void _cut(FibonacciNodeT* x, FibonacciNodeT* y); void _cascading_cut(FibonacciNodeT* y); public: FibonacciHeap() : min_node_(nullptr), node_num_(0) {} ~FibonacciHeap(); // 公开接口插入、获取最小值、合并、降低关键字、删除最小值 FibonacciNodeT* insert(T key); T get_minimum(); void merge(FibonacciHeapT other); void decrease_key(FibonacciNodeT* x, T new_key); T extract_min(); };初始时堆是空的min_node_为nullptr。这种简洁的管理结构正是其高效的基础。3. 关键操作的原理解析与C实现理解了结构我们来看最核心的几个操作是如何利用这种“松散”结构达到摊还低复杂度的。3.1 插入与合并O(1)时间的奥秘插入一个新节点x其键值为k过程简单得令人惊讶创建一个新节点x。如果堆为空则min_node_指向x根链表就是x自身构成的环。如果堆不为空则将x插入到根链表中通常插入到min_node_的左侧因为这是环形链表插入是O(1)操作。比较x的键值与min_node_的键值如果x更小则更新min_node_为x。增加node_num_。template typename T FibonacciNodeT* FibonacciHeapT::insert(T key) { FibonacciNodeT* new_node new FibonacciNodeT(key); // 如果堆为空 if (min_node_ nullptr) { min_node_ new_node; } else { // 将新节点插入到根链表中插入到min_node_左侧 new_node-left min_node_-left; new_node-right min_node_; min_node_-left-right new_node; min_node_-left new_node; // 更新最小节点指针 if (new_node-key min_node_-key) { min_node_ new_node; } } node_num_; return new_node; // 返回节点指针为后续decrease_key操作提供句柄 }合并两个斐波那契堆H1和H2更简单本质上就是拼接两个环形根链表并更新min_node_和node_num_。这个操作也是O(1)。这里有一个非常重要的细节合并后原来两个堆的节点就共享了同一个根链表这意味着你不能简单地去delete另一个堆对象否则会导致悬垂指针。在实际应用中合并操作往往意味着其中一个堆将被“吞噬”并不再被单独使用。3.2 抽取最小值与整理摊还分析的核心体现extract_min()是最复杂的操作它包含了斐波那契堆“延迟整理”思想的集中体现。它的摊还时间复杂度是O(log n)。移除最小节点将min_node_从根链表中移除。将其子节点提升为根将min_node_的所有子节点的parent指针置为nullptr并将它们整个子节点环形链表合并到根链表中。执行合并操作这是最关键的一步_consolidate()。它的目标是整理根链表确保根链表中任意两个节点的度数子节点数都不同。这通过一个“度数数组”来实现数组下标对应度数。遍历根链表中的每一个节点x。查看度数数组degree_array中下标为x-degree的位置是否为空。如果为空则将x放入该位置。如果不为空则说明存在另一个度数相同的节点y。将键值较大的节点链接为键值较小的节点的子节点通过_link函数。链接后度数增加然后继续用新的x现在是链接后的根去检查度数数组直到找到空位。_link操作会将y从根链表移除使其成为x的子节点并更新x的度数和y的parent指针同时将y插入到x的子节点环形链表中。重建根链表与寻找新的最小节点遍历_consolidate后留在度数数组中的所有节点它们现在度数都不同将它们重新链接成一个新的根链表并在此过程中找到新的min_node_。template typename T void FibonacciHeapT::_consolidate() { // 计算最大可能的度数斐波那契堆的性质保证了度数在O(log n)范围内 int max_degree static_castint(log2(node_num_)) 1; std::vectorFibonacciNodeT* degree_array(max_degree 1, nullptr); // 我们需要遍历根链表但由于在遍历过程中会修改链表结构所以先收集所有根节点 std::vectorFibonacciNodeT* root_list; FibonacciNodeT* current min_node_; if (current) { do { root_list.push_back(current); current current-right; } while (current ! min_node_); } for (FibonacciNodeT* x : root_list) { int d x-degree; // 当度数数组d位置不为空时需要合并相同度数的树 while (degree_array[d] ! nullptr) { FibonacciNodeT* y degree_array[d]; // 确保x是键值较小的根 if (x-key y-key) { std::swap(x, y); } _link(y, x); // 将y链接为x的子节点 degree_array[d] nullptr; // 清空该位置 d; // x的度数增加了 } degree_array[d] x; // 将合并后的树放入新的度数位置 } // 重建根链表并找到最小节点 min_node_ nullptr; for (FibonacciNodeT* node : degree_array) { if (node ! nullptr) { // 将node加入新的根链表 if (min_node_ nullptr) { min_node_ node; node-left node-right node; // 形成单节点环 } else { // 插入到根链表 node-left min_node_-left; node-right min_node_; min_node_-left-right node; min_node_-left node; // 更新最小节点 if (node-key min_node_-key) { min_node_ node; } } } } }_consolidate操作保证了根链表中树的数目最多为O(log n)这是extract_min操作复杂度为O(log n)的关键。3.3 降低关键字与级联切断维持平衡的艺术decrease_key是斐波那契堆的另一个王牌操作摊还时间复杂度为O(1)。它需要一个指向目标节点的指针这正是insert操作返回指针的原因。将节点x的键值减小为new_key。如果新键值不小于其父节点的键值且x是根节点无父节点则无需调整。否则如果减小键值后破坏了最小堆性质即x的键值小于其父节点y的键值则需要将x从y的子节点链表中切断并提升为根节点。执行_cut(x, y)。关键步骤级联切断。节点y失去了一个子节点将其marked标记为true。如果y已经被标记过marked true说明它已经失去过一个子节点那么此时需要递归地将y也从其父节点切断并提升为根然后检查y的父节点依此类推。这个过程就是_cascading_cut(y)。template typename T void FibonacciHeapT::decrease_key(FibonacciNodeT* x, T new_key) { if (new_key x-key) { // 通常应该抛出异常或报错这里简单返回 return; } x-key new_key; FibonacciNodeT* y x-parent; // 如果违反了堆性质且x不是根 if (y ! nullptr x-key y-key) { _cut(x, y); _cascading_cut(y); } // 更新最小节点因为x可能变成了根且比当前最小节点还小 if (x-key min_node_-key) { min_node_ x; } } template typename T void FibonacciHeapT::_cut(FibonacciNodeT* x, FibonacciNodeT* y) { // 将x从y的子节点链表中移除 if (x-right x) { // x是y的唯一子节点 y-child nullptr; } else { x-left-right x-right; x-right-left x-left; if (y-child x) { y-child x-right; // 更新y的子指针 } } y-degree--; // y的度数减1 // 将x添加到根链表中 x-left min_node_-left; x-right min_node_; min_node_-left-right x; min_node_-left x; x-parent nullptr; x-marked false; // 新提升的根节点标记为false } template typename T void FibonacciHeapT::_cascading_cut(FibonacciNodeT* y) { FibonacciNodeT* z y-parent; if (z ! nullptr) { if (!y-marked) { y-marked true; // 第一次失去子节点标记 } else { // 已经标记过说明这是第二次失去子节点需要切断y _cut(y, z); _cascading_cut(z); // 递归检查z } } }级联切断保证了任何节点除根节点外最多失去一个子节点后就会被提升到根层从而确保了树的“瘦高”程度被有效控制这是实现decrease_key摊还O(1)复杂度的关键。4. 内存管理与析构实现斐波那契堆包含大量动态分配的节点手动管理内存是C实现中必须谨慎处理的部分。析构函数需要递归地释放所有节点。template typename T FibonacciHeapT::~FibonacciHeap() { if (min_node_ ! nullptr) { _delete_all_nodes(min_node_); } } template typename T void FibonacciHeapT::_delete_all_nodes(FibonacciNodeT* start) { if (start nullptr) return; FibonacciNodeT* current start; do { FibonacciNodeT* next current-right; // 先保存右兄弟 if (current-child ! nullptr) { _delete_all_nodes(current-child); // 递归删除子节点 } delete current; // 删除当前节点 current next; } while (current ! start); // 环形链表回到起点结束 }这里使用递归删除因为每个节点的子节点链表也是环形的。需要特别注意环形链表的遍历终止条件。5. 实战应用改进Dijkstra算法理论再美也需要实践检验。斐波那契堆最经典的应用场景就是加速单源最短路径算法——Dijkstra算法。在标准的基于二叉堆的Dijkstra实现中每次从优先队列中取出距离最小的顶点u然后对其邻接顶点v进行“松弛”操作如果dist[u] weight(u, v) dist[v]则更新dist[v]并将新的(dist[v], v)对插入或更新到优先队列中。在二叉堆中更新操作即降低关键字需要先找到元素然后调整复杂度是O(log n)。而使用斐波那契堆我们可以这样做将(dist[v], v)对封装成一个斐波那契堆节点。insert操作是O(1)。当需要更新dist[v]时我们持有该节点指针直接调用decrease_key摊还代价O(1)。extract_min操作是O(log n)。对于稀疏图边数E远小于顶点数V的平方Dijkstra算法中总共进行V次extract_min和最多E次decrease_key。因此使用二叉堆的复杂度是O((VE) log V)而使用斐波那契堆的摊还复杂度是O(V log V E)。当图非常稀疏时例如EO(V)斐波那契堆有显著的理论优势。一个简单的代码框架示意void dijkstra_fibheap(Graph g, int src) { FibonacciHeappairint, int heap; // 键值距离 数据顶点ID vectorFibonacciNodepairint, int* node_map(g.V, nullptr); vectorint dist(g.V, INF); dist[src] 0; node_map[src] heap.insert({0, src}); while (!heap.is_empty()) { auto [d, u] heap.extract_min(); // 取出当前距离最小的顶点 if (d ! dist[u]) continue; // 懒惰删除如果取出的不是最新距离则丢弃 for (auto [v, w] : g.adj[u]) { int new_dist dist[u] w; if (new_dist dist[v]) { dist[v] new_dist; if (node_map[v] nullptr) { // 第一次访问插入 node_map[v] heap.insert({new_dist, v}); } else { // 已存在降低关键字 heap.decrease_key(node_map[v], {new_dist, v}); } } } } // 输出dist数组... }注意上述代码是概念性示意。实际实现中decrease_key需要节点指针并且键值比较需要正确处理pair。此外由于extract_min后节点被删除node_map中对应的指针会失效需要置空或采用“懒惰删除”策略即节点被取出时检查其存储的距离是否与当前dist数组一致不一致则丢弃。这是实现中的一个重要技巧。6. 调试心得与常见陷阱实现斐波那契堆的过程就是与指针和环形链表搏斗的过程。以下是我在实现和调试中踩过的坑和总结的经验环形链表的插入/删除这是最容易出错的地方。在修改left和right指针时顺序非常重要。一个安全的模式是// 将new_node插入到existing_node的左侧 new_node-left existing_node-left; new_node-right existing_node; existing_node-left-right new_node; // 务必先修改原左节点的右指针 existing_node-left new_node;删除节点x时x-left-right x-right; x-right-left x-left; // 如果需要清空x的左右指针避免野指针 x-left x-right x; // 或 nullptr取决于上下文_consolidate中的遍历陷阱在_consolidate函数中我们遍历根链表并修改它通过_link将节点移出。直接使用while(current ! min_node_)这样的循环会因链表结构改变而出错。安全的做法是像前面代码那样先将当前根链表的所有节点指针保存到一个临时数组或向量中然后遍历这个容器。decrease_key的指针有效性decrease_key操作依赖于一个有效的节点指针。这个指针必须在节点存在于堆中时使用。一旦节点被extract_min删除其指针就失效了。因此在上面的Dijkstra示例中需要配合一个node_map来管理指针并在节点被提取后置空该指针或者采用“懒惰删除”策略。标记位的重置在_cut操作中当一个节点被提升为根时必须将其marked设置为false。这是斐波那契堆定义的一部分因为只有非根节点才可能被标记。忘记重置会导致级联切断逻辑错误。度数的更新在_link操作中将y链接为x的子节点后x的度数要加1同时y的parent要指向x。这些细节缺一不可。内存泄漏检查由于结构复杂务必使用Valgrind或AddressSanitizer等工具进行内存泄漏检查。确保析构函数能正确遍历并释放所有节点包括根链表和所有子节点链表。测试策略不要一上来就测试复杂图算法。先编写单元测试测试插入和get_min。测试多次插入后extract_min的顺序是否正确。测试decrease_key操作特别是触发级联切断的情况。测试合并两个堆。使用随机生成的连续操作序列进行压力测试并与标准库的std::priority_queue二叉堆对比结果是否一致。实现一个可用的斐波那契堆大约需要300-500行C代码。它不会让你的程序立刻飞起来因为其常数因子较大在小数据量下不如二叉堆。但这个过程对于深入理解数据结构、指针操作和摊还分析是一次绝佳的锻炼。当你看到它在大规模稀疏图的最短路径计算中展现出理论优势时那种成就感是对所有调试痛苦的最佳回报。

相关新闻

STM32F103核心开发板硬件设计与开发环境搭建指南

STM32F103核心开发板硬件设计与开发环境搭建指南

1. STM32F103核心开发板概述STM32F103系列作为意法半导体(ST)推出的经典Cortex-M3内核微控制器,在工业控制、消费电子和物联网领域已有十多年的广泛应用历史。这款被称为"蓝精灵"的芯片以其出色的性价比和丰富的片上资源,成为嵌入式开发者入门…

2026/10/18 11:35:48 阅读更多 →
嵌入式开发实战:EMIFA中断、NAND Flash ECC与GPIO寄存器深度解析

嵌入式开发实战:EMIFA中断、NAND Flash ECC与GPIO寄存器深度解析

1. 项目概述:从寄存器手册到嵌入式实战的深度解析 如果你是一名嵌入式软件或驱动开发工程师,面对动辄上千页的芯片技术参考手册,尤其是其中那些密密麻麻的寄存器描述表格,是否曾感到无从下手?手册告诉你每个位域是干什…

2026/10/18 11:34:44 阅读更多 →
Codex:AI编程搭档的核心原理与应用实践

Codex:AI编程搭档的核心原理与应用实践

1. Codex:AI编程搭档的革命性突破OpenAI推出的Codex标志着人工智能辅助编程进入全新阶段。这个基于云端运行的软件工程智能体,本质上是一个能够理解自然语言指令并生成高质量代码的AI系统。与传统的代码补全工具不同,Codex具备完整的任务执行…

2026/10/18 12:40:51 阅读更多 →

最新新闻

HTTPS加密过程详解:从TLS握手到密钥协商与排障实战

HTTPS加密过程详解:从TLS握手到密钥协商与排障实战

前阵子某公司同事拿了个抓包文件来找我,说某个内部系统的接口访问时好时坏,偶尔卡住十几秒然后报错。我打开抓包软件扫了一眼,发现大量TLS握手中的重传报文,再往下看,原来他压根没把HTTPS加密过程当回事——以为是“握…

2026/10/19 13:01:18 阅读更多 →
Agentic AI落地社会价值:提示工程架构师如何拆解信任与责任难题

Agentic AI落地社会价值:提示工程架构师如何拆解信任与责任难题

Agentic AI的社会价值最近被反复讨论,但真正落到实际业务场景里,落地障碍往往不是模型能力,而是隐藏在提示工程里的一系列细节。作为一个长期和Agent打交道的提示工程架构师,我越来越清楚一件事:我们写的每一段Prompt&…

2026/10/19 13:01:18 阅读更多 →
售货机管理系统设计:设备状态、库存预警与远程指令三重闭环

售货机管理系统设计:设备状态、库存预警与远程指令三重闭环

简介:本资源是一套基于RuoYi框架(SpringBootVue前后端分离)开发的售货机管理系统,面向Java全栈初学者、Web应用开发者及后台管理平台实践者,聚焦智能硬件运营场景中的商品、库存、设备、支付与销售数据等核心管理需求。…

2026/10/19 13:01:18 阅读更多 →
CMSSW离线数据处理框架:从LHC原始数据到物理分析的全链路实践

CMSSW离线数据处理框架:从LHC原始数据到物理分析的全链路实践

简介:CMSSW(Compact Muon Solenoid Software)是CERN大型强子对撞机CMS实验核心的开源离线数据分析框架,面向粒子物理研究人员、高能物理方向研究生及计算科学开发者,用于处理海量探测器数据、执行事件重建、物理对象识…

2026/10/19 13:01:17 阅读更多 →
ROS 2中VS Code无法识别rclcpp?从环境变量到CMake配置全排查

ROS 2中VS Code无法识别rclcpp?从环境变量到CMake配置全排查

前阵子在技术群里看到一位刚转到ROS 2的同行提问&#xff1a;新建的rclcpp项目在VS Code里打开&#xff0c;#include <rclcpp/rclcpp.hpp>这一行直接画了红色波浪线&#xff0c;代码提示完全失灵&#xff0c;尝试编译又报出find_package找不到rclcpp包的错误。这个问题我…

2026/10/19 13:01:17 阅读更多 →
C标准库源码深度剖析:从printf到malloc的底层实现与移植指南

C标准库源码深度剖析:从printf到malloc的底层实现与移植指南

简介&#xff1a;这份资源是C标准库的完整源代码集合&#xff0c;面向希望深入理解C语言底层实现、提升系统级编程能力的开发者与学习者。它涵盖输入输出、字符串处理、内存管理、数学运算、时间日期、文件系统接口等核心模块&#xff0c;适合在阅读源码、优化性能或进行底层开…

2026/10/19 13:00:15 阅读更多 →

日新闻

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

简介&#xff1a;本资源为电源技术方向的学术研究文档&#xff0c;聚焦介质阻挡放电型臭氧发生器电源的频率跟踪型PWM控制策略&#xff0c;面向电力电子、高压电源及臭氧应用领域的科研人员与工程技术人员。针对负载呈容性、等效电容随外加电压升高而增大、谐振频率随之漂移的难…

2026/10/19 0:00:47 阅读更多 →
Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

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

2026/10/19 0:00:47 阅读更多 →
HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

【免费下载链接】HundredCV-Chat 项目地址&#xff1a; https://ai.gitcode.com/hf_mirrors/Jax-dan/HundredCV-Chat 点击查看 免费下载 HundredCV-Chat 是一个由 100 位青年简历数据开发而来的中文多轮对话数据集&#xff0c;共包含 24,750 组日常闲聊对话和 4,871 个话题标签…

2026/10/19 0:00:47 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/19 10:02:04 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/19 1:30:52 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/18 12:16:16 阅读更多 →