算法一千零一夜 · 第十七夜 长街点灯

第十七夜 · 长街点灯

上一封信的末尾,我们说起大厂的面试,说起考官、铡刀,还有那扇看不见的门。信寄出去没几天,我自己倒先想起另一场面试。那一场比大厂早得多,也黑得多。

那年我坐在一间漆黑的屋子里面试,屋子里什么也看不见,面试官就在黑暗里开了口。后来我跟人讲起那一场,都是这么讲的。

「当时面试官问我在一个漆黑的房间里开灯。有好多人在排队,开啊关、关啊开,人来人往的。几千个人。几千盏灯怎么样才能找到配对的方法?证明灯是谁关的?全部玩过之后,又有哪些灯被亮?把罪魁祸首找出来。」

黑屋子里什么也看不见,他偏出了一道用眼睛做不了的题。这道题搬到你认得的那条长街上正合适。比特王国,第十四夜我们看过那里的灯,一盏灯就是一位;第十六夜海关的死亡名单,也是一排灯。今天灯多了,街尾新修的一段路上,立起一百盏路灯,门牌从 1 编到 100,天黑下来,全街是灭的。守灯人是个一板一眼的角色,规则只有一条。第 1 晚,把一百盏灯全部点亮;第 2 晚,把门牌是 2 的倍数的按一下;第 3 晚按 3 的倍数;一路往下,第 100 晚按完。问你,天亮以后,街上还亮着几盏?

面试原题是几千个人几千盏灯。别急,先把一百盏街上的数算清楚,再回黑屋去。

最老实的办法,是照规则真按。一百个开关装得下,你早学过这条街的存法,一个 int 有 32 位,一格记一盏灯,4 个 int 就把整条街扛在肩上。按一下怎么写?亮变灭,灭变亮,翻一次面。异或,第九夜山洞里翻牌的老工具。程序埋头按了一百个晚上,天亮清点,街上亮着十盏。

黑格只有十个,散在格盘上,门牌是 1,4,9,16,25,36,49,64,81,100。从小到大念,相邻两个的间隔是 3,5,7,9,11,一路上全是奇数。这一排号码有个共同的名字,叫平方数。

答案有了,可我心里过不去。当年在那间黑屋子里,我交出去的卷子不是这张。交卷之前,我在纸上画过一张更乱的图。

「我还记得那时候啊,我想到了配对方法。把每个灯和每个人的编号连起来。但是我依旧没有看出他们之间是什么关系,直到线密密麻麻,直到我有一天玩了连连看。发现一个方块在左边,一个方块在右边,它们左右颠倒没关系。」

这张图这么画。左边一列方块,写晚上的编号;右边一列方块,写灯的门牌。第 d 晚按了第 n 号灯,d 和 n 之间就连一条线。拿 12 号灯看,它身上连着六条线,从 1、2、3、4、6、12 六个方块来。1 牵 12,2 牵 6,3 牵 4,三对,每一对两头的乘积都是 12。乘法不管谁站左边谁站右边,3 排 4,4 排 3,排出来都是同一盏 12 号。这就是左右颠倒没关系。每一对线其实是同一件事说了两遍,按了两下,一亮一灭,等于没按。

16 号灯身上连着五条线。1 牵 16,2 牵 8,都成双。剩下一条,4 牵 4,两头是同一个方块,笔直立在正中,自己连自己。全图只有这一条线不跟别人交叉。它只按了一下,没有配对的第二下来抵消它。灯就这么亮了。

线密密麻麻的时候看不出关系,线一多,结构自己浮出来。成双的线抵消,自连的线点亮。黑屋里看不见灯,也看不见线,那就不看。把问法倒过来,不去猜哪盏灯亮,挨个去问每一盏灯,你被按了几下。12 号答六下,16 号答五下。答奇数下的站出来,就是亮着的。问题问对了,答案自己会站出来。

还剩最后一层窗户纸。那条自己连自己的线,为什么只在平方数的门前出现?把门牌拆开看。12 拆成两个 2 和一个 3,12=2\^2\\times 3,它的因子个数是 (2+1)(1+1)=6,偶数。16 拆成四个 2,16=2\^4,因子个数是 4+1=5,奇数。规律在这里。因子个数,等于每个素因子的指数各加一,再连乘。要这个连乘出奇数,每一项都得是奇数,每个指数都得是偶数。指数全偶,号码就是一个平方数。素数是数字的原子,平方数是原子两两成双盖起来的楼。原子自己的故事,下一夜讲。

现在回黑屋去交卷。几千个人,几千盏灯,题目一分不减。一千盏的街,天亮后亮三十一盏,从 1 到 961,31 的平方是最后一个站在街口的,32 的平方出了街。灯是谁关的?每一盏灯的最后一手都是它自己的号码,第 n 晚,第 n 号灯被第 n 个人按最后一下,点灯的是它,关灯的也是它。可让它亮着不放的,是那个没有搭档的平方根。罪魁祸首捉拿归案,全场几千人,只有他,一个方块在左,一个方块在右,两边都是自己。

祖母说过,把落单的留给对手。长街替她补了下半句,落单的那一位,自己点亮了自己。

java 复制代码
public class Night17Lamps {

    public static void main(String[] args) {
        // 守灯人的一百晚:真去按。一百盏灯装进 4 个 int,按一下翻一次面
        int[] lamps = new int[4];
        for (int d = 1; d <= 100; d++) {
            for (int n = d; n <= 100; n += d) {
                lamps[n / 32] ^= 1 << (n % 32);      // 异或:亮变灭,灭变亮
            }
        }
        StringBuilder lit = new StringBuilder();
        for (int n = 1; n <= 100; n++) {
            if (((lamps[n / 32] >> (n % 32)) & 1) == 1) lit.append(n).append(' ');
        }
        System.out.println("simulation: " + lit.toString().trim());
        // 黑屋里看不见,就挨个问:你被按了几下
        StringBuilder asked = new StringBuilder();
        for (int n = 1; n <= 100; n++) {
            int presses = 0;
            for (int d = 1; d * d <= n; d++) {
                if (n % d == 0) {
                    presses += 2;                     // d 和 n/d 成双,各一下
                    if (d * d == n) presses--;        // 落单的平方根,只一下
                }
            }
            if (presses % 2 == 1) asked.append(n).append(' ');
        }
        System.out.println("pairing   : " + asked.toString().trim());
        // 面试原题口径:几千盏灯,先看一千盏
        int on = 0;
        for (int n = 1; (long) n * n <= 1000; n++) on++;
        System.out.println("lamps up to 1000: " + on + " lit (31*31=961, 32*32=1024 leaves the street)");
    }
}

实跑输出如下。

复制代码
simulation: 1 4 9 16 25 36 49 64 81 100
pairing   : 1 4 9 16 25 36 49 64 81 100
lamps up to 1000: 31 lit (31*31=961, 32*32=1024 leaves the street)

这一夜的回信就写到这里。下一夜我们去见那些再也拆不开的号码,见一见数字的原子。


① 真伪核对 逐条照实登记。

 Ⅰ 题源 一百盏灯按倍数开关为流传已久的经典谜题,通行于趣味数学读物与科技公司的面试题库,常见包装有百盏灯、一百扇门等,未查到权威首出记载(真,已联网核查,标民间流传)。本夜题源打头为秋天的怀念的面试亲历,黑屋原题与配对顿悟两段口述照录入文(真,口述实录)。

 Ⅱ 机制 第 n 号灯共被按其因子个数次,因子首尾成对(d 与 n/d),唯有平方数含自配对的平方根,故按奇数次的灯恰为平方号门牌;百盏亮十盏(真,算术,双算法实跑互证一致)。

 Ⅲ 千盏口径 一千盏灯天亮后亮三十一盏,31²=961、32²=1024(真,算术,实跑验证)。

 Ⅳ 素数拆解 12=2\^2\\times 3,因子个数 (2+1)(1+1)=6;16=2\^4,因子个数 4+1=5;各素因子指数全偶则号码为平方数(真,算术)。

 Ⅴ 学名 因子个数通称除数函数(divisor function),记号 \\tau(n),亦记 d(n)、\\sigma_0(n)(真,已联网核查)。

 Ⅵ 连连看 配对顿悟一节为口述实录;连连看系流行的配对消除游戏,玩法为连接成对的方块(常识)。

 Ⅶ 演示件 Night17Lamps 位图模拟与因数配对双路互证一致,输出与文中一致(真,JDK 21 实跑验证)。

② 术语对照卡 本夜新增一条,定义不含比喻。

 Ⅰ 除数个数,学名除数函数(divisor function),记号 \\tau(n)。n 的全部因子的个数;因子首尾配对后唯有平方数落单一次,故 \\tau(n) 为奇当且仅当 n 为平方数。

③ 今夜习题 三道,评论区见。

 Ⅰ 换一条一千盏灯的长街,规则不变,天亮后亮着几盏?最靠街口的那一盏,门牌是多少?

 Ⅱ 2024 号灯最后亮着吗?一百盏里被按了恰好六次的灯,最小编号是几号?

 Ⅲ 守灯人第七晚和第十一晚偷懒没按,天亮后亮着的门牌相比原规则变了哪几盏?


本书目录导航 上一篇:第十六夜 十只白鼠 下一篇:第十八夜 万物皆数到底是谬论还是神明的裁决 起点:卷首语·算法童话


版权声明:本文为作者原创,受著作权法保护。未经授权禁止转载、搬运、摘编、改编及任何形式的二次创作;个人学习引用请注明作者与原文出处。转载授权请联系作者(CSDN 私信)。侵权必究。

相关推荐
CV爱数码1 小时前
论文精读|RED-CNN:残差编解码网络,低剂量 CT 去噪的经典基线
网络·人工智能·算法·cnn·ct·重建·x-ray
不吃鱼的羊1 小时前
如何应对恐惧
java
不吃鱼的羊1 小时前
如何建立高自尊
java·服务器·前端
Magicdata大数据1 小时前
AI辅助教学:调用大模型从教材中生成习题,方便学员练习
java·spring boot·大模型·知识图谱·neo4j·ai辅助教学
香蕉大香蕉(06敬畏市场)1 小时前
高频八股 · Java 并发编程(10):异步、ThreadLocal 与虚拟线程(收官)
java·开发语言·网络·jvm·python·ai·实习
想做小南娘,发现自己是女生喵1 小时前
LAN Device Monitor 第二周学习文档
java·服务器·数据库
JustNow_Man1 小时前
【Harness】本地文本向量化搜索解决方案--qmd
git·python·算法
Wang's Blog2 小时前
Java 项目实战: 交友项目 - 查询推荐动态
java·开发语言·交友
jaysee-sjc2 小时前
【苍穹外卖】Day10:定时任务与 WebSocket 实时提醒实战
java·开发语言·javascript·数据库·vue.js·websocket·网络协议