| @@ -1,7 +1,9 @@ | |||
| import collections | |||
| def render(grid, brush): | |||
| def render(grid, brush=None): | |||
| if brush is None: | |||
| brush = {v: v for v in grid.values()} | |||
| if isinstance(brush, str): | |||
| brush = {i: c for i, c in enumerate(brush)} | |||
| xmin, *_, xmax = sorted(int(p.real) for p in grid) | |||
| @@ -15,25 +17,29 @@ def render(grid, brush): | |||
| return rendered | |||
| def bsearch(fn, goal, lo, hi): | |||
| while hi - lo > 1: | |||
| mid = lo + (hi - lo) // 2 | |||
| if goal < fn(mid): | |||
| lo, hi = lo, mid | |||
| else: | |||
| lo, hi = mid, hi | |||
| # check | |||
| a, b = fn(lo), fn(hi) | |||
| assert a <= goal, 'lower bound too high' | |||
| assert goal <= b, 'higher bound too low' | |||
| return lo | |||
| def read_image(text): | |||
| grid = collections.defaultdict(str) | |||
| for y, line in enumerate(text.splitlines()): | |||
| for x, cell in enumerate(line): | |||
| grid[complex(x, y)] = cell | |||
| return grid | |||
| def shortest_path(start, end, move): | |||
| seen = {} | |||
| edge = {start: None} | |||
| while end not in seen: | |||
| seen.update(edge) | |||
| edge = { | |||
| adj: pos | |||
| for pos in edge | |||
| for adj in move(pos) | |||
| if adj not in seen | |||
| } | |||
| if not edge: | |||
| raise RuntimeError('Path not found', seen) | |||
| path = [] | |||
| while end: | |||
| path.append(end) | |||
| end = seen[end] | |||
| return path[::-1] | |||
| @@ -3,8 +3,6 @@ import math | |||
| import re | |||
| import sys | |||
| from toolkit import bsearch | |||
| text = sys.stdin.read() | |||
| cookbook = {} | |||
| @@ -29,5 +27,21 @@ def fuel_to_ore(state): | |||
| return state | |||
| def bsearch(fn, goal, lo, hi): | |||
| while hi - lo > 1: | |||
| mid = lo + (hi - lo) // 2 | |||
| if goal < fn(mid): | |||
| lo, hi = lo, mid | |||
| else: | |||
| lo, hi = mid, hi | |||
| # check | |||
| a, b = fn(lo), fn(hi) | |||
| assert a <= goal, 'lower bound too high' | |||
| assert goal <= b, 'higher bound too low' | |||
| return lo | |||
| print(fuel_to_ore({'FUEL': 1})['ORE']) | |||
| print(bsearch(lambda n: fuel_to_ore({'FUEL': n})['ORE'], 1E12, 1, 10_000_000)) | |||
| @@ -0,0 +1,119 @@ | |||
| import collections | |||
| import itertools | |||
| import sys | |||
| from toolkit import read_image, render | |||
| def read_in(text): | |||
| grid = read_image(text) | |||
| elements = {v: k for k, v in grid.items()} | |||
| keys = frozenset(k for k in elements if 'a' <= k <= 'z') | |||
| return dict(grid), elements, keys | |||
| def move(pos): | |||
| for ori in [1, -1, 1j, -1j]: | |||
| if grid[pos + ori] != '#': | |||
| yield pos + ori | |||
| def shortest_path(start, end, move): | |||
| seen = {} | |||
| edge = {start: None} | |||
| while end not in seen: | |||
| edge = { | |||
| adj: pos | |||
| for pos in edge | |||
| for adj in move(pos) | |||
| if adj not in seen | |||
| } | |||
| seen.update(edge) | |||
| if not edge: | |||
| raise RuntimeError('No path found') | |||
| path = [] | |||
| while end != start: | |||
| path.append(end) | |||
| end = seen[end] | |||
| return path | |||
| def trim(states): | |||
| groups = collections.defaultdict(list) | |||
| for state in states: | |||
| tips = frozenset(seq[-1] for seq in state) | |||
| bulk = frozenset(' '.join(state)) | |||
| groups[bulk, tips].append(state) | |||
| return [min(groups[k], key=calc_total) for k in groups] | |||
| def precalc_moves(entrances): | |||
| requirements = collections.defaultdict(dict) | |||
| lengths = collections.defaultdict(dict) | |||
| for a, b in itertools.permutations(keys.union(entrances), 2): | |||
| try: | |||
| path = shortest_path(elements[a], elements[b], move) | |||
| except RuntimeError: | |||
| continue | |||
| lengths[a][b] = len(path) | |||
| requirements[a][b] = { | |||
| e.lower() | |||
| for e, v in elements.items() | |||
| if e.isupper() | |||
| and v in path | |||
| } | |||
| return requirements, lengths | |||
| def calc_total(state): | |||
| return sum(sum( | |||
| lengths[a][b] | |||
| for a, b in zip(s, s[1:]) | |||
| if {a, b} < set(lengths) | |||
| ) for s in state) | |||
| def mod_grid(text): | |||
| grid, elements, keys = read_in(text) | |||
| pos = elements['@'] | |||
| grid[pos] = '#' | |||
| for char, ori in zip('*&^@', [1, -1, 1j, -1j]): | |||
| grid[pos + ori] = '#' | |||
| grid[pos + ori + ori * 1j] = char | |||
| return render(grid, {k: k for k in grid.values()}) | |||
| def solve(text): | |||
| global grid, elements, keys, lengths | |||
| grid, elements, keys = read_in(text) | |||
| stacks = {k for k in elements if not k.isalnum()} - {'#', '.'} | |||
| requirements, lengths = precalc_moves(stacks) | |||
| states = [stacks] | |||
| for _ in range(len(keys)): | |||
| states = trim( | |||
| state - {seq} | {seq + b} | |||
| for state in states | |||
| for seq in state | |||
| for b, reqs in requirements[seq[-1]].items() | |||
| if b not in seq and reqs.issubset(''.join(state)) | |||
| ) | |||
| final = min(states, key=calc_total) | |||
| print(' '.join(sorted(final))) | |||
| print(calc_total(final)) | |||
| text = sys.stdin.read() | |||
| text = ''' | |||
| ############# | |||
| #g#f.D#..h#l# | |||
| #F###e#E###.# | |||
| #dCba...BcIJ# | |||
| #####.@.##### | |||
| #nK.L...G...# | |||
| #M###N#H###.# | |||
| #o#m..#i#jk.# | |||
| ############# | |||
| ''' | |||
| solve(text) | |||
| text = mod_grid(text) | |||
| solve(text) | |||
| @@ -0,0 +1,19 @@ | |||
| import sys | |||
| from intcode import compute | |||
| def check(x, y): | |||
| return next(compute(text, iter([x, y]))) | |||
| text = sys.stdin.read() | |||
| tiles = {(x, y) for x in range(50) for y in range(50) if check(x, y)} | |||
| print(len(tiles)) | |||
| x, y = 0, 0 | |||
| while not check(x + 99, y): | |||
| y += 1 | |||
| while not check(x, y + 99): | |||
| x += 1 | |||
| print(x * 10_000 + y) | |||
| @@ -0,0 +1,66 @@ | |||
| import itertools | |||
| import sys | |||
| from toolkit import read_image, shortest_path | |||
| def move(pos): | |||
| for ori in [1, -1, 1j, -1j]: | |||
| adj = pos + ori | |||
| if adj in grid: | |||
| yield adj | |||
| if adj in duals: | |||
| yield from move(duals[adj]) | |||
| def move_with_depth(state): | |||
| pos, level = state | |||
| for ori in [1, -1, 1j, -1j]: | |||
| adj = pos + ori | |||
| if adj in grid: | |||
| yield adj, level | |||
| if adj in duals: | |||
| name = grid[adj] | |||
| if adj in inner: | |||
| yield from move_with_depth((duals[adj], level + 1)) | |||
| elif (level > 0) and (name not in {'AA', 'ZZ'}): | |||
| yield from move_with_depth((duals[adj], level - 1)) | |||
| # base | |||
| text = sys.stdin.read() | |||
| grid = {pos: val for pos, val in read_image(text).items() if val not in ' #'} | |||
| for pos, val in grid.copy().items(): | |||
| if val.isupper(): | |||
| for im in [1, 1j]: | |||
| A, B, C = [pos - im, pos, pos + im] | |||
| seq = ''.join(grid.get(p, ' ') for p in [A, B, C]) | |||
| if seq.endswith('.'): | |||
| grid[B] = grid.pop(A) + grid.pop(B) | |||
| if seq.startswith('.'): | |||
| grid[B] = grid.pop(B) + grid.pop(C) | |||
| elements = {v: k for k, v in grid.items()} | |||
| # matrix | |||
| duals = {} | |||
| portals = {pos for pos, cell in grid.items() if cell.isupper()} | |||
| for pA, pB in itertools.combinations(portals, 2): | |||
| if grid[pA] == grid[pB]: | |||
| duals[pA] = pB | |||
| duals[pB] = pA | |||
| # part1 | |||
| start, end = elements['AA'], elements['ZZ'] | |||
| path = shortest_path(start, end, move) | |||
| print(len(path) - 3) | |||
| # categorize portals | |||
| _, *xmid, _ = sorted({p.real for p in portals}) | |||
| _, *ymid, _ = sorted({p.imag for p in portals}) | |||
| inner = {pos for pos in portals if pos.real in xmid and pos.imag in ymid} | |||
| outer = set(portals) - inner | |||
| # part2 | |||
| start, end = (elements['AA'], 0), (elements['ZZ'], 0) | |||
| path = shortest_path(start, end, move_with_depth) | |||
| print(len(path) - 3) | |||