-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathday10.py
More file actions
293 lines (228 loc) · 8.56 KB
/
Copy pathday10.py
File metadata and controls
293 lines (228 loc) · 8.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
from collections import deque
from functools import lru_cache
import numpy as np
from itertools import combinations, product
filepath = "day10.txt"
def read_file():
return [line.strip().split(" ") for line in open(filepath)]
def min_presses(target: int, masks, n_lights: int) -> int:
"""
BFS on light configurations (bitmasks) to find the minimum number
of button presses to reach `target` from all-off (0).
"""
start = 0
if target == 0:
return 0
max_state = 1 << n_lights
dist = [-1] * max_state
dist[start] = 0
q = deque([start])
while q:
state = q.popleft()
d = dist[state]
for mask in masks:
next_state = state ^ mask
if dist[next_state] == -1:
dist[next_state] = d + 1
if next_state == target:
#print(f"Found solution: {dist[next_state]}")
return dist[next_state]
q.append(next_state)
# If there is somehow no solution
raise RuntimeError("No solution for this machine")
def solve_part_I():
lines = read_file()
print(lines)
total_presses = 0
for line in lines:
buttons = [list(map(int, button[1:-1].split(","))) for button in line[1:-1]]
lights_bits = ''
lights_string = line[0][1:-1]
for sign in lights_string:
if sign == '#':
lights_bits += '1'
else:
lights_bits += '0'
lights_int = int(lights_bits[::-1], 2)
masks = []
for button in buttons:
mask = 0
for num in button:
mask |= 1 << num
masks.append(mask)
print(f"Lights: {lights_string}, reversed bits: {lights_bits[::-1]} as int: {lights_int} and buttons: {buttons} translated to bits: {masks}")
presses = min_presses(lights_int, masks, len(lights_string))
total_presses += presses
print(f"{lights_string} -> target={lights_int}, buttons={masks}, presses={presses}")
return total_presses
# result_1 = solve_part_I()
# print(result_1)
def build_button_matrix(buttons, m):
n = len(buttons)
A = np.zeros((m, n), dtype=int)
for j, btn in enumerate(buttons):
for i in btn:
A[i, j] = 1
return A
def ranks_consistent(A, b):
Af = A.astype(float)
bf = b.astype(float).reshape(-1, 1)
rA = np.linalg.matrix_rank(Af)
rAug = np.linalg.matrix_rank(np.hstack([Af, bf]))
return rA, rAug, (rA == rAug)
def solve_unique_if_possible(A, b, tol=1e-9):
"""
If A has full column rank (rank == n), then either:
- no solution, or
- exactly one solution. If it's integral & nonneg, it's the minimum presses.
"""
m, n = A.shape
rA, rAug, ok = ranks_consistent(A, b)
if not ok:
return None
if rA == n: # full column rank => unique solution (if consistent)
x, residuals, _, _ = np.linalg.lstsq(A.astype(float), b.astype(float), rcond=None)
xr = np.rint(x).astype(int)
if np.max(np.abs(x - xr)) > tol: # solution not ints
return None
if np.any(xr < 0): # solution with negatives
return None
if not np.array_equal(A @ xr, b):
return None
return int(xr.sum()), xr
return None
def independent_rows(A, tol=1e-9):
"""Greedy: keep rows that increase rank (uses NumPy rank)."""
rows, r = [], 0
Af = A.astype(float)
for i in range(A.shape[0]):
cand = rows + [i]
r2 = np.linalg.matrix_rank(Af[cand, :], tol=tol)
if r2 > r:
rows, r = cand, r2
return np.array(rows, dtype=int)
def min_presses_branch_and_bound(A, b, chunk=200_000, tol=1e-8):
A = np.asarray(A, dtype=int)
b = np.asarray(b, dtype=int)
m, n = A.shape
# Consistency check via ranks (works even if A is singular)
Af = A.astype(float)
rA = np.linalg.matrix_rank(Af)
rAug = np.linalg.matrix_rank(np.hstack([Af, b.reshape(-1, 1).astype(float)]))
if rA != rAug:
return None # no solutions
# Per-button upper bounds
ub = np.zeros(n, dtype=int)
for j in range(n):
idx = np.where(A[:, j] == 1)[0]
ub[j] = 0 if idx.size == 0 else int(b[idx].min())
# Unique-solution fast path (full column rank)
if rA == n:
x = np.linalg.solve(Af, b.astype(float))
xr = np.rint(x).astype(int)
if np.max(np.abs(x - xr)) <= tol and np.all(xr >= 0) and np.array_equal(A @ xr, b):
return int(xr.sum()), xr
return None
# Reduce to independent rows so we can form square bases
R = independent_rows(A)
Ar = A[R, :].astype(float)
br = b[R].astype(float)
r = Ar.shape[0]
d = n - r
best = None
best_x = None
cols = np.arange(n)
for basic in combinations(cols, r):
basic = np.array(basic, dtype=int)
B = Ar[:, basic]
if np.linalg.matrix_rank(B) < r:
continue
free = np.array([j for j in cols if j not in set(basic)], dtype=int)
Afree = Ar[:, free]
# ---- super fast case: 1 free variable (your problematic machine) ----
if d == 1:
j = free[0]
t = np.arange(ub[j] + 1, dtype=float) # all candidates at once
rhs = br[:, None] - Afree[:, [0]] * t[None, :] # (r, T)
Xb = np.linalg.solve(B, rhs) # (r, T)
Xbr = np.rint(Xb)
ok = np.max(np.abs(Xb - Xbr), axis=0) <= tol
if not np.any(ok):
continue
Xbr = Xbr[:, ok].astype(int)
t_ok = t[ok].astype(int)
# nonnegativity
ok2 = np.all(Xbr >= 0, axis=0)
if not np.any(ok2):
continue
Xbr = Xbr[:, ok2]
t_ok = t_ok[ok2]
# Build candidate full X and check exactness
Q = Xbr.shape[1]
X = np.zeros((n, Q), dtype=int)
X[basic, :] = Xbr
X[j, :] = t_ok
mask = np.all(X >= 0, axis=0) & np.all(X <= ub[:, None], axis=0) & np.all((A @ X) == b[:, None], axis=0)
if not np.any(mask):
continue
X = X[:, mask]
totals = X.sum(axis=0)
k = int(np.argmin(totals))
tot = int(totals[k])
if best is None or tot < best:
best, best_x = tot, X[:, k].copy()
continue
# ---- general d>1: chunked vectorized enumeration (still non-recursive) ----
dims = (ub[free] + 1).astype(int)
P = int(np.prod(dims))
for start in range(0, P, chunk):
end = min(P, start + chunk)
idx = np.arange(start, end, dtype=np.int64)
grid = np.array(np.unravel_index(idx, dims), dtype=int).T # (Q, d)
rhs = br[:, None] - Afree @ grid.T # (r, Q)
Xb = np.linalg.solve(B, rhs)
Xbr = np.rint(Xb)
ok = np.max(np.abs(Xb - Xbr), axis=0) <= tol
if not np.any(ok):
continue
Xbr = Xbr[:, ok].astype(int)
grid = grid[ok, :]
ok2 = np.all(Xbr >= 0, axis=0)
if not np.any(ok2):
continue
Xbr = Xbr[:, ok2]
grid = grid[ok2, :]
Q = Xbr.shape[1]
X = np.zeros((n, Q), dtype=int)
X[basic, :] = Xbr
X[free, :] = grid.T
mask = np.all(X >= 0, axis=0) & np.all(X <= ub[:, None], axis=0) & np.all((A @ X) == b[:, None], axis=0)
if not np.any(mask):
continue
X = X[:, mask]
totals = X.sum(axis=0)
k = int(np.argmin(totals))
tot = int(totals[k])
if best is None or tot < best:
best, best_x = tot, X[:, k].copy()
return best
def min_presses_joltage(target, buttons):
button_matrix = build_button_matrix(buttons, len(target))
presses = solve_unique_if_possible(button_matrix, np.array(target))
if presses is not None:
print("Solved as unique solution")
return presses
return min_presses_branch_and_bound(button_matrix, np.array(target))
def solve_part_II():
lines = read_file()
total_presses = 0
for line in lines:
print(f"Line: {line}")
buttons = [list(map(int, button[1:-1].split(","))) for button in line[1:-1]]
target = list(map(int, line[-1][1:-1].split(",")))
presses = min_presses_joltage(target, buttons)
total_presses += presses if isinstance(presses, int) else presses[0]
print(f"Buttons: {buttons} and target: {target}; presses: {presses}")
return total_presses
result_2 = solve_part_II()
print(result_2)