🎄 Advent of Code 2025 挑战全手写代码 Day 10 - 工厂
大家好,昨天 day 9 的题目有没有难到你?今天的题目 Factory 难度依旧困难(四星 ⭐⭐⭐⭐),核心考察:位运算(异或)、组合枚举、以及线性方程建模、广度优先搜索 等知识点。
📖 题目速览
- 题目地址: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),在有限顶点上筛选非负整数解;
- 亦可在小规模下做分支限界 / 动态规划优化,或组合“下界剪枝 + 递归”减少搜索。
- 使用整数线性规划(ILP),约束
⏱️ 复杂度分析
- Part 1:最坏
O(2^m),实际按子集大小分层,命中后提前退出。 - Part 2:解线性方程在实数域多项式时间,但严格的整数解需要 ILP 或其他离散优化方法。
结语
Day 10 的 Part 2 还在打磨中;即使暂时卡壳也不要气馁,只做完 Part 1 也有一颗星 ⭐。保持对复杂度的敏感度,逐步完善整数约束与剪枝策略,明天继续冲榜!🚀