| 🐄 | |||||
| 🐰 |
| .SILENT: | .SILENT: | ||||
| FILE := $(shell find . -path "./y????/p??.*" -type f | xargs ls -rt | tail -n 1) | FILE := $(shell find . -path "./y????/p??.*" -type f | xargs ls -rt | tail -n 1) | ||||
| PYTHON := docker compose run --rm advent python | |||||
| YEAR := $(shell echo ${FILE} | sed -E 's/.+y([0-9]+).+/\1/') | YEAR := $(shell echo ${FILE} | sed -E 's/.+y([0-9]+).+/\1/') | ||||
| DAY := $(shell echo ${FILE} | sed -E 's/.+p([0-9]+).+/\1/' | bc) | DAY := $(shell echo ${FILE} | sed -E 's/.+p([0-9]+).+/\1/' | bc) | ||||
| DATA := $(BASE).dat | DATA := $(BASE).dat | ||||
| TEST := $(BASE).dtt | TEST := $(BASE).dtt | ||||
| main: ${TEST} | main: ${TEST} | ||||
| echo 'test': | |||||
| cat ${TEST} | docker compose run --rm advent python -u ${CODE} | |||||
| echo 'test:' | |||||
| cat ${TEST} | ${PYTHON} -u ${CODE} | |||||
| clean: ${DATA} | clean: ${DATA} | ||||
| echo 'real:' | echo 'real:' | ||||
| cat ${DATA} | docker compose run --rm advent python -u ${CODE} | |||||
| cat ${DATA} | ${PYTHON} -u ${CODE} | |||||
| save: | save: | ||||
| git add . | git add . | ||||
| test `git log -1 --format=%s` == `cat VERSION` \ | test `git log -1 --format=%s` == `cat VERSION` \ | ||||
| && git commit --amend --reuse-message=head \ | && git commit --amend --reuse-message=head \ | ||||
| || git commit -m `cat VERSION` | || git commit -m `cat VERSION` | ||||
| git push -f | |||||
| ${DATA} ${TEST}: | ${DATA} ${TEST}: | ||||
| # avoid spam in the lead up to the event | # avoid spam in the lead up to the event |
| import collections | |||||
| import itertools | |||||
| import re | |||||
| def part1(H): | |||||
| """ | |||||
| 2 variables, 2 equations | |||||
| px1 + vx1 * t1 == px2 + vx2 * t2 | |||||
| py1 + vy1 * t1 == py2 + vy2 * t2 | |||||
| t1 = (px2 - px1) / vx1 + (vx2 / vx1) * t2 | |||||
| t1 = (py2 - py1) / vy1 + (vy2 / vy1) * t2 | |||||
| (px2 - px1) / vx1 - (py2 - py1) / vy1 = (vy2 / vy1 - vx2 / vx1) * t2 | |||||
| t2 = c1 / c2 | |||||
| c1 = (px2 - px1) / vx1 - (py2 - py1) / vy1 | |||||
| c2 = (vy2 / vy1 - vx2 / vx1) | |||||
| """ | |||||
| n = 0 | |||||
| lo, hi = (7, 27) if len(H) < 100 else (2e14, 4e14) | |||||
| for aa, bb in itertools.combinations(H, 2): | |||||
| px1, py1, pz1, vx1, vy1, vz1 = aa | |||||
| px2, py2, pz2, vx2, vy2, vz2 = bb | |||||
| c1 = (px2 - px1) / vx1 - (py2 - py1) / vy1 | |||||
| c2 = (vy2 / vy1 - vx2 / vx1) | |||||
| if c2 == 0: continue | |||||
| t2 = (c1 / c2) | |||||
| t1 = (px2 - px1) / vx1 + (vx2 / vx1) * t2 | |||||
| if t1 <= 0 or t2 <= 0: continue | |||||
| if not lo < px1 + vx1 * t1 < hi: continue | |||||
| if not lo < py1 + vy1 * t1 < hi: continue | |||||
| if not lo < px2 + vx2 * t2 < hi: continue | |||||
| if not lo < py2 + vy2 * t2 < hi: continue | |||||
| n += 1 | |||||
| return n | |||||
| def part2(H): | |||||
| """ | |||||
| 6 + t variables, 3 * t equations | |||||
| """ | |||||
| try: | |||||
| import z3 | |||||
| except: | |||||
| return 'Need z3-solver' | |||||
| Q = {k: z3.Real(k) for k in 'px py pz vx vy vz t0 t1 t2'.split()} | |||||
| solver = z3.Solver() | |||||
| solver.add(H[0].px + H[0].vx * Q['t0'] == Q['px'] + Q['vx'] * Q['t0']) | |||||
| solver.add(H[0].py + H[0].vy * Q['t0'] == Q['py'] + Q['vy'] * Q['t0']) | |||||
| solver.add(H[0].pz + H[0].vz * Q['t0'] == Q['pz'] + Q['vz'] * Q['t0']) | |||||
| solver.add(H[1].px + H[1].vx * Q['t1'] == Q['px'] + Q['vx'] * Q['t1']) | |||||
| solver.add(H[1].py + H[1].vy * Q['t1'] == Q['py'] + Q['vy'] * Q['t1']) | |||||
| solver.add(H[1].pz + H[1].vz * Q['t1'] == Q['pz'] + Q['vz'] * Q['t1']) | |||||
| solver.add(H[2].px + H[2].vx * Q['t2'] == Q['px'] + Q['vx'] * Q['t2']) | |||||
| solver.add(H[2].py + H[2].vy * Q['t2'] == Q['py'] + Q['vy'] * Q['t2']) | |||||
| solver.add(H[2].pz + H[2].vz * Q['t2'] == Q['pz'] + Q['vz'] * Q['t2']) | |||||
| solver.check() | |||||
| model = solver.model() | |||||
| return model.eval(Q['px'] + Q['py'] + Q['pz']) | |||||
| text = open(0).read() | |||||
| Hail = collections.namedtuple('Hail', 'px py pz vx vy vz') | |||||
| H = [Hail(*[int(n) for n in re.findall(r'-?\d+', line)]) for line in text.splitlines()] | |||||
| print(part1(H)) | |||||
| print(part2(H)) |
| import random | |||||
| import itertools | |||||
| import copy | |||||
| import collections | |||||
| def make_graph(text): | |||||
| graph = collections.defaultdict(set) | |||||
| for aa, *bbs in map(str.split, text.replace(':', '').splitlines()): | |||||
| graph[aa] |= set(bbs) | |||||
| for bb in bbs: | |||||
| graph[bb] |= {aa} | |||||
| return graph | |||||
| def cut(graph, wires): | |||||
| graph = graph.copy() | |||||
| for aa, bb in wires: | |||||
| graph[aa] = graph[aa] - {bb} | |||||
| graph[bb] = graph[bb] - {aa} | |||||
| return graph | |||||
| def subgraphs(graph): | |||||
| conns = [] | |||||
| while graph: | |||||
| state = {next(iter(graph))} | |||||
| seen = set() | |||||
| while state: | |||||
| state = {new for old in state for new in graph[old] if new not in seen} | |||||
| seen |= state | |||||
| conns.append(seen) | |||||
| for node in seen: | |||||
| graph.pop(node) | |||||
| return conns | |||||
| def shortest_path(graph, aa, bb): | |||||
| state = {aa: None} | |||||
| seen = {} | |||||
| while bb not in state: | |||||
| state = {new: old for old in state for new in graph[old] if new not in seen} | |||||
| seen |= state | |||||
| path = [bb] | |||||
| while path[-1] != aa: | |||||
| path.append(seen[path[-1]]) | |||||
| return path | |||||
| def rank_wires(graph, n_cycles=100, n_top=20): | |||||
| tally = collections.Counter() | |||||
| for aa, bb in sorted(itertools.combinations(graph, 2))[:n_cycles]: | |||||
| path = shortest_path(graph, aa, bb) | |||||
| tally.update(frozenset(pair) for pair in zip(path, path[1:])) | |||||
| return [k for k, _ in tally.most_common(n_top)] | |||||
| text = open(0).read() | |||||
| graph = make_graph(text) | |||||
| for triplet in itertools.combinations(rank_wires(graph), 3): | |||||
| mod_graph = cut(graph, triplet) | |||||
| subs = subgraphs(mod_graph) | |||||
| if len(subs) == 2: | |||||
| print(len(subs[0]) * len(subs[1])) | |||||
| break |