利用Duckdb求解Advent of Code 2025第7题 分离器

原题地址
— 第 7 天:实验室 —
你感谢了头足类动物的帮助,离开了垃圾压缩机,发现自己身处北极研究区熟悉的走廊中。

根据一个写着"传送器中心"的大标志,他们似乎在研究传送;你忍不住自己尝试了一下,踏上了那个巨大的黄色传送器平台。

突然,你发现自己在一个陌生的房间里!这个房间没有门;唯一的出路就是传送器。不幸的是,传送器似乎在泄漏魔法烟雾。

由于这是一个传送器实验室,周围散落着许多备用零件、手册和诊断设备。连接上一个诊断工具后,它很有帮助地显示了错误代码 0H-N0,这显然意味着其中一个快子歧管出现了问题。

你很快找到了快子歧管的图表(你的谜题输入)。一束快子束从标记为 S 的位置进入歧管;快子束总是向下移动。快子束可以自由通过空的空间(.)。然而,如果快子束遇到一个分离器^),光束会停止;取而代之的是,从分离器的紧邻左侧紧邻右侧各自继续发出一束新的快子束。

例如:

.......S.......
...............
.......^.......
...............
......^.^......
...............
.....^.^.^.....
...............
....^.^...^....
...............
...^.^...^.^...
...............
..^...^.....^..
...............
.^.^.^.^.^...^.
...............

在这个例子中,入射的快子束(|)从 S 向下延伸,直到遇到第一个分离器:

.......S.......
.......|.......
.......^.......
...............
......^.^......
...............
.....^.^.^.....
...............
....^.^...^....
...............
...^.^...^.^...
...............
..^...^.....^..
...............
.^.^.^.^.^...^.
...............

此时,原始光束停止,从分离器发射出两束新光束:

.......S.......
.......|.......
......|^|......
...............
......^.^......
...............
.....^.^.^.....
...............
....^.^...^....
...............
...^.^...^.^...
...............
..^...^.....^..
...............
.^.^.^.^.^...^.
...............

这些光束继续向下,直到遇到更多分离器:

.......S.......
.......|.......
......|^|......
......|.|......
......^.^......
...............
.....^.^.^.....
...............
....^.^...^....
...............
...^.^...^.^...
...............
..^...^.....^..
...............
.^.^.^.^.^...^.
...............

此时,两个分离器总共只产生三束快子束,因为它们都把快子注入到它们之间的同一位置:

.......S.......
.......|.......
......|^|......
......|.|......
.....|^|^|.....
...............
.....^.^.^.....
...............
....^.^...^....
...............
...^.^...^.^...
...............
..^...^.....^..
...............
.^.^.^.^.^...^.
...............

这个过程一直持续,直到所有快子束都到达分离器或离开歧管:

.......S.......
.......|.......
......|^|......
......|.|......
.....|^|^|.....
.....|.|.|.....
....|^|^|^|....
....|.|.|.|....
...|^|^|||^|...
...|.|.|||.|...
..|^|^|||^|^|..
..|.|.|||.|.|..
.|^|||^||.||^|.
.|.|||.||.||.|.
|^|^|^|^|^|||^|
|.|.|.|.|.|||.|

为了修复传送器,你首先需要了解快子歧管的光束分离特性。在这个例子中,快子束总共被分离了 21 次

分析你的歧管图表。光束将被分离多少次?

你的谜题答案是 1658。

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

— 第二部分 —
完成对歧管的分析后,你开始修复传送器。然而,当你打开传送器的侧面更换损坏的歧管时,你惊讶地发现这不是一个经典的快子歧管——它是一个量子快子歧管

使用量子快子歧管时,只有单个快子粒子被送入歧管。快子粒子会同时每个遇到的分离器的左路径和右路径。

由于这不可能实现,手册推荐了量子快子分离的多世界诠释:每当一个粒子到达分离器时,实际上时间本身发生了分裂。在一个时间线中,粒子向左走;在另一个时间线中,粒子向右走。

为了修复歧管,你真正需要知道的是一个单粒子完成其所有可能的旅程后,有多少个时间线是活跃的

在上面的例子中,存在许多时间线。例如,有一个粒子总是向左走的时间线:

.......S.......
.......|.......
......|^.......
......|........
.....|^.^......
.....|.........
....|^.^.^.....
....|..........
...|^.^...^....
...|...........
..|^.^...^.^...
..|............
.|^...^.....^..
.|.............
|^.^.^.^.^...^.
|..............

或者,有一个粒子在每个分离器处交替向左和向右走的时间线:

.......S.......
.......|.......
......|^.......
......|........
......^|^......
.......|.......
.....^|^.^.....
......|........
....^.^|..^....
.......|.......
...^.^.|.^.^...
.......|.......
..^...^|....^..
.......|.......
.^.^.^|^.^...^.
......|........

或者,有一个粒子最终到达与交替时间线相同的点,但走了一条完全不同的路径到达那里的时间线:

.......S.......
.......|.......
......|^.......
......|........
.....|^.^......
.....|.........
....|^.^.^.....
....|..........
....^|^...^....
.....|.........
...^.^|..^.^...
......|........
..^..|^.....^..
.....|.........
.^.^.^|^.^...^.
......|........

在这个例子中,粒子最终总共有 40 个不同的时间线。

将量子快子分离的多世界诠释应用到你的歧管图表上。一个单粒子最终会出现在多少个不同的时间线上?

解题思路,
第一问,看起来很像一个异或操作,快子和分离器两个1在同一个列相遇变成0,0和0还是0,后来仔细一看不是异或,因为我要0遇到1还是0,请教DeepSeek后它给出了正确的操作表达式:x & (~y)
SQL如下:

with recursive t as(
select
'.......S.......
...............
.......^.......
...............
......^.^......
...............
.....^.^.^.....
...............
....^.^...^....
...............
...^.^...^.^...
...............
..^...^.....^..
...............
.^.^.^.^.^...^.
...............' t)
, b as(select row_number()over()rn ,translate(b, '.S^', '011')  b from(select unnest(string_split(t, chr(10)))b from t)where instr(b, 'S')>0 or instr(b, '^')>0)
,a as(select 1 lv, b::BITSTRING::int m from b where rn=1
union all
select 1+lv, m & ~( b::BITSTRING::int)| (b::BITSTRING::int <<1) |(b::BITSTRING::int >>1)  from a, b
where lv+1=rn)
, o as(select lv, m::bitstring v,bit_count(v)bc from a)
, p as(select lv, v, bc, lead(v)over(order by lv)&v l ,bit_count(l)bc2  from o)
select sum(bc)-sum(bc2) from p where l is not null;

对于示例数据,它正确得出了分裂次数21, 但是正式输入数据一行有140字符,超过了DuckDB的hugeint范围,所以让DeepSeek把上述SQL改写成Python程序,修改几处BUG后如下所示。
它添加的注释很好地说明了算法,就不多解释了。

# 原 SQL 中的递归 CTE 部分用循环和列表处理来模拟

# 1. 原始数据
data = """.......S.......
...............
.......^.......
...............
......^.^......
...............
.....^.^.^.....
...............
....^.^...^....
...............
...^.^...^.^...
...............
..^...^.....^..
...............
.^.^.^.^.^...^.
..............."""

# 2. 将数据按行分割,并转换为二进制整数
lines = data.split('\n')

# 处理每一行:将 '.'、'S'、'^' 转换为二进制数字
# 规则:'.' -> 0, 'S'/'^' -> 1
binary_rows = []
for line in lines:
    if 'S' in line or '^' in line:
        bin_str = line.replace('.', '0').replace('S', '1').replace('^', '1')
        binary_rows.append(int(bin_str, 2))  # 转换为整数

# 3. 模拟递归计算(实际上是逐行迭代计算)
lv = 1
m = binary_rows[0]  # 第一行
results = [(lv, m, bin(m))]  # 存储每行结果 (层级, 整数, 二进制字符串)

# 处理第 2 行到最后行
for i in range(1, len(binary_rows)):  # 确保不超过数据行数
    lv += 1
    b = binary_rows[i]  # 当前行的二进制整数
    # 核心公式: m & ~b | (b << 1) | (b >> 1)
    m = (m & ~b) | (b << 1) | (b >> 1)
    results.append((lv, m, bin(m)))

# 4. 计算 bc (bit_count) 和 bc2
from math import log2

def bit_count(num):
    """计算整数的二进制表示中 1 的个数"""
    return bin(num).count('1')

# 将结果转换为便于处理的列表
o = [(lv, v, bit_count(v)) for lv, v, _ in results]
# print(o)
# 计算相邻两行之间的重叠比特数
total_bc = 0
total_bc2 = 0

for i in range(len(o) - 1):
    lv, v, bc = o[i]
    next_lv, next_v, next_bc = o[i + 1]
    
    # 当前行与下一行的位与结果
    l = v & next_v
    bc2 = bit_count(l)
    
    total_bc += bc
    total_bc2 += bc2

# 5. 最终计算结果
result = total_bc - total_bc2
print(f"结果: {result}")

将它用于正式输入,通过了。

第二问有些复杂,SQL不会写,让DeepSeek帮助给出python代码,但提示了好多次,也得不到正确结果,最后我手工计算例子的每一行,总结出具体操作步骤,提示他

对于下图

 第4层: 不一致 (计算=19, 给定=13)
  给定: [(3, 1), (5, 4), (7, 3), (8, 3), (9, 1), (11, 1)]
  计算: [(3, 1), (5, 4), (7, 6), (8, 3), (9, 4), (11, 1)]
  符号层3: ....|^|^|^|....,所有|的路径数从左到右分别为 1,3,3,1
  符号层4: ...|^|^|||^|... 

你应该先从第4层删除所有的|,把他们都恢复为“.”,然后从第4层第1个出现的^开始分析,它的正上方是第3层的第一个|,所以分叉,在^左侧1格位置放上|,它的路径数=第3层的第一个|路径数,然后在^右侧1格位置放上|,它的路径数先暂时算上第3层的第一个|路径数1,然后分析第4层第2个出现的^,它的正上方是第3层的第2个|,所以分也分叉,把第3层的第2个|的路径数加到左侧的|,即刚才暂时算上的|,所以第4行第2个|最终路径数=1+3=4,再在第4层第2个出现的^右侧放上|,它的路径数先暂时算上第3层的第2个|路径数,注意在处理第4行第3个^时还发现,它没有接在第3层第3个|正下方,所以在第3层第3个|正下方的位置放置一个直通的|,路径数3,依次类推,直到第4行的全部^处理完毕。

终于写出来了


def construct_paths_from_symbols(symbol_layers):
    rows = len(symbol_layers)
    cols = len(symbol_layers[0])
    
    # 存储每一层 | 的位置和路径数
    result_pipes = []
    
    # 第0层:处理 S
    layer0_pipes = []
    for i, ch in enumerate(symbol_layers[0]):
        if ch == 'S':
            layer0_pipes.append((i, 1))  # S 视为 |,路径数=1
    result_pipes.append(layer0_pipes)
    
    # 从第1层开始处理
    for r in range(1, rows):
        current_symbols = symbol_layers[r]
        
        # 获取上一层 | 的路径数映射
        prev_pipes = {pos: val for pos, val in result_pipes[r-1]}
        
        # 初始化当前层路径数映射(只记录 | 的位置)
        path_map = {}
        
        # 第一步:处理每个 ^ 的分叉
        for i, ch in enumerate(current_symbols):
            if ch == '^':
                # 检查正上方是否是 |
                above_pos = i
                if above_pos in prev_pipes:
                    path_val = prev_pipes[above_pos]
                    
                    # 左侧位置(如果位置有效)
                    left_pos = i - 1
                    if left_pos >= 0:
                        path_map[left_pos] = path_map.get(left_pos, 0) + path_val
                    
                    # 右侧位置(如果位置有效)
                    right_pos = i + 1
                    if right_pos < cols:
                        path_map[right_pos] = path_map.get(right_pos, 0) + path_val
        
        # 第二步:处理直通(上一层的 | 直通到正下方)
        for prev_pos, prev_val in prev_pipes.items():
            # 检查正下方位置
            below_pos = prev_pos
            if below_pos < len(current_symbols):
                # 如果正下方不是 ^(因为 ^ 已经处理过分叉了)
                if current_symbols[below_pos] != '^':
                    # 直通到正下方
                    path_map[below_pos] = path_map.get(below_pos, 0) + prev_val
        
        # 第三步:收集当前层所有 | 的位置和路径数
        # 只有 path_map 中有路径数的位置才有 |
        current_pipes = []
        for pos in sorted(path_map.keys()):
            if path_map[pos] > 0:  # 路径数大于0的位置才有 |
                current_pipes.append((pos, path_map[pos]))
        
        result_pipes.append(current_pipes)
    
    return result_pipes


def read_symbol_layers(filename):
    symbol_layers = []
    with open(filename, 'r') as f:
        for i, line in enumerate(f):
            line = line.strip()
            if line and not all(ch == '.' for ch in line):  # 跳过全.行
                symbol_layers.append(line)
    return symbol_layers


symbol_layers =read_symbol_layers('2507-input.txt')
# 构造路径并计算
constructed_pipes = construct_paths_from_symbols(symbol_layers)
"""
print("构造得到的 | 路径数:")
for i, pipes in enumerate(constructed_pipes):
    print(f"第{i}层: {pipes}")
    if pipes:
        sum_val = sum(val for _, val in pipes)
        print(f"  总和: {sum_val}")
    else:
        print(f"  总和: 0")
"""
# 输出最后一行的结果
print("\n=== 最后一行的结果 ===")
last_layer = constructed_pipes[-1]
#print("最后一行的 | 位置和路径数:")
#for pos, val in last_layer:
#    print(f"  位置 {pos}: 路径数 = {val}")

total_paths = sum(val for _, val in last_layer)
print(f"\n最后一行的总路径数: {total_paths}")
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值