"""Day 10: Advent of Code 2025.""" import time import re from aoc import read_lines from itertools import combinations, product from scipy.optimize import milp, LinearConstraint, Bounds import numpy as np def parse_targets(s: str) -> list[int]: """Parse {28,20,8,20} -> [28, 20, 8, 20] (target values per position).""" match = re.search(r'\{([^}]+)\}', s) if not match: return [] return [int(x) for x in match.group(1).split(',')] def parse_total(s: str) -> int: """Parse [.##.] -> 0b0110 (binary representation).""" match = re.search(r'\[([.#]+)\]', s) if not match: return 0 pattern = match.group(1) return int(pattern.replace('.', '0').replace('#', '1'), 2) def parse_groups_as_positions(s: str) -> list[list[int]]: """Parse (3) (1,3) (2) -> [[3], [1,3], [2], ...]. Returns list of groups, where each group is a list of positions it affects. """ groups = re.findall(r'\(([^)]+)\)', s) result = [] for group in groups: positions = [int(x) for x in group.split(',')] result.append(positions) return result def build_matrix(groups: list[list[int]], width: int) -> list[list[int]]: """Build matrix where matrix[pos][group] = 1 if group affects position pos. Rows = positions (0 to width-1) Columns = groups """ matrix = [] for pos in range(width): row = [] for group in groups: row.append(1 if pos in group else 0) matrix.append(row) return matrix def solve_system(matrix: list[list[int]], targets: list[int]) -> int | None: """Solve the system and find minimum sum of k values. Uses Mixed Integer Linear Programming: - Minimize: sum of all k values - Subject to: matrix @ k = targets, k >= 0, k are integers Returns minimum sum of k values, or None if no solution. """ if not matrix or not matrix[0]: return None num_groups = len(matrix[0]) # Convert to numpy arrays A_eq = np.array(matrix, dtype=float) b_eq = np.array(targets, dtype=float) # Objective: minimize sum of k (coefficients all 1) c = np.ones(num_groups) # Bounds: k >= 0 (upper bound = max target value is safe) max_val = max(targets) + 1 bounds = Bounds(lb=np.zeros(num_groups), ub=np.full(num_groups, max_val)) # Equality constraints: A @ k = b constraints = LinearConstraint(A_eq, b_eq, b_eq) # All variables must be integers integrality = np.ones(num_groups) # 1 = integer # Solve result = milp(c, constraints=constraints, bounds=bounds, integrality=integrality) if not result.success: return None return int(round(result.fun)) def parse_groups(s: str, width: int) -> list[list[int]]: """Parse (3) (1,3) (2) -> [[bitmask, inverse], ...]. (1,3) with width 4 -> positions 1 and 3 from left -> 0101 (decimal 5) Then also the inverse -> 1010 (decimal 10) """ groups = re.findall(r'\(([^)]+)\)', s) result = [] mask_all = (1 << width) - 1 # All bits set for inverse calculation for group in groups: nums = [int(x) for x in group.split(',')] # Combine all positions into one bitmask (from left, so invert position) bitmask = 0 for n in nums: # Position n from left = bit (width - 1 - n) from right bitmask |= (1 << (width - 1 - n)) # Duplicate the bitmask (using same value twice XORs to 0) result.append([bitmask, bitmask]) return result def parse_line(line: str) -> tuple[int, list[list[int]]]: """Parse full line, return (total_bitmask, groups).""" # Get width from the [...] pattern match = re.search(r'\[([.#]+)\]', line) width = len(match.group(1)) if match else 0 total = parse_total(line) groups = parse_groups(line, width) return total, groups def combine_bitmasks(*bitmasks: int) -> int: """Combine bitmasks with XOR.""" result = 0 for b in bitmasks: result ^= b return result def find_min_index_sum(total: int, groups: list[list[int]]) -> int: """Find minimum index sum for combos that XOR to total. Tries subsets of groups starting from smallest (r=1, r=2, ...). Within each r, minimum possible sum is r (all index 0 -> r * 1). If we find a match with sum S, we can skip any r where r > S. """ n = len(groups) # Max possible sum: n groups * 2 (max index 1 -> +2 each) best = n * 2 + 1 for r in range(1, n + 1): # Minimum possible sum for r groups is r (all index 0 = 1 each) if r >= best: break for group_indices in combinations(range(n), r): selected_groups = [groups[i] for i in group_indices] # Try all index combinations within selected groups for indices in product(*[range(len(g)) for g in selected_groups]): index_sum = sum(idx + 1 for idx in indices) # Skip if can't beat best if index_sum >= best: continue bitmasks = [selected_groups[i][idx] for i, idx in enumerate(indices)] if combine_bitmasks(*bitmasks) == total: best = index_sum return best def part1(data): result = 0 for line in data: total, groups = parse_line(line) result += find_min_index_sum(total, groups) return result def part2(data): """Solve part 2.""" result = 0 for line in data: groups = parse_groups_as_positions(line) targets = parse_targets(line) width = len(targets) matrix = build_matrix(groups, width) result += solve_system(matrix, targets) return result if __name__ == "__main__": DAY = 10 # <- ändra till rätt dag data = read_lines(DAY) t0 = time.perf_counter() p1 = part1(data) t1 = time.perf_counter() print(f"Part 1: {p1} ({(t1-t0)*1000:.2f} ms)") t0 = time.perf_counter() p2 = part2(data) t1 = time.perf_counter() print(f"Part 2: {p2} ({(t1-t0)*1000:.2f} ms)")