利用DeepSeek辅助Python求解Advent of Code 2025第10题 电子工厂

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

原题地址

— 第 10 天:工厂 —
就在大厅对面,你发现了一个大型工厂。幸运的是,这里的精灵们有充足的时间来装饰。不幸的是,这是因为工厂的机器都离线了,而且没有一个精灵能弄明白初始化程序。

精灵们确实有机器手册,但详细说明初始化程序的部分被一只柴犬吃掉了。手册中剩下的只是一些指示灯图表、按钮接线示意图以及每台机器的电压要求。

例如:

[.##.] (3) (1,3) (2) (2,3) (0,2) (0,1) {3,5,4,7}
[...#.] (0,2,3,4) (2,3) (0,4) (0,1,2) (1,2,3,4) {7,5,12,7,2}
[.###.#] (0,1,2,3,4) (0,3,4) (0,1,2,4,5) (1,2) {10,11,11,5,10,5}

手册每行描述一台机器。每行包含一个用 [方括号] 表示的指示灯图表、一个或多个用 (圆括号) 表示的按钮接线示意图,以及用 {大括号} 表示的电压要求。

要启动一台机器,其指示灯必须与图表中显示的匹配,其中 . 表示熄灭,# 表示点亮。机器拥有图表所示的指示灯数量,但其指示灯最初都是熄灭的。

因此,像 [.##.] 这样的指示灯图表意味着机器有四个指示灯,最初都是熄灭的,目标是将第一个灯配置为熄灭,第二个灯点亮,第三个灯点亮,第四个灯熄灭。

你可以通过按下列出的任何一个按钮来切换指示灯的状态。每个按钮列出了它切换哪些指示灯,其中 0 表示第一个灯,1 表示第二个灯,依此类推。当你按下按钮时,每个列出的指示灯要么点亮(如果原来是熄灭的),要么熄灭(如果原来是点亮的)。你必须按每个按钮整数次;没有"按0.5次"这回事(也不能按负次数)。

因此,像 (0,3,4) 这样的按钮接线示意图意味着每次按下该按钮,第一、第四和第五个指示灯都会在亮灭之间切换。如果指示灯状态是 [#.....],按下按钮会将它们变成 [...##.]

因为所有机器都没有运行,电压要求无关紧要,可以安全忽略。

你可以按每个按钮任意多次。然而,为了节省时间,你需要确定正确配置列表中所有机器的所有指示灯所需的最少总按压次数

有几种方法可以正确配置第一台机器:

[.##.] (3) (1,3) (2) (2,3) (0,2) (0,1) {3,5,4,7}
  • 你可以将前三个按钮各按一次,总共 3 次按钮按压。
  • 你可以按 (1,3) 一次,(2,3) 一次,以及 (0,1) 两次,总共 4 次按钮按压。
  • 你可以将所有按钮除了 (1,3) 之外各按一次,总共 5 次按钮按压。

然而,所需的最少按钮按压次数是 2。一种实现方法是按下最后两个按钮((0,2)(0,1))各一次。

第二台机器可以用最少 3 次按钮按压来配置:

[...#.] (0,2,3,4) (2,3) (0,4) (0,1,2) (1,2,3,4) {7,5,12,7,2}

一种实现方法是将最后三个按钮((0,4)(0,1,2)(1,2,3,4))各按一次。

第三台机器共有六个指示灯需要正确配置:

[.###.#] (0,1,2,3,4) (0,3,4) (0,1,2,4,5) (1,2) {10,11,11,5,10,5}

正确配置它所需的最少按压次数是 2;一种实现方法是将按钮 (0,3,4)(0,1,2,4,5) 各按一次。

因此,正确配置所有机器指示灯所需的最少按钮按压次数是 2 + 3 + 2 = 7

分析每台机器的指示灯图表和按钮接线示意图。正确配置所有机器指示灯所需的最少按钮按压次数是多少?

— 第二部分 —
所有的机器都开始上线了!现在,是时候担心电压要求了。

每台机器都需要精确配置到指定的电压水平才能正常工作。每台机器的按钮下方有一个大杠杆,你可以用它来将按钮从配置指示灯切换到增加电压水平。(忽略指示灯图表。)

每台机器都有一组数值计数器来跟踪其电压水平,每个电压要求对应一个计数器。计数器最初都设置为零。

因此,像 {3,5,4,7} 这样的电压要求意味着机器有四个计数器,最初为 0,目标是将第一个计数器配置为 3,第二个配置为 5,第三个配置为 4,第四个配置为 7。

按钮接线示意图仍然相关:在这种新的电压配置模式下,每个按钮现在指示它影响哪些计数器,其中 0 表示第一个计数器,1 表示第二个计数器,依此类推。当你按下按钮时,每个列出的计数器增加 1。

因此,像 (1,3) 这样的按钮接线示意图意味着每次按下该按钮,第二个和第四个计数器各增加 1。如果当前的电压水平是 {0,1,2,3},按下按钮会将它们变为 {0,2,2,4}

你可以按每个按钮任意多次。然而,你的手指因为按了太多按钮而感到酸痛,因此你需要确定正确配置每台机器的电压水平计数器以匹配指定电压要求所需的最少总按压次数

再考虑一下之前的例子:

[.##.] (3) (1,3) (2) (2,3) (0,2) (0,1) {3,5,4,7}
[...#.] (0,2,3,4) (2,3) (0,4) (0,1,2) (1,2,3,4) {7,5,12,7,2}
[.###.#] (0,1,2,3,4) (0,3,4) (0,1,2,4,5) (1,2) {10,11,11,5,10,5}

配置第一台机器的计数器需要最少 10 次按钮按压。一种实现方法是按 (3) 一次,(1,3) 三次,(2,3) 三次,(0,2) 一次,以及 (0,1) 两次。

配置第二台机器的计数器需要最少 12 次按钮按压。一种实现方法是按 (0,2,3,4) 两次,(2,3) 五次,以及 (0,1,2) 五次。

配置第三台机器的计数器需要最少 11 次按钮按压。一种实现方法是按 (0,1,2,3,4) 五次,(0,1,2,4,5) 五次,以及 (1,2) 一次。

因此,正确配置所有机器的电压水平计数器所需的最少按钮按压次数是 10 + 12 + 11 = 33

分析每台机器的电压要求和按钮接线示意图。正确配置所有机器的电压水平计数器所需的最少按钮按压次数是多少?

第一问其实就是二进制运算,将上面问题中的.#看做二进制数0和1,圆括号中的数字改写成数字代表那些位为1的二进制数,问题就变成0和后面圆括号的数多少次异或操作得到方括号的数,求助DeepSeek就得到了以上思路程序

import re
from functools import lru_cache

def parse_line(line):
    """解析一行数据,返回目标状态和按钮列表"""
    # 匹配方括号部分
    match_bracket = re.search(r'\[([.#]+)\]', line)
    if not match_bracket:
        return None, None
    
    target_str = match_bracket.group(1)
    # 将.#转换为二进制数(0表示关,1表示开)
    target = 0
    for i, ch in enumerate(target_str):
        if ch == '#':
            target |= (1 << i)
    
    # 匹配所有圆括号部分
    buttons = []
    pattern = r'\(([^)]+)\)'
    matches = re.findall(pattern, line)
    
    for match in matches:
        # 解析圆括号内的数字
        nums = [int(x.strip()) for x in match.split(',')]
        # 创建按钮掩码
        mask = 0
        for num in nums:
            mask |= (1 << num)
        buttons.append(mask)
    
    return target, buttons

def solve_machine(target, buttons):
    """解决单个机器的最小按钮按压次数"""
    n = len(buttons)
    
    @lru_cache(None)
    def dfs(state, idx):
        """当前状态为state,处理到第idx个按钮的最小按压次数"""
        if state == target:
            return 0
        if idx == n:
            return float('inf')
        
        # 两种选择:按或不按当前按钮
        # 不按当前按钮
        not_press = dfs(state, idx + 1)
        
        # 按当前按钮(可以按1次,但按两次等于不按,所以最多按1次)
        new_state = state ^ buttons[idx]
        press_once = 1 + dfs(new_state, idx + 1)
        
        return min(not_press, press_once)
    
    result = dfs(0, 0)
    return result if result != float('inf') else 0

def main():
    total_presses = 0
    
    # 读取输入文件
    with open('input.txt', 'r') as f:
        lines = f.read().strip().splitlines()
    
    for i, line in enumerate(lines):
        target, buttons = parse_line(line)
        if target is None or not buttons:
            continue
        
        min_presses = solve_machine(target, buttons)
        print(f"机器 {i+1}: 最少需要 {min_presses} 次按压")
        total_presses += min_presses
    
    print(f"\n总最少按压次数: {total_presses}")
    return total_presses

if __name__ == "__main__":
    main()

一字不改,这个dfs程序可以在瞬间算出结果,因为结果的2进制数很小。

第二问其实性质和第一问一样,只不过每个电压值不是01那么小,正式输入数据有200多。还沿用第一问的程序改成256进制行不通,示例数据尚可,正式数据第一个就卡住了。DeepSeek给出了一个线性规划+备用贪心算法程序,前10个都能算出来,先放在这里,继续研究优化方法。贪心算法经实验也不那么有效,只有一个只有4位数的电路板能算出来。

import re
import numpy as np
from scipy.optimize import linprog

def parse_line_part2(line):
    """解析一行数据,返回电压要求和按钮列表(第二部分)"""
    # 匹配大括号部分
    match_brace = re.search(r'\{([^}]+)\}', line)
    if not match_brace:
        return None, None
    
    # 解析电压要求
    joltage_str = match_brace.group(1)
    target = [int(x.strip()) for x in joltage_str.split(',')]
    
    # 匹配所有圆括号部分
    buttons = []
    pattern = r'\(([^)]+)\)'
    matches = re.findall(pattern, line)
    
    for match in matches:
        # 解析圆括号内的数字
        nums = [int(x.strip()) for x in match.split(',')]
        buttons.append(nums)
    
    return target, buttons

def solve_machine_part2(target, buttons):
    """解决单个机器的电压配置问题(最小按钮按压次数)"""
    n_counters = len(target)
    n_buttons = len(buttons)
    
    # 构建系数矩阵 A
    A = []
    for btn in buttons:
        row = [0] * n_counters
        for idx in btn:
            row[idx] = 1
        A.append(row)
    
    # 转置A,因为每个按钮按x次对计数器的贡献是A^T * x
    A = np.array(A, dtype=int).T  # 形状: (n_counters, n_buttons)
    
    # 线性规划:最小化按压次数总和
    c = [1] * n_buttons  # 目标函数系数:最小化总按压次数
    
    # 约束:A * x = target, x >= 0 且为整数
    # 这里先解线性规划,然后四舍五入到整数解
    
    # 变量边界
    bounds = [(0, None)] * n_buttons
    
    # 解线性规划
    res = linprog(c, A_eq=A, b_eq=target, bounds=bounds, method='highs')
    
    if not res.success:
        return 0
    
    # 获取解并四舍五入到整数(因为按压次数必须是整数)
    x = res.x
    # 简单的四舍五入(对于小问题通常有效)
    x_int = np.round(x).astype(int)
    
    # 检查是否满足约束
    if np.allclose(A @ x_int, target):
        return int(np.sum(x_int))
    else:
        # 如果需要,可以尝试整数规划或搜索
        # 这里使用简单的贪心方法作为备选
        #return greedy_solution(target, buttons, A)
        return 0

def greedy_solution(target, buttons, A):
    """贪心方法求解整数解"""
    n_counters = len(target)
    n_buttons = len(buttons)
    
    # 初始化解向量
    x = [0] * n_buttons
    current = [0] * n_counters
    
    # 贪心地选择能最接近目标的按钮
    while current != target:
        best_btn = -1
        best_score = float('inf')
        
        for i in range(n_buttons):
            # 计算按这个按钮一次能减少多少差距
            temp_current = current.copy()
            for idx in buttons[i]:
                temp_current[idx] += 1
            
            # 计算差距的平方和
            score = sum((t - c) ** 2 for t, c in zip(target, temp_current))
            
            if score < best_score:
                best_score = score
                best_btn = i
        
        if best_btn == -1:
            break
        
        # 按最佳按钮一次
        x[best_btn] += 1
        for idx in buttons[best_btn]:
            current[idx] += 1
    
    return sum(x) if current == target else 0

def main_part2():
    total_presses = 0
    
    # 读取输入文件
    with open('input.txt', 'r') as f:
        lines = f.read().strip().splitlines()
    
    for i, line in enumerate(lines):
        target, buttons = parse_line_part2(line)
        if target is None or not buttons:
            continue
        
        min_presses = solve_machine_part2(target, buttons)
        print(f"机器 {i+1}: 电压配置最少需要 {min_presses} 次按压")
        total_presses += min_presses
    
    print(f"\n总最少按压次数: {total_presses}")
    return total_presses

if __name__ == "__main__":
    main_part2()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值