154 lines
4.5 KiB
Python
154 lines
4.5 KiB
Python
#!/usr/bin/env python
|
|
|
|
# https://adventofcode.com/2025/day/10
|
|
|
|
from itertools import combinations
|
|
from pulp import LpProblem, LpMinimize, LpVariable, LpInteger, LpStatus
|
|
from pulp import lpDot, lpSum, value
|
|
|
|
# imports for function part2()
|
|
from itertools import product
|
|
from sympy import symbols, Matrix, solve_linear_system
|
|
|
|
|
|
f = open("day10input.txt", "r")
|
|
|
|
# regular input
|
|
input = f.readlines()
|
|
# testinput
|
|
#input = "[.##.] (3) (1,3) (2) (2,3) (0,2) (0,1) {3,5,4,7}\n[...#.] (0,2,3,4) (2,3) (0,4) (0,1,2) (1,2,3,4) {7,5,12,7,2}\n[.###.#] (0,1,2,3,4) (0,3,4) (0,1,2,4,5) (1,2) {10,11,11,5,10,5}".split("\n")
|
|
|
|
|
|
# prep data
|
|
machines = []
|
|
for input_line in input:
|
|
line = input_line.rstrip("\n").split(" ")
|
|
machine = {}
|
|
|
|
# final state
|
|
fs = line[0][1:-1].replace(".", "0").replace("#", "1")
|
|
machine["fs"] = int(fs, base=2)
|
|
|
|
# switches
|
|
switches = []
|
|
for switch in line[1:-1]:
|
|
s = ["0"] * len(fs)
|
|
for i in [int(j) for j in switch[1:-1].split(",")]:
|
|
s[i] = "1"
|
|
switches.append("".join(s))
|
|
machine["s"] = sorted([int(s, base=2) for s in switches], key=int.bit_count)
|
|
|
|
# joltage req's
|
|
machine["j"] = [int(n) for n in line[-1][1:-1].split(",")]
|
|
|
|
machines.append(machine)
|
|
|
|
|
|
def print_machines(machines: list[dict]) -> None:
|
|
for i in range(len(machines)):
|
|
print(f"Machine no. {i}")
|
|
for k in machines[i].keys():
|
|
print(f"key:\t{k}\tvalue:\t{machines[i][k]}")
|
|
|
|
|
|
def part1_bfs(m):
|
|
for steps in range(1, len(m["s"])):
|
|
# check if current step no yields a solution
|
|
for c in list(combinations(m["s"], steps)):
|
|
lights = 0
|
|
for i in c:
|
|
lights ^= i
|
|
if lights == m["fs"]:
|
|
print(f"Success with combo {i} and step count of {steps}")
|
|
return steps
|
|
return None
|
|
|
|
|
|
# my own attempt to solve each problem with sympy
|
|
# problem is that sympy computes not a minimum but a general solution
|
|
# So for each problem I have to back-substitute possible combinations for the free variables and find the minimum with brute force
|
|
# the function works, runs a long time, and the final solution is off by 1 :-/
|
|
def part2(m):
|
|
switches = [list(map(int, list("{0:b}".format(switch).zfill(len(m["j"]))))) for switch in m["s"]]
|
|
joltages = m["j"]
|
|
|
|
# define sympy symbols and equations
|
|
x = symbols(f"x:{len(switches)}", integer=True)
|
|
system = Matrix([[switch[j] for switch in switches] + [joltages[j]] for j in range(len(joltages))])
|
|
print(f"Equation system to solve: {system}")
|
|
|
|
# solve system with sympy
|
|
solutions = solve_linear_system(system, *x)
|
|
print(f"Solutions: {solutions}")
|
|
|
|
# isolate free variables
|
|
free_vars = [var for var in x if var not in solutions.keys()]
|
|
print(f"{free_vars=}")
|
|
|
|
# find smallest solution
|
|
smallest_sum = None
|
|
|
|
# loop over the cartesian product of sensible numbers for button presses times the number of free variables
|
|
for lv in product(range(max(joltages)), repeat=len(free_vars)):
|
|
current_sum = sum(lv)
|
|
for v in solutions.values():
|
|
for i in range(len(lv)):
|
|
v = v.subs(free_vars[i], lv[i])
|
|
if v < 0:
|
|
current_sum = -1
|
|
break
|
|
current_sum += v
|
|
if current_sum < 0:
|
|
continue
|
|
|
|
# check current sum
|
|
if not current_sum.is_Integer:
|
|
continue
|
|
elif not smallest_sum or current_sum < smallest_sum:
|
|
smallest_sum = current_sum
|
|
|
|
print(f"Smallest number of presses found: {smallest_sum}")
|
|
return smallest_sum
|
|
|
|
|
|
# Second attempt: solving each problem with pulp, a Python library for linear programming
|
|
# runs blazingly fast, returns correct solution
|
|
def part2_pulp(m):
|
|
switches = [list(map(int, list("{0:b}".format(switch).zfill(len(m["j"]))))) for switch in m["s"]]
|
|
joltages = m["j"]
|
|
|
|
# define problem via pulp
|
|
problem = LpProblem("Advent of Code 2025, Day 10, Part 2", LpMinimize)
|
|
|
|
# define variables
|
|
x = LpVariable.matrix("x", list(range(len(switches))), 0, None, LpInteger)
|
|
|
|
# add objective function to problem
|
|
problem += lpSum(x)
|
|
|
|
# add equations to problem
|
|
for j in range(len(joltages)):
|
|
problem += lpDot(x, [switch[j] for switch in switches]) == joltages[j], f"Equation {j}"
|
|
problem.solve()
|
|
|
|
# the minimum number of steps necessary to reach the desired joltages is the sum of all variables
|
|
return sum([value(xi) for xi in x])
|
|
|
|
|
|
#print_machines(machines)
|
|
|
|
# solve part 1
|
|
print(sum(list(map(part1_bfs, machines))))
|
|
|
|
# solve part 2
|
|
steps = 0
|
|
for machine in machines:
|
|
# solve the machine using sympy and back-substitution
|
|
# steps += part2(machine)
|
|
|
|
# solve the machine using pulp
|
|
steps += part2_pulp(machine)
|
|
|
|
print(f"current total step number: {steps}")
|
|
print(f"final total step number: {steps}")
|