2026-01-14 23:33:00
我们遇到的 Harbor 的另外一个问题是 image 的下载瓶颈。在容灾的时候,我们需要在短时间内启动几万个容器。Harbor 这里就成了瓶颈,抛开所有的数据库和文件系统的瓶颈不说,网络这里就需要 Tib 级别的带宽。这是不现实的。
用户构建的 image 质量参差不齐,大于 10GiB 的 image 比比皆是,尽管这些 image 都有很大的优化空间,但是期望所有的用户都按照构建 image 的最佳实践1进行优化,也是不现实的。
于是问题就成了:如何才能够大量的 worker 节点迅速扩容上万的容器?
此外,在平时,遇到工作日多个系统发布的时候,或者整点的时候运行定时任务(定时任务每次都会下载 image),harbor 的压力也非常大,经常满载运行。如果能解决这个问题,平时的负载问题也可以缓解。
之前介绍过 Spegel 的下载方案2,Spegel 的想法很好,基本思想是,先去其他已经存在这个 image 的机器上去下载,如果找不到,再 fallback 到 Harbor 下载。可惜的是,Spegel 把 P2P 下载和 P2P 服务发现混为一谈了,导致服务发现的性能极低。
什么意思呢?去其他的机器上下载 image,需要的一个信息是:哪一个机器有这个 image?这就是下载源的服务发现。这个服务发现和 P2P 本质上没有什么关系,用什么都可以。P2P 技术解决的是下载上的瓶颈,只有能有一个方法记录每一个机器上现在都有什么 image 就可以,用 Etcd 也可以,不一定也得用 P2P 形式的。
但是 Spegel 这里是用 P2P (libp2p)做的服务发现。在我看来这是完全没有必要的。我们在实际的部署中遇到的问题有:
在实际的部署中,Spegel 的缓存命中率在 25% 左右。
当然,使用 P2P 做服务发现也是有好处的,好处就是部署简单,不需要额外的存储依赖。只不过这个好处和它带来的问题相比就微不足道了。
我觉得如果解决服务发现的问题,使用 P2P 下载的方法,是可以解决资源的瓶颈问题的。
所以设计的方案是:使用中心化的,高性能的服务发现,去中心化的 P2P image 下载。
原本 Spegel 的代码,服务发现层是独立的模块,所以我从 fork 它的代码来添加一个新的服务发现方式开始。但是随着修改,发现问题越来越多,比如 subscribe containerd events 的时候没有正确 defer 关闭,没处理好 containerd 重启的情况,等等,最后开始了一个独立的仓库,叫 Piccolo。代码4依然是开源的,但是还没时间写文档和注释,这篇博客先写一下原理。
项目主要分两部分:
Pi – 安装在每台机器上的 daemonset,负责:
Piccolo Server 是服务发现的源,全局只有一组(几个实例就够用了),提供 3 个接口:

部署之后,97% 的下载请求可以在 Pi 完成而不必请求 Harbor。

从这个监控可以看出,随着 Piccolo 的发布,Harbor 的流量骤降。而且每个小时的峰值流量也几乎没有了。
Pi 部署在每一个机器上,使用的资源也非常少,平均 CPU 用了一个 core 的不到 1%,可以忽略不计。平均内存用了 22M 左右,也可以忽略不计。
Piccolo 为集群提供了大约 8Tib 的下载带宽,没有消耗额外的资源,几乎是免费的 8Tib 带宽。

Piccolo server 方面,性能也很高,一台 8C8G 的 instance 足以支撑 5 万个 Pi (实际的物理机 worker 节点)。秘诀就是注重细节的性能优化。
比如全量同步的资源消耗较大,一起部署的机器会定时发送 keeplive,通过对这些定时执行的 API 加随机偏移,可以保持这些 API 的频率几乎是均匀的,解决了资源的峰值问题。

服务发现的核心,是用一张 MySQL 表,存储了 blob 和 IP 的对应关系。服务发现请求主要是通过这张表的查询完成的。Piccolo 支持把不同的 group(同一个 group 的 Pi 可以互相发现,不同的 group 的 Pi 不可以互相发现。其实这个功能也可以通过部署多个 Piccolo Server 来实现)放到不同的数据库中,加上索引优化(极致的索引优化,所有的查询都是 index-only 的),每一个库 2千万的数据,请求在 200 QPS,耗时在 10ms 以内,已经足够使用了。
Piccolo server 在服务发现接口返回的时候,会根据请求者的 IP 地址,把所有的资源拥有者的 IP,根据和请求者的 IP 相似度(距离)排序,返回。这样 Pi 在下载的时候,会从距离和它最近的邻居开始尝试,这样可以最大程度减少跨网络设备的带宽流量。
所有的 API 接口都有重试和指数时间退让,这样在大规模部署的时候,可以分散一些请求,不至于大家一起失败。
在高可用方面,由于 Piccolo server 是无状态的,所以部署多个实例即可。在预防未知的 bug 上,系统的每一个阶段都是可以降级的:
︎
︎
︎
︎
2026-01-12 22:49:00
我和欣都很喜欢看电影,我们一起看了很多电影。我最喜欢的导演是韦斯·安得森,现在最喜欢的电影是《布达佩斯大饭店》,看了很多遍。欣最喜欢的电影是《沙丘》,也看了很多遍。科幻电影是我们共同喜欢的类型。
《布达佩斯大饭店》每一帧都很美,故事性也很强,台词简洁但是话又很多。和其他的电影很不一样,后来我才知道,这属于「艺术电影」的范畴。导演的个人特色太鲜明,有一次我在看《腓尼基计划》,欣看了一眼,就问,「这又是那个怪导演拍的?」
《沙丘》作为老牌科幻,背景世界构建的宏大而又符合逻辑,导演 Denis Villeneuve 实力很强,把这么难拍的电影也拍的像艺术一样。每一帧也都很美。
我们在一起看了很多电影。用过各种各样的设备和方法一起看。
高铁从上海到山东需要 5 个小时,看电影是最好的消磨时间的方法了。问题是,一直没有找到比较好的可以两个人一起看电影的方案。即两个人一起看,但是各自用自己的降噪耳机。
在网上找到了一个 Mac 可以使用的方法1,原理是用 Audio MIDI 创建一个新的输出设备,实际输出的物理设备是 2 个耳机,步骤如下:
- First, on your Mac, go to System Preferences > Bluetooth. Turn it on and pair the two pair of AirPods with your Mac. Now only one pair of AirPods can be connected.
Now open Finder and click on Applications > Utilities > Audio MIDI Setup.- Look for the plus sign and click on it to create multi-output device.
- Check the box next to the two pairs of AirPods and check the Drift Correction box next to the second pair of AirPods.
- Click on System Preference > Sound, followed by Multi-Output Device.
- Once done, the audio of your Mac will be sent to both pairs of AirPods. So this how to connect multiple AirPods to Mac.
有的时候不想拿出来 Mac,或者有时候没有带,就用下面这个万能的办法,不过仅适用于提前下载好的电影:
这样也算一起看了。
近几年大部分时间都是在家里一起看,买过 HBO,Amazon Prime,Disney+,Netflix 等等,在电视上看了不少电视和剧,简单也方便,体验很不错。《克拉克森的农场》,《Severance》,《浴血黑帮》,《大西洋帝国》,《For All Mankind》,等等。
去年买了一个新的电视,型号是三星的 S95F,可能是最近几年买过的最满意的电子产品了,OLED 屏幕非常惊艳,这一款还有一个不反光的特性,白天看也不用拉窗帘。最近又发现一个惊艳的功能。
有天家里有人睡觉了,我戴着耳机在客厅里看电视,欣过来问我,你看电视怎么没有声音?我说用的耳机。欣惊讶到,这电视居然能连接蓝牙耳机。我觉得这是很正常的功能。
于是她拿出来自己的耳机要和我一起看,我说不能连接两个耳机。她说可以,这是正常的功能。
我觉得肯定不行,因为之前用的所有的设备在不 hack 的情况下都不支持同时输出到 2 个蓝牙设备(实际上,iOS 在 2019 年好像就支持 audio share 了)。但是决定还是用事实证明。于是我打开蓝牙,连接上她的耳机。预期是之前连接的我的耳机会断开。结果两个耳机都连接成功。
然后打开声音输出,预期是只能选择一个耳机。结果电视自动弹出来对话框,问是否要切换到 multiconnect 功能?选择「是」之后,两个耳机都完美地有了声音。

后来一想,这个功能确实挺符合直觉并且实用的。现在的问题是,为什么之前那么多设备反而不支持?
2026-01-10 12:15:54
最近读了《征服C指针》,是一本好书。日本人写的计算机技术书籍像娓娓道来的技术博客那样,假设读者没有(或者有很少)相关的背景知识,围绕一个主题,除了介绍干巴巴的知识,还会谈自己的经验和理解,对其他的书和论点做点评,指出一些常见的谬误,这样读者可以更容易理解书里讲的东西。如果只有干巴巴的知识,那么去读维基百科就好了。但是维基百科(无论中文还是英文)的说明一般晦涩难懂,因为百科的首要目标是准确,消除歧义,而不是易于理解。而好的技术书里面会旁征博引,运用举例和比喻,用多的篇幅详细说明难懂的部分,加上作者的经验让读者知道哪些内容是经常使用的,哪些是晦涩又不常用的,哪些是可以辅以技巧理解的。这是我理解的优秀的技术书籍。比如,《流畅的 Python》就是这样一本好书,这本书不光介绍 Python 的编程知识,还有对 Python 语言设计的点评,和其他编程语言的对比,作者自己的理解和评价,读完之后对于整个编程概念的理解都会有提升。
这本书其实就不只是 C 语言的指针,还设计内存分配,C 的语法和编译器,CPU 等知识。从初学者到老手都可以从中有所收获。
另外书中对于有些概念可以给出确定的定义,而不是给出模棱两可的答案。如果永远模棱两可,那么永远都不会错,但是这种内容读起来也是浪费时间。一位得高望重的星际2游戏解说曾经说过:「专业解说要敢于下判断。」
以下是一些笔记。每一段引用都是书中的原文,引用如果不是连续的,就来自于不同的段落。非引用的格式是我的评论。
在C语言中,记录指针指向何种类型是只到编译器为止的,到了运行的时候就已经没有相关信息了。在运行时,指针的值就只是单纯的地址而已。“要从这个地址里取出哪种类型的值”这一信息只残留在编译器生成的机器码中。无论是在指针的值中,还是在指针指向的变量的内存空间中,都没有类型的信息。因此,如果把指向int的指针转换成了void*,就不可能再知道它原来是指向 int 的了。
指针的类型,主要是为了告诉编译器信息。
int hoge = 5;
void *hoge_p;
hoge_p = &hoge; <-- 不会报错
printf("%d\n", *hoge_p); /* 输出hoge_p所指向的内容 */会报错如下:
warning: dereferencing `void *' pointer
error: invalid use of void expression只告知了内存上的地址,却没有告知那里保存的是什么类型的数据,当然无法读取。
改成下面这样可以运行:
5: printf("%d\n", *(int*)hoge_p); /* 将hoge_p转换成int */这正是指针运算的特征。在 C 语言中,对指针加1 后,其地址就增加该指针所指向的类型的长度。示例程序中的 hoge_p 是指向 int 的指针,而在我的环境中 int 的长度是 4,所以对地址来说,加 1 就是前进 4 字节,加 3 就是前进 12 字节。
对指针+1,地址移动的单位是「指针所指向的类型的长度」,这个也是编译器计算的,这是指针需要类型的主要原因。
由于空指针可以确保与任何非空指针进行比较都不相等,所以经常作为返回指针的函数发生异常时的返回值使用。
例如我工作的地方位于日本名古屋市某栋大楼的 5 楼,某人爬一层楼需要 10秒,那么从地面上到 5 楼需要花费多少秒?50 秒?很遗憾,正确答案是 40秒。想必大家在中学都学过等差数列,等差数列的第 n 项等于“首项 + 公差× (n –1)”。每个都要减 1,真麻烦……此外,“1900 年代”并不是 19 世纪,它的一大半属于 20 世纪。更加复杂的情况是,2000 年不属于 21 世纪,而属于 20 世纪。这些问题分别可通过以下方式回避。把大楼里与地面等高的那层计作第 0 层把数列的首项计作第 0 项把最初的世纪计作 0 世纪,把公历最初的年份计作 0 年这种“差 1 错误”的问题在编程中经常发生。因此,普遍认为在一般情况下如果以 0 为基准编号,那么通常(并不是所有)能回避这类问题。
延伸阅读:为什么要“包含头不包含尾”?
要点
p[i] 是 *(p + i) 的简便写法。
下标运算符 [] 只有它原本的意义,与数组毫无关系。
要点
【比上面的要点更重要的要点】
但是,千万别写成那样。
在 get_word() 中使用下标运算符访问 buf 的内容,会让人觉得从 main() 传递过来的就是buf 数组。然而,这是个错觉,从 main() 传递过来的说到底只是指向 buf 的初始元素的指针。
函数传递只能传递指针而不能传递数组。即使数组和指针不同,但是在函数传递的时候,数组也会转换为指针,指向数组开头的元素。
在 C 语言中,参数全部都是通过值传递的。
即便是像全局变量那样在函数外部定义的变量,一旦加上 static,其作用域就只限定在当前源文件内。指定为 static 的变量(函数)对于其他源文件是不可见的(函数也是一样的)。
另外,对于函数(非 static 限定)和全局变量,只要名称相同,即便位于不同的源文件中,也会被当作相同的对象处理。
因此,根据操作系统及CPU的不同,需要规定不同的调用方法,这就叫作调用约定(calling convention)。本书中说明的调用方法是在x86系列处理器中被称为cdecl的调用约定。该方法中所有的参数都通过压栈的方式进行传递。
malloc() 会遍历链表,搜寻空块,若该块大小足够,就将其分割出来,做成使用中的块,并向应用程序返回紧邻管理区域的下一个地址。free() 会改写管理区域的标志,将该块置为空块,如果上下有空块,就顺便将它们合并成一个块。这是为了防止块碎片化。 这种操作称为 coalescing。当没有足够大的空块满足 malloc() 的要求时,就向操作系统请求(在 UNIX 中需要通过 brk()系统调用)扩充内存空间。
补充
Valgrind正如前面多次提到的那样,与动态内存分配相关的Bug往往出现在距离它被发现的位置很远的地方,因此调试非常困难。在Linux上可以使用Valgrind工具追踪这类Bug。Valgrind工具用于检测对malloc()分配的内存空间越界读写、忘记free()(内存泄漏)或者对同一块内存空间多次free()这类问题。
如果运气好,标准库 glibc 也可以为我们检测出这个问题。
调用 malloc() 之后必定写上相应的free() 是一种谨慎的编程风格。程序员就应该小心翼翼地将 malloc()和 free() 对应起来。“因为调用了 exit(),所以就没必要free() 了”的想法是不负责任的偷工减料行为,是不良的编程风格。不管怎么说,程序员也是人,人就是这么一种在可能犯错的地方必定会犯错的生物。可是,“必须 free() 派”却偏要大肆宣扬无论如何都要“谨慎地”编码,这种论调其实是于事无补的。
我认为,“谨慎地”编码并没有什么了不起的,那些能够尽可能地回避“麻烦事”的人才是优秀的程序员。在我心中,理想的程序员是下面这样的:在能够安全地偷懒的地方尽可能地偷懒,并且尽可能地依靠工具而不是肉眼来进行检查,但在无论如何都需要人工处理麻烦的事情时,会在心中坚定地起誓“总有一天要将它自动化”。
这是我最喜欢的一段话。Python 的初学者写的代码,会在所有的函数入口都写上 try-catch,称之为防御性编程。我觉得这么做的人肯定会有这样的疑惑:「怎么这么麻烦?」对于这个疑惑,有两类人,一类是认为「这么做一定有道理,作为程序员我们要不辞劳苦地做好工作。」另一类人认为「一定有更方便的方式」。
正如图 2-17 所示,填充有时会被放到结构体的末尾。因为在创建结构体数组时,填充是必要的。在将 sizeof 运算符应用到这样的结构体上时,返回的是包含末尾填充部分大小的长度。将结果和元素个数相乘,就可以获取数组整体的长度。
——有关结构体的对齐
小端与大端到底哪一种更好呢?这个话题经常引起人们的争论,此处就不再深入讨论了。它们各有各的优点。人类在用纸和笔做加法时也会从低位开始相加,所以对 CPU 来说,或许采用小端的方式更轻松一些,而在人类看来,大端的方式或许更容易理解。
C 语言的声明要用英语阅读
我们可以遵循以下步骤解释 C 语言声明。
先看标识符(变量名或函数名)。
从贴近标识符的地方开始,按照如下优先级解释派生类型(指针、数组、函数):
①用于整合声明的括号;
②表示数组的 []、表示函数的 ();
③表示指针的 *。
完成对派生类型的解释之后,通过 of、to 或returning 连接句子。添加类型修饰符(位于左侧,比如 int、double)。如果不擅长英语,可以用中文解释。
能正确地阅读 C 指针的声明,函数参数中有关指针的声明,已经 sizeof 中的声明,是读此书最大的收获了。
像这样可以确定长度的类型,在标准中被称为对象类型(object type)。然而,函数类型不是对象类型。C 语言中不存在函数类型的变量,因而我们无法(也没必要)确定其长度。我们说过,数组类型是由若干个派生源类型排列而成的类型。因此,数组类型的总长度为:
派生源类型的长度×数组的元素个数
但是,由于函数类型的长度无法确定,所以也就无法从函数类型派生出数组类型。也就是说,无法创造出“函数的数组”这种类型。但是,可以生成“指向函数的指针”这一类型。只是指向函数类型的指针是不能进行指针运算的,因为我们无法确定指针指向的类型的长度。
当表达式代表的是某处的存储空间时,该表达式就称为左值。与此相对,当表达式仅代表值时,该表达式称为右值。表达式中有时存在左值,有时不存在左值。例如,变量名是左值,而 5 这样的常量、1 +hoge 这样使用运算符的表达式就不是左值。
当作为 sizeof 运算符的操作数时
在以“sizeof 表达式”的形式使用 sizeof 运算符时,由于这里的操作数是表达式,所以即使是对数组使用 sizeof,数组也会被解读为指针,从而只能获取指针的长度——或许有人是这样认为的,但其实在数组作为 sizeof 运算符的操作数的情况下,将数组解读为指针这一规则是无效的,在这种情况下返回的是数组整体的长度。
总之,关于指向函数的指针的 C 语言的语法是比较混乱的。造成这种混乱的罪魁祸首就是“函数在表达式中会被解读为指向函数的指针”这一意图不明(难不成是为了与数组保持一致?)的规则。
在表达式中,数组会被解读为指向该数组初始元素的指针,因此代码可以写成下面这样。
int *p;
int array[10];
p = array; <-- 将指向array[0]的指针赋给p但是,反过来写成下面这样就不行。array = p;
数组和指针截然不同。
“不要误会我对 goto 语句持有任何教条主义的执念。我只是担忧,很多人把这件事给神化了,甚至认为仅凭某个编程技巧或某个简单的编程原则,就能解决编程语言的概念问题!”
2025-12-30 22:47:42
在新加坡的第五年整。
今年和欣好像一直在找房子。之前住在湖畔的一个 HDB,房东决定要卖房子,所以我们不续租了,从湖畔的 HDB 搬到了碧山的 Condo。住了不到一年,年底又搬了一次家。第一次住完全没有家具的房子,又花了时间去买沙发,床,桌子,组装家具,搬家,年底要处理的事情太多太多。
买了一台大电视,三星的 S95F,被惊艳到了,OLED 屏幕,还有不反光的特性,效果非常好。于是年底的假期大部分时间都和欣窝在沙发上看电视,买了 HBO,把过去看过的很多电影又看了一遍。把《权力的游戏》也从头开始看了一遍。
今年玩的游戏不多,为了准备玩《GTA 6》买了一台电脑放在客厅,但是这游戏居然又跳票了。缝合怪《潜水员戴夫》拿到了完美通关,《荒野大镖客2》玩到了 50% 左右,然后和欣一起玩《双人成行》,还没有通关。
又买了一台电子书设备和微信读书会员,看了莫言的《檀香刑》,《酒国》以及一些杂文集,一些余华和刘震云的小说,以及其他一些专业书。
学会了一个新的技能:双拼。不过感觉刚刚够得到之前全拼的速度,再用一段时间速度应该更快。
旅行。年前我和欣带爸妈来新加坡和普吉岛旅行。普吉岛去过 3 次了,但是一次博客都没有写过,拖着拖着就有一些原因不想写了。9 月份去上海「旅行」了一趟。在上海生活了这么多年,其实并没有作为游客去过一些地方,这次回去,体验了脱口秀(笑得肚子疼),逛了南京路(和当初上学的时候很不一样了,傣妹居然还活着,其他的店大部分都换了),看了一个剧。年底准备了关西的旅行,但是因为其他事情取消了。
今年工作上的难度越来越高。我比较擅长用技术来解决问题,但是今年公司的管理风格向重视流程转移,试图通过流程上的规范来提高整体的可用性,导致审批越来越多,流程越来越复杂,一个简单的 API 调用需要花费之前数倍的成本来实现。
另一方面,在金融方面的发展带来了更多的合规要求,从而带来了巨量的运维工作。可高层在「降本增效」方面的努力没有停止的迹象,仅仅靠运维工作是没有「绩效」的,如果要拿到比较好的绩效以及晋升,就需要拿出时间去做亮点项目。这就造成了另一个矛盾点,也造成了另一部分人的离职。身边的同事越来越少。
在《凤凰项目》一书中,安全部门的主管发现项目不需要安全团队就可以通过一项审计,意识到自己的工作可有可无,在酒吧喝得酩酊大醉。他问运维部门的主管:「我们就真的对你们一点帮助也没有吗?」运维主管尽管很想安慰他,但是又不想说谎,只好说:「对不起,一点也没有。」
每当安全团队要我们把一个毫无敏感信息的 API 用最高的安全等级来要求的时候,我就会想起来这一段。
尽管如此,今年还是做了不少值得骄傲的事情:
在产品方面,不再负责 Service Mesh 项目了。我从加入公司就开始维护这个项目,经历了几十个版本,也经历过它造成的(我加入之后)公司最大的故障,经历了几代开发团队变迁。开发团队的同事技术方面无可挑剔,产品在近几年没有出过重大事故,也支持了业务的需求。不足在产品的易用性上,配置过于复杂,难以理解,学习成本高。整体架构需要改变,现在是 daemonset 部署模式,难以实现资源隔离和审计。
产品运维的工作中心转到了 SDN 上面,也是我感兴趣的方面。SDN 也有很多问题:易用性和控制面的可用性太差;组件太多过于复杂;需要硬件兼容软件而不是软件兼容硬件,等等。这些都是需要解决的问题。
明年的工作会尝试一下结合 eBPF 来做架构上的可观测性,通过图数据库自动整理软件的依赖。
就写这么多吧,明年还是继续在网络的领域耕耘,学习和分享更多的知识。
其他的年终总结列表:
2025-12-27 14:13:38
我的姥姥今年去世了,没有痛苦。我们之前都预料到了这一天的到来,大家在五月份都陆续去看望了她,大家也都知道这是告别。
小姨十年前告诉我,姥姥诊断出了肺癌。但是年纪太大,家里商量之后,决定不治疗,让她安享晚年,想吃什么就吃什么,想做什么就做什么。姥姥晚年喜欢抽烟,吃肉。每次去看望姥姥,我都带两条烟,十斤肉,或者红包。
这几年每一次去看姥姥,她下炕的次数越来越少,头发越来越花白,和她说话的声音需要越来越大。
姥姥的去世,我没有为她感到伤心,只为我以后再也见不到她了而伤心。至少她的晚年可以抽烟吃肉,没有把时间都花在了 ICU 里面。《长命百岁》里面提到, 现代医学专注于延长人类的寿命,但是这些寿命的质量也同样重要。人固有一死,我希望自己离开世界的时候也可以这样。
十年之后,姨们过年聚在一起,终于觉得当初的诊断不对,应该不是肺癌。但是也没有必要再去纠结了,管它是什么吧。
姥姥裹小脚,有六个女儿,一个儿子,重男轻女,我上高中的时候,过年的压岁钱姥姥就只给我和哥哥了,姐姐和妹妹没有。到了晚年,姥姥和姥爷俩人依然住在那诸城县边上的一个村子里,在那里生活了近一个世纪。姥姥和姥爷的感情是很多人都羡慕的。
姥姥的一生几乎都住在那几间房子里,没有去过太远的地方,可能没有出过省,没有吃过什么山珍海味。但也许她的一生是幸福的。这又让我思考起来那个这一年我不断思考的问题:「怎么度过这一生才是值得的呢?」
一路走好,姥姥。
2025-12-12 23:53:21
[剧透警告]
今年又来玩了 Advant of Code1,最近看 C 语言的代码比较多,就用尽量用 C 语言做。

第 9 和和第 10 天最难,尤其是第 10 天,看着挺简单,实际是从来没有听说过的整数线性规划问题2。
第 11 天一看要用 dict,放弃 C 了。用 Python 很快就过了,一个搜索加路径缓存即可。但是如何用 C 写出来完全没有思路,最后问 chatGPT,想起来还有邻接表这种巧妙的表示方法。
第 12 天最搞笑,以前是做 part1 顺利,做 part2 的时候时间爆炸。day 12 做 sample 的时候就感觉时间要爆炸了。加了几个剪枝,part1 居然过了。我想完了,part2 肯定是让我找所有的可能的情况,一定会爆炸。结果点开 part——就通关了。今年居然只有 12 天?那距离圣诞节还有 13 天呢,我做啥?

fuglede 的代码太优雅了:https://github.com/fuglede/adventofcode/tree/master/2025,不愧是做量化的。