Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

总览

「解谜末班车」自壬寅年底(2023)起办,每年一期,定于除夕前两日,为期 36 小时。

「解谜末班车」每期布下七道题目。并非每一期都要求玩家解出全部的题目。事实上,通关的判据只有一个:通过第七关

本站提供历年谜题解答,以供参考。

壬寅年概览

时间:2023-01-19 20:00:00 至 2023-01-21 08:00:00

这是我第一年出解谜游戏,仓促出了几道题,结果很快就被杀穿了。

第一位通关者是 Midoria7,在游戏开始 104 分钟后通关。

当时我的设想是「可以不择手段地通关(只要别交流答案)」,所以没有设计诸如防跳关、防脚本作答等措施,结果真的有人跳关了(对着 url 扫描然后跳到了 List),也真的有人写脚本撞答案了(Systematic 有人拿脚本从 0 刷到了 400000,但没想到答案是个负数。)。

当时我还没有保留资料的意识,所以很多与此次解谜有关的信息(包括部分题目的内容)都遗失了,还望读者见谅。

门对千棵竹

门对千棵竹

解析

「门对千棵竹」是一则对联,出自相声《解学士》,下联为「家藏万卷书」。

本题的 URL 是 www.cpphusky.xyz/MenDuiQianKeZhu,可以想见,应当把 URL 改成 www.cpphusky.xyz/JiaCangWanJuanShu,这样就可以进入第二关。

家藏万卷书

在本关中,每个玩家每次刷新时,有 的概率刷出两本书——这就意味着可以通关;否则只能刷出一本书,无法通关。

解析

的初始值为 每次增加 。这个概率看起来很低,但只要大家合力刷新,其实很快就可以刷到比较高的值了。

唯独有一点需要注意:不要刷新得太快,把本来可以通关的页面给刷没了。

Systematic

解析

本题是由五个化学元素符号组成的算式,所以要考虑把元素符号转换成序数来计算。

其中前四个符号只需查元素周期表即可得到;但第五个 需要通过系统命名法(Systematic element name)推算得到。于是

Blank

Blck frtlzr wll vltlz whn t s gry; gry frtlzr wll vltlz whn t s blck.

解析

本题基于一个观点:对于有一定英语语感的人来说,仅仅通过辅音字母的部分,就可以还原出整个句子。

所以本题的意图在于让玩家做完形填空,补全缺失的元音字母。(但是因为题目意思不够明显,导致不少人误会)

顺带一提,因为 gray 也可以写作 grey,所以对应位置的 a 换作 e 也是可以的。

答案是:aeiieioaiieeiiaaeiieioaiieeiiaaeiieioaiieeiieeeiieioaiieeiia

Morse

本关提供了一段摩斯电码音频,玩家需要听译得出其中暗含的信息。

解析

直接听译当然是可以的,不过可能好好训练一下自己的耳朵;当然,也可以直接上网找一个摩斯电码译码器,把音频发上去,解得 MMDCLIV

这是一段罗马数字,对应的阿拉伯数字为 2654

List

8 2 1 4 8 0 8 6 5 1 3 2 8 2 3 0 6 6 4 7 0 9 3 8 4 4 6 0 9 5 5 0 5 8 2 2 3 1 7 2 5 3 5 9 4 0 8 1 2 8

请填接下来的 10 位。

解析

本关提供的50个数字分别是圆周率的第 101-150 位。直接百度,或者在 OEIS 中搜都可以看出来。

那么接下来要填的数字想必就是圆周率的 151-160 位:4817114502,有无空格皆可。

Final

潜藏在谜题背后的事物,请在 305856724 中寻找。

解析

本关的输入框是个幌子,填什么都是错的。

题目中提供的信息 305856724 个QQ群号,加群就算通关了。

(本群已解散,请勿搜索和加群。如误加其他群导致出现任何问题,出题人概不负责)

癸卯年概览

时间:2024-02-07 19:15:00 至 2024-02-09 07:15:00

这是第二年出解谜了。吸取了前一次的教训,今年我调高了难度,并且不再允许扫路径和跳关了。

第一位通关者是 Midoria7,在游戏开始 972 分钟后通关。

在规定时间内,共有 12 人通过了本次解谜。

Fishing

我们怀疑你是人类,请完成机器人验证。 captcha

解析

如果不细看的话可能会把这关当做真的人类验证码,然后填个 0.3 就过关了。

但是如果你真的细看了的话……嘿嘿,如果填了 0.30000000000000004,那就会被判断为机器人然后喜提封禁 60 秒,这就是“钓鱼”的真缔。

至于为什么是 0.30000000000000004,就请读者看 这段内容 吧。

Capitalist

每分钟为一个回合。当玩家停留在该页面时,他每分钟可以获得1个单位的商品(只有一种商品)。

有一个中央市场,可以按照一定的价格收购或销售商品。市场价会随着全体用户的购买/出售行为而发生变化。总体的规律是,如果市场的收购量多于销售量,那么下一回合的价格会走低;如果销售量多于收购量,那么下一回合的价格会走高。

你完全有可能通过大量买入来哄抬价格,然后紧接着卖出同样多的商品,赚取差价。不过……你并不知道其它玩家的行为,而且他们的买卖操作也会影响市场价格。你可以看到从开始时间为止的全部市场价格变化图表。

解析

既然大额交易会导致大幅亏损,那么玩家就必须在「靠金钱通关」和「靠商品通关」这两个思路中进行选择。在这两个思路之间不停跳变是完全不可取的。如果要靠商品通关的话,思路就应该是要么「仅生产」,要么少量低买高卖,总之别亏商品数量;如果要靠金钱通关的话,那就应该多卖出,尤其是在市场价格高的时候多卖。

市场价 0.625 是一个关键节点,因为对于通关来说,25 金钱和 40 商品是等价的。如果市场价远高于 0.625,那么把商品都换成钱就要更赚;如果远低于 0.625,那么把钱都换成商品就更赚。

对于前期玩家来说,市场价是 1.2,这个时候大家很容易陷入到「全部换钱」的思维陷阱中。但是如果玩家有研究过这个规则的话就会发现:所有玩家都只能生产商品而没有人可以生产金钱!也就是说,在交易过程中,不可避免地会产生这样的现象:大家都在生产商品,然后卖出;但没有人可以生产金钱,所以市场价的下跌趋势是必然的结果!

当然这是长期趋势。对于很多玩家来说,短期的赚差价还是更重要一些;所以但凡参与交易,肯定会在高价时卖出而低价时买入。这样就会形成一涨一跌的局面,大家就会找到规律,这关也就没什么意思了。所以我本人出马,开始凭借大量初始资源,通过大额买入/卖出来操纵市场!

capitalist

不过总体来说,这关不太可能卡关,大家用或长或短的时间都可以过。而且这次解谜的战线拖得挺长,差几分钟到十几分钟也不是什么大问题。

RGB

初始位置在 puzzle.cpphusky.xyz/red

在本关我们只能看到一个色块,而通关所需要信息以背景色隐写在了色块中央。

本关已给提示:去年解法,故技重施。

解析

如果你在电脑上用鼠标划一下,就很容易发现 red 页面中的隐写内容 1F。手机也有可能看出隐写内容,你在屏幕中央长按一会儿就有可能选中对应文字。有的大神直接 Ctrl+A 一键发现隐写内容,确实可行。另外还可以按 F12 进行网页元素审查,一样能发现这个隐写内容。

接下来可以想一想,1F 和红色能让你联想到什么?有可能是色号。但倘若是色号的话,单纯一个红色 1F 是不能说明什么信息的,还应该有绿色和蓝色。

那么问题来了,绿色和蓝色应该到哪里去找呢?答案就在 url 中。原本的网址 url 是 https://puzzle.cpphusky.xyz/red,如果改成 https://puzzle.cpphusky.xyz/greenhttps://puzzle.cpphusky.xyz/blue 会怎么样呢?

哎,改完了之后发现还各有一个页面,其上的信息分别是 1E 和 33.这样一个色号就拼成了。

如果你游玩过一款创新立体 3D 节奏游戏《韵律源点 Arcaea》的话,你就会知道 #1f1e33 是由曲师 Camellia 制作的一首曲目(同时还是游戏主角对立的代表色)

但是,我们还始终没看到答题区域。那么最后尝试把这个色号放到 url 中,也就是 https://puzzle.cpphusky.xyz/1f1e33(这里我们设计了容错机制,大小写均可以),就会出现又一个页面。这里有四个答题框,终于有了答题区域。该页面还有一条隐写信息 C,M,Y,K,这是 CMYK 颜色表示法。下面的答题区总共有四个,看起来它们之间有着一一对应的关系。

随便百度一个“RGB 转 CMYK”,把 #1F1E33 这个色号转换成 CMYK,就会得到39, 41, 0, 80这个结果,填进去就通关了。

关于提示:去年的门对千棵竹就是改 url 的题目,今年又来一道改 url 的题,所以说是故技重施。

Calculator

【数字】

本关已给提示:珠。

解析

一开始很多人都猜上半部分三进制,下半部分六进制,但是试了好多种解释方式之后就会发现,都不对。

所以在起初的时候这关的通关人数廖廖无几,但是给了提示之后 10 分钟内就有 8 位玩家做出来了,可见这一个字的信息量之大。

没错,这关就是象形的算盘(老式 calculator)!我们可以根据图形中的算珠分析出对应的数字来,它们分别是 682, 829, 7,加起来就是 1518,填进去就对了!

(据反馈,有的玩家在解谜的时候想到了算盘的可能性,但“感觉不是”就换思路了,提示公布之后才意识到。所以说一定要敢于尝试啊!不敢尝试的话那么 Art 那关也过不了。)

Introduction

【成语】

本关已给提示:线索要到题外找;如果不需要对应 呢;韵

解析

(这关难度比我想象中高,因为大家好像都没有第一时间注意到分号。)

这关上来只能看到一个图,所以我们应该首先从这个图中获取尽可能多的信息。这里我们看到的是三条向量四个点,而答案要求一个成语。绝大多数成语都是四个字的,所以我们可以推测这四个点与四个字之间有对应关系。

(有通关玩家说,下意识想的到是看图猜成语,但是试了各种可能性都不对)

这个图中还呈现出了什么信息呢?就是四个点的坐标。我们可以横向与 15 相比较,根据比例测出四个点的横坐标;纵向与 40 相比较,根据比例测出四个点的纵坐标(15 和 40 是用在这里的,相当于比例尺)。这样一来我们就得到这四个点的坐标:

(11,14), (2,4), (5,11), (10,38)

接下来问题来了,我们要去哪里找字呢?第一个线索给出之后,各位应该可以想到本题的标题 Introduction。这个词可能意味着引言,或者介绍;而且对应的内容出现在题目之外。

这个时候我们起码要把这个网站本身的信息搜罗一遍,然后再去想其它的可能性吧。然后我们就会在「关于」页面中找到 「cppHusky 的自我介绍」

看上去大概率是这段内容了,那么怎么把这四个坐标对应到这个文段中的特定文字呢?

这时候大家好像普遍在思考如何把这段内容排成 的样子,但是最后会发现无论如何也做不到。

后来给了第二个提示说,你未必需要排成 嘛。然后确实就有两位大神发现解法了;第二天我醒了之后发现还是只有两个人通关,所以我又给了第三个提示:韵。

如果你念一遍这段自我介绍的话就会发现……这段内容朗朗上口,因为它各部分都是押韵的。突破点就在这里。我们可以按照韵脚(当初我考虑到玩家可能注意不到押韵,还特地在不同韵脚部分间加了分号),把这段内容分成以下若干韵部:

  1. 偷懒起晚胡侃自满卖惨造反借钱不还胡搅蛮缠天方夜谭狡诈奸馋推波助澜五毒俱全大错不犯小错不断外强中干反攻倒算人穷志短思维迟缓好高骛远杀鸡取卵遇事不服管吃饭不付款
  2. 胡思乱想无话不讲行事草莽烧杀偷抢举止异常丧心病狂顺手牵羊狗急跳墙空手套白狼完事就躲藏
  3. 思想懈怠泼皮耍赖吃里扒外四方为害明里放债暗里使坏
  4. 发如蓬蒿眼如熊猫嘴如弯刀背如年糕腹如沙包腿如高跷做事不过脑花钱伸手讨太平盛世挖墙脚大难临头拔腿跑
  5. 平日无事很悠闲一出急事就失联小组讨论不发言问卷表单都不填一催就说在酣眠再催就说等明年
  6. 油嘴滑舌骗人唾手可得口若悬河画饼不拘一格天天翘课出门只顾吃喝玩乐十恶不赦百里之内声名显赫
  7. 虚与委蛇囤货居奇众叛亲离本性难移指东打西心口不一狗眼看人低仗势把人欺皮球飞来一脚踢不管三七二十一
  8. 取巧钻营独断专行下手无情变卦不停致使鸡犬不宁
  9. 为人蝇营狗苟经商专欺童叟说话祸出其口做事四处掣肘
  10. 乘人之危使人万念俱灰卷钱不归让你哑巴吃亏讲话似是而非黑锅从来不背任务不做积成堆死线一到人人催
  11. 趁火打劫破坏和谐表面顽劣内心胆怯
  12. 好为人师无畏无知落井下石随处碰瓷无的放矢寡廉鲜耻无所事事玩物丧志
  13. 装聋作哑弄虚作假装疯卖傻指鹿为马因小失大冥顽不化对手一来我就怕对手一走我就骂
  14. 一领奖励挥金如土一领任务装穷叫苦一到饭点如狼似虎一到开会六神无主

接下来,我们找到第 11 韵部第 14 字「心」,第 2 韵部第 4 字「想」(很多人看到这就直接填「心想事成」就过了),第 5 韵部第 11 字「事」,第 10 韵部第 38 字「成」,这样就解决本关了。

Palindrome

【七言绝句】

解析

(我本来以为本关会有点难度的——就算不难,起码也会拖点时间;但是没想到大家解决的速度有点快于我的预期了。后来一问才知道,大家是直接对照模板来填词的,根本没考虑那么多……)

Palindrome 是回文的意思。所以想必很多玩家都在研究怎么用回文,然后会发现无论怎么回文,写出来的诗都极不通顺,不像正常的诗(当然也不会通关)。(甚至有个大神在这关卡了快一天,最终错失奖金)

百度一下「回文诗」(第一名当时的做法是搜「圆形十四个字诗歌」,也可),然后很快就会发现这些东西……

这不就是和本题的图很像吗?所以有的人看懂了这类解法之后就直接照着模板填然后过关了!

(其实我就应该把这个圆圈转一转,因为预期当中读者应该要学点诗词格律常识才能做出这题的……)

绝句的基本格律要求是:四句一绝,二、四两句必须押韵,第三句不押韵,第一句可押韵或不押韵。如果读者知道这个,应该先找韵脚,然后再绕圈推演整首诗。这才是预期解法。

如果你懂的更多,还可以按照绝句的平仄格律来分析,它属于「平平仄仄仄平平」(平起首句入韵)式。当然,本题不需要这么深的知识;有则更好。

至于用韵的标准……很幸运,这道题的诗既可以用平水韵解,又可以用新韵解,这两种标准都是没问题的。

(至于诗本身……我水平不怎么样,想了半年也想不出一首更好的,只能写到看起来不尬的境界,各位多多包涵……)

答案为:

阔空清夜有繁星,夜有繁星伴月明。

伴月明灯光入海,灯光入海阔空清。

Art

原题内容如下,读者可以自行复制到记事本中打开。

''''''''''`";!i<~~~~<il;"'''''''''''''''`;i!"'''''^Iii"`Ii!"`I>!"^I>i"^I>!"`''''^l>!^'''''^l>!""l>!^^l>l^'''''''''''''''^:li<~+++~>iI,`'''''''''''''''''`;>+______________+i:`''''''''''':_--<`''''I_--~;--->l--->!---~;]]]i`'''`l-]->'''''i-]->i]]]>!]]]l`'''''''''''"!+-]------------_<I`''''''''''''^i+____________-_-_--__+l`'''''''''`;!l"`''''`;il"`;il"`;il"^;il^`;il^`'`'`^Iil^'''''^I>I^^Iil`^I>l^'''''''''';~---------___-------__>,''''''''`I+_____~I,^`''```^,!+____-+:''''''''">++I`''''''''''''''"~_+I:<__I''''',+_+I:~_+;''''':+_+;''''',+-+:''''''''"+]----->,"``'''``";<_-____i^'''`'^i_____>"`''''`'''''''`,<_____!`'''''':_-->`'''''''''''''';---i!_]]<'''''l]-->!-]->'''''!-]]i`''''l]]]l`''''''I_]---+;`'''''''''''''^l_____<,'''^i____+"`''''``,;;"``'''``;+_--_!`'''''`^,"`''''''''''''''''^""``^,"`````''^"^`'^"^```````^:"`'```'`^"^`''''''I_]-]-!`'''''`";I:^''''''^>____~"'`l____<`''''"<_____-__!`'''',+---+;''''',+--i`'''''''''''''''''''I_-->:---!`''''''''';-]-l!-]-!l-]]!'''''''''',~]--_I'''''I_------_~:''''`l____>^^____~"`''`l____----____:'''';_--->"'''`"~--!`''''''''''''''''''':+--!,_--I`''''''''':_--I;-]_lI_]];'''''''''`!]--]i''''"+---------__>^'''`i____:;____l`''':+_________-__+"'''`~---+:''''`,l;^`"l;^''''''''''`,l;^`,l;```'''',l;`'''''`:l;``:!:``:l:``:l:`''''"<]---"'''`<----------___l`''`:____il____I`''`l___-__-____-_+;''''>---_;'''':_--<I---<"'''''''''I--->!---~`''''!]]]>`'''`l]]]i>]]]>i]]]>!]]]l`'''"~----`''',~---------____i^''`,+___<I____l`''`;_____--___--_+:'''`~---+:''''^!~~:`i+<;`'''''''''`>+<:"i+~,'''''^<+<:'''''">+<,"<+<,,>+<^"<+<"''''"~----^'''^~---------____!`''',____>"____<^'''^i__-__-______l`''',_---<"''''^!~<,`''''''''''''''''''''''''''''''`'''"i+<"''''''''''''''`">+>"''''^i--]-I'''';+------_____<,'''`!____I`i____!`'''`I~___-___-~:''''^<---_l`'''`:_--<``''''''''''''''''''''''''''''''''`l-]]>'''''''''''''''l]]-l`'''':+---+,''''">_----___+!^'''':____~^'"~____i`''''`,Ii>>!;"`''''"<---->^'''''`,I;^''``'''''''```'''^`''`^`''```''''''`:!I`'```''`^`''`^`'`;!;`'''''`l_---+:'''''^;!i>il:`'''''I____+:`'`,<____+;`''''''''''''''^l+----<^'''''''''''"+-_!`'''`,+-_l:_-_l;+--!,+]_I`'''';+--l:_]-II-]_lI_]-;;_]_;`'''''`!----_i"''''''''''''''`,<____~;`''''"i_____+!,`''''''''`:i_-----!`'''''''''''',---i^'''`:_--!;-]-!I_]]i:_]-l`''''I_]]!;-]-ll]]-!l-]]II-]-I`''''''`;_----_<;`'''''''''^I<_____<,`''''''`,<__-____+~<>><~+_------>"'''''''''`";:`'`"^`'"I:`'`"^`'"!;`'^"^'`,!:`''''``:!:''`"^'':i;``^"^``:!:`'''''''''`l_-----_+~<>>><~+______~;`''''''''''`"!___-_--_-_-------+l^''''''''''`,_--<`''''I_]-~`''''I]]->^'''`;--]i`'''`l-]]>'''''!]]]i`''''l]]-l`''''''''''^;~-----____________>,`'''''''''''''''`^;>+_-_-----+i:^`''''''''''''`^>-_;`''''"<--;'''''^~-+;`''''"~]_:`'''',~]_,'''`',_]+:`'''`"+-~,''''''''''''''`,l~_-___-__+<I"`''''''''''''''''''''''''```````''''''''''''''''''`I~>"'''''`l+>^'''''''`''`l+>^`l+i^''''''''''^!+i^`i+i^'''''`!~!^'''''''''''''''''''````````''''''''''''''''''''''''''''''''''''''''''''''''''''`:---~`''''I_--~`''''''''`!-]]+I]]]>`''''''''`l]]]i<]]]>`''''!]--!`''''''''''''''''''''''''''''''''''''''''''''''``'''``''''''''``''''''''``'''``'`;>!^'```'`;<i^'''''''``'`I<!^`I~!^''`'''''''`I<!^`l~!^'''''`l<l^''''''``''''''''``'''``'''``'''``'''``'''''''^>__i"i__l'''''^<-_!`''''`<-_l,<--l`''''^~]_l,<--I'''''"+]_I`''''"+]_;,_]_I`''''''''',_]+;''''''''''''''`:~-+"''''';+_~,;+_~":+_<,I+_<,;~+<^''''',+__~I~--~`'''':+--~^'''',_-->l+--~`'''':-]->l_]]<`'''';-]-i^'''';-]]!l-]-i`'''''''''!]]-!`'''''''''''''`l---I''''`i___!!___;!__+Ii__+l!+++:''''''^::``^:,`''''''"lI`''''''^:,`'^:,``^""`'^:,`'^;,`''''''"!;`'^""`'^:"`'^;,`'^,^'`^"^`':!:`'''''''''''''''`:l,'`^"^''";"'`":^''":^'`:l"'`:I"`'''''''''''''''''''',+--<^'''''''''''''',+--i`'''''''''''''':-]-iI_]]>''''''''''I-]]!;-]-ll-]-!`'''''''''''''`I_--;l__+;`'''''''''''''`i+_+;l+++,''''''''''''''''''''"<-_i`''''''''''''''"<-_l`''''''''''''''"+]_l;~--l'''''''''':+]-I:+]_;;_]+I''''''''''''''`;+-+,;+_~:'''''''''''''''I~+<,;~+<"''''''''''`,ll"',ll"'```''''''`:!l"`:!l"'```''''''''''''''''''``'`;il^'''''`I!I^''``'`IiI^`I>I^^I!I``I!;^`I!I^^I!;``I!;`'''''''''''''''^I!:`'``'''''''''''`:+--+"---<,''''''''`,---<!_--+^''''''''''''''''''''''''l-]]~'''''!]]]>`'''`l]]]!i]-->!---il---l>---i!--_l!___I`'''''''''''''`>+++I'''''''''''''''`I<<:`I<>:`''`''''''`l~>:^l~<:``'''''''''''''''''''''''"!+<,'''''^i+>,'''''^i~i""i+i""i~i^"i~i""i~i""i<!^"i<!"''''''''''''''',i<l^'''''`!+~I`''''^!++I'!++I^i++l`'''''''''^i++I^i++;`>_~I`'''''''''''''''''''''''''''`'''''''''''''''''''''"<+<,''''',<+<^''''''''''''''',<~i"''''''''''"__-~,'''':+--+,---<l_--~^''''''''';_--+;-]-~I--]<"''''''''''^l~++++++++++++++++++i"''''''''''''''''l---l`'''`l-__l'''''''''''''''i__+I`''''''''''^::`''````^;:`'^;:^`^;:``'`'''''''`";:`'";:``^I:`''''''''';_]]]]]]]]]]]]]]]]]]]]]]]-!`'''''''''''''`";,`'''''`,;"''''''''''''''''`,;"`'''''''''''''''"~-_<^'''''''''"+--<:~-->"_--i;+]-<`''''''''''''''''`i-]]]]-]]]]]]]]]]]]]]]]]]]]-<^'''''''''''''''''''''`I___:'''''l__+;I+++,I++~:''''`I++~,'''''"~--~^''''''''',+--<:+--<"_-->I+--~`'''''''''''''''':+]]]]]]]]]]]]]-]]]]]]]]]]-]]-l'''''''''''''''''''''`l___;''''`!+_+ll+_+,l++~;''''`l++~,`^:,^`^::^'^:,`'''''''``'''``'''``'''``''''''''''''''''''I_]]]]]]]]]->"'''`!-]]]]]]-]]-i`'''''''''''`":"`'''''`":"``":"`''`'''``''''`'''''''`,:^'^_--<I+--+I+--+`'''''''''''''''''''''''''''''''''''''''''I_]]]]]]]]-,'''...'`+]]]]]-]]-i`'''''''''''l---l`'''`l___I!___I`''''''''''''''''''^!+++:`!++l"i++!"!+~l''''''''''''''''''''''''''''''''''''''''''I_]]]]]]]]i''.....'':]]]]]]]--i`''''''''''',<+<,`'''',>~<",<~>,'''''''''''''''''''`:><i^':ii:`''''`;ii,';>i:^;>i:'''''`I>i,`'''''''''`I>i,^I<i,''I_]]-]]]]];'......''"-]]]-]]--i`'^l>l""l>!^^l>l^'''''"lil`^liI^'''''''''''''''"li;`'''''^_--~,'''`:+--+"---<l_--+^'''',-]]<"''''''''';]]]<!_]]~`'I_]]]]]]]<^.........'!]]]-]---i`'!---i!---il--_l`'''`l___l!__+I`'''''''''''''`>++~I'''''`,!!,`''''`:!l"':!!,`;!!,'''''';!l"`'''''''''`;il"`;il"''I_]]]]]]_:'.......'..^~]]]----!`'`IiI^^I!I``I!I^'''''^I!I`^I!;^'''''''''''''''^I!;`'''''''''''''''"i__!`>_+!,>__!`''''''''''''''"<__I`'''',<-_l''I_]]]]]]>,`'......''`,l-]-----!`':~_~;''''''''''''''`:<+<,`''''''''`;<~>":<~>,I<~>,:<~i^'''''''''',~--~^_--<I_--~^'''''''''''''`,_]]>`'''';_]]<`'I_]]]]]]--+^'....'''i-_-------!`'l--_!`'''''''''''''`l___;'''''''''`l+++;!++~;i++~Il~~~,''''''`""``^;:`'^,"``^I;`'`"^`'`"^''''''`"I:`''''''"I:`''I_]]]]]]]]<"'''``''^!-]-------!`''"I,`''''''`"`''`"`'`,;"```"`''''''`^"^'',;"``,;"'`,:^'''''',+--~I~--~`'''':+--~;+--<,--->^'''',_]->`'''';_]]<`':+]]]]]]]]]]]]]]]]]-]]-------_l''l--_!`''''I___Ii__+!l___;l+_+;`'''''''''!++~;i~+~Il~~~,'''''^<-->,>--i'''''^~-->,<--!^+]-!`'''`"~]]l`'''',~]-!''`!-]]]]]]]]]]]]]]-]-]-------->`'':_-+I''''',+_+:;__+;;~_+":+_~:'''''''''';++<,I~+<,;~~<"''''''"!!^''''''''''',il^''''''''''',il``,!I^'''''''''''''':_]]]]]]]]]]]]-]--]---]--_I`''''''''''''''''''`;!;`''''''''''`;!:'`;!,`'''''`;!,''''''''''':+--+,''''''''';_--+"'''''''''I_--_;--]~`'''''''''''''''`:!<~~~~~~<<<<<<<<<<i;`'''''''''''''''''''''i___!'''''''''`i+++!!+++I''''`>+~~I''''''''''`l++I''''''''''`!+~;''''''''''`!++:^!+~,`'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''^>~i"''''''''''"i~i^,i<!`''''',i<!`'''''`l++I`'''`^l++I`!_+I`''''^!_+;`i_+;`'''`'''''^>-+;`'''''''''^<-+:`''''''''''''''">_~,">_<,`''''''''''''''''''',>+>^'''''''''''''''''''',>~!^,i<!^^_--~"'''':+--+"---<"'''':_]]~:---<"''''''''';]]]<^'''''''''I]]]>^'''''''''''''`l--->I---l`''''''''''''''''''`l___I'''''''''''''''''''`i~~~ll~~~:'"II^'`^^`'"II^`"lI"``^^''"II^'"lI^'`^^''''''',l;^'`"^''`^^'`,l;^''''''`^``'`^^'`:I:``:!;`'`^`'''''''`^`''''''`;!:''`^`''''''''''''`^`'`;l,``:l,''''''"<--<^'''''''''"+-->`''''''''',+-]>`''''''''':+]]i,+]]!`''''''''',_]_I;--_l`'''`:+-_I;_-+I`'''':+_+:`'''`;~_+,;++~:`''''''''';~~<:l~~<:I~~<"'''''"~--<^''''''''`,+-->`''''''''',+-->`''''''''':+]]i:_]]!`''''''''',_--I;--_l`'''':_-_II+_+I`'''':+_+;`'''`;~_+,;++~:`''''''''';~+<:l~~<:I~~<"`^;;^'`^^'''''''^I;^'`^^'''''''"I;^'`"^'''''''''''':il``:iI^'''''''''''`^^`'`"^'`,I:`'`^^``:i;`'''''`:!:``:I,``:l:'`:l:``:I,``,;"''`^`''^^`''`^`'^_--~,'''''''''"---<"''''''''',-]-<"''''''''''''''I_]]~I]]]<^''''''''''''''''''`I_--i'''''!___!`''''l___Ii_++!l+++Il+++Ii++~ll+++:'''''''''''''''`!_+l`'''''''''`!__I`'''''''''`i-+I`''''''''''''''">-_;">-+;`'''''''''''''''''''">_~,'''''"<_<,'''''">+>",<+>,,>+>^,>~i",>~>",>~i^''''''''''''''''''`''''''''''''''`'''''''''''''`'''''''`l+<"'''''`!+~,^!+<"`!+>,^!~<"^l~>"'''''^!~>^^!~i"^i~i"'''''`!<!^'''''''`''''`''^i<!^''`''''''''''''"!iI`'''''''''''''''''''''''''''''''''''''''':-]]~`''''l-]]+I]]]<!]]]<i--]+I---i`'''`l_-->l-__!i___i`''''l___I`'''''''''''''`i++~I''''''''''''''^!~~~:''''''''''''''`''``'''''''''''''''''''''`:il^'''''`:>l``:il^`:>l^`;il``:iI`'''`'`:iI'`:i;``;>;`'''''`:!;`'''''''''''''''`Ii:''''''''''''''''`;l,''''''''''`"!+--]-]]--]-+l^`'''''''''''''"~]]l`'''',+]]l'''''"_]_l:+]-I"+-_;,_-_I:~-_;,+_+;`'''':~__,,+_~,''''':~+~"`'''';~+<,;<~<^'''''''''`:<~>^''''`'`,<-----]]]]--------->"''''''''''':-]]<`''''I_]]~`'''';-]-i!_--<;---!l_--il_-_iI___l`'''`l+__lI+++I`'''`I+++;''''`!~+~ll~~~:'''''''''`l~~<,''''`;+------_+~>ii>~+_--]]]]~"''''''''''`,"`''''''`,^`'^"^``^I:`'"I,''`,^`'`,^''^,^''^"^`''''''^,`'`";"`''''''^"`'''''''";"'`^"`''''''`^^`'`":^`''',<-----~l^`''''''''`"!+-]]]]>^'''''''''''''''''''''`:-]]iI---il_-->''''''''''''''''''''''''''''''I__+;`''''''''''''''l~+~;'''''''''`!~~<;I~~~,'`:+----+:'''''''''''''''`I_]]]-~"''''''''''''''''''''',~-_I,+-+I:~-_I'''''''''''''''''''''''''''''',<+<,`'''''''''''''';<~>,'''''''''';><i,:><i^`,+----!`''''`:!<~~>l,'''''"<-]]]<^'''''`:!l"`;!l"'''''''''''''''`;!l^`;!I^''''''''''`;!;^`;!;^^;l;``;l;`^;l;`^;l;``;l:`^;l:`'''''`;I:`^;I:`^;I:``<----l`'''^l_-]------+;''''`<]]]-!`'''':-]]~I]]]<^'''''''''''''`l_--<;---i`''''''''`I___!i___i!+_+!l+_+Ii+++!l+++I!++~Ii~~~I'''''!~~~Ii~~<Il~~<:,----<^'''^>-]-------]]-i`'''"-]]]~,''''`I<i,`l<i:`''''''''''''''^l>i"^l>!"''''''''''^l>l"^l>l""l>l^^lil^^l>l^^IiI`^liI^"liI^'''''^l!;^"l!;^"I!;`l----l`''`I-]]-----]]]]]_:''''~]]]_;'''''''''^<_+I`''''">+~;"<+~;,>+~;'''''"<+<:,>~<,">~>,''''',>~>""><>",><i,,i<>^,><i"'''''''''''''''''''''''''l----I`''`l--]----]]]]]-_I''''<]]]-;''''''''';]]]<^'''';-]->l---i!_--<`''''l-__il+__iI___l`'''`l+__!I+++Ii+++!I+++;l+~~;`'''''''''''''''''''''''';----!`''`;_------]]]]]]+"'''`_]]]_:''''''''''";:^'''''`";,``";,``";,`''`''`":,``":,``":,`'''''`":"``":"``":"``","'`":"`'''''''''''''''''''''''''^_---_"`''`I_----]]]--]_,''''I-]]]<^'''',_]]i`'''':+-->:+--!:_-_!;+--i,+-_I`'''';+__l:+_+I`''''''''':++~:`''''''''';~~<:l<~<;I<~<,'''''''''`;<<>"'I--]-+`''''^i--]]]]]-I`'''':_]]]_;''''',_]]i`'''':+]->:_--!:---!I+--!,_--I`'''';+__l:___I`''''''''':+++;`''''''''';~~<:I~~~;I~~~"'''''''''`;<<<"'^i-]---,`''''''^""^`'''''`l-]]]-!`''''''^;,`'^;,`'^;,``";,`''''''";,``'''''''''`";"``":"`'";"``":^'''''''''''''''''''''`":^``":^'''''''''''`":^`''^i---]-~,`''''''''''''`;+]]]]-l`'''''':-]]~I]]-<!_]-~;--->^''''l_--<`''''''''`I___>I___li+_+!l+++!'''''''''''''''''''`!~~~ll~~~:'''''''''`l<<<:'''`I+]]]]]_i,^`````^";<]]]]]]+,''''''''^>_+;`<_+I">_+;">+~:`''''">+~:''''''''''">~<,">~<,"<~>,,i~<"''''''`''''''''''''',><i",i>i^'''''''''`,i>!^'''''`!_------]]]]]]]]]]]]]-+;`''''''''''''''''''''`'''''`''`l<i"^l<i"'''''''''''''''''`''^l>l^''''''''''^l>I^'''''^liI^^liI^^IiI`^l!;^"li;`"I!;`'''''''',i+-]]]]]]]]]]]]-+!,''''''''''''''''''''''''''''''''I--->!_--<''''''''''''''''''''l+++!`'''''''''!++~l'''''l~~~Ii~~<ll~~~:!~~<Ii<<<Il<<<:''''''''''`^:l><~~~~<il,`'''''''''''''''''''''''''''''''''''`;!I^`;!l^''''''''''''''''''''`;!;^''''''''''`Il;`'''''`;l:`^;l:`^;I:``;l:`^;I:`^;I,`

解析

这是直接引用第一名 Midoria7 的解释,有删改:

这个题一开始就发现了唯一能操作的地方就是右下角的拖拉的地方。但是并没有深究,在尝试图中表示的是清明上河图,某些画有龙的名画,含有数字的验证码,需要斜着才能看到字的图片之后都不对。此时已经是凌晨 3 点了,本人太困所以就睡觉了。

第二天早晨 9 点开始看这道题是怎么个事。在拖拉之后发现模样渐渐显露,这是一个 QQ 群二维码。

但是这样还是无法识别。然后尝试了一系列别的方式加强清晰度都无果。

最终认命尝试叠图手描一遍,这样就能扫出来了。

右下角有一个很隐蔽的文本框调整手柄,这是本题的突破口之一(也有人直接调浏览器窗口大小了,这确实也能做)。当你拖动这个手柄的时候文字内容会改变宽度,然后所有的字符都会重排。在这个过程中,你可能碰巧看到一个神似二维码的图案——这就是本次解谜的通关群二维码!

当然这关还有一些其它方面的突破口,比如说,可以复制到记事本中统计一下,发现一共有 11455 个字符,而它的质因数分解结果是 。那么整幅画的字符长度和宽度只能在这三个数当中产生。我们很容易先想到 两种可能,而其中的一种正是标准答案。

再比如,当你按下 F12 之后, 浏览器会差点卡死, 就会看到显示内容中有大量的暗示,比如等宽字体、resize 显示、break-all 的换行方式等,这几乎就是在暗示以内容中的字符为单元进行布局。

总而言之,我们发现这是个二维码,但是……

这个二维码用 QQ 是扫不出来的。这时很多人都来问我,但是我都笑而不语。

没错,这道题至此还没有结束!下一步,你需要根据这个画,把二维码还原成能扫的样子!

这时候大家就八仙过海各显神通了,以下展示通关群中各位互相交流的解法:

甲辰年概览

时间:2025-01-26 19:19:19 至 2025-01-28 07:19:19

这是第三年出解谜了。本次我设计了大量的交互题,把玩家们折腾得够呛。明年还出

第一位通关者是 脉冲星,在游戏开始 331 分钟后通关,历经三个周目。

在规定时间内,共有 8 人通过了本次解谜。

前六题的一血名单如下:

  • 军火商于 19:20:50 拿下 CAPTCHA
  • Hexropt于 19:42:19 拿下 CountLightsOut
  • 在原七海于 19:57:07 拿下 BesiegeWithoutAssault
  • 赵丽于 20:01:46 拿下 DigitalCircuit
  • Evan于 20:02:18 拿下 EncryptedDialog
  • 脉冲星于 21:54:52 拿下 CatchGlowworm

CAPTCHA

本关是一道模拟验证码的题目,要求玩家在给定的 16 个图片中选出符合要求的内容。

但是,由我出的验证码怎么会像普通验证码那样简单呢……

可选题目一共有 7 个,接下来我将逐题解析:

bug

请选出以下包含 bug 的所有图片:

错误共有五处:

  • 头文件 studio.h 错误,应为 stdio.h
  • struct Node 定义时,next 之后未加分号;
  • create 函数中,for 循环迭代操作不是 i++ 而是 i--(这是唯一一个不能靠编译器检查出来的错);
  • create 函数中,调用 insert 函数时应当传入 struct Node** 类型的参数,所以要用 &head_ref
  • main 函数中,struct Node* head_ref, dup; 这样定义的 dup 其实是 struct Node 类型。若要正确定义,应写作 *dup

resistor

请选出以下包含 电阻 的所有图片:

这道题要用色环电阻法读出每个电阻的阻值。

注意,不是每个电阻都要从左向右读数!读数的起始位置取决于中间的色环离哪一侧更近。比如说,第 1 行第 3 列的色环离右侧近,所以从右向左读出

第一列第二列第三列第四列

planet

请选出以下包含 行星 的所有图片:

这道题的坑点在于,除了人们熟知的太阳系八大行星以外,还有「系外行星」这回事。

  • 太阳当然是恒星。
  • 月球当然是卫星。
  • 火星当然是行星。
  • 谷神星可能要查一下。其实它只是太阳系内的一个「小行星」。(「小行星」和「行星」是完全不同的)
  • 比邻星是离太阳系最近的恒星
  • 冥王星在过去被人们普遍认为是行星,但早在 2006 年它就被划为矮行星了。(矮行星也不是行星)
  • 木卫一,顾名思义,就是木星的卫星。
  • 墨提斯是木卫十六的别名,当然也是卫星。
  • 河鼓二、参宿五、瑶光、开阳,这些古人就能看得见的系外天体当然不可能是(不发光的)行星,一般来说都是恒星。至于系外行星,那都是 19 世纪之后才开始发现的。
  • 飞马座 51 是飞马座的一个恒星。
  • 飞马座 51b 是属于飞马座 51 的天体,它是不是行星呢?查一下会发现它确实是一个系外行星。
  • 15760 Albion 是一个介于海王星和冥王星之间的海王星外天体。(既然它是在太阳系内的,那我们直接照八大行星的名单排除就行了)
  • Beta Persei 也就是英仙座β,是一个由恒星构成的多星系统。

unit

请选出以下包含 长度单位 的所有图片:

这题也算是有点坑,一个是微观度量,一个是天文度量,还有一个很年轻的新单位,网上资料不多。

  • Bq(贝克勒)是放射性活度单位。
  • dB(分贝)是比例单位,在声学、电子学等领域都有涉及。
  • eV(电子伏特)是能量单位。
  • Np(奈培)是和 dB 相似的比例单位。
  • mmHg(毫米汞柱)是压强单位。
  • Å(埃斯特朗)是长度单位,一般用于微观长度的计量,
  • kn(节)是速度单位,一般用于航海、航空。
  • e 是基本电荷量,它在某些场合也可作为单位使用。
  • ha(公顷)是面积单位。
  • rad 有两种可能的解读,一说是弧度单位,一说是辐射吸收剂量单位。但不管怎么说,都不是长度单位。
  • Wb(韦伯)是磁通量单位。
  • ◦(度)是角度单位。
  • sr(球面度)是立体角的单位。
  • au(天文单位)是长度单位。起初人们把日地之间的距离定义为 1au。
  • Hz(赫兹)是频率单位。
  • Qm(昆米)是长度单位。这里的 Q 表示 ,是 2022 年启用的新词头。

tone

请选出以下包含 入声字 的所有图片:

这题没有任何坑,上网找本《广韵》自己查一遍就好了。

当然,如果你有相关的方言基础,这题甚至可以不用查资料,读一遍就过了。

答案:職、月、物、竹、活

math

请选出以下包含 大于2的数学常数 的所有图片:

这道题没什么坑,当然前提是你得知道它们都是什么。

  • 是圆周率,约为 3.14。
  • 是黄金分割比的比值,约为 1.618。
  • ,显然它小于 1,不用再算了。
  • 是自然对数的底,约为 2.718。
  • 是欧拉常数,约为 0.577。
  • Lemniscate 常数,约为 2.622。
  • 约为 1.414。
  • 约为 0.693。
  • 用到了黎曼 zeta 函数,它表示 ,约为1.202。
  • 用到了伽马函数,它表示 ,等于 1。
  • 用到了对数积分函数,它表示 ,这个结果为负无穷。
  • 用到了误差函数,它表示 ,约为 0.843。
  • Catalan 常数的值约为 0.916。
  • Artin 常数的值约为 0.374。
  • Claisher 常数的值约为 1.282。
  • Gauss 常数的值约为 0.835。

chordate

请选出以下包含 脊索动物 的所有图片:

本题所用的物种名全部采用双名法格式,所以需要先查出每个物种名,再下判断。

这道题同样也没坑,属于只要花点时间查很快就能做出来的题。

  • Homo sapiens:人类
  • Apis mellifera:西洋蜜蜂
  • Panthera leo:狮子
  • Canis lupus:狼
  • Equus ferus:野马
  • Gallus gallus:红原鸡
  • Drosophila melanogaster:黑腹果蝇
  • Octopus vulgaris:普通章鱼
  • Lissachatina fulica:非洲大蜗牛
  • Salmo salar:大西洋鲑(三文鱼)
  • Bufo bufo:大蟾蜍
  • Chelonia mydas:绿海龟
  • Pavo cristatus:蓝孔雀
  • Asterias rubens:普通海星
  • Balaenoptera msculus:蓝鲸
  • Lumbricus terrestris:普通蚯蚓

BesiegeWithoutAssault

在平面直角坐标系中,敌军的初始位置是 ,你可以选择任意一个位置,向敌军发动突袭。

突袭开始后,敌军总是朝着远离你的方向移动(此运动过程通过 300 步的近似连续过程来模拟),你可以指定任意位置进行移动。你与敌军的速度之比为

你需要做到「围而不攻」,既不能过分靠近敌军(距离 ),又不能让敌军逃出包围圈()的范围。

当敌军累计走过的路线长度达到 时,你的任务完成。

示意图

解析

视频演示

这个问题乍看起来还挺难想的,因为范围不大,而且双方的速度相差无几,所以一不小心就会让敌军逃出包围圈,追都追不上。

一开始我把这题的速度比设计成 ,但是测题时发现太简单,乱走都能过;所以经过反复测试,最后我选定了 的速度,这样就不能乱走了,必须仔细思考对策才行。

实际上,这道题的思路源自于一个「你追我逃」的模型。我们不妨做此假设:

原始问题

在一个圆形斗兽场内,一个奴隶和一头猛兽正在相互追逐。奴隶的速度比猛兽要快(假设速度比是 ,但他不能逃出斗兽场的范围;而猛兽总是朝着奴隶的方向进行追逐。

斗兽场的大小可以足够大,但并不是无限大的。问:是否有一种逃跑方案可以使猛兽永远也追不上奴隶?

解决这个问题的方案如下图所示,只要按照图示的位置关系跑成一个圆,猛兽就永远也追不上奴隶。

「稳定状态」

我们先来证明这个状态是稳定的。只要这个状态是稳定的,那么猛兽就永远追不上奴隶,二者只会在各自的轨道上无限画圆。

因为 ,所以二者的角速度 。那么经过一小段时间 之后,二者关于圆心的夹角 仍然保持不变。换句话说,在运动轨道上,奴隶仍然保持比猛兽超前 角度的位置关系。

而在这段时间内 是不会发生变化的,因为猛兽的速度方向刚好垂直于运动半径。

既然 都不变,那么我们就会发现一个事实:由 三边构成的三角形可以始终保持不变,二者就会永远这样运动下去。这就是我所说的稳定状态。

引入变因

接下来我们假设奴隶的运动方向略微偏离了原轨道,那样会发生什么呢?

如果奴隶的运动方向向外偏,那么运动半径 就会增大。又因为线速度不变,所以奴隶的角速度将会变小。这样一来, 也会变小**。

而奴隶速度向外偏,会使得运动速度在 方向上的分量增大,拉开二者的距离,所以** 会变大**。

问题来了: 会如何变化?以下给出四种假说:

  • 增大或不变, 减小或不变。这种情况是根本不可能发生的,因为在邻边 都增大的情况下, 增大或不变就必然意味着对边 减小。
  • 减小, 减小或不变。这种情况也不可能发生,因为 都减小就意味着另一个内角增大,而这个内角增大就意味着猛兽的运动方向会外偏,这样是不可能推导出 减小或不变的结论的。
  • 增大或不变, 增大。这种情况是可能的,不会产生任何矛盾。
  • 减小, 增大。这种情况也是可能的,不会产生任何矛盾。

会怎么变化,我们暂时不能下定论,但是 增大是必然的。而 增大就意味着,猛兽的运动方向也会外偏

同样的道理,我们可以得出,如果奴隶的运动方向内偏,那么猛兽的运动方向也将内偏。

但是无论如何偏转,只要奴隶以一个新的圆心、新的半径开始做圆周运动,那么猛兽的运动方向当然也就会趋近于半径比等于线速度比的同心圆。

把思路逆转过来

说了这么多,这个问题和本关之间到底有什么联系呢?

你可以尝试把整个运动过程逆转过来:不是猛兽追奴隶,而是奴隶追猛兽;猛兽不是朝着奴隶跑,而是背对着奴隶,朝着远离他的方向跑。那么整个问题就会变成:

至于奴隶还是野兽,我军还是敌军,换个名字的事情而已。

不知读者有没有发现,在这里,「围而不打」的要义不一定是追得越近越好,也不是一定非要朝着他的方向追,而是保持在一个稳定状态,和对手一直这样耗下去。如果一上来就思考这个「围而不打」的问题,我们恐怕很难得出这么清晰的结论。

稳态的初始化

问题到这里还没有结束。我们的斗兽场,或者说包围圈,只是一个很小的范围。我们真的能做到把敌军控制在这么小的范围之内吗?不妨试试。

我们的初始选点是随意的,但不能离敌军太近,也就是 才行。

有了 之后我们还需要确定圆周运动的圆心。因为 ,且 ,所以我们很容易算出

如果我们选点 的话,那么圆周运动的圆心就是 亦可),这样一来,只要满足 ,我们就可以确保敌军总是在包围圈内运动,如图所示:

我自己做题的时候希望这个半径不要太大,又不要太小,所以设定在 左右是比较合适的,那么对应的 就是 左右。

不过实际做起来我们没办法走曲线,那就只能用较短的直线来逼近曲线上的一段弧。这样做肯定也会带来精度损失,所以我们需要频繁调整我们的方向,争取把敌军控制在包围圈内。下图是我自己走的效果,在这里敌军总计走过的路程达到了 260,将近三个 95。

CountLightsOut

本题是 LightsOut 游戏的加强版。

在一个 6×6 的棋盘中,有些灯是开着的,有些灯是关着的(就像普通的 LightsOut 游戏那样)。

但是,你不知道哪些灯是开着的,哪些灯是关着的。每次你进行操作后,我只会告诉你「现在还有几盏灯亮着」。

你需要把所有灯都灭掉,才能通关。

这道题的过法很多,可以通过逻辑推演找试探解,也可以通过理论推导找解析解。从大家的做题效果上来说,如果找试探解的话,只要运气好,几分钟做完也没什么问题。但要找解析解就很麻烦了,光是写代码都得好一段时间。

不过,对于多周目玩家来说,寻找解析解则能一劳永逸地解决问题。找到稳定解法就不用再靠随机应变了,效率更高。

相比于朴素的 LightsOut 游戏而言,这关加强的点在于,玩家并不知道每盏灯的状态,只知道一个「总体情况」。那么如果能通过一定的技巧还原出每盏灯的状态,我们不就可以把它转换成朴素的 LightOut 了吗?至于朴素的 LightsOut 游戏,我们用不着自己解,上网找个 lightsout solver 就可以直接帮我们推导解题方法了。

试探解

视频演示

绝大部分玩家初次做题时都会寻找试探解。一般来说大家有两种不同的策略:

  • 一边分析灯的状态,一边灭灯,同步进行。
  • 不急于灭灯,先把每盏灯的状态都分析出来。

两种策略都是可行的。在这里我就以第二种策略来介绍。

基本思路

首先,如果我们除了初始状态以外一无所知,那么在什么情况下我们可以笃定一盏灯的状态呢?

其实只有一种情况,那就是当点击这盏灯时,亮灯数的变化刚好和此格周围的的灯数相等。

就以下图为例,如果点击了左下角的位置,然后发现亮灯数从 变成了 ,这说明什么?当然说明原来这三盏灯的状态都是「关」,而当我们点灯之后,三盏灯个状态都变成了「开」。

同样的道理,如果亮灯数从 变成了 ,那就说明原来三盏灯都是开着的,点灯之后它们都关上了。

但是假如说,我们点灯之后发现 变成了 或者 的话,那我们就没办法直接下判断了,因为只知道这个范围内「有几盏灯亮着」,却不知道「亮灯的都是哪些」。

假如说我们知道了一部分灯的状态,那么我们就可以据此进一步推导周围灯的状态。以下图为例,红色表示「已知是开着的灯」,黑色表示「已知是关着的灯」,那么当我们点击这个范围时,就可以根据亮灯数的变化,判断那个未知格的状态了。

如果从 变成了 ,那就说明那个未知格原本是关灯状态;如果从 变成了 ,那就说明那个未知格原本是开灯状态。

按照这个思路,我们就可以一步步通过已知信息推导出未知信息。

解析解

的格子解释起来还是太麻烦了点,我先用 的情况来作说明吧。

第一步

如图所示,我们给每个格子一个布尔值,用 表示灯开着, 表示灯关着。

而我们又知道,经过一次点击行为后,亮灯数的变化预示着「受影响范围内有多少盏灯原本是亮着的」

举个例子,如果我们点击了左下角的位置,那么 会受到影响,变成它的非值;而亮灯数从 变成了 ,这就说明在 三盏灯中,有两盏原本是亮着的:

列出这个式子之后,记得再点一下左下角,恢复原始状态。

接下来我们如法炮制,还可以再点击 所在格,列出如下方程( 自行推算):

同理,还有四个方程可列:

这样一来我们就构造了一个六元方程组。方便起见,我们把它写成矩阵形式:

这个方程组的系数行列式等于 ,意味着方程组不一定有解。但是别担心,「现实」能够保证它是有解的——因为 不是凭空赋值的,它对应的是真实世界中某个可解的情况。

据此,我们就能够通过解方程组把 都解出来了。于是第一步完成。

第二步

现在我们知道了每盏灯的状态,接下来我们还需要一个方案来求解「如何灭灯」。

我们知道,两次(以及偶数次)点灯相当于没点,而三次(以及奇数次)点灯相当于只点了一次。所以说对于每盏灯来说,我们要么「点」,要么「不点」,只有这两种选择。

我们设 表示是否需要点第 盏灯。以第一盏灯为例,如果 ,那么我们就需要保证所有影响到该灯的操作之异或和等于 ,这样才能关掉该灯;如果 ,那么我们就需要保证所有影响到该灯的操作之异或和等于 ,这样才能不把该灯打开。因此:

其中 表示异或加,这点与第一步中的操作不同。

同样道理,我们也可以列出剩下的五个式子:

我们仍然把它写成矩阵形式:

这里的系数矩阵和刚才一模一样,因为系数矩阵正是由这个图的规格决定的。

接下来只要解出全部的 ,我们就知道该点哪些灯了。

情形的推广

接下来回到我们的 关灯游戏,我们会发现,整个问题的框架和 的版本并没有什么不同。不同的地方只是系数矩阵发生了变化而已。

该矩阵是一个 36 阶方阵,这里不方便写,我就在这里提供一段用于生成此矩阵的 C++ 代码:

//这里N=M=6
void gaussian::getcoef(bool **coefficient){
	for(int i=0;i<N*M;i++)
		for(int j=0;j<N*M;j++){
			int ix{i/M},iy{i%M},jx{j/M},jy{j%M};
			if(ix==jx){
				int dy{iy-jy};
				if(dy<=1&&dy>=-1)
					coefficient[i][j]=true;
			}
			else if(iy==jy){
				int dx{ix-jx};
				if(dx<=1&&dx>=-1)
					coefficient[i][j]=true;
			}
			else
				coefficient[i][j]=false;
		}
}

更多相关代码参见我的GitHub仓库

使用 z3 库求解

YouXam 还提供了一个使用 z3 库进行求解的方案。相比于自己手写高斯消元,直接调库还是快多了。

from z3 import * # pip install z3-solver
cells = [[BitVec(f"cell_{i + 1}_{j + 1}", 8) for j in range(6)] for i in range(6)]
flips = [[0 for _ in range(6)] for _ in range(6)]
now = int(input('当前有多少个灯亮: '))
print('现在从左到右,从上到下,按顺序点击每个格子,每次点击后,输入有多少个灯亮')
counts = [[int(x) for x in input(f'输入第 {i + 1} 行(空格分割): ').split()] for i in range(6)]
nowsum = sum([sum(row) for row in cells])
s, k = Solver(), Solver()
s.add(*[Or(cell == 0, cell == 1) for row in cells for cell in row], nowsum == now)
for x in range(6):
    for y in range(6):
        for dx, dy in [(0, 0), (0, 1), (0, -1), (1, 0), (-1, 0)]:
            if 0 <= x + dx < 6 and 0 <= y + dy < 6:
                nowsum += 1 - 2 * (cells[x + dx][y + dy] ^ flips[x + dx][y + dy])
                flips[x + dx][y + dy] ^= 1
        s.add(nowsum == counts[x][y])
s.check()
clicks = [[BitVec(f"click_{i + 1}_{j + 1}", 1) for j in range(6)] for i in range(6)]
k.add(*[0 == (s.model()[cells[x][y]].as_long() ^ flips[x][y])
        + sum([clicks[x + dx][y + dy] for dx, dy in [(0, 0), (0, 1), (0, -1), (1, 0), (-1, 0)]
            if 0 <= x + dx < 6 and 0 <= y + dy < 6]) for x in range(6) for y in range(6)])
k.check()
print('以任意顺序点击值为 1 的灯:\n' + '\n'.join([' '.join([str(k.model()[clicks[x][y]]) for y in range(6)]) for x in range(6)]))

EncryptedDialog

本题是 AI 交互题。你只能用英语来和它交流(更准确地说,只能使用 ASCII 可打印字符,如果 AI 回复了非 ASCII 可打印字符,会使用 □ 来代替)。

你向 AI 输入的内容将会被一个「输入码表」转换成密文,再发送给 AI 作为输入;AI 的输出又会被一个「输出码表」转换成密文,再显示给你。

你事先无从知道这两份码表,我只能告诉你:码表是由26个英文字母到26个英文字母的一一映射(大小写不敏感)。

你的目标是让 AI 输出一字不差的 The quick brown fox jumps over the lazy dog.(注意这里的输出是最终显示到你屏幕上的输出)

解析

视频演示

游戏时发现有人说直接送一个引号套着的字符串就可以让它原模原样输出,我就试了一下。没想到第一次就成了。

但是当我兴冲冲地把码表排好了,准备让他复述的时候,它却再也不复述了。可见这不是稳定解法啊……

基本思路

每个回合的流程如下图所示。我们的输入 I 通过输入码表替换成加密输入 E,用于对 AI 的输入;而 AI 的输出 O 又要通过输出码表替换成加密输出 D,显示到我们的屏幕中。

相信大家玩一小会儿就可以慢慢找到 O 和 D 之间的映射关系,我就把它叫做 D2O 吧。有了 D2O,我们至少能理解 AI 每次的输出是什么意思了。

但这样还不足以解决整个问题。如果你希望它原模原样输出你的话(当然,得碰一点运气),那么你至少需要知道 D 和 I 的映射关系 D2I,才能达到目的。而如果你希望给 AI 发指令让他理解你的意思,那你还需要知道 E2I 才行。

D2O 的测定

D2O 就是一个很简单的单表替换过程,没什么难度可言,随便找个解码网站(比如quipquip)都能做了。

如果要自己做的话,可以先把多个回合的 D 整理到一起,做个字频统计,然后就能看出一些常用字母(比如字频 12% 的 e)。接下来再靠语感和不断尝试,把整个替换表解出来就可以了。

D2I 的测定

D2I 的过程虽然很长,但是对玩家来说,它是最直观的:你知道你自己的输入 I,你也能看到显示的输出 D。可是麻烦点在于,中间的 AI 环节是一个黑箱,我们能否想办法消除这个环节带来的影响呢?当然可以,那就是引导它进行复述

在引导复述时,比较好的方案要么是直接套引号变成引文,要么是直接放代码块要求它解释(它可能会把源代码复述一遍再扯些有的没的),比如说:

这样就很容易把 D2I 统计出来了。

E2I 的测定

T 如果你还需要测定 E2I 的话,别急,刚才的结果还有用。

我们想,我们已经知道了 D2O,而在复述过程中,存在 O=E 的部分,那么我们就可以得到 D2E 吧。

我们刚才又得到了 D2I,那么根据 E2D(D2E 的逆映射)和 D2I,我们就可以求出 E2I 了!

指令构造

现在我们掌握了充分的信息,我们可以构造一个指令让 AI 输出我们想要的结果了。

举个例子:output:"The quick brown fox jumps over the lazy dog."

这段输入包含「说明」和「内容」的部分。说明部分是 E,要让 AI 能看懂的;而内容部分是 D,要显示到屏幕上的。

所以我们应该用 E2I(output) 把说明部分转换成输入,用 D2I(The quick brown fox jumps over the lazy dog.) 把内容部分转换成输入。最后,拼成我们要输入的 I。

CatchGlowworm

在三维空间直角坐标系中,捕虫网的初始位置是 ,而萤火虫与捕虫网的初始距离为

捕虫网受你的控制而移动,而与此同时,萤火虫也总是朝着远离捕虫网的方向移动,此过程使用 1000 步的近似连续过程来模拟。

你的目标是,在萤火虫逃出 的范围之前,抓住萤火虫。

捕虫网与萤火虫的速度之比为 ;抓住萤火虫的判定标准为:捕虫网与萤火虫的距离不大于

每回合输入你希望捕虫网移动到的位置 ,其中 为浮点数,表示捕虫网移动的目标位置。

解析

本题有两种解题思路。一种是逐步试探,不断逼近萤火虫;一种是用小步位移先求解萤火虫的位置,再直接追至目标位置。两种方法都是可以的,不过要走多周目的话,还是建议求出解析解。

试探法

在这道题中,你只知道萤火虫的距离,而不知道具体的方向,这是很麻烦的。那么,有没有什么方法可以确定这个方向呢?当然有。

试想,如果你向某个方向前进了 距离,而萤火虫与你的距离增加了 ,这说明什么?

只有一种情况:萤火虫和你的前进方向完全相反。

再试想,如果你向某个方向前进了 距离,而萤火虫与你的距离减少了 ,这说明什么?当然是萤火虫和你的前进方向完全相同。

我们再来考虑更一般的情况,即,当你向某个方向前进 时,萤火虫与你的距离变化了

如果 ,这就说明我们正在南辕北辙,方向不对;如果 ,这就说明我们的方向确实是朝着靠近萤火虫的那一侧。 越大,就说明我们的方向越接近于真实的方向。就这样,我们可以一边调整,一边试探,不断逼近萤火虫,从而把距离缩减到 以内。

解析法

在此之前,我们只是根据 的大小来粗略判断「我们距离萤火虫多远」。但其实,我们更希望充分利用这份信息,判断出萤火虫的具体方向。

假设我们朝某个方向(设方向向量为 )从点 移动到点 ,而萤火虫从点 移动到点 。我们可以证明 一定在 三点确定的平面内(因为萤火虫运动的方向向量一定平行于此平面)。

那么我们首先在这个平面内思考萤火虫的运动过程(在三维空间里描述起来还是太麻烦了点):

在该图中,萤火虫的行动路线是个曲线;但只要 足够小,我们就可以把它近似地当作是直线。

这样一来, 将会构成一个三角形,而这个外角 ,正是我们期望得知的「方向角」。

如何求出 呢?我们可以使用余弦定理:

整理一下这个式子,就可以得到

现在我们知道了方向和距离,那么就能在平面上确定两个点(因为我们尚不知晓 的正负),这两个点就是萤火虫可能位置的近似解。

然而,在三维空间中我们无法根据现有的信息判断「这个平面在哪」,因此我们得到的结果只能说明萤火虫可能的位置在一个圆环上。

接下来我们再选择一个与 线性无关的方向向量 ,重复以上工作,得到另一组解。两个圆至多只有两个交点,这是一个巨大的进展。

接下来我们再选择一个与 线性无关的方向向量 ,重复以上工作,得到另一组解。三个圆至多只有一个交点,这就是我们需要的解!

以上只是对可行性的阐述;而实际上我们没必要真的在空间中求三个圆的交点。我选择的方案是:投影

我们通过在 方向上的运动能够得到一组解,那是一个圆环。我们不能确定萤火虫在圆环上的哪个位置;但有一件事是确定的,那就是它的位置在 方向上的投影。

举个例子,如果我们向 轴正方向移动了 ,据此求出的角度是 ,那岂不就是说,萤火虫的位置在 轴的投影就是 吗?

同样的道理,如果我们再分别向 轴和 轴移动一小段距离的话,我们也可以求出萤火虫位置在 轴和 轴上的投影。这样一来,**我们就完全求出了萤火虫的位置!**那么接下来的问题就迎刃而解了!

误差分析

现在我们来做一点误差分析。

本题的要求比较松,只要距离在 以内都可以判定为通关,所以我们对计算误差的可接受范围就是

哪些地方产生了误差呢?

首先是,我们假设当 很小时,萤火虫的运动近似于直线

曲线运动带来的误差是很难估计的,但我们可以为它估计一个上界。

萤火虫偏转的角度约等于此图中的 。而当 足够小的时候,我们可以粗略认为

而萤火虫走过的距离为 ,它的位置误差不会超过半径为 ,弧度为 的扇形区域,也就是

假如说我取 ,那么这个误差大概会到达 量级,可以说是完全没有影响了。

另一个误差来源在于:我们分别测定 坐标时必然存在次序;当我们测定其中一个值时,必须要让萤火虫运动起来,那么我们先前测定好的值就不准确了。

假设我们的测定顺序是 ,而我们每次都移动 距离,那么 轴上的误差积累了两回合,也就是不超过 轴上的误差积累了一回合,不超过 ;而 轴上,我们姑且认为没有误差。

所以累积误差是多少呢?不超过 。所以只要 取得较小,我们就可以尽量避免出现很大的误差。

不过,也不是说 就要取得越小越好。本题的浮点数精度只有七位有效数字,换句话说,即便是 在数值上有 的差异,经过四舍五入之后都有可能放大到 大小。如果你用 量级的 来测定结果的话,那么这个结果的准确度反而要大打折扣了。

示例代码

下面的 Python 代码是我测题时使用的,可供参考:

import sys
rate=.5
delta=.001
d0=float(sys.argv[1])#初始状态下的距离
d1=float(sys.argv[2])#向x轴方向移动delta后的距离
d2=float(sys.argv[3])#向y轴方向移动delta后的距离
d3=float(sys.argv[4])#向z轴方向移动delta后的距离
dx=d3*((d0+rate*delta)**2-delta**2-d1**2)/(2*delta*d1)#相对距离在x方向上的投影
dy=d3*((d1+rate*delta)**2-delta**2-d2**2)/(2*delta*d2)#相对距离在y方向上的投影
dz=d3*((d2+rate*delta)**2-delta**2-d3**2)/(2*delta*d3)#相对距离在z方向上的投影
x=2*dx+delta
y=2*dy+delta
z=2*dz+delta
print(f'{x} {y} {z}')#前往此处即可抓到萤火虫

算法简化

脉冲星为我提供了一个更简单的思路,如图所示:

在这里,我们根本没必要求出哪个具体的角度,只需要在两个直角三角形中根据勾股定理列出如下方程组:

便可以解出 。这就是萤火虫与你在 轴上的相对距离。

另外两步也可以如法解出,它的思路比使用余弦定理更简洁,但没有本质上的区别。

DigitalCircuit

本关是一个由不同逻辑门(可能包含与非门、异或门等)构成的数字电路,包含 4 个输入端和 8 个输出端,初始输出为 00000000

你需要通过合理输入,使得此电路能够输出 11111111

本关的情况有点出乎意料,因为很多人过关不是依靠真的把题解出来,而是「一不小心碰出全 1 然后就通关了」,可见题目的复杂度还有待提升。我的预期好歹也是大家要测测电路,找找规律才行。

解析

这道题对于没学过数电的人来说并不友好,因为它不是一个简单的组合逻辑电路。在组合逻辑电路中,由确定的输入只会得出确定的输出。但在本题,你只要连续使用同样的输入来观测输出,就会发现输出结果是变化的。所以说,这个电路很有可能是时序逻辑电路。

具体的找规律过程就不提了,我直接说结果。

设输入为 ,上一轮的输出为 ,本轮输出为 ,那么:

就等于你的输入,也就是说

只有在「本次输入与上次输入(上次输入的结果间接记录在了上次输出当中)完全相反」时,才会得到 1;否则得到 0

组成二位计数器,也就是说

则等于所有输入 、输出 的异或和。

以下 C++ 代码可以实现由输入和上次输出推算本次输出。

std::array<bool,preset::OutputNum> operate::signal(std::array<bool,preset::InputNum> &input,std::array<bool,preset::OutputNum> &output){
	std::array<bool,preset::OutputNum> result;
	//result[7,5,3,1]记录了本次的输入
	result[7]=input[3];
	result[5]=input[2];
	result[3]=input[1];
	result[1]=input[0];
	//result[6]只有在「本次输入与上次输入完全相反」时,才会得到true
	result[6]=input[3]^output[7]&&input[2]^output[5]&&input[1]^output[3]&&input[0]^output[1];
	//result[4,2]组成二位计数器
	result[4]=output[4]^output[2];
	result[2]=!output[2];
	//result[0]等于所有输入、输出的异或和
	result[0]=false;
	for(int i=0;i<preset::InputNum;i++)
		result[0]^=input[i];
	for(int i=0;i<preset::OutputNum;i++)
		result[0]^=output[i];
	return result;
}

回到我们的问题。我们的初始状态是 00000000,而我们希望到达全 11111111,这其实就是一个在有向图上寻找路径的问题,那么无论做广度优先搜索还是求最短路都是可行的。

人工推导

当然,这题不编程也完全可以做,我们试着寻找一下思路:

就像走迷宫那样,有的时候从终点开始要比从起点开始更加容易。

要想达到 11111111,最后一步的输入必定要是 1111,因为输出当中就有四位数等于输入。

而我们又要保证 等于 1,那么前一次的输入必须是 0000 才行。

同时, 构成二位计数器,也就是说,在前一回合,它们的输出将是

接下来的流程也可以照猫画虎地做,不过分岔会越来越多,很难逐个分析;因此我建议能编程的玩家还是试试编程求解一下。

编程求解

我们可以运用图论的知识建立一个有向图 ,图上的点表示「输出的结果」,图中共有 个点;边表示「经过一次输入,某个状态可以转移成另一个状态」,图中共有 条边。

我们还可以推断出这个图的一些特征,例如:因为有两位计数器每回合都会发生变化,所以任何一个状态在经历一次输入之后不可能返回它自身,因而 中没有自环。(当然这是题外话了)

接下来就可以在图上搜索或者求最短路。我用的是 Floyd 算法。

#include<array>
#include<limits>
#include"operate.hpp"
#include"preset.hpp"
void operate::floyd(
	std::array<std::array<int,1<<preset::OutputNum>,1<<preset::OutputNum> &dist,
	std::array<std::array<int,1<<preset::OutputNum>,1<<preset::OutputNum> &prev
){
	for(int o=0;o<1<<preset::OutputNum;o++)
		for(int r=0;r<1<<preset::OutputNum;r++){
			dist[o][r]=std::numeric_limits<int>::max()/2;
			prev[o][r]=-1;
		}
	for(int o=0;o<1<<preset::OutputNum;o++){
		dist[o][o]=0;
		prev[o][o]=o;
		std::array<bool,preset::OutputNum> output;
		for(int d=0;d<preset::OutputNum;d++)
			output[d]=o&1<<d;
		for(int i=0;i<1<<preset::InputNum;i++){
			std::array<bool,preset::InputNum> input;
			for(int d=0;d<preset::InputNum;d++)
				input[d]=i&1<<d;
			std::array<bool,preset::OutputNum> result{operate::signal(input,output)};
			int r{0};
			for(int d=0;d<preset::OutputNum;d++)
				if(result[d])
					r+=1<<d;
			dist[o][r]=1;
			prev[o][r]=o;
		}
	}
	for(int k=0;k<1<<preset::OutputNum;k++)
		for(int i=0;i<1<<preset::OutputNum;i++)
			for(int j=0;j<1<<preset::OutputNum;j++)
				if(dist[i][j]>dist[i][k]+dist[k][j]){
					dist[i][j]=dist[i][k]+dist[k][j];
					prev[i][j]=prev[k][j];
				}
}

Floyd 算法应该是这里面最麻烦的了,不过我有不得不用的理由——那就是分析整个图的性质。

不过这些内容就是出题时需要考虑的了,如果我有空的话,之后会对此做进一步说明。

WhatHappened

这关虽然有一个填写框,但却没有「正确答案」,所以这道题所谓的 分是不可能达成的。

解谜需要的信息都包含在提交产生的错误答案中。本关不可以重置进度,所以如果玩家不慎忽略了这些信息,那就只能开新周目再次获取信息了。

解析

  1. 你需要通过前面全部 6 道题,才可以获得我的情报哦。

有一小部分玩家玩到这里就认命回去做剩下的那道题了,实在可惜;总是对出题人言听计从可是要吃亏的。

  1. 不过就算只通过了 5 道题,那也是可以的啦。

看吧,哪怕你多试一次,反转就会出现。

  1. 我有好消息和坏消息要告诉你,我猜你想先听坏消息。你不想听也没关系,反正嘴长在我身上。
  2. 坏消息是:这道题没有正确答案。换言之,没有人可以通过这关,别白费力气了。
  3. 好消息是:只有这一条坏消息了。
  4. 哦,或许你在听到这条好消息之前还有抱什么侥幸心理,以为我会告诉你更有价值的东西。
  5. 那只是因为你做我的解谜游戏做得还是太少了。
  6. 经常参与我的解谜游戏的人都知道,我不怎么出解谜游戏。
  7. 但是即便真的要出解谜游戏,也不会出得这么常规吧,比如直接把这个回合的回合数当作答案什么的。

如果玩家有注意到的话,好歹应该试试填 9 嘛,毕竟也没别的线索了。

  1. 呃……你刚刚是不是填了 9 来着?这不是答案,而且我说了,这题没有正确答案。
  2. 不过嘛,说不定它是个什么幸运数字,你可别忘了哦。
  3. 提到这个,顺便一说……其实,我说这么多废话,费那么多口舌,目的也很单纯。
  4. 就是为了说着说着让你忘了我之前都说了些什么。

到这里玩家应该意识到要对前面的内容做记录了;而就在此时,第一条重要信息刚过去,玩家多少该留有印象才对。

  1. 本关的回合数是不可以重置的哦。之前我说过的那些话,你不会再看到第二遍了。
  2. 当然也不是完全没有办法的啦。你可以重新创建一个账号,再把前面的题做一遍。
  3. 不过因为本次解谜出了一大堆巨恶心的交互题,所以我觉得,任何一个正常人都不会喜欢把这些题再做一遍的。

事实上的确如此……很多玩家错失了第一周目的信息,但因为麻烦不想开二周目重做一遍。

  1. 我自己也不例外。我还要一遍一遍调试那些题目,都快做吐了。
  2. 但是一想到玩家们要绞尽脑汁解这些巨恶心的交互题,我就感到心情舒畅。
  3. 我每年过年的好心情就押在看大家做题上了。
  4. 当然……我也不是那么喜欢幸灾乐祸的人啦。
  5. 每年解谜我都有为通关者准备奖励的说。
  6. 只不过……这关没有答案来着,你过不了。
  7. 经常参与我的解谜游戏的人都知道,我喜欢「故技重施」。
  8. 所以,前两次解谜的最后一关都没有设置答案。

这是一个关键信息。既然前两次解谜的最后一关都没有设置答案,那么之前的那些人是如何通关的呢?

我们翻阅之前的解谜答案,会发现前两次解谜都是通过引导玩家加 QQ 群来通关的。既然这次也是「故技重施」,那么就有理由怀疑本题会给你提供有关 QQ 群的信息

  1. 这样大家都过不了关,那奖励不就是我的了吗?
  2. 一开始还挺忐忑;后来看到大家一边做题一边骂,我就放心了。
  3. 哎呀,这不叫私吞,没有那么恶劣,充其量只是物归原主而已。
  4. 但是还是有一些幸运儿可以拿到奖励的。不过……我不建议你抱这样的侥幸心理哦。
  5. 经常参与我的解谜游戏的人都知道,每次解谜能拿到奖励的人,屈指可数。
  6. 你凭什么以为自己可以得到这个机会?凭你和我聊了三十几个回合的天吗?
  7. 还是你觉得自己可以技压群雄,在众多参与者中占有自己的一席之地?
  8. 放弃吧,不要痴人说梦了。(对话结束)
  9. (对话已结束)
  10. (对话已结束)
  11. (对话已结束)
  12. (对话已结束)
  13. (对话已结束)
  14. (对话已结束)
  15. (对话已结束)
  16. (对话已结束)

这里的「对话已结束」没有骗到多少玩家。多数人心里还是清楚的,这题还没完呢。

  1. 真是阴魂不散……你有完没完啊?
  2. 你在这里一直自言自语地提交些我不想看的内容,到底是为了什么?
  3. 为了金钱吗?不可能的,我今年的宣传帖没说有一分钱的奖金。
  4. 为了名声吗?不可能的,这种藉藉无名的小比拼,就算赢了又能得到多少人关注呢。
  5. 为了什么?你告诉我,就在此时此地告诉我。
  6. 哦……原来如此。
  7. 你要是真有这么自信的话,那我先来问你一个问题。它的答案是一个阿拉伯数字哦。
  8. 「鸿运当头,便可托物言志。」

接下来的这几关都是谜语题。如果你答对了,会提示「正确」;如果答错了,会提示「错误」;如果答案本身错误,但可以作为其它题目的答案,会提示「错误,这是另一个问题的答案」。说白了,这是一个群号 Wordle 游戏。

「鸿运当头」意为运气好,而「托物言志」是指用某个事物来表示此意,那么之前提到的幸运数字 9 就是正确答案了。

  1. 接下来我再考你几个问题。每个问题的答案都是一个阿拉伯数字。
  2. 老规矩,你只有一次机会。如果你答对了,我会告诉你;如果你答错了,我也会告诉你——告诉你答对了还是答错了。要是答错了,那你就自己去猜答案吧!
  3. 请听第二题:「微言大义,何数藏于题面?」

题面就两句,其中最值得注意的便是那一串黑色方块。复制下来或者开 F12 看一眼,就能发现其中的第九个方块与众不同。(脉冲星:「遇到不明字符感觉复制加f12都是基操了」)

  1. 请听第三题:「象形指事,及至转注假借。」

这是六书理论的内容。六书:象形、指事、会意、转注、假借。

  1. 请听第四题:「勾陈当空,何人指路于此?」

勾陈乃北极星。北斗七星指向北极星。

  1. 请听第五题:「目之所及,何数亘古不变?」

**整个屏幕遍是目之所及的范围,而唯一不变的就是通关率 0。别忘了,这题没有正确答案,所以大家都通不了关。

  1. 请听第六题:「算珠参差,上三去五进一。」

在珠算进位加法时,若满10破5,则有「八上三去五进一」之口诀。

  1. 请听第七题:「满腹经纶,诗书礼易春秋。」

儒家五经:《诗》《书》《礼》《易》《春秋》。

  1. 请听第八题:「气之始源,柏拉图之己见。」

在柏拉图的四元素理论中,柏拉图把四种正多面体与四种元素联系起来。而气元素对应的是正八面体。

  1. 请听第九题:「恒星之始,亦为宇宙之初。」

恒星生命早期的大部分成分都是氢;宇宙演化早期的绝大部分原子也是氢。氢的原子序数为 1。

  1. 好了,所有题目都问完了。

以上九个问题的答案组合起来,便是本次解谜通关群的群号 996708581

乙巳年概览

时间:2026-02-14 19:19:19 至 2026-02-16 07:19:19

这是第四年也是最后一年出解谜了。本次我把填空题和交互题平衡了一下,但因为错误估计了 ColorBlending 的难度,导致不少玩家直接通过这关直达 ReadItOverAndOverAgain 了。

第一位通关者是 12345qwerth,在游戏开始 133 分钟后通关。

在规定时间内,共有 17 人通过了本次解谜。

前六题的一血名单如下:

  • Tanpinsary 于 19:21:03 拿下 Festival
  • Kaedesnow 于 19:30:38 拿下 Chain
  • 12345qwerth 于 19:55:44 拿下 UnoRubik
  • huichen83 于 20:32:00 拿下 QuaterPrime
  • huichen83 于 20:41:22 拿下 SwitchAndTranspose
  • 0xffffff 于 21:15:41 拿下 ColorBlending

Festival

下图展示了 12 张 Husky 头像,每张都对应着一个中/西方传统节日,请指出它们的名字。你必须使用简体中文,一次性答对全部问题才可以通关。

解析

这关大部分的问题都很好猜,但有几个真的很抽象,AI都做不出来。

不过这关会告诉你错了几个问题,所以你可以很快锁定出错的问题。

复活节

彩蛋意味着复活节,这个很简单。

万圣节

南瓜头意味着万圣节,这个也很简单。

端午节

粽叶意味着端午节。

情人节

图中丘比特的弓箭是情人节的象征。

春节

放鞭炮当然是过春节。

圣诞节

戴圣诞帽当然是过圣诞节。

七夕节

这个头像被评价为太过后现代了,大家很难看懂。其实它描绘的是一群喜鹊在搭桥,所以是七夕节的“鹊桥”。

中秋节

背后巨大的月亮当然是中秋节的象征。

元宵节

虽然并不是只有元宵节存在吃元宵的习俗,但元宵节无疑是其中最重要的一个。

腊八节

腊八粥都扣头上了,只能是腊八节吧。

清明节

清明节上柱香,祭拜一下。

重阳节

这个也是很难猜的,其实耳中所插是茱萸。

Chain

这关的题目 Chain 暗示了这些字母可能连成一个词或句子。

既然如此,字母的方向就可能意味着不同字母间接续的关系。有可能字母的“头”指向下一个,也有可能字母的“尾”指向下一个。

但是,有些字母高度对称,比如 I, O, H, S, Z,它们的方向有多种可能性,所以需要你多做些尝试才能解出来。

另外,有些玩家不慎把 W 认成 M 了,但其实它们在字体上是有显著差别的。

这题可以编程求解或者硬看,硬看也不难。

另外有玩家提及,题面的 SVG 文件本身就暴露了字母的顺序,这点我确实始料未及,早知该用 PNG 的。

编程求解

把一些字母连成一条线,这是一个 Halmilton 路径问题,可以运用一些图论知识求解。

#include<iostream>
#include<vector>
#include<string>
std::vector<char> info{'H','S','Y','N','O','A','D','S','I','Z','O','N','O','I','R','W','I','N','E','O','S','T','H','H'};//以H,S,Y,N,O为第一行,从此处开始,记录每个下标对应的字符
std::vector<std::vector<int>> edges{
    {5},{0,2},{1},{2,4},{3},
    {6},{10},{2,12},{7,9},{4,14},
    {5,15},{10,12},{8,16},{9,17},{13},
    {20},{15,17},{21,13},{23},{14},
    {16},{22},{18},{19},
};//以邻接表的形式记录有向图
void bfs(int node,int depth){
    static std::string stack(info.size(),0);
    static std::vector<bool> visited(info.size(),false);
    if(visited[node])
        return;
    visited[node]=true;
    stack[depth]=info[node];
    std::clog<<stack<<std::endl;
    if(depth==info.size()-1)
        std::cout<<stack<<std::endl;
    else
        for(int next:edges[node])
            bfs(next,depth+1);
    stack[depth]=0;
    visited[node]=false;
}//通过深度优先搜索,找寻所有可能的解
int main(){
    bfs(11,0);//通过观察,发现只有编号11的节点是没有入度的;因此,它必然是搜索的起点
    return 0;
}

执行一下会发现,所有字母都出现的结果只有 NOISYSHADOWSINTHEHORIZON 一种(当然也有可能是倒过来的),所以答案就是 noisy shadows in the horizon(大小写不敏感,也不强求打空格)。

UnoRubik

你在一个三阶魔方表面的格子上移动,需要从初始位置起,走到目标位置橙色的0。

你可以旋转这个三阶魔方,其规则与一般三阶魔方无异。但你在魔方格子之间的移动受到以下规则的限制:

  • 你只能在同一个面的格子间移动;
  • 你只能移动到上、下、左、右直接相邻的格子上;
  • 你只能移动到颜色相同,或数字相同的格子上。

思路

在本题中,你只能看到魔方的主视图,所以首要工作是掌握它的全貌。直接翻面把所有面都过一遍就好了。

因为起点和终点所在格的颜色不同,所以至少需要用到一次“相同数字”的规则来实现跨颜色的移动。

而红格和橙格间的共同数字只有0和9,于是我们自然期望通过0或9来实现跨颜色。

但试过之后就会很快发现问题所在:无论怎么转魔方,你也不能一次性从红0/9走到橙0/9。原因在于,它们不具备位置上相邻的可能性

为了便于分析,我们将魔方表面的 54 个格分为两大类:

  • I 类格分布于每个面的四角和中心,共 30 个;
  • II 类格分布于每个面的棱上,共 24 个。

不难发现:I 类格只能与 II 类格相邻,反之亦然。

所以红9和橙9都是 I 类格,它们就不可能相邻,也不存在一步走到的可能性。

因此要实现跨颜色移动,必须要找到两个相同数字,而且它们一个在 I 类格上,一个在 II 类格上。

接下来对魔方所有格上的数字进行统计,发现:

颜色I 类格II 类格
0, 1, 8, 9, 122, 5, 11, 12
0, 4, 8, 9, 161, 2, 5, 11
绿0, 4, 7, 13, 142, 5, 11, 16
0, 3, 4, 13, 147, 10, 11, 15
0, 4, 6, 13, 143, 10, 15, 17
0, 4, 8, 9, 136, 10, 15, 17

因此无论怎么规划路线,必定经过的几步移动都是:

  • 从红 0 出发先移动到红 1;
  • 借助数字 1 从红格移动至白格;
  • 借助数字 16 从白格移动至绿格;
  • 借助数字 7 从绿格移动至蓝格;
  • 借助数字 3 从蓝格移动至黄格;
  • 借助数字 6 从黄格移动至橙格;
  • 最后移动到橙 0。

理解以上原理后,实际移动时,只需通过合理的旋转,将两个相同数字的异色格转至相邻位置,即可实现跨色移动;再通过相同颜色的格子作为中介,实现跨数字移动即可。

QuaterPrime

你可以在任何时机提交。

你的作答根本无关紧要。

本关被普遍认为是最难的关卡,通关率不及 ColorBlending(当然主要原因是我高估了 ColorBlending 的难度),而且很多人做出来不是靠发现了规律,而是运气太好碰出来了。

本关的原本要求是,玩家必须在 UNIX 时间的「质数秒」点击提交按扭(因为信息发送到服务器可能有延迟,所以给了 3 秒的容忍度),连续且不重复 4 次。但实际上我忘了实现“不重复”这个逻辑,所以很多人只要坚持不懈地连点,就不难达到期望的结果。

根据素数定理可以估算,在当前 UNIX 时间 附近质数的密度大约是 4.70%,再加上 3 秒的容错时间,就是 4 倍的概率,要刷出一个 SimPrime 是很简单的。如果遇到接连的四个素数距离比较接近,运气好就可以直通了。

思路

题面的意思是说时间很重要,而作答内容不重要。

「QuaterPrime」中的「Quater」是拉丁语数字前缀,意为 4 次;而「Prime」是质数的意思。

如果要和时间有关的话,四次质数……?可能意味着你需要四次质数时间,可能是小时、分钟、秒,可能是绝对时间或时间间隔。

本题涉及的拉丁语数字前缀共有以下几个:

前缀含义
Nulli0 次
Sim1 次
Bi2 次
Ter3 次
Quater4 次

因此你可以通过反馈信息,看出你当前有几次符合条件的提交,进一步佐证自己的推论。

要想到使用 UNIX 时间本身相当困难,但证伪一些不成立的可能性还是很简单的。

你可以看到自己的提交记录,它们为你提供了一些可用的信息。服务端的 3 秒容错成了干扰,但它只会干扰你证明一种方法有效,不会太干扰你证明一种方法无效。

要证伪时间间隔的作用,可以等时间间隔地进行提交。

要证伪特定的时、分、秒的作用,可以控制变量,保持其它因素不变,测试出变化量,便可逐步排除这些可能性。

SwitchAndTranspose

有一个 汉字矩阵,其中所有 25 个字都是左右结构的。你可以对该矩阵执行以下操作:

  • Row Switching:交换矩阵的两行;
  • Column Switching:交换矩阵的两列;
  • Submatrix Transposing:选定一个 子矩阵,进行转置。

你需要通过反复进行以上操作,实现:

  • 一行的左偏旁相同;
  • 一列的右偏旁相同。

preview

本关并不如它初看起来的那样难,稍微花一些时间就能解决;而且也没有什么解题新花样,我就不赘述了。下文只证明任何一个这样的问题都有解,以及给出一定可解的方案。如果想要优化,就请自行探索吧。

解析

置换群及其基本元素

这是一个置换群问题。例如,我们可以把 上的转置记作 。括号中表示的是一个循环,即前一个元素所在位置由下一个元素代替,最后一个元素所在位置由第一个元素代替。

再比如,第1、2行置换可以记为 。简洁起见,我们简写为 。列置换同理,我们简写为

定义置换群 表示在这个汉字矩阵中能进行的所有置换。特别地, 表示不做任何操作,这也是 的单位元。乘法运算符 表示左操作循环和右操作循环的叠加。注意到它不满足交换率,但若左操作数和右操作数中没有相关元素,它们也可以交换。

根据上面的举例我们知道,所有的 都是 的元素。但注意,不意味着。如果我们要下此论断,还得严格证明才行。

我们希望这个矩阵中的任意两个元素都是可置换的(仅置换这两个元素,而其它元素位置不变),即 。而我们已经知道:

  • (置换一整行);
  • (置换一整列);
  • (置换次对角方向的相邻元素)。

那么接下来我们的目标就是找到更多属于 的元素。

可置换关系

汉字矩阵上两个位置的元素可置换,这是一个二元关系。我们定义二元关系“可置换”,表示在矩阵中仅置换这两个元素,且其它元素位置不变,记为 当且仅当

可置换关系具有传递性,即如果 ,那么 。证明很简单:

我们已经知道次对角方向的相邻元素可以置换。根据可置换关系的传递性可以推出,同在一个次对角线上的所有元素都可以置换。举个例子,如果要置换 ,只需要这样:

主对角线方向的置换

转置操作只能置换次对角线上的相邻元素;要实现主对角线上元素的置换,就要另寻他法。

不妨思考,如果把需转置的两行先置换一次,原本在主对角线上的两个元素不就到次对角线上了吗?这样就可以置换它们了。最后只需再把这两行置换回来,就可以保证两个目标元素得到置换,且其它元素位置不变。

举个例子,假如我们要置换

现在我们能证明主对角线方向上的相邻元素是可置换的。根据可置换关系的传递性,我们得到结论:同在一个主对角线上的所有元素都可以置换

根据传递性,我们进一步得到结论:通过对角线方向可达的任意两个元素都是可置换的

沿行/列方向的置换

接下来我们要证明同行的相邻元素是可置换的。因为行与列在本问题中地位相等,所以证明了行方向上可置换,就能证明列方向上可置换。

为例:

如果要置换 呢?可以先把第一行置换到第二行,再如法炮制,最后置换回去就好了。

现在我们能证明沿行/列方向上的相邻元素是可置换的。根据可置换关系的传递性,我们得到结论:同在一行/一列上的所有元素都可以置换。进一步通过传递性可以得到结论:任意两个元素都可以置换

一般来说,沿对角线方向进行置换所需的步数较少。所以在实操过程中,建议先沿对角线方向移动,使要置换的两个元素靠近,再沿行/列方向进行移动。

ColorBlending

在由五个方块构成的十字形中,每个方块都有一个特定颜色。

你可以通过在方块上添加 透明度的前景色进行颜色合成,从而改变方块颜色。

你可以选择的前景色只有 8 种:黑、白、红、绿、蓝、青、紫、黄。

请注意:你可以任意选择十字形的四个边缘方块进行颜色堆叠,而中心方块总会受到相同的堆叠效果。

你需要把操作区内每个方块的颜色都变成和目标颜色完全相同,才可以通关。

这关大家搞出了思路各异的玩法,可以说是本次解谜的精华题,让我找到了当年 Art 那题大家八仙过海的感觉。

本关可行的解法多种多样。它们的思路大致可以分为以下三类:

微调解法

这是我在一开始出题时构思的解法。它是相当麻烦的方法,但对玩家要求最低。你哪怕什么都不会,也可以解开本题。

我的论证过程可能有些冗长,但一定严谨。

TL;DR

你会发现,当你使用较小的 时,如果前景色选择得当,你可以做到只改变一个颜色的某一个分量,而保持其它分量不变。

更进一步,你也可以做到只改变一个颜色,而另一个颜色不变。

恭喜,你已经知道了怎么解题,现在就去搞定它吧!


中心方块的颜色受到的影响太多,不方便研究,我们先来考虑最简单的情况:只给定一个方块,如何合成目标颜色?

基本公式和运算符

一个颜色是一个三维向量,可以使用 表示它。

根据维基百科我们查到,在背景色 上叠加 透明度的前景色 ,得到的新颜色 为(当然,如果颜色值域是离散的,那么结果值也会进行舍入):

为了易于表示,我们定义一个新运算 ,表示以左操作数为背景色,叠加 透明度的右操作数作为前景色。那么上面的公式也可以表示成

我们简单分析一下 的性质。对于任意给定的

所以它不满足交换律;对于任意给定的

所以它不满足结合律,对于任意给定的

说明如果 它满足消去律;对于任意给定的

所以它满足幂等律。

舍入逼近

先来思考一个问题:从 出发,如何构造

如果选择 作为前景色,那就存在一个问题:R 分量和 G 分量会同步增长,结果就是 ,其中

而如果选择 作为前景色,我们就需要找到一个 使得 ,那我们就可以解方程组:

这个 似乎是无解的。但还有一个重要问题被我们忽视了:计算结果的舍入

还是以 作背景色,我们取 作前景色。我们知道,当 时,结果是 ;当 时,结果是 ;可当 时呢?

为例:我们发现

16 比特位的色号无法支持小数,那么在遇到此类情况时,程序会如何处理呢?你实测一下就会发现它最后变成了 ,用的是四舍六入的规则。

如果取 或者 ,我们就会得到

这个 的取值很湊巧,多一点或者少一点都会得不到我们想要的结果;而恰恰是 这个值帮我们成功构造出了

这个值是通过解以下不等式得到的:

利用结果舍入来逼近我们希望达到的颜色,是整个谜题的最核心方法。

但是别高兴太早,我们再来思考一个问题:只用一步操作,可以将 变成 吗?

使用以上思路进行计算,我们选取 作为前景色,列出以下不等式:

不用往下做,一眼就看得出这个式子它是无解的。

但我们仍可能通过多步操作到达 。我们只需先将 变成 ,然后再把 转化成 即可。

我们发现,由 是无法一步到达 的;但由 却可以一步到达。这说明它们之间一定存在着某种更重要的差异。

为了得到一个完整的方法论,我们还需要做些系统性的思考。

分量容忍度

我们先考虑单个维度的分量,比如 分量。假定使用的前景色之红色分量为 ,那么在 较小时 的变化量因为小于 而不发生实质上的改变;而在 较大时 的变化量因大于 而发生实质上的改变。

这之间存在一个 的临界值,我们先随便找个字母 表示它。 的值可以这样计算出来:

因此当 时, 不会改变;当 时, 分量会变大。

同理,如果前景色之红色分量为 ,那么 也会有一个临界值,我们这次用字母 表示它。 的值可以这样计算出来:

因此当 时, 不会改变;当 时, 分量会变小。

描述的是一个分量能够容忍多大的 对自己起作用,而使自己保持不变的能力。我们将这种能力定义为一个分量的分量容忍度。如果前景色是 ,我们用大写字母下标 ;如果前景色是 ,我们用小写字母下标

对于一个任意背景色 ,它都有以下 6 个分量容忍度,可以按以下公式求出:

由这组公式不难看出,容忍度的大小取决于它到前景色的距离。比如 ,它的六个容忍度分别为

更近,容忍度就更高,所以要改变它,需要使用 ;而它离 更远,容忍度就更低,所以要改变它,只要使用 就够了。正因如此,由 出发一步构造 才成为可能。

容忍度将为我们带来一个重要结论。真正试过本关的人一定经受过“好不容易控制好一个分量,却又在调节另一个分量时把它给改了”的绝望感。

而由式 (3) 我们知道,一个分量到它的前景色距离越近,容忍度越高。容忍度高就意味着它不容易被改变。

因此我们如果先固定那些容忍度高的分量,再用它们能容忍的 去改变那些容忍度低的分量,就可以保证前者不再会受到后者的二次影响。

举个例子。我们希望由黑色 出发,得到 。我们发现, 分量离边界的距离是最近的,那么我们可以先选择白色作为前景色,构造出

接下来我们看到 的距离比 的距离更近,所以应该再设法固定 分量。 的分量容忍度为:

所以只要选择 的值然后用 一步一步逼近 即可。

最后我们处理 分量。 的容忍度为

所以只要选择 的值然后用 一步一步逼近,就能得到 了。


现在我们可以搞定外围颜色的合成;我们再来看中心颜色的合成。

在合成中心颜色时,我们将遇到更大的麻烦。因为中心颜色的叠加总是与外围颜色同步进行,所以在改变中心颜色的过程中,稍有不慎就会破坏原本已经调整好的外围颜色,且难以复原。

因此我们需要研究一个新的问题:如何改变一个颜色,同时保持另一个颜色不变

方向容忍度

在一个 RGB 三维空间上,所有可能的 RGB 值构成了一个正方体。8 种可选前景色位于正方体的八个顶点上。(关于此更详细的论述在下文插值解法中)

当我们选择使用某一个前景色进行叠加时,我们的操作其实是把背景色在 RGB 空间中的点向着顶点的方向拉动。

如果我们不希望改变一个颜色,我们就需要使得 小于它朝这个方向的每个分量容忍度。举个例子: 遇到前景色 时,要维持不变,使用的 不应超过多少?我们可以求出

所以我们使用的 不应超过三者的最小值 ,我们用符号 来记录它。

如果换一个前景色,比如 ,我们就应该计算 ,我们用符号 来记录它。

看得出来,一个背景色对不同前景色能容忍的 值也是不尽相同的。我们定义方向容忍度,表示一个背景色对某个前景色(方向)能保持自身不变的最大 值。

每个颜色具有八个方向容忍度,它们的公式分别为:

一个颜色的各方向容忍度取决于它们的各分量容忍度;而分量容忍度又取决于它们到每个分量最高值(255)/最低值(0)的距离。由此分析可知,一个颜色的各方向容忍度和它到各顶点的距离有关。

分析四个外围颜色的目标值可以发现,它们总是存在一个离得比较近的方向,分别是青、紫、黄、黑(这是我预先设计好的),而中心格的目标颜色位于它们之间,到各个方向的容忍度都比较低。

就以题目一开始的截图中展示的目标颜色为例,四个外围颜色到青、紫、黄、黑的方向容忍度分别为:

而中心格到这四个方向的容忍度是

所以只要使用小于 的青色作用于上方格,小于 的紫色作用于下方格,小于 的黄色作用于左方格,小于 的黑色作用于右方格就可以保持外围不动,而又能改变中心格颜色了。

插值解法

颜色空间与向量

我们将 构成的点放在三维空间中去看。因为每个维度的取值范围都是 ,所以所有 点组成的集合是一个正方体点阵。这种离散的空间不便于我们的后续研究,所以我们姑且假设 的取值可以是实数,也就是

根据维基百科我们查到,在背景色 上叠加 透明度的前景色 ,得到的新颜色 为(当然,如果颜色值域是离散的,那么结果值也会进行舍入):

回顾我们在中学时学过的向量运算知识,我们知道,这个式子意味着 位于线段 上,如下图所示:

越大, 就离 越近,离 越远。更精确地讲:

因此我们可以把叠加操作解读为一种发生在正方体上的“拖动”:在 之间画一条线段,按 的长度比例把 方向拖动。

现在给你一个颜色 ,求问:怎样合成出这个颜色?

因为 位于线段 上,而我们刚好有前景色 ,所以就能够合成出它来。只需求出

接下来再给你一个颜色 ,求问:怎样合成出这个颜色?

我们注意到 在线段 上,而它在修约之后就会得到 。所以我们可以用刚才合成的 和前景色 合成

再给你一个颜色 呢?

我们注意到 在线段 上,所以用刚才合成的 和前景色 就能合成出来了。

上述操作中各个颜色之间的关系可以由下图呈现出来。

对外围颜色插值

那么,假设没有刚才的那些步骤,而是直接给定一个 要合成,那我们该怎么做呢?答案是,反过来求出中介颜色。

第一步,我们由 出发,经过 作一条射线,它一定穿过 三个平面之一。求出这个交点,它是 。我们把它视为

第二步,我们由 出发,经过 作一条射线,它一定穿过 两坐标轴之一。求出这个交点,它是 。我们把它视为

这样两步下来,我们就得出了两个中介颜色,可以用它们来进行正向叠加了。

但是这里有个潜在问题。我们把 直接视为 来处理,又把 视为 来处理,这之间必然存在误差。在有些情况下,这种误差会大到超过 ,致使我们插值时求出的交点与正向推导时使用的中介颜色并不相同。这样的话,我们还能十分自信地使用这个插值结果去做叠加吗?

其实不必为此担心,我画个简单的草图你就可以理解了。

假设 是前景色, 是合成 所用的背景色。

叠加进行合成时,首先得到 ,而 会修约到 ;射线 又与另一个边界交于 ,而 被我们修约为

问题来了:现在拿着与 不同的 作为背景色再用 叠加,得到的 会修约为 以外的值吗?从图中看当然是不会的。

原因在于,在合成之前, 本来就不会超过 ;而合成之后的 只会比它更小,那就更不可能超过 了。合成操作是会缩小误差的,所以我们在插值结果中人为引入的那些误差其实根本不值一提。

读者可能会好奇用 为何也能得到正确的结果——但其实这是很自然的事情。虽然 是大于 的,但在合成操作之后,这个差值也被缩小到小于 ,自然也会修约到 了。

下面的 SageMath 代码是我做插值分析时使用的,可供参考。其实它的计算量不大,所以手算也绰绰有余了。

def calc_alphas(c1,c2,c3):#Ensure c1>=c2>=c3
	alpha_1=c3/255.;
	c2_1=(c2-c3)/(1-alpha_1);
	c1_1=(c1-c3)/(1-alpha_1);
	alpha_2=c2_1/255.;
	c1_2=(c1_1-c2_1)/(1-alpha_2);
	alpha_3=c1_2/255.;
	return [alpha_3,alpha_2,alpha_1];
def breakdown_color(r,g,b):
	items=[("red",r),("green",g),("blue",b)];
	items=sorted(items,key=lambda kv:kv[1],reverse=True);
	alphas=calc_alphas(items[0][1],items[1][1],items[2][1]);
	accumulated=[];
	current=[];
	for name,alpha in zip([name for name,_ in items],alphas):
		current.append(name);
		accumulated.append(("+".join(current),alpha));
	return accumulated;

对中心颜色插值

通过刚才的演算我们可以只用 12 步就调好外围颜色。但中心颜色呢?

有一位玩家提出了这种思路:

在第一步混出外围格的时候记录一下三种颜色加黑色的比例,然后可以把中心格分解成周围格的不同透明度叠加。

因为之前有 ,所以可以构造 。而对中心格 ,这个操作就是

这样就可以实现维持 的同时改变

乍看起来这个推导似乎是成立的,但事实真的如此吗?不然。

叠加操作是不满足结合律的。我们不能简单地把 当作单个颜色,直接叠加到 上,而必须把 逐个叠加到前一颜色上。

而在逐步叠加的过程中,我们仍可以总结出一套经得起推敲的解法。

如果我们分别用 的透明度去进行叠加 ,并期望合成结果仍为 ,我们将会得到下述等量关系:

而对 也同步做这一系列叠加操作,并将式 (4) 代入其中,我们将会得到

我们记 ,上式也可以写作

它的含义非常明确:按 的长度比例把 方向拖动。

本关有一则隐含信息——中心格的目标颜色一定位于外围格构成的四面体内部。因此我们可以运用与刚才相似的插值方法,通过两个中介颜色,把 分解成四个颜色的叠加,如图所示。

颜色分解我们已经解决了,接下来的问题是如何对每个外围颜色,求出一组合适的 ,从而保证一轮操作下来,维持外围颜色不变,又能拖动中心颜色。

根据式 (4) 我们得到

试图求解这个式子还是有些难度的,不建议手算,但确实可以求出来。

整个操作的步骤应为 24 步,包含前期处理外围格的 12 步和后期处理中心格的 12 步。感兴趣的读者可以尝试一下。

搜索解法

不少玩家使用了编程方法进行解题。比如模拟退火、进化算法等,效率较高的玩家可以在 15 步以内稳定完成任务。

我对这些算法并不熟悉,无法提供解析,就放一份 0xffffff 玩家的求解代码好了。

import torch
import numpy as np
import time
from scipy.optimize import minimize

DEVICE = torch.device("cuda")
POPULATION_SIZE = 1000000 # 种群大小:100万
MAX_STEPS = 15            # 操作15步足够了
GENERATIONS = 500         # 进化代数
MUTATION_RATE = 0.1       # 变异率

TARGET_DICT = {
    "top":    [81, 32, 23],
    "left":   [79, 157, 254],
    "right":  [55, 240, 78],
    "bottom": [221, 68, 229],
    "center": [156, 157, 140]
}

# 题目里的调色板颜色, 归一化到 0-1 之间
PALETTE_RGB = torch.tensor([
    [0, 0, 0], [255, 255, 255], [255, 0, 0], [0, 255, 0],
    [0, 0, 255], [0, 255, 255], [255, 0, 255], [255, 255, 0]
], device=DEVICE, dtype=torch.float32) / 255.0
COLOR_NAMES = ["黑", "白", "红", "绿", "蓝", "青", "紫", "黄"]
SIDE_NAMES = ["top", "left", "right", "bottom"]

#目标 Tensor [5, 3] (top, left, right, bottom, center)
target_list = [TARGET_DICT[s] for s in SIDE_NAMES] + [TARGET_DICT["center"]]
TARGET_TENSOR = torch.tensor(target_list, device=DEVICE, dtype=torch.float32) / 255.0

def simulate_batch(actions, alphas):
    """
    actions: [Pop, Steps] (int: 0-31, 编码了 side 和 color)
    alphas:  [Pop, Steps] (float: 0-1)
    返回: [Pop, 5, 3] (每个个体的最终5个方块颜色)
    """
    batch_size = actions.shape[0]
    
    # 初始化状态:全白 [Pop, 5, 3]
    current_state = torch.ones((batch_size, 5, 3), device=DEVICE, dtype=torch.float32)
    
    # 解码动作
    # side_idx: 0-3, color_idx: 0-7
    side_indices = actions // 8  # [Pop, Steps]
    color_indices = actions % 8  # [Pop, Steps]
    
    # 时间步循环 (无法避免,但只有11次循环,开销很小)
    for t in range(MAX_STEPS):
        # 获取当前步的参数 [Pop]
        s_idx = side_indices[:, t] # Side
        c_idx = color_indices[:, t] # Color
        a_val = alphas[:, t].unsqueeze(1) # Alpha [Pop, 1]
        
        # 获取颜色向量 [Pop, 3]
        color_vec = PALETTE_RGB[c_idx] 
        
        # 更新逻辑
        # 1. 创建掩码 Mask [Pop, 5]
        # side_indices 对应 0:top, 1:left, 2:right, 3:bottom
        # center 中心方块 (索引4) 永远受影响
        
        # 生成基础掩码 (Center总是受影响) mask shape: [Pop, 5]
        # 对每个样本,生成一个 one-hot 向量表示哪个边缘受影响
        # 然后把第5列(center)全设为1

        mask = torch.zeros((batch_size, 5), device=DEVICE)
        mask.scatter_(1, s_idx.unsqueeze(1), 1.0) # 设置边缘
        mask[:, 4] = 1.0 # 设置中心
        
        mask = mask.unsqueeze(2) # [Pop, 5, 1]
        
        # 2. 混合颜色
        # Target_Color [Pop, 1, 3]
        target_color_expanded = color_vec.unsqueeze(1)
        
        # New = Old * (1 - alpha) + Color * alpha
        # 仅当 mask=1 时应用 alpha,否则 alpha视为0 (保持原样)
        effective_alpha = a_val.unsqueeze(1) * mask # [Pop, 5, 1]
        
        current_state = current_state * (1.0 - effective_alpha) + target_color_expanded * effective_alpha
        
    return current_state

def run_evolution():
    print(f"种群大小: {POPULATION_SIZE}, 最大步数: {MAX_STEPS}")
    
    # 初始化种群
    # 动作: 0-31 (4 sides * 8 colors)
    pop_actions = torch.randint(0, 32, (POPULATION_SIZE, MAX_STEPS), device=DEVICE)
    pop_alphas = torch.rand((POPULATION_SIZE, MAX_STEPS), device=DEVICE) * 0.9 + 0.05
    
    best_loss = float('inf')
    best_ind = None
    
    start_time = time.time()
    
    for gen in range(GENERATIONS):
        # 评估
        final_colors = simulate_batch(pop_actions, pop_alphas)
        # 计算误差
        # [Pop, 5, 3] - [5, 3] -> [Pop, 5, 3]
        diff = (final_colors - TARGET_TENSOR) * 255.0 # 放大回0-255区间计算误差
        loss = torch.mean(torch.abs(diff), dim=(1, 2)) # L1 Loss [Pop]

        # 选出前 10% 的个体作为精英
        elite_size = int(POPULATION_SIZE * 0.1)
        sorted_loss, sorted_indices = torch.sort(loss)
        current_best_loss = sorted_loss[0].item()
        if current_best_loss < best_loss:
            best_loss = current_best_loss
            best_ind = (pop_actions[sorted_indices[0]].clone(), pop_alphas[sorted_indices[0]].clone())
            print(f"Gen {gen}: Best Error = {best_loss:.4f}")
            
            if best_loss < 0.8: # 误差收敛说明找到了!
                print("停止进化,进入微调阶段。")
                break
        
        elite_indices = sorted_indices[:elite_size]
        elite_actions = pop_actions[elite_indices]
        elite_alphas = pop_alphas[elite_indices]
        
        # 繁殖
        # 完全基于精英进行变异生成下一代, 复制精英填充整个种群
        repeat_factor = POPULATION_SIZE // elite_size + 1
        next_actions = elite_actions.repeat(repeat_factor, 1)[:POPULATION_SIZE]
        next_alphas = elite_alphas.repeat(repeat_factor, 1)[:POPULATION_SIZE]
        
        # 变异 随机改变 10% 的步骤的动作
        mask_act = torch.rand_like(next_actions.float()) < MUTATION_RATE
        rand_act = torch.randint(0, 32, next_actions.shape, device=DEVICE)
        next_actions = torch.where(mask_act, rand_act, next_actions)
        
        # 变异 Alpha在原有基础上加噪声
        mask_alpha = torch.rand_like(next_alphas) < MUTATION_RATE
        noise = torch.randn_like(next_alphas) * 0.1
        next_alphas = torch.clamp(next_alphas + noise * mask_alpha, 0.01, 0.99)
        
        # 保留上一轮的最强精英不变
        next_actions[0] = elite_actions[0]
        next_alphas[0] = elite_alphas[0]
        
        pop_actions = next_actions
        pop_alphas = next_alphas
        
    return best_ind

# L-BFGS
def fine_tune(best_action_tensor, best_alpha_tensor):
    print("微调")
    # 转为 CPU numpy
    actions = best_action_tensor.cpu().numpy() # [Steps]
    init_alphas = best_alpha_tensor.cpu().numpy() # [Steps]
    target_flat = TARGET_TENSOR.cpu().numpy().flatten() * 255.0
    
    # 步骤结构
    steps_struct = []
    for i in range(MAX_STEPS):
        act = actions[i]
        steps_struct.append({
            'side': act // 8,
            'color': act % 8,
            'rgb': PALETTE_RGB[act % 8].cpu().numpy()
        })

    # 优化目标函数
    def objective(alphas):
        curr = np.ones((5, 3)) # White
        for i, step in enumerate(steps_struct):
            a = alphas[i]
            side_idx = step['side']
            col_vec = step['rgb']
            # Mask
            mask = np.zeros((5, 1))
            mask[side_idx] = 1
            mask[4] = 1 # Center
            curr = curr * (1.0 - a * mask) + col_vec * (a * mask)
            
        # 误差不取整,为了梯度
        diff = (curr * 255.0).flatten() - target_flat
        return np.sum(diff**2)

    # 优化
    bounds = [(0.0, 1.0)] * MAX_STEPS
    res = minimize(objective, init_alphas, bounds=bounds, method='L-BFGS-B', tol=1e-8)
    
    return steps_struct, res.x


if __name__ == "__main__":
    best_actions, best_alphas = run_evolution()
    
    final_struct, final_alphas = fine_tune(best_actions, best_alphas)
    
    print("\n" + "="*40)
    print("计算结果")
    print("="*40)
    
    # 模拟验证
    curr_int_chk = np.ones((5, 3)) * 255.0
    for i, step in enumerate(final_struct):
        side_name = SIDE_NAMES[step['side']]
        color_name = COLOR_NAMES[step['color']]
        alpha = final_alphas[i]
        
        # 过滤掉 alpha 极小的无效步骤
        if alpha > 0.001:
            print(f"步骤 {i+1}: 颜色【{color_name}】 Alpha: {alpha:.4f} -> 点击【{side_name}】")
            col = step['rgb'] * 255.0
            mask = np.zeros((5, 1))
            mask[step['side']] = 1; mask[4] = 1
            curr_int_chk = curr_int_chk * (1-alpha*mask) + col * (alpha*mask)
            
    final_res = np.round(curr_int_chk).astype(int)
    
    print("-" * 40)
    print("混色验证:")
    sides = ["top", "left", "right", "bottom", "center"]
    total_err = 0
    for i, s in enumerate(sides):
        pred = final_res[i]
        targ = (TARGET_TENSOR[i].cpu().numpy() * 255).astype(int)
        err = np.sum(np.abs(pred - targ))
        total_err += err
        match_mark = "✓" if err == 0 else f"✗ (Diff {err})"
        print(f"{s.upper():<7}: 目标{targ} | 结果{pred}  {match_mark}")
        
    print("误差: ",total_err)

ReadItOverAndOverAgain

我们知道,在数学上,∞=∞+5=∞+999995。

因此如果你通过本关,你的分数就是∞。

所有通关者的分数都是∞。大家人人平等,并无高下之分。

或许你是完成了ColorBlending的、难得一见的解谜高手,凭借自己坚韧的决心、超凡的智慧、渊博的学识和敏锐的感知完成那道题,才能在排行榜上一马当先,获得甩开众人几个数量级的得分。

说实话,我从未期望过有人可以在短短一天半的时间内解开这道题。光是自己求解就花了我几天的时间。

因此解开ColorBlending的玩家理应得到比另一条线上的玩家们更多的嘉奖,不是吗?

但很遗憾,没有。

ColorBlending看似简单,但实际做下去就会发现它极度复杂。

相反,一些看上去很可怕的关卡,诸如作为另一条线之压轴关的SwitchAndTranspose,解起来反倒不费多少时间。

真是抱歉,在我的解谜游戏中,很多东西都不像看上去的那样理所当然。

包括这题。

当你看到这题之前,你可能还在浮想联翩。作为解谜末班车的最后一期的最后一题,你或许已经设想过999种可能性吧。

不过你是否想过会是这样一篇又臭又长,还言之无物的文案?

呃……好吧,已经有2025年1月26日的前一期作为先例了。

但在那一次,至少你还能和系统进行交互,而系统也最终在你的软磨硬泡下松了口。

那之后我便觉得最后一题出得还是太简单,怎能将答案轻易予人呢?

分明应当把答案自玩家手中夺走才对。

所以今年我连输入框都省了,半点交互也没有。如何呢,是不是很惊喜?

这种事情的原因就像191*251=47941的原因一样,根本没有解释的必要。

我只是非常享受折磨玩家的过程罢了。

去年不少玩家因为没有仔细看内容,错过了不可挽回的情报。遗憾之下只好重启一个新周目,再刷一遍那些交互题。

不知那些玩家今年如果再来本期谜题,是否会对此记忆犹新?又是否会吸取教训,仔细看这些内容呢?

唔……不过,这些话对于没仔细看这段内容的玩家们来说,是完全无效的。

这是一个死锁。

为了提醒玩家仔细看完这些烦人的内容,我设置了多重机制:

1去掉了所有的交互功能。玩家不能交互,就只好在文本中找情报。

2直接在标题中提示玩家。顺便一提,我原本打算把标题叫做ReadItAgainAndAgain,但我转念一想,标题越长才越有气势嘛,于是改成了现在这个。

3在交流群强调,文本至关重要。(韵脚成功押到)

4直接在文本中提示玩家,这么做意义不大,因为会看到这段文本的玩家也用不着我提示。

有了这些机制,我就可以继续冠冕堂皇地说:瞧啊,我给你们的提示是多么到位!

然后心安理得地罔顾玩家的辛苦付出。Who 他妈 cares?

我看的就是你们绞尽脑汁也百思不得其解的过程。

因此我花整整一年时间思考难题。

我的鬼点子很多,但把它们转化成谜题的效率很低,因此总是耽误不少时间。

我每周能想到45个点子,它们大都在我醒来时消失得无影无踪,难觅其形。

能保留下来已然不易,怎样把它变得可以实现又是一个难题。

高端的灵感往往只需要采用最朴素的实现方式。

像是2024年最后一题的二维码,就是用很朴素的ASCII艺术字实现的。

在那之后,我开始了对“交互题”近乎癫狂的追求。而在AI显著进步的当下,这些交互题也确实起到了“把AI问冒烟”的效果。

不过那只是因为对玩家而言,交互题要构造合适的prompt更难吧,我不知道。

另外值得一提的是,交互题倒把YouXam累得够呛。

因为每道题都要实现一套完全不同的逻辑,所以工作量非常大。

而传统的填空题就很简单了,反正放个题目写个正则表达式作判断就行了,小学生都能学会。

而今年轮到我自己写代码了。不过实际情况没有那么不堪,我在2月13日就早早写完代码了。

你若是问我有什么不传之秘,很简单,用AI啊。Claude,Codex都不错。

不过Anthropic或者OpenAI连点广告费也不曾给我,我是不是应该把这段有广告嫌疑的内容隐藏一下比较好?

呃,算了,还有更重要的事要做。

说回正题。

今年是解谜末班车的最后一期了,我还是对这个活动有些怀念的。

想我第一次办解谜的时候,我还没办过解谜,难免有些生疏。

细数下来,似乎每次解谜都有被称道出得很妙的题,当然每年也有被喷得很惨的题。

今年最后一期恐怕也不能免俗。我个人还是很期待玩家们提供反馈的。

我本人对于玩家指出的谜题专业性问题向来都是坦诚承认,欣然接受。

而对于玩家指出的谜题难度问题,表面上是不置可否,内心里巴不得再出难点要你们叫苦连天。

我看的就是这个桥段好不好。

但一味地加难度也没什么意思。其实,我一直追求谜题的趣味性。我认为1道有趣的谜题能超过888道无趣的谜题。

因此本来有些专业性很强(如果你和我一样认为吃数理基础算作是专业性很强的话)但很无聊的问题也被我忍痛割爱了。

但你不必为此遗憾。

有朝一日,如果还有机会,我会带着那些边角料卷土重来的。只是不再以“解谜末班车”之名义。

顺便一说,各位不要再以各种名义拉我去做解谜了。我真的不擅长解决难题;我只擅长难为别人。

依稀记得中学时出过一道化学题(其实是数学题),在全班面前讲了半节课。隔壁班的老师讲不明白,后来他们班学生跑来问我。

但我的化学真的没上过90分,也很少上80分。

同样,也不要因为我擅长出题,就觉得我善于揣摩出题人的意图。我中学语文常年在及格线徘徊。

而且你都走到这一步了,应该也能发现我的出题思路很别具一格吧……难道你还想指望我能理解其他出题人吗,痴人说梦。

好啦,不说废话了。“末班车末班车”还在等你呢,快想办法加入吧。

解析

这关的难点在于三处(对于看了网页源代码的人来说是四处):

  • 反复阅读并发现有些数字消失了;
  • 理解数字消失的机制,整理出它们之间的顺序;
  • 发现有一个数字从一开始就消失了。
  • (如果你看了网页源代码)跳出我为 Hacker 们精心设计的陷阱。

由于这关是纯前端题目,你可以直接看到源代码,再交给 AI 解读,让它为你解释这关的逻辑。

本关冗长的文本其实都是障眼法,核心在于题目中出现的会随时间消失的数字。

  • 在第 5 分钟数字 6 消失;
  • 在第 9 分钟数字 4 消失;
  • 在第 12 分钟数字 8 消失;
  • 在第 14 分钟数字 4 消失;
  • 在第 16 分钟数字 9 消失;
  • 在第 18 分钟数字 4、7 同时消失;
  • 在第 20 分钟数字 3 消失。

但这里只有 8 个数字,且搜索结果也不是“末班车末班车”,说明还有些数字缺失了,但我们没有发现。

如果本题的规律是一致的,那么文本自身未体现的缺失数字应当理解为:在时间为零时它就已经消失了

这个数字会是多少呢,其实也就 10 种可能性,直接全试一次也试出来了。

如果要从文本中找到的话……那就是第一行的 999995。ColorBlending 这关的分值是 9999994,加 Festival 的 1 分,如果玩家从 ColorBlending 支线走到这里,应该有 9999995 分而不是 999995 分。这说明有个 9 从一开始就消失了。

如果你看了源代码,你可以在少于 20 分钟的时间内就意识到上述信息。否则你至少需要 20 分钟才能找齐全部线索(而这离你解开谜题还有相当一段距离)。

消失的数字遍布各处,它们在存在和消失时都不会让文本太违和,所以不少玩家都以为自己记错了,而没发现文本内容真的变过;直到第 18 分钟之后乘法计算 191*251=47941 的结果都显然错了(941)。

只要你意识到让数字消失的机制所在,剩下的就都好办了。

如果你会看源代码,那你一定强到可以看穿陷阱吧

在源代码中,我有意调整了 4 和 7 的顺序,这样看前端代码的人会误以为 7 在 4 之前,从而构造出错误的群号。但事实真的如此吗?

注意到在源代码中,4 和 7 都在同一时间消失,且它们同属于一个数字 47941,这时从前端的执行结果来看,应该是:47 作为一个整体一起消失。那么正确的顺序是 47 而非 74!

有几位玩家没有意识到这一点,结果与通关失之交臂,实在有些可惜。