利用Duckdb求解Advent of Code 2025第1题 刻度盘

【投稿赢 iPhone 17】「我的第一个开源项目」故事征集:用代码换C位出道! 10w+人浏览 1.7k人参与

原题地址
— 第 1 天:入口 —
精灵们有好消息,也有坏消息。

好消息是,他们发现了项目管理!这为他们提供了防止往常那种圣诞紧急情况所需的工具。例如,他们现在知道北极装饰需要尽快完成,以便其他关键任务能够按时开始。

坏消息是,他们意识到自己遇到了另一种紧急情况:根据他们的资源规划,他们中没有一个人有时间去装饰北极了!

为了拯救圣诞节,精灵们需要你在 12 月 12 日之前完成北极的装饰工作。

通过解决谜题收集星星。每天将提供两个谜题;当您完成第一个谜题后,第二个谜题将被解锁。每个谜题奖励一颗星星。祝你好运!

你到达了北极基地的入口,准备开始装饰。不幸的是,密码似乎已被更改,所以你无法进入。墙上贴着一份文件,它很有帮助地解释说:

“由于新的安全协议,密码被锁在下面的保险箱里。请查看随附文件以获取新密码组合。”

保险箱有一个刻度盘,上面只有一个箭头;刻度盘周围按顺序排列着数字 0 到 99。当你转动刻度盘时,每到达一个数字就会发出轻微的咔哒声。

随附的文件(你的谜题输入)包含一系列旋转指令,每行一个,这些指令告诉你如何打开保险箱。每条旋转指令以一个 LR 开头,表示旋转方向是向左(朝向较小的数字)还是向右(朝向较大的数字)。然后,旋转指令有一个距离值,表示刻度盘应向该方向旋转多少下。

因此,如果刻度盘指向 11,旋转 R8 将导致刻度盘指向 19。之后,旋转 L19 将导致它指向 0。

因为刻度盘是一个圆,从 0 向左转动一下会使它指向 99。同样地,从 99 向右转动一下会使它指向 0。

因此,如果刻度盘指向 5,旋转 L10 将导致它指向 95。之后,旋转 R5 将导致它指向 0。

刻度盘初始指向 50。

你可以按照指令操作,但你最近参加的官方北极入口安全培训研讨会告诉你,这个保险箱实际上是一个诱饵。真正的密码是在执行序列中任何一次旋转后,刻度盘恰好指向 0 的次数。

例如,假设随附文件包含以下旋转指令:

L68
L30
R48
L5
R60
L55
L1
L99
R14
L82

遵循这些旋转指令将导致刻度盘移动如下:

  • 刻度盘初始指向 50。
  • 刻度盘旋转 L68 指向 82。
  • 刻度盘旋转 L30 指向 52。
  • 刻度盘旋转 R48 指向 0。
  • 刻度盘旋转 L5 指向 95。
  • 刻度盘旋转 R60 指向 55。
  • 刻度盘旋转 L55 指向 0。
  • 刻度盘旋转 L1 指向 99。
  • 刻度盘旋转 L99 指向 0。
  • 刻度盘旋转 R14 指向 14。
  • 刻度盘旋转 L82 指向 32。

因为在这个过程中刻度盘总共指向 0 三次,所以这个例子中的密码是 3。

分析你随附文件中的旋转指令。打开门的实际密码是什么?

此谜题的第一部分已完成!它提供了一颗金星:*

— 第二部分 —
你确信那就是正确的密码,但门没有打开。你敲了敲门,但没有人回应。你一边思考一边堆了一个雪人。

在你为雪人滚雪球的时候,你发现了另一份一定掉进雪里的安全文件:

“由于更新的安全协议,请使用密码方法 0x434C49434B 直到另行通知。”

你从培训研讨会上记得,“方法 0x434C49434B” 意味着你实际上应该计算任何咔哒声导致刻度盘指向 0 的次数,而不管它发生在旋转过程中还是旋转结束时。

按照与上述示例相同的旋转指令,在旋转过程中,刻度盘会有几次额外指向零:

  • 刻度盘初始指向 50。
  • 刻度盘旋转 L68 指向 82;在此次旋转过程中,它指向 0 一次。
  • 刻度盘旋转 L30 指向 52。
  • 刻度盘旋转 R48 指向 0。
  • 刻度盘旋转 L5 指向 95。
  • 刻度盘旋转 R60 指向 55;在此次旋转过程中,它指向 0 一次。
  • 刻度盘旋转 L55 指向 0。
  • 刻度盘旋转 L1 指向 99。
  • 刻度盘旋转 L99 指向 0。
  • 刻度盘旋转 R14 指向 14。
  • 刻度盘旋转 L82 指向 32;在此次旋转过程中,它指向 0 一次。

在这个例子中,刻度盘在旋转结束时指向 0 三次,加上在旋转过程中又指向 0 三次。所以,在这个例子中,新密码将是 6。

请注意:如果刻度盘指向 50,像 R1000 这样的单个旋转将在返回 50 之前,导致刻度盘指向 0 十次!

使用密码方法 0x434C49434B,打开门的密码是什么?

第一部分很简单,我用两种方法做了,第一种递归CTE,很慢,第二种分析函数,很快。
方法1

with recursive t as(select 'L68
L30
R48
L5
R60
L55
L1
L99
R14
L82' t), 
b as(select row_number()over()rn,b from(select unnest(string_split(t,chr(10)))b from t))
,a as(select 1 lv,50 v
union all
select lv+1,(v+ case when substr(b,1,1) ='L' then -1 else 1 end * substr(b,2)::int +1000)%100  from a,b where lv <=(select count(*) from b)and rn=lv
)
select count(*) from a where v=0; 

这里之所以要加1000, 是因为转的格数最多有3位数,这样保证结果是正数。
因为正式输入数据有4530行,每轮都数一遍b的行数,太笨了。
方法2

with recursive t as(select 'L68
L30
R48
L5
R60
L55
L1
L99
R14
L82' t), 
b as(select row_number()over()rn,b from(select unnest(string_split(t,chr(10)))b from t))
,a as(select (sum(case when substr(b,1,1) ='L' then -1 else 1 end * substr(b,2)::int)over(order by rn)+50 +1000)%100 v from b
)
select count(*) from a where v=0; 

第二部分,我被一个数学题“开区间(a,b)中间有几个100的倍数”困住了,求助DeepSeek的解答也不对,求助张泽鹏先生,他给了我如下公式:floor((b - 1) / 100) - floor(a / 100),一试果然好用,而把floor改为duckdb整除//运算,结果就不对了。
思路是把每步运算前后指针的实际区间(化为小于100的非负整数前)计算出来,然后用上述公式计算中间经过0点的次数。

with recursive t as(select 'L68
L30
R48
L5
R60
L55
L1
L99
R14
L82' t), 
b as(select row_number()over()rn, case when substr(b,1,1) ='L' then -1 else 1 end * substr(b,2)::int b from(select unnest(string_split(t,chr(10)))b from t))
, c as(select rn,b, sum(b)over(order by rn)+50 a, (a+1000)%100 v from b order by rn)
, d as(select c.* , [least(coalesce(lag(v)over(order by rn),50)+b, coalesce(lag(v)over(order by rn),50)), greatest(coalesce(lag(v)over(order by rn),50)+b, coalesce(lag(v)over(order by rn),50))] rg , 
floor((rg[2]- 1) / 100) - floor(rg[1] / 100) zcnt 
--(rg[2]- 1) // 100 - (rg[1] // 100) zcnt
from c)
select sum(zcnt)::int +count(case when v=0 then 1 end) from d;
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值