POSTS / 日常心算星期
之前,在网上偶然看到一种 心算某一天是星期几 的实用算法。原文:心算某一天是星期几——康威裁决日算法 – 大老李聊数学
发明者是 数学家 J. H. 康威(John Horton Conway)——怎么又是你🤣。他喜欢“不务正业”,在趣味数学中贡献巨大,提出过生命游戏、超现实数、边看边说序列之类许多有趣又深刻的内容1。
康威发明的这个算法简单实用,只需少量记忆和计算,完美符合日常生活场景需求。我试过几次后就喜欢上了,到现在差不多已经用了一年多了。熟悉这个算法后,我日记里都彻底不写星期了。
我也经常反向使用这个算法根据星期推算日期(可能频率比用日期推星期还高些),因为我总是记不清今天是几号……
由于真的在用,所以也有做些调整。本文会基于我的实践经验,重新梳理康威提出的这个心算星期几的算法,使之能够轻松用于日常生活。并补充了一点数学推导来解释这个神奇的算法为什么是对的。
因此有些地方与大老李的文章稍有不同,可能不做标注。
注:以下经常使用「对7取模」。因为1星期=7天,算出“星期八”甚至“星期十五”时应当能够正确地对7取模得到「星期一」。
核心原理
如果我们已经知道了本月某一天的星期,那么计算其他天的星期就很容易了。比如,已知 7月11日 是周五,那么就能算出 9日是周三、14日是周一,以此类推。
那么,我们只要为1年12月的每个月都背下某一天的星期,不就可以心算每一天的星期了吗?但是这样要背12个日期的星期,未免记忆量过大,还容易出错。
康威算法的精髓就在于,它在每个月中挑选出特殊的一天作为基准点,称作“裁决日”(Doomsday),这些裁决日具有相同的星期,又容易记忆。
2月是一年中唯一天数会变化的月份(平年28天,闰年29天)。因此,康威将「2月的最后一天」作为裁决日。这很自然,而且还可以自动处理闰年问题。
那么其他月呢?我们要求它们的裁决日与2月的最后一天具有相同的星期,然后尽量好记。
1月、2月、3月特殊处理;4月到12月:
- 偶数月:4月4日、6月6日、8月8日、10月10日、12月12日。日期=月数,很好记。
- 奇数月:5月9日,9月5日,7月11日,11月7日。朝九晚五、7-11便利店。
1月、2月、3月:
- 2月的最后1天,2月28/29日。或者减去4周,得到 0/1日。我更喜欢后者,更方便计算。
- 3月是3月0日。不难注意到,“3月0日”=2月的最后一天。
- 1月的3/4日,取决于是否是闰年。1314 谐音“一生一世”。
只要算一算裁决日之间间隔的天数,就能知道它们为什么具有相同的星期——日期之差恰好是7的倍数。比如,偶数月的裁决日日期总要+2的原因是30+31 ≡ 5 ≡ -2 (mod 7)
.
高度优化的算法
不知道如何用自然语言简明阐述,直接上伪代码吧。
按顺序提供几个函数:
- 日期和星期之间的偏移量(星期减日期)已知。反向使用就是 星期=>日期。
- 同月份的某日的星期已知。
- 同年份的裁决日的星期已知。
fn day_to_weekday(&self, day) {
(day + self.offset) % 7
}
fn weekday_to_day(&self, weekday) {
(day - self.offset) % 7
}
fn set_offset(&mut self, doomsday) {
self.offset = (self.doomsweekday - doomsday) % 7
}
fn doomsday(month) -> {
match month {
1 => if is_leap_year() { 4 } else { 3 }, // 一生一世
2 => if is_leap_year() { 1 } else { 0 }, // 或 if is_leap_year() { 29 } else { 28 }
3 => 0, // 3月0日 = 2月的最后一天
4 | 6 | 8 | 10 | 12 => month,
5 => 9, // 朝九
9 => 5, // 晚五
7 => 11, // 7-11
11 => 7, // 7-11
_ => unreachable!("Month must be between 1 and 12")
}
}
根据实践经验优化了方案:月内专门缓存一个“偏移量”,而不总是从裁决日开始推。这样可以减少计算步骤。
比如,2025年7月的裁决日11日是周五,那么偏移量是 5-11 ≡ -6 ≡ +1 (mod 7)
,那么我在这个月里只需要记住偏移量 +1
就可以了。这样,下次计算时就可以将2次加减法减少为1次。比如计算25日的星期:25+1=26
,模7得5
,所以25日是星期五,轻松心算。
例题:已知2025年的裁决日是星期五。请问2025年的9月30日是星期几?
- “朝九晚五”,9月的裁决日是5日。
- 那么5日是星期五,偏移量5-5=
0
. - 30日的星期是
(30 + 0) % 7 = 2
.
2025年的9月30日是星期二2。
计算某年的裁决日星期
要心算其他年份的某个日期的星期怎么办?把每一年的裁决日星期都背下来,也太难了,难免会记不清。
康威当然也研究了这类情况。我们可以记下某个“基础年份”的裁决日星期,然后计算目标年份与基础年份的“偏移量”,就可以算出目标年份的裁决日星期了。基础年份通常取「世纪首年」。
为什么取世纪首年?因为闰年在100的倍数又有特判(闰年的定义:能被4整除但不能被100整除的年份,或者能被400整除的年份),考虑进来的话就太麻烦了,限制在同一世纪内可以避开这条规则🤣。
具体来说,其实我们只会用到2个世纪:
$ date -d 2000-02-29 +%A
星期二
$ date -d 1900-02-28 +%A # 这个我一次都没用到过
星期三
那么接下来把 目标年份相对于基础年份的偏移量 算出来就好了。康威对此也给出了一个漂亮的算法,我用自己的方式重新陈述如下,绝对好记又好算。
计算给定年份的裁决日星期:
- 取两位年份;
- 浮现一个对12的带余除法,维护商和余数;
- 把注意力集中在余数,算出除以4的商放在下面;
- 商下面的空位正好放置基准年的裁决日星期;
- 把脑海中的这4个数模7求和。
12 和 4 这两个数都非常容易联想:一年共有12个月,闰年是每4年一次。太完美了!
虽然后面推导时会发现这里的12跟一年12月没有任何关系,但不影响我们这样记忆。
例题:2026年1月1日是星期几?
首先计算 2026年 的裁决日星期。
- 取两位年份:
26
- 浮现一个对12的带余除法,维护商和余数:
26 / 12 = 2 ... 2
2 | 2 |
---|---|
? | ? |
- 把注意力集中在余数,算出除以4的商放在下面:
2 // 4 = 0
2 | 2 |
---|---|
? | 0 |
- 商下面的空位正好放置基准年的裁决日星期:2000年的裁决日是星期
2
。
2 | 2 |
---|---|
2 | 0 |
- 把脑海中的这4个数模7求和:
6
.
2026年的裁决日是星期六。
这年是平年,1月的裁决日是3
日。偏移量是6-3=3
。那么1日的星期就是1+3=4
.
2026年1月1日是星期四3.
大老李还介绍了另一个“奇+11”算法;但那个算法光是「包含分支」就让我避而远之了,不值一提。
算法推导
上面这个算法看起来很难相信是正确的:又商又余数又再除一次再全加起来,这就对了?12是怎么回事?
此外,我们也想知道,会不会有更方便的算法?
所以来推导一下吧。
基本原理:365 % 7 =1
,366 % 7 = 2
。裁决日星期每年+1,闰年再额外多+1。
则有递推公式:
doomsweekday += 1 + is_leap_year()
——其实,这个递推公式本身就很实用了:每年根据「今年是不是闰年?」,在去年的基础上更新一下即可。
而由于经常用到,当年的裁决日星期很自然地就会记下来,然后就无需重新计算了。比如2025年的裁决日星期是周五,敲这句话我不假思索。读者不难验证:2024年的裁决日星期是周四,2026年是周六、2027年是周日,而2028年是周二。
不过,要算其他年份的话还是不方便,还是需要通项公式。继续推导吧。
根据这个递推公式,可以立刻得到计算偏移量的最直接的算法:yy + yy // 4
. 其中yy指两位年份。
但这显然不便于心算!
平年+1,闰年+2,这样 连续4年则模7余5。我们当然想凑个简单的,比如余0或余1。
显然连续28年则余0。逐个计算过去还能发现一个很不错的:连续12年则余1。
以28年为一组,则得 let remainder = yy % 28; remainder + remainder // 4
。
以12年为一组,则得 let (quotient, remainder) = divmod(yy, 12); quotient + remainder + remainder // 4
。
实践表明,以12年为一组,中间数值都很小,最方便心算。并且12恰巧能与「1年12月」对应上,非常好记。这正是康威原版算法。
更一般的情况?
更一般的情况的话,要应对闰年在100和400倍数时的特殊处理,麻烦很多,算了吧。反正这已经不是日常了,可以从容动用工具。
例:
- 曾经跟同学课间闲聊,给同学演示边看边说序列,但是把定义记错了,记成了数“总数”,给同学演示时发现不对劲。这形成了全新的变种“计数序列”。高二一年+高三上,投入许多时间研究了计数序列,并把主要成果整理成一篇论文,水了个地区赛事奖项。 康威在2020年因新冠疫情逝世。我当时刚升入大学,偶然从某篇文章的注释中得知这个不幸的消息,蓦然有种连接又断开的感觉。受此刺激,当时就创建了知乎专栏 数数中的奥秘——计数序列的世界 - 知乎,基于论文整理了几篇半成品文章,主要是结论和口语化解释,没有整理完整证明(敲公式太麻烦了;原本的论文是用Word写的)。如今看来风格太过稚嫩;不过到现在都还没重新回顾整理。 永远怀念。↩
- 如果你愿意在这天给我发一句「生日快乐!」,我会很开心。↩
- 提前祝新年快乐!↩