利用Duckdb求解Advent of Code 2025第3题 电池组

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

原题地址
— 第 3 天:大厅 —
你走下短楼梯,进入大得惊人且空无一人的大厅,并迅速通过了安全检查点。然而,当你到达主电梯时,你发现每个电梯上方都亮着红灯:它们都离线了。

"抱歉,"一位精灵一边摆弄着附近的控制面板一边道歉。“某种电涌似乎把它们烧坏了。我会尽快让它们上线。”

你解释了你需要继续深入地下。“嗯,你至少可以乘自动扶梯下到印刷部门,不过没有电梯工作你也无法走得更远。也就是说,如果自动扶梯没有离线的话,你是可以的。”

“但是,别担心!它没坏;只是需要电力。也许你可以在我继续修理电梯的时候让它运转起来。”

附近有电池可以为自动扶梯提供紧急电力,正是为了应对这种情况。每个电池都标有它们的伏特等级,一个从 1 到 9 的值。你记下了它们的伏特等级(你的谜题输入)。例如:

987654321111111
811111111111119
234234234234278
818181911112111

电池被排列成组;你输入的每一行数字对应一组电池。在每组内,你需要恰好打开两个电池;该组产生的伏特数等于你所打开电池上的数字组成的数字。例如,如果你有一组 12345 并且你打开了第 2 和第 4 个电池,该组将产生 24 伏特。(你不能重新排列电池。)

你需要找出每组电池能产生的最大可能伏特数。在上面的例子中:

  • 987654321111111 中,你可以通过打开前两个电池来产生最大可能的伏特数 98。
  • 811111111111119 中,你可以通过打开标有 8 和 9 的电池来产生最大可能的伏特数,产生 89 伏特。
  • 234234234234278 中,你可以通过打开最后两个电池(标记为 7 和 8)来产生 78。
  • 818181911112111 中,你可以产生的最大伏特数是 92。

总输出伏特数是每组最大伏特数的总和,所以在这个例子中,总输出伏特数是 98 + 89 + 78 + 92 = 357

你面前有很多电池。找出每组可能的最大伏特数;总输出伏特数是多少?

— 第二部分 —
自动扶梯没有动。精灵解释说,它可能需要更多的伏特来克服系统的静摩擦力,并按下了一个大大的红色"伏特限制安全覆盖"按钮。你数不清她需要确认多少次"是的,我确定",于是你一边等待一边稍微装饰了一下大厅。

现在,你需要在每个电池组中恰好打开十二个电池来产生最大的伏特数。

每个电池组的伏特输出仍然由你打开的电池上的数字组成;唯一的不同是,现在每个电池组的伏特输出将有 12 位数字,而不是两位。

再考虑一下之前的例子:

987654321111111
811111111111119
234234234234278
818181911112111

现在,伏特数变得大得多:

  • 987654321111111 中,可以通过打开除末尾的一些 1 之外的所有电池来产生最大的伏特数 987654321111
  • 在数字序列 811111111111119 中,可以通过打开除一些 1 之外的所有电池来产生最大的伏特数 811111111119
  • 234234234234278 中,可以通过打开除开头附近的一个 2、一个 3 和另一个 2 电池之外的所有电池来产生最大的伏特数 434234234278
  • 818181911112111 中,通过打开除开头附近的一些 1 之外的所有电池,产生了伏特数 888911112111

现在总输出伏特数要大得多:987654321111 + 811111111119 + 434234234278 + 888911112111 = 3121910778619

新的总输出伏特数是多少?

解题思路:
第一部分很简单,用笛卡尔积就能解决,一个表提供十位数,另一个提供个位数,合并后取最大值即可。

with recursive t as(select '987654321111111
811111111111119
234234234234278
818181911112111' t), 
b as(select row_number()over()rn,b from(select unnest(string_split(t,chr(10)))b from t))
,c as(select rn, row_number()over()rn2,b from(select rn,unnest(string_split(b,''))b from b))
,d as(select max(c.b||c2.b)maxb2 from c,c c2 where c.rn=c2.rn and c.rn2<c2.rn2 group by c.rn)
select sum(maxb2::int) from d;

鉴于正式数据每行有100个数字,第二部分用笛卡尔积硬算所有的排列不切实际,需要更好的算法。
我向DeepSeek求助,他提供了如下算法实现:

好的,我们先明确题意:

有一个 100 位正整数,每位都是 1~9 之间的数字,我们要保持数字的相对顺序(即从左到右挑出 10 个数字,不能改变在原数字中的前后关系),组成一个 10 位数,并且让这个 10 位数尽可能大。

这是一个在数字序列中按顺序取 k 个数使其最大的经典问题,可以用贪心 + 单调栈的思路解决,复杂度 O(n)。


贪心思路

给定数字序列 num(长度为 n=100),要选 k=10 位,使最终的数字最大。

算法步骤:

  1. 维护一个栈(列表),用于存放最终选的数字。
  2. 遍历 num 的每一位数字 ch(字符):
    • 如果栈非空,且栈顶的数字比 ch 小,并且后面还有足够的数字使得即使 pop 掉栈顶,最终还能选满 k 位,则弹出栈顶(因为选后面更大的数字更优)。
    • 否则,将 ch 入栈。
  3. 注意:我们要选 k=10 位,但最后栈的长度可能超过 k,因为不能无限制 pop,必须保证剩余足够多的数字来凑满 k。
  4. 遍历完整个 num 后,取栈的前 k 位作为结果(因为如果栈长度 > k,说明后面数字太小无法替换前面的,就直接取前 k 位;但按照经典算法,长度一般是等于 k)。

具体来说,“剩余足够的数字”条件为:

栈长度 + (n - 当前索引 - 1) >= k

即当前已处理的数字位置为 i(0-based),n-i-1 是后面还未处理的数字个数,加上栈里已有的数字,必须保证最后能凑齐 k 个。


Python 代码实现

def largest_number_from_digits(num_str, k):
    stack = []
    n = len(num_str)
    for i, ch in enumerate(num_str):
        # 当栈非空,且栈顶小于当前数字,且还有足够剩余数字可以凑齐k位时
        while stack and stack[-1] < ch and len(stack) + (n - i - 1) >= k:
            stack.pop()
        if len(stack) < k:
            stack.append(ch)
    # 栈的长度正好是k,直接组合
    return ''.join(stack)

result = largest_number_from_digits(num_str, 10)
print("选出的最大10位数:", result)

用下述语句把它定义为duckdb自定义函数, 然后对每行字符串都执行这个函数即可。

import duckdb

from duckdb.typing import *

def largest_number_from_digits(num_str: str, k:int) -> str | None:
    if not num_str:
        return None	
    stack = []
    n = len(num_str)
    for i, ch in enumerate(num_str):
        # 当栈非空,且栈顶小于当前数字,且还有足够剩余数字可以凑齐k位时
        while stack and stack[-1] < ch and len(stack) + (n - i - 1) >= k:
            stack.pop()

        if len(stack) < k:
            stack.append(ch)

    # 栈的长度正好是k,直接组合
    return ''.join(stack)


duckdb.create_function(
    "largest_number_from_digits",
    largest_number_from_digits,
    null_handling="special"
)

num_str ='15162378'
result = largest_number_from_digits(num_str, 3)
res = duckdb.sql("SELECT largest_number_from_digits('15162378', 3)").fetchall()
print(res)

s="""
with recursive t as(select '987654321111111
811111111111119
234234234234278
818181911112111' t), 
b as(select row_number()over()rn,b from(select unnest(string_split(t,chr(10)))b from t))
select sum(largest_number_from_digits(b, 12)::bigint) from b;
"""
res = duckdb.sql(s).fetchall()
print(res)

以上代码适用于DuckDB v1.3.2。
执行结果如下

python 2503udf.txt
[('678',)]
[(3121910778619,)]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值