利用Duckdb求解Advent of Code 2024第5题 打印队列

编程达人挑战赛·第5期 10w+人浏览 277人参与

原题地址

— 第5天:打印队列 —

在满意地搜索了谷神星之后,学者小队建议接下来扫描17号子地下室(sub-basement 17)的文具堆。

在如此临近圣诞节的时候,北极打印部门比以往任何时候都要繁忙。当历史学家们继续搜索这个具有历史意义的设施时,一位操作着一台非常熟悉的打印机的精灵示意你过去。

那位精灵肯定认出了你,因为他们没有浪费时间解释新的雪橇发射安全手册更新无法正确打印。未能更新安全手册的后果确实会很严重,所以你主动提出帮忙。

安全协议明确规定,安全手册的新页面必须按照非常特定的顺序打印。符号 X|Y 表示,如果更新中需要制作页码 X 和页码 Y,那么页码 X 必须在页码 Y 之前的某个时间点打印。

精灵为你提供了页面排序规则和每次更新需要制作的页面(你的谜题输入),但无法确定每个更新的页面顺序是否正确。

例如:

47|53
97|13
97|61
97|47
75|29
61|13
75|53
29|13
97|29
53|29
61|53
97|53
61|29
47|13
75|47
97|75
47|61
75|61
47|29
75|13
53|13

75,47,61,53,29
97,61,53,29,13
75,29,13
75,97,47,61,53
61,13,29
97,13,75,29,47

第一部分指定了页面排序规则,每行一条。第一条规则 47|53 表示,如果更新中包含页码 47 和页码 53,那么页码 47 必须在页码 53 之前的某个时间点打印。(47 不一定需要紧挨在 53 之前;中间允许有其他页面。)

第二部分指定了每次更新的页码。由于大多数安全手册不同,更新所需的页面也不同。第一次更新 75,47,61,53,29 表示更新由页码 7547615329 组成。

为了尽快启动打印机,首先要识别哪些更新已经顺序正确。

在上面的例子中,第一次更新 (75,47,61,53,29) 顺序正确:

  • 75 正确地位于第一,因为有规则将其他每个页面都放在它之后:75|4775|6175|5375|29
  • 47 正确地位于第二,因为 75 必须在它之前(75|47),并且根据 47|6147|5347|29,其他所有页面都必须在它之后。
  • 61 正确地位于中间,因为 7547 在它之前(75|6147|61),而 5329 在它之后(61|5361|29)。
  • 53 正确地位于第四,因为它在页码 29 之前(53|29)。
  • 29 是剩下的唯一页码,因此正确地位于最后。

因为第一次更新没有包含某些页码,所以涉及这些缺失页码的排序规则将被忽略。

根据规则,第二次和第三次更新也顺序正确。和第一次更新一样,它们也没有包含每个页码,因此只有部分排序规则适用——在每个更新内部,涉及缺失页码的排序规则不被使用。

第四次更新 75,97,47,61,53 顺序不正确:它将 75 打印在 97 之前,这违反了规则 97|75

第五次更新 61,13,29 顺序也不正确,因为它违反了规则 29|13

最后一次更新 97,13,75,29,47 由于违反了几条规则而顺序不正确。

出于某种原因,精灵们还需要知道每次打印更新的中间页码。因为目前你只打印顺序正确的更新,所以你需要找出每个顺序正确更新的中间页码。在上面的例子中,顺序正确的更新是:

  • 75,47,61,53,29
  • 97,61,53,29,13
  • 75,29,13

它们的中间页码分别是 615329。将这些页码相加得到 143

当然,你需要小心:实际的页面排序规则列表比上面的例子更大、更复杂。

确定哪些更新已经顺序正确。将这些顺序正确的更新的中间页码相加,你会得到什么?

— 第二部分 —

当精灵们开始打印顺序正确的更新时,你有一点时间来修复其余的更新。

对于每个顺序不正确的更新,使用页面排序规则将页码排成正确的顺序。对于上面的例子,以下是三个顺序不正确的更新及其正确的顺序:

  • 75,97,47,61,53 变为 97,75,47,61,53
  • 61,13,29 变为 61,29,13
  • 97,13,75,29,47 变为 97,75,47,29,13

在仅取出顺序不正确的更新并将其正确排序后,它们的中间页码是 472947。将它们相加得到 123

找出顺序不正确的更新。如果只将这些更新正确排序,然后将它们的中间页码相加,你会得到什么?

解题思路:

首先想到把规则包含的所有页面按正确顺序排列,然后从输入中找到各页在全表中所在的位置,如果输入的每个位置都是升序的,那么它的顺序正确。按照此思路的实现如下

with recursive t as(
select
'47|53
97|13
97|61
97|47
75|29
61|13
75|53
29|13
97|29
53|29
61|53
97|53
61|29
47|13
75|47
97|75
47|61
75|61
47|29
75|13
53|13

75,47,61,53,29
97,61,53,29,13
75,29,13
75,97,47,61,53
61,13,29
97,13,75,29,47' t)
, b as(select row_number()over()rn , string_split(b,'|') b from(select unnest(string_split(t, chr(10)))b from t)where instr(b, '|')>0)
, d as(select row_number()over()rn , string_split(b,',') b from(select unnest(string_split(t, chr(10)))b from t)where instr(b, ',')>0)
, b1 as(select rn, b[1]::int l , b[2]::int r from b)
, e as(select 1 lv, l, r, [l, r]a from b1 
union all
select lv+1, e.l, b1.r, a||[b1.r]from b1, e where b1.l=e.r
)
, e1 as(select a from e where lv=(select max(lv)from e))
,m as(select b, [ list_position(a,i::int) for i in b] od, reduce(od,lambda x,y:case when x<y then y end) res, a[od[len(od)//2+1]] m from d, e1 where res is not null)
select sum(m)from m;

但是上述代码在处理正式输入数据时无法完成。因为输入规则有1176条,不同的页码有49个,如果把它们全连起来就是1176的49次方,不可能罗列出来。而且检验发现,出现在左侧和右侧的页码集合也完全一致,
即所有页码都既可能作为左侧,也可能作为右侧,如果将它们串联,最后必然成为一个环,这就无限循环了。而题目中的示例数据不是这样,比如13就没有出现在左侧。这就是Advent of Code比赛的好处,你可以针对输入进行分析,而不是像有的线上编程答题只知道对错。
后经过对数字的分析,1176=49*(49-1)/2, 正好是两个页码的组合数,这意味着所有页码的先后顺序都已经给出,不用自己计算空缺的左右关系组合。然而把它们组成一个严格单调递减的数列仍然不可能,因为只知道左右,没法知道中间能包含哪些页码,计算量极大,何况环形的关系实际无法用一个列表表示。所以改换思路,将查输入页码在单个列表中的位置改为在多个左右对中找匹配,这个思路的实现如下:

with recursive t as(
select
'47|53
97|13
97|61
97|47
75|29
61|13
75|53
29|13
97|29
53|29
61|53
97|53
61|29
47|13
75|47
97|75
47|61
75|61
47|29
75|13
53|13

75,47,61,53,29
97,61,53,29,13
75,29,13
75,97,47,61,53
61,13,29
97,13,75,29,47' t)
, b as(select row_number()over()rn , string_split(b,'|') b from(select unnest(string_split(t, chr(10)))b from t)where instr(b, '|')>0)
, d as(select row_number()over()rn , string_split(b,',') b from(select unnest(string_split(t, chr(10)))b from t)where instr(b, ',')>0)
, b1 as(select rn, b[1]::int l , b[2]::int r from b )
, a as (select list([l::text, r::text]) a from b1)
,m as(select d.b, reduce(d.b,lambda x,y:case when list_position(a.a, [x, y])>0 then y end  )res, d.b[len(d.b)//2+1]::int m from d, a where res is not null)
select sum(m)from m;

与前面的代码相比去掉了递归“查漏补缺”的步骤,因为所有页码的先后顺序都已经给出,没有遗漏。经检验示例数据结果不变。正式输入数据的结果也通过了。

第二问要把那些页码顺序错误的列表排成正确顺序,找错误容易,把前面sql中res为NULL的行取出就行了。怎么还原正确顺序?根据规则,输入行中不会出现环形的情况,那么只要把只包含输入页码的顺序对从完整列表中取出,根据这个子集排出的顺序自然就是正确顺序,
问题回到了把顺序对排成链表的问题,还可以继续用第一问的递归思路构造,示例数据也确实能跑出来,但用到正式数据,较短的列表能算出来,长的就无能为力了。最长的输入行包括23个数字,按每个数字在左侧时,平均包含11条右侧页码顺序规则计算,11的23次方仍然是天文数字。
改变思路,假定某个n元素列表已经有序,它具有这么个特点,第一个元素,它的右侧有n-1个元素,那么相应的右侧页码顺序规则就有n-1条,第二个元素,它的右侧有n-2个元素,相应规则就有n-2条,以此类推。
那么只要数出在规则子集中的每个在左侧页码的规则条数,再用n+1减去条数,就是它的位置序号。更进一步,题目只要求排序后的中间元素求和,甚至都无需重新把整个列表重新排序,只要算出哪个元素应该放中间位置即可,而这个放中间位置的元素规则条数就是(n-1)/2, 按照此思路实现的代码如下。

with recursive t as(
select
'47|53
97|13
97|61
97|47
75|29
61|13
75|53
29|13
97|29
53|29
61|53
97|53
61|29
47|13
75|47
97|75
47|61
75|61
47|29
75|13
53|13

75,47,61,53,29
97,61,53,29,13
75,29,13
75,97,47,61,53
61,13,29
97,13,75,29,47' t)

, b as(select row_number()over()rn , string_split(b,'|') b from(select unnest(string_split(t, chr(10)))b from t)where instr(b, '|')>0)
, d as(select row_number()over()rn , string_split(b,',') b from(select unnest(string_split(t, chr(10)))b from t)where instr(b, ',')>0)
, b1 as(select rn, b[1]::int l , b[2]::int r from b )
, a as (select list([l::text, r::text]) a from b1)
,m as(select d.b, reduce(d.b,lambda x,y:case when list_position(a.a, [x, y])>0 then y end  )res, d.b[len(d.b)//2+1]::int m from d, a where res is null)
,w as(select row_number()over()rna, b from m) --待重新排序
,b2 as(select * from b1,w where l::text in b and r::text  in b) --只出现在输入b中的大小关系,rna为主键
,m2 as(select rna,b, l ,  count(*) from b2 group by rna,b, l having count(*)=(len(b)-1) //2) --通过右侧条目数=(总数-1)//2找出中间的元素
select sum(l)from m2;

算出结论之后,回过来想,其实列表递归重排序也可以优化为第一次找出右侧规则最多的页码,然后排除它,下一轮找余下规则最多的,以此类推。不过这是过程语言的算法,每个列表都要计算,就要实现为两重递归嵌套SQL。不如数据库group by操作一步到位优雅和高效。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值