Advent of Code 2025 day10
文章分为两部分:第一部分讨论通过按按钮改变灯的状态,每个按钮最多按一次即可;第二部分涉及调整灯的电压值至目标状态,采用整数线性规划和Z3库求解最小操作次数。 2025-12-15 03:3:0 Author: taxodium.ink(查看原文) 阅读量:6 收藏

day10 的输入是:

[.##.] (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,3) 表示一个按钮,连接了下标 1 和下标 3 的灯,按一次,下标 1 和 3 的灯就会亮起,变成 # ;再按一次,下标 1 和 3 的灯就会熄灭,变回 .

part1 的问题是,你可以按任意的按钮任意次,使得灯的状态变成每一行开头 [] 里的状态,找到每一行需要的最少次数,累计求和。


针对一个按钮,按 1 次,对应下标的灯会亮起;按 2 次,对应下标的灯会熄灭,相当于没按过;按 3 次和按 1 次的效果是一样的。

也就是说,对一个按钮,按奇数次,会把对应下标的灯亮起,偶数次相当于这个灯没被按过。所以对任何一个灯,最多按 1 次就够了。

那么问题就变成了,对任意的灯按 1 次,它们的组合使得灯的状态变成 [] 中的状态。

假设有 A、B、C 三个按钮,我可以有很多组合:

  • 只按 1 次 A / 只按 1 次 B / 只按 1 次 C (总操作数 1 次)
  • 只按 1 次 A 和 B / 只按 1 次 A 和 C / 只按 1 次 B 和 C (总操作数 2 次)
  • 只按 1 次 A、B、C (总操作数 3 次)

对每一行,我只要枚举所有的按钮组合,都按 1 次,从组合数量少的开始遍历(这样次数就是最少的),找到一个组合,使得灯的状态和 [] 中的状态一致就好了。

得到按钮的所有组合,需要用到 回溯算法,在 python 中也可以利用 itertools.combinations

判断状态的时候,我是按照按钮的下标拼接成 .# 组合的字符串,和 [] 中的字符串做比较,效率可能比较低。

Yordi Verkroost 在 Advent of Code 2025 - Day 10 中使用二进制表示按钮,用 XOR(异或) 运算得到最终的状态,效率应该会更好。


part2 的输入不变,不过此时需要关注的是 {} 里的部分。

[.##.] (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}

还是看第一行, [] 可以忽略, {3,5,4,7} 表示的是电压,下标 0 的电压是 3;下标 1 的电压是 5;依次类推。{} 中的电压默认都是 0。按钮 (1,3) 可以使得下标 1、3 的电压增加 1。

part2 的问题是,你可以按任意按钮任意次,最终使得电压达到 {} 里面的要求,找到需要的最少次数,累计每一行的最小次数求和。


一开始没什么头绪,我尝试随便按一个按钮看看会如何,例如我先尝试按 (0,2) ,先满足把下标 0 的电压:

  • 按 1 次,电压变成 {1,0,1,0}
  • 再按 1 次,电压变成 {2,0,2,0}
  • 再按 1 次,电压变成 {3,0,3,0}

此时我就不能继续按 (0,2) 了,因为下标 0 的电压已经达到了 {3,5,4,7} 中的要求,再按就会超了。也就是说,现在我不能操作下标包含 0 的按钮了。

继续尝试操作其他按钮,再满足一个下标的电压,直到达到 {} 中的电压要求。

为什么我先开始满足下标 0 的电压?

因为在 {3,5,4,7} 中,下标 0 的电压是最小的,无论我操作哪个按钮,当下标 0 的电压达到要求的时候,其他下标的电压也不会超出。

按钮 (0,2)(0,1) 都能使得下标 0 的电压增加 1,我怎么判断用哪个呢?我不知道,干脆穷举算了。

我先尝试都用 (0,2) 满足下标 0 的电压,下标 0 电压满足了,我就排除所有包含下标 0 的按钮,从剩下的按钮里,继续满足当前电压较小的,不断循环这个过程。

最后如果找到一个组合,能够满足 {} 中的电压要求,就记录按钮的操作次数,再找到操作次数的最小值。

尝试完 (0,2) ,就继续尝试 (0,1) ,看看哪个的操作次数是最少的。

折腾了好久写出一段代码,通过了示例输入,但死活过不了实际输入,看来思路是不对了。


没什么思路,于是去 reddit 找找思路,发现很多人提到了 Z3ILP (Integer Linear Programming),都是我不了解的东西。

和 LLM 进行了一些对话,把 part2 的问题丢给了它,让它分析问题,我大概明白了应该如何做。

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

有这么几个按钮:

  • (3) → 下标 3 加 1
  • (1,3) → 下标 1 和 3 加 1
  • (2) → 下标 2 加 1
  • (2,3) → 下标 2 和 3 加 1
  • (0,2) → 下标 0 和 2 加 1
  • (0,1) → 下标 0 和 1 加 1

假设:

  • a: (3) 的次数
  • b: (1,3) 的次数
  • c: (2) 的次数
  • d: (2,3) 的次数
  • e: (0,2) 的次数
  • f: (0,1) 的次数

设最终需要的操作总次数是 T,那么 T = a + b + c + d + e + f。

其中:

下标 0 的电压要求是 3,(0,2) 和 (0,1) 会影响下标 0,操作 e 次 (0,2) 和 f 次 (0,1) 最终应该达到电压 3,所以有 e + f = 3。

类似的:

  • 下标 1 的电压要求是 5: b + f = 5
  • 下标 2 的电压要求是 4: c + d + e = 4
  • 下标 3 的电压要求是 7: a + b + d = 7

于是得到了几个线性方程:

  • T = a + b + c + d + e + f
  • e + f = 3
  • b + f = 5
  • c + d + e = 4
  • a + b + d = 7
  • a、b、c、d、e、f 都是整数,且大于或等于 0 (每个按钮可以不操作或操作任意次)

目标是使得 T 最小。


此时再看看 线性规划 (Linear programming, LP) 的定义:

线性规划(LP),也称为线性优化,是一种在数学模型中实现最佳结果的方法,该模型的各项要求和目标均由线性关系表示。

[…]

更正式地说,线性规划是一种优化线性目标函数的技术, 该函数受线性等式和线性不等式约束。其可行域是一个凸多面体,该多面体定义为有限多个半空间的交集,每个半空间都由一个线性不等式定义。其目标函数是定义在该多面体上的实值仿射(线性)函数。如果存在这样的点,线性规划算法会在多面体中找到该函数具有最大(或最小)值的点。

说实话我没太看懂,但大体的意思是,有一种数学模型,可以用一些线性等式和线性不等式来表示,同时存在一些约束条件。通过这样的模型,最终可以找到最值。

而当线性规划中,变量被约束为整数,问题就会变成了 整数线性规划 (Integer Linear Programming, ILP)

此时回过头来看看上面得到的结论:

  • T = a + b + c + d + e + f
  • e + f = 3
  • b + f = 5
  • c + d + e = 4
  • a + b + d = 7
  • a、b、c、d、e、f 都是整数,且大于或等于 0 (每个按钮可以不操作或操作任意次)

目标是使得 T 最小。

可以发现这个模型是符合整数线性规划的:

  • 可以用线性等式表达问题
  • 存在约束条件,例如 e + f 需要等于 3
  • 变量 a,b,c,d,e,f 要求是整数

所以,这就是一个整数线性规划问题,而在 python 中,可以利用 Z3PuLP 来处理整数线性规划问题。

不过就算知道可以用 Z3,我也不会用 _​(:3 」∠)_​。

还是求助了 LLM,得到这样的一个函数,并问它每一行代码的含义:

from z3 import Int, Optimize, sat

def min_operations_z3(ops, target):
    """使用 Z3 求解最小操作次数,返回整数最优值(若无解返回 None)"""
    n = len(ops)           # 操作数量
    m = len(target)        # 索引数量

    # 创建变量,表示的是每个按钮的操作次数,x0 表示第一个按钮的操作次数
    vars = [Int(f'x{i}') for i in range(n)]

    opt = Optimize()
    # 添加约束,操作次数只能大于或等于 0
    for v in vars:
        opt.add(v >= 0)

    # 遍历所有的目标电压
    for i in range(m):
        # 遍历所有的按钮,当按钮会影响目标电压,则累计操作次数,相当于构造 e + f = 3
        coeff_sum = sum(vars[j] for j in range(n) if i in ops[j])
        opt.add(coeff_sum == target[i])

    # 最小化总操作次数,使用 minimize 求最小值
    total = sum(vars)
    opt.minimize(total)

    # 当所有约束都能满足的时候,得到这个模型对象,获取操作总次数的最小值
    if opt.check() == sat:
        model = opt.model()
        return model.evaluate(total).as_long()
    else:
        return None

使用 Z3 问题就很轻松就解决了。

如果你对 PuLP 的方法感兴趣,可以看看 Yordi Verkroost 的 Advent of Code 2025 - Day 10


day10 依然是让人折磨的一天 _​(:3 」∠)_​,希望以后我碰到类似问题能想起来可以用线性规划解决。


文章来源: https://taxodium.ink/aoc-2025-day-10.html
如有侵权请联系:admin#unsafe.sh