Advent of Code 2025 挑战全手写代码 Day 10 - 工厂

51 阅读4分钟

🎄 Advent of Code 2025 挑战全手写代码 Day 10 - 工厂


大家好,昨天 day 9 的题目有没有难到你?今天的题目 Factory 难度依旧困难(四星 ⭐⭐⭐⭐),核心考察:位运算(异或)组合枚举、以及线性方程建模广度优先搜索 等知识点。

advent-of-code-2025-day-10

📖 题目速览

  • 题目地址:adventofcode.com/2025/day/10
  • 背景:走廊对面是一座大型工厂,机器全部离线。初始化流程的手册关键部分被柴犬吃掉,只剩下指示灯图、按钮接线和电压需求。你需要先用按钮配置指示灯(Part 1),再拉下杠杆切换到电压模式,用同一组按钮把各计数器累加到指定值(Part 2),并最小化总按键次数。
  • 每台机器一行:方括号是指示灯目标(Part 2 忽略,Part 1 使用)、括号是按钮影响的灯位(或计数器索引),花括号是电压(Part 1 忽略,Part 2 使用)。

🧠 解题思路 (Python 🐍)

  • Part 1:用“异或”建模并按子集大小逐步枚举

    • 本题中灯只有开、关两个状态,对应二进制 0 和 1。而按钮的作用是“切换灯的状态”,即 0 变 1,1 变 0。
    • 切换操作正好可以用位运算的异或(^)实现:0 异或 1 等于 1,1 异或 1 等于 0。
    • 每个按钮对应一个位掩码;按一次按钮就是把当前状态与该掩码做异或。
    • 虽然题目说“可以按一个按钮任意多次”,但因为异或的交换律和自反性(同一按钮按两次等于没按),问题等价于“每个按钮最多按一次”。
    • 从小到大枚举按钮子集,第一次异或出目标就得到最少按键次数。
    def part1(self) -> int:
        out: int = 0
        for light, buttons, _, _ in self.entries:
            breaked: bool = False
            for k in range(len(buttons)):
                for comb in combinations(buttons, k + 1):
                    status: int = 0
                    for num in comb:
                        status ^= num
                    if status == light:
                        out += k + 1
                        breaked = True
                        break
                if breaked:
                    break
        return out
    

今日挑战全手写代码失败!

  • Part 2:把“电压累加”建成线性方程,使用 SymPy 求解(尚未完成整数约束)

    • 切换到电压模式后,每次按按钮会让被影响的计数器各自加 1。
    • 把每个按钮的“作用次数”记作未知量 x_j,每个计数器的目标是一个等式:Σ A[i][j] * x_j = b[i]
    • 使用 sympy.solve 在实数域里求解,随后在所有解集中选取 sum(x_j) 最小的那个,并把它加到总按键次数上。
    • 当前代码尚未加入 x_j ∈ Z_≥0 的整数与非负约束,属于阶段性实现,后续会完善。
    def part2(self) -> int:
        out: int = 0
        for _, _, button_bits, joltage_req in self.entries:
            button_bits: list[list[int]] = [
                [int(char) for char in bits] for bits in button_bits
            ]
            equations: list[Eq] = []
            for i in range(len(joltage_req)):
                equations.append(
                    Eq(
                        sum(
                            int(button_bits[j][i]) * symbols(f"x_{j}")
                            for j in range(len(button_bits))
                        ),
                        joltage_req[i],
                    )
                )
            solutions: list[dict] = solve(equations, dict=True)
            min_sum = None
            for solution in solutions:
                total = sum(solution.values())
                if min_sum is None or total < min_sum:
                    min_sum = total
            out += min_sum
        return out
    

🧩 关键代码片段

  • 输入解析:把方括号的灯图转成二进制目标;用括号里的索引构造每个按钮的位掩码;读取花括号的电压目标。
    def _process_entry(self, line: str) -> tuple[int, list[int], list[list[str]], list[int]]:
        splits = line.split(" ")
        light_diagram = splits[0][1:-1]
        light = int(light_diagram.replace(".", "0").replace("#", "1"), 2)
        raw_buttons = splits[1:-1]
        buttons, button_bits = [], []
        for raw_button in raw_buttons:
            bits = ["0"] * len(light_diagram)
            for num in raw_button[1:-1].split(","):
                bits[int(num)] = "1"
            button_bits.append(bits)
            buttons.append(int("".join(bits), 2))
        joltage_req = list(map(int, splits[-1][1:-1].split(",")))
        return light, buttons, button_bits, joltage_req
    

✨ 代码复盘 & 优化思考

  • Part 1:当前用组合枚举,复杂度在按钮数 m 较大时指数级;但本题输入规模小,能在样例和常规数据下跑通。若按钮变多,可改用“状态 BFS”(灯状态作为节点,边为异或按钮)在 O(2^n · m) 内求最短步数。
  • Part 2:目前用 SymPy 在线性方程组中找“最小和”的实数解,尚未加整数约束(x_j ∈ Z_≥0)。下一步的思路:
    • 使用整数线性规划(ILP),约束 x_j 为非负整数并最小化 Σ x_j
    • 或枚举线性系统的“列基”(basic feasible solutions),在有限顶点上筛选非负整数解;
    • 亦可在小规模下做分支限界 / 动态规划优化,或组合“下界剪枝 + 递归”减少搜索。

⏱️ 复杂度分析

  • Part 1:最坏 O(2^m),实际按子集大小分层,命中后提前退出。
  • Part 2:解线性方程在实数域多项式时间,但严格的整数解需要 ILP 或其他离散优化方法。

结语

Day 10 的 Part 2 还在打磨中;即使暂时卡壳也不要气馁,只做完 Part 1 也有一颗星 ⭐。保持对复杂度的敏感度,逐步完善整数约束与剪枝策略,明天继续冲榜!🚀