import math import sys from fractions import Fraction def parse_list(s): return [Fraction(int(p, 10)) for p in s[1:-1].split(',')] def parse_line(line): parts = line.rstrip().split() buttons = [parse_list(part) for part in parts[1:]] target = buttons.pop() return buttons, target def build_equations(buttons, target): a = [] for i in range(len(target)): row = [] for j in range(len(buttons)): row.append(Fraction(1 if i in buttons[j] else 0)) row.append(target[i]) a.append(row) return a def solve_equations(a): i = 0 used = set() fixed = set() while len(used) < len(a) and i + 1 < len(a[0]): # find first non-empty row k = None for j in range(len(a)): if j not in used and a[j][i] != 0: k = j used.add(k) fixed.add(i) break if k is not None: for ii in range(i + 1, len(a[0])): a[k][ii] /= a[k][i] a[k][i] = Fraction(1) # iterate rows and remove k for j in range(len(a)): if j != k and a[j][i] != 0: for ii in range(i + 1, len(a[0])): a[j][ii] -= a[k][ii] * a[j][i] / a[k][i] a[j][i] = Fraction(0) i += 1 conditions = [] not_fixed = sorted(set(range(len(a[0]) - 1)) - fixed) for i in range(len(a[0]) - 1): if i in fixed: for row in a: if row[i] == 1: conditions.append([*[-row[j] for j in not_fixed], row[-1]]) break else: conditions.append([ *[Fraction(1 if i == j else 0) for j in not_fixed], Fraction(0), ]) return conditions def _get_lower_bound(c, i, lower, upper): bound = -c[-1] for j in range(len(c) - 1): if i == j: continue elif c[j] == 0: continue elif c[j] > 0: bound -= c[j] * upper[j] elif c[j] < 0: bound -= c[j] * lower[j] else: return None return bound / c[i] def get_bounds(conditions, total): n = len(conditions[0]) - 1 lower = [0 for _ in range(n)] upper = [total for _ in range(n)] dirty = True while dirty: dirty = False for c in conditions: for i in range(n): if c[i] > 0: bound = _get_lower_bound(c, i, lower, upper) if bound is not None and bound > lower[i]: lower[i] = math.ceil(bound) dirty = True elif c[i] < 0: bound = _get_lower_bound([-x for x in c], i, upper, lower) if bound is not None and bound < upper[i]: upper[i] = math.floor(bound) dirty = True return lower, upper def evaluate(c, x): return sum(c[i] * x[i] for i in range(len(x))) + c[-1] def test(conditions, x): for c in conditions: y = evaluate(c, x) if y.denominator != 1 or y < 0: return False return True def resolve_conditions(conditions, total): s = [sum(c[i] for c in conditions) for i in range(len(conditions[0]))] if all(x == 0 for x in s[:-1]): return s[-1] lower, upper = get_bounds(conditions, total) best = None x = [v for v in lower] while True: if test(conditions, x): y = evaluate(s, x) if best is None or y < best: best = y upper = [min(y, v) for v in upper] for i in reversed(range(len(x))): x[i] += 1 if x[i] <= upper[i]: break x[i] = lower[i] else: break return best if __name__ == '__main__': result = 0 with open(sys.argv[1]) as fh: for line in fh: buttons, target = parse_line(line) a = build_equations(buttons, target) conditions = solve_equations(a) result += resolve_conditions(conditions, sum(target)) print(result)