Coverage for Day10 / part1.py: 75%
115 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-12 09:47 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-12 09:47 +0000
1#! /usr/bin/env python
2# -*- coding: utf-8 -*-
4"""
5Advent Of Code 2025
6===================
7Day : 10
8Part : 1
10Résolution du problème sous GF(2) : identification du nombre minimal
11d'appuis de boutons permettant de reproduire un motif binaire ciblé.
13.. codeauthor:: Alexandre Condette <alexandre.condette@wanadoo.fr>
14"""
16# %% ========================================================================
17# LECTURE DE L’INPUT
18def get_input(day: int = 1, example: bool = False) -> list:
19 """
20 Lit le fichier d'entrée associé au jour demandé.
22 :param day: Numéro du jour AoC.
23 :param example: True → example.txt, False → input.txt.
24 :return: Liste de chaînes, chaque ligne du fichier sans retour à la ligne.
25 """
26 file = "example.txt" if example else "input.txt"
27 with open(f"./Day{day}/{file}", "r", encoding="utf-8") as f:
28 return [line.rstrip("\n") for line in f]
30# ===========================================================================
32# %% ========================================================================
33# Fonctions utiles
34def gauss(A: list, b: list):
35 """
36 Réduction de Gauss en arithmétique mod 2.
38 :param A: Matrice binaire n×m (liste de listes).
39 :param b: Vecteur binaire de taille n.
40 :return:
41 - is_ok : bool — une solution existe ?
42 - x0 : solution particulière (liste binaire)
43 - null_basis : base du noyau (liste de vecteurs binaires)
44 """
45 n = len(A)
46 m = len(A[0]) if n > 0 else 0
48 M = [row[:] + [rhs] for row, rhs in zip(A, b)]
49 row = 0
50 pivots = []
52 # Pivotisation sur chaque colonne
53 for col in range(m):
54 sel = None
55 for r in range(row, n):
56 if M[r][col] == 1:
57 sel = r
58 break
60 if sel is None:
61 continue
63 # Échange de lignes
64 M[row], M[sel] = M[sel], M[row]
65 pivots.append((row, col))
67 # Élimination
68 for r in range(n):
69 if r != row and M[r][col] == 1:
70 for c in range(col, m + 1):
71 M[r][c] ^= M[row][c]
73 row += 1
74 if row == n:
75 break
77 # Détection d'incohérence
78 for r in range(row, n):
79 if all(M[r][c] == 0 for c in range(m)) and M[r][m] == 1: 79 ↛ 80line 79 didn't jump to line 80 because the condition on line 79 was never true
80 return (False, None, None)
82 # Solution particulière
83 x0 = [0] * m
84 for r, c in reversed(pivots):
85 acc = M[r][m]
86 for cc in range(c + 1, m):
87 acc ^= (M[r][cc] & x0[cc])
88 x0[c] = acc
90 # Base du noyau
91 pivot_cols = {c for _, c in pivots}
92 free_cols = [c for c in range(m) if c not in pivot_cols]
93 null_basis = []
95 for f in free_cols:
96 v = [0] * m
97 v[f] = 1
98 for r, c in reversed(pivots):
99 s = 0
100 for cc in range(c + 1, m):
101 s ^= (M[r][cc] & v[cc])
102 v[c] = s
103 null_basis.append(v)
105 return (True, x0, null_basis)
107# ---------------------------------------------------------------------------
108def weight(vec: list) -> int:
109 """
110 Calcule le poids de Hamming d’un vecteur binaire.
112 Le poids correspond au nombre total de bits égaux à 1.
113 Utilisé pour mesurer le nombre d'appuis (ou la "taille") d'une solution
114 dans l’espace GF(2).
116 :param vec: Liste d'entiers 0/1.
117 :return: Nombre d’éléments valant 1.
118 """
119 return sum(vec)
121# ---------------------------------------------------------------------------
122def add_mod2(u: list, v: list) -> list:
123 """
124 Effectue une addition binaire terme à terme (XOR).
126 Cette opération correspond à l'addition dans le corps GF(2) :
127 0⊕0=0, 1⊕0=1, 0⊕1=1, 1⊕1=0.
128 Employé pour combiner une solution particulière et des vecteurs du noyau.
130 :param u: Premier vecteur binaire (liste 0/1).
131 :param v: Second vecteur binaire (liste 0/1), même taille que u.
132 :return: Nouveau vecteur résultant de u ⊕ v.
133 """
134 return [(a ^ b) for a, b in zip(u, v)]
136# ---------------------------------------------------------------------------
137def min_presses(target_str: str, buttons: list) -> int:
138 """
139 Calcule le nombre minimal d'appuis nécessaires pour reproduire
140 un pattern binaire donné.
142 :param target_str: Motif cible ('.' ou '#').
143 :param buttons: Liste de tuples ; chaque bouton liste les positions togglées.
144 :return: Nombre minimal d'appuis ou None si impossible.
145 """
146 n = len(target_str)
147 b = [1 if c == "#" else 0 for c in target_str]
148 m = len(buttons)
150 # Construction de A (n × m)
151 A = [[0] * m for _ in range(n)]
152 for j, btn in enumerate(buttons):
153 for idx in btn:
154 A[idx][j] = 1
156 ok, x0, null_basis = gauss(A, b)
157 if not ok: 157 ↛ 158line 157 didn't jump to line 158 because the condition on line 157 was never true
158 return None
160 k = len(null_basis)
161 best = None
163 # Cas dimension faible → exploration exhaustive
164 if k <= 24: 164 ↛ 175line 164 didn't jump to line 175 because the condition on line 164 was always true
165 for mask in range(1 << k):
166 x = x0[:]
167 for i in range(k):
168 if (mask >> i) & 1:
169 x = add_mod2(x, null_basis[i])
170 w = weight(x)
171 if best is None or w < best:
172 best = w
174 # Bruteforce direct si peu de boutons
175 elif m <= 24:
176 for mask in range(1 << m):
177 state = [0] * n
178 presses = 0
179 for j in range(m):
180 if (mask >> j) & 1:
181 presses += 1
182 for idx in buttons[j]:
183 state[idx] ^= 1
184 if state == b and (best is None or presses < best):
185 best = presses
187 # Heuristique sinon
188 else:
189 x = x0[:]
190 improved = True
191 while improved:
192 improved = False
193 for v in null_basis:
194 cand = add_mod2(x, v)
195 if weight(cand) < weight(x):
196 x = cand
197 improved = True
198 best = weight(x)
200 return best
202# ===========================================================================
204# %% ========================================================================
205# Résolution
206def solve(lines: list) -> int:
207 """
208 Parse chaque ligne du fichier d'entrée puis applique la résolution GF(2).
210 Format attendu :
211 [.##.] (3) (1,3) (2) (2,3) (0,2) (0,1)
213 :param lines: Liste de lignes du fichier d'entrée.
214 :return: Somme des minima pour toutes les lignes.
215 """
216 total = 0
218 for line in lines:
219 if not line.strip(): 219 ↛ 220line 219 didn't jump to line 220 because the condition on line 219 was never true
220 continue
222 diag = line.split("]")[0].split("[")[1]
224 parts = line.split("]")[1].strip().split(")")
225 buttons = []
226 for p in parts:
227 if "(" not in p:
228 continue
229 inside = p.split("(")[1]
230 if inside.strip() == "": 230 ↛ 231line 230 didn't jump to line 231 because the condition on line 230 was never true
231 buttons.append(())
232 else:
233 buttons.append(tuple(int(x) for x in inside.split(",") if x != ""))
235 res = min_presses(diag, buttons)
236 total += res
238 return total
240# ===========================================================================
242# %% ========================================================================
243# MAIN
244if __name__ == "__main__":
245 RESULT = solve(get_input(10, False))
247 print("\n" + "═" * 60)
248 print(" 🔐 Advent of Code 2025 — Day 10 | Part 1".center(60))
249 print("═" * 60)
250 print(f"Résultat : \033[96m{RESULT}\033[0m")
251 print("═" * 60 + "\n")
253# ===========================================================================