🐄 | |||||
🐰 |
.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 |