├── 2021 ├── 01.py ├── 02.py ├── 03.py ├── 04.py ├── 05.py ├── 06.py ├── 07.py ├── 08.py ├── 09.py ├── 10.py ├── 11.py ├── 12.py ├── 13.py ├── 14.py ├── 15.py ├── 16.py ├── 17.py ├── 18.py ├── 19.py ├── 20.py ├── 21.py ├── 22.py ├── 23.py ├── 24.py └── 25.py ├── 2022 ├── 01.py ├── 02.py ├── 03.py ├── 04.py ├── 05.py ├── 06.py ├── 07.py ├── 08.py ├── 09.py ├── 10.py ├── 11.py ├── 12.py ├── 13.py ├── 14.py ├── 15.py ├── 16.py ├── 17.py ├── 18.py ├── 19.py ├── 20.py ├── 21.py ├── 23.py └── 25.py ├── 2023 ├── 01.py ├── 02.py ├── 03.py └── 04.py ├── 2024 ├── 01.py ├── 02.py ├── 03.py ├── 04.py ├── 05.py ├── 06.py ├── 07.py ├── 08.py ├── 09.py ├── 10.py ├── 11.py ├── 12.py ├── 13.py ├── 14.py ├── 15.py ├── 16.py ├── 17.py ├── 18.py ├── 19.py ├── 20.py ├── 21.py ├── 22.py ├── 23.py ├── 24.py └── 25.py ├── .gitignore ├── README.md ├── requirements.txt └── santa └── __main__.py /.gitignore: -------------------------------------------------------------------------------- 1 | /**/__pycache__/ 2 | /**/.venv/ 3 | /**/venv/ 4 | 5 | .TOKEN 6 | .AOC_TOKEN 7 | /**/.cache/ 8 | /**/in 9 | /**/input 10 | /**/*.in 11 | /**/input.* 12 | -------------------------------------------------------------------------------- /2021/01.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2021/01: Sonar Sweep """ 3 | 4 | import sys 5 | 6 | depths = [ 7 | int(depth) for depth in sys.stdin.read().strip().split("\n") 8 | ] 9 | 10 | print("Part 1:", sum(a < b for a, b in zip(depths, depths[1:]))) 11 | 12 | # The "middle" two numbers don't matter, since they are added to both 13 | # sliding windows. 14 | print("Part 2:", sum(a < b for a, b in zip(depths, depths[3:]))) 15 | -------------------------------------------------------------------------------- /2021/02.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2021/02: Dive! """ 3 | 4 | import sys 5 | 6 | commands = [ 7 | (command, int(argument)) 8 | for command, argument in map(str.split, sys.stdin.read().strip().split("\n")) 9 | ] 10 | 11 | position, depth = 0, 0 12 | for command, argument in commands: 13 | if command == "forward": 14 | position += argument 15 | elif command == "up": 16 | depth -= argument 17 | elif command == "down": 18 | depth += argument 19 | 20 | print("Part 1:", position * depth) 21 | 22 | position, depth, aim = 0, 0, 0 23 | for command, argument in commands: 24 | if command == "forward": 25 | position += argument 26 | depth += aim * argument 27 | elif command == "up": 28 | aim -= argument 29 | elif command == "down": 30 | aim += argument 31 | 32 | print("Part 2:", position * depth) 33 | -------------------------------------------------------------------------------- /2021/03.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2021/03: Binary Diagnostic """ 3 | 4 | import sys 5 | 6 | report = sys.stdin.read().strip().split("\n") 7 | 8 | gamma, epsilon = ( 9 | "".join(max(x, key=x.count) for x in zip(*report)), 10 | "".join(min(x, key=x.count) for x in zip(*report)) 11 | ) 12 | 13 | print("Part 1:", int(gamma, 2) * int(epsilon, 2)) 14 | 15 | def filter(report, fn): 16 | i = 0 17 | while len(report) > 1: 18 | bit = fn("".join(n[i] for n in report)) 19 | 20 | # Remove values which don't meet the filtering criteria. 21 | report = [ 22 | n for n in report if n[i] == bit 23 | ] 24 | 25 | i = i + 1 26 | 27 | return report[0] 28 | 29 | o2 = filter(report, lambda x: "0" if x.count("0") > x.count("1") else "1") 30 | co2 = filter(report, lambda x: "0" if x.count("0") <= x.count("1") else "1") 31 | 32 | print("Part 2:", int(o2, 2) * int(co2, 2)) 33 | -------------------------------------------------------------------------------- /2021/04.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2021/04: Giant Squid """ 3 | 4 | import sys 5 | 6 | calls, *boards = sys.stdin.read().strip().split("\n\n") 7 | 8 | calls = [int(n) for n in calls.split(",")] 9 | boards = [ 10 | [ 11 | [ int(el) for el in ln.split() ] 12 | for ln in board.strip().split("\n") 13 | ] 14 | for board in boards 15 | ] 16 | 17 | def score(board, calls): 18 | return ( 19 | sum( 20 | board[i][j] if board[i][j] not in calls else 0 21 | for i in range(5) 22 | for j in range(5) 23 | ) 24 | * calls[-1] # assume: `calls` isn't longer than it needs to be 25 | ) 26 | 27 | winners = {} 28 | for i in range(len(calls)): 29 | called = set(calls[:i]) 30 | 31 | for j, board in enumerate(boards): 32 | if j in winners.keys(): 33 | continue 34 | 35 | # Does this board score a bingo after `i` calls are called? 36 | if ( 37 | any(all(board[k][l] in called for k in range(5)) for l in range(5)) 38 | or any(all(board[l][k] in called for k in range(5)) for l in range(5)) 39 | ): 40 | winners[j] = i 41 | 42 | first = min(winners, key=winners.get) 43 | last = max(winners, key=winners.get) 44 | 45 | print("Part 1:", score(boards[first], calls[:winners[first]])) 46 | print("Part 2:", score(boards[last ], calls[:winners[last ]])) 47 | -------------------------------------------------------------------------------- /2021/05.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2021/05: Hydrothermal Venture """ 3 | 4 | import sys 5 | import collections as cl 6 | 7 | vents = [ 8 | tuple( 9 | tuple( int(coordinate) for coordinate in point.split(",") ) 10 | for point in ln.strip().split(" -> ") 11 | ) 12 | for ln in sys.stdin.read().strip().split("\n") 13 | ] 14 | 15 | floor = cl.defaultdict(int) 16 | 17 | for (x1, y1), (x2, y2) in vents: 18 | if (x1 == x2) or (y1 == y2): 19 | for d in range(abs(x2 - x1) + abs(y2 - y1) + 1): 20 | floor[min(x1, x2) + d*(x1 != x2), min(y1, y2) + d*(y1 != y2)] += 1 21 | 22 | print("Part 1:", sum(x > 1 for x in floor.values())) 23 | 24 | for (x1, y1), (x2, y2) in vents: 25 | if (x1 != x2) and (y1 != y2): 26 | dx = (x2 > x1) - (x1 > x2) 27 | dy = (y2 > y1) - (y1 > y2) 28 | for d in range(abs(x2 - x1) + 1): 29 | floor[x1 + dx*d, y1 + dy*d] += 1 30 | 31 | print("Part 2:", sum(x > 1 for x in floor.values())) 32 | -------------------------------------------------------------------------------- /2021/06.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2021/06: Lanternfish """ 3 | 4 | import sys 5 | import collections as cl 6 | 7 | lanternfish = cl.Counter( 8 | int(fish) for fish in sys.stdin.read().strip().split(",") 9 | ) 10 | 11 | def reproduce(fish, generations=80): 12 | for _ in range(generations): 13 | offspring = cl.Counter() 14 | for age, number in fish.items(): 15 | if age <= 0: 16 | offspring[6] += number 17 | offspring[8] += number 18 | else: 19 | offspring[age - 1] += number 20 | 21 | fish = offspring 22 | 23 | return fish 24 | 25 | p1 = reproduce(lanternfish) 26 | 27 | print("Part 1:", sum(p1.values())) 28 | print("Part 2:", sum(reproduce(p1, generations=(256 - 80)).values())) 29 | -------------------------------------------------------------------------------- /2021/07.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2021/07: The Treachery of Whales """ 3 | 4 | import sys 5 | import math 6 | import statistics 7 | 8 | positions = [ 9 | int(position) for position in sys.stdin.read().strip().split(",") 10 | ] 11 | 12 | def cost(positions, f=lambda x: x): 13 | min_x = min(positions) 14 | max_x = max(positions) 15 | 16 | return min( 17 | sum(f(abs(position - d)) for position in positions) 18 | for d in range(min_x, max_x + 1) 19 | ) 20 | 21 | # Since there are only 1000 crabs, it's easy enough to brute-force -- it takes 22 | # about a second. 23 | if False: 24 | print("Part 1:", cost(positions)) 25 | print("Part 2:", cost(positions, f=lambda x: x * (x + 1) // 2)) 26 | 27 | # ....however, we can do better! 28 | # 29 | # - For Part 1, we can align the crabs to the median of all their positions, 30 | # since that will minimize the sum of the absolute deviations (which is 31 | # equivalent to the puzzle.) In the case that our median is not an integer, 32 | # we can arbitrarily round up or down; both will consume the same amount of 33 | # fuel. 34 | # 35 | # - For Part 2, we can instead align the crabs to the mean of all their 36 | # positions, since that will minimize the sum of the squares of the 37 | # absolute differences. 38 | # 39 | # NB: This isn't equivalent to the puzzle -- the puzzle asks us to minimize 40 | # quantity (n * (n + 1)) / 2 = (n^2 + n) / 2. However, it can easily 41 | # be shown that cheapest alignment point will fall within +/- 0.5 of 42 | # the mean. It is trivial to check both floor(mean) and ceil(mean) and 43 | # pick the cheaper one. 44 | # 45 | 46 | median = math.floor(statistics.median(positions)) 47 | print("Part 1:", sum(abs(position - median) for position in positions)) 48 | 49 | means = [ 50 | f(statistics.mean(positions)) for f in [ math.floor, math.ceil ] 51 | ] 52 | print("Part 2:", 53 | min( 54 | sum((lambda x: x * (x + 1) // 2)(abs(position - mean)) for position in positions) 55 | for mean in means 56 | ) 57 | ) 58 | -------------------------------------------------------------------------------- /2021/08.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2021/08: Seven Segment Search """ 3 | 4 | import sys 5 | 6 | displays = [ 7 | tuple( 8 | [ frozenset(digit) for digit in section.split() ] 9 | for section in display.split(" | ") 10 | ) 11 | for display in sys.stdin.read().strip().split("\n") 12 | ] 13 | 14 | print("Part 1:", 15 | sum( 16 | sum(len(digit) in {2, 3, 4, 7} for digit in display[1]) 17 | for display in displays 18 | ) 19 | ) 20 | 21 | total = 0 22 | for clues, challenge in displays: 23 | # This is a really clever way to pull the known numbers out; thanks 24 | # u/RodericTheRed! 25 | _1, _7, _4, *unknown, _8 = sorted(clues, key=len) 26 | 27 | _9 = next(d for d in unknown if len(_4 & d) == 4); unknown.remove(_9) 28 | _3 = next(d for d in unknown if len(d - _7) == 2); unknown.remove(_3) 29 | _2 = next(d for d in unknown if len(_9 & d) == 4); unknown.remove(_2) 30 | _0 = next(d for d in unknown if len(_1 & d) == 2); unknown.remove(_0) 31 | _6 = next(d for d in unknown if len(d ) == 6); unknown.remove(_6) 32 | _5 = next(d for d in unknown ); unknown.remove(_5) 33 | 34 | # Now, it's easy to figure out what the four-digit "challenge" decodes to. 35 | digits = { 36 | v: str(i) 37 | for i, v in enumerate([_0, _1, _2, _3, _4, _5, _6, _7, _8, _9]) 38 | } 39 | 40 | total = total + int( 41 | "".join(digits[digit] for digit in challenge) 42 | ) 43 | 44 | print("Part 2:", total) 45 | -------------------------------------------------------------------------------- /2021/09.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2021/09: Smoke Basin """ 3 | 4 | import sys 5 | import math 6 | import networkx as nx 7 | 8 | depths = { 9 | (x, y): int(depth) 10 | for y, ln in enumerate(sys.stdin.read().strip().split("\n")) 11 | for x, depth in enumerate(ln.strip()) 12 | } 13 | 14 | def neighbours(x, y): 15 | yield (x - 1, y) 16 | yield (x + 1, y) 17 | yield (x, y - 1) 18 | yield (x, y + 1) 19 | 20 | print("Part 1:", 21 | sum( 22 | depth + 1 23 | for (x, y), depth in depths.items() 24 | if all( 25 | depth < depths.get(neighbour, math.inf) 26 | for neighbour in neighbours(x, y) 27 | ) 28 | ) 29 | ) 30 | 31 | g = nx.Graph() 32 | for (x, y), depth in depths.items(): 33 | if depth == 9: 34 | continue 35 | 36 | for neighbour in neighbours(x, y): 37 | if depths.get(neighbour, 9) != 9: 38 | g.add_edge((x, y), neighbour) 39 | 40 | basins = sorted( 41 | len(basin) for basin in nx.connected_components(g) 42 | ) 43 | 44 | print("Part 2:", basins[-3] * basins[-2] * basins[-1]) 45 | -------------------------------------------------------------------------------- /2021/10.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2021/10: Syntax Scoring """ 3 | 4 | import sys 5 | 6 | lines = sys.stdin.read().strip().split("\n") 7 | 8 | syntax, completion = [], [] 9 | for line in lines: 10 | stack = [] 11 | for c in line: 12 | if c in "([{<": 13 | stack.append(c) 14 | elif c in ")]}>": 15 | if (stack[-1] + c) in ("()", "[]", "{}", "<>"): 16 | stack = stack[:-1] 17 | else: 18 | syntax.append({ 19 | ")": 3, 20 | "]": 57, 21 | "}": 1197, 22 | ">": 25137 23 | }[c]) 24 | break 25 | else: 26 | assert False 27 | else: 28 | # Otherwise, this line is valid but incomplete. 29 | score = 0 30 | for c in reversed(stack): 31 | score = 5 * score + " ([{<".index(c) 32 | 33 | completion.append(score) 34 | 35 | print("Part 1:", sum(syntax)) 36 | print("Part 2:", sorted(completion)[len(completion) // 2]) 37 | -------------------------------------------------------------------------------- /2021/11.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2021/11: Dumbo Octopus """ 3 | 4 | import sys 5 | import itertools as it 6 | 7 | octopuses = { 8 | (x, y): int(energy) 9 | for y, ln in enumerate(sys.stdin.read().strip().split("\n")) 10 | for x, energy in enumerate(ln) 11 | } 12 | 13 | def evolve(octopuses): 14 | for (x, y), energy in octopuses.items(): 15 | octopuses[x, y] += 1 16 | 17 | flashed = set() 18 | while any( 19 | (x, y) not in flashed and energy > 9 20 | for (x, y), energy in octopuses.items() 21 | ): 22 | for (x, y), energy in octopuses.items(): 23 | if (energy > 9) and ((x, y) not in flashed): 24 | flashed.add((x, y)) 25 | 26 | for dx in [-1, 0, 1]: 27 | for dy in [-1, 0, 1]: 28 | if (x + dx, y + dy) in octopuses.keys(): 29 | octopuses[x + dx, y + dy] += 1 30 | 31 | for (x, y) in flashed: 32 | octopuses[x, y] = 0 33 | 34 | return len(flashed) 35 | 36 | flashes = 0 37 | for step in range(1, 100 + 1): 38 | flashes = flashes + evolve(octopuses) 39 | 40 | print("Part 1:", flashes) 41 | 42 | # assume: Part 2 happens /after/ Part 1. 43 | for step in it.count(step): 44 | if evolve(octopuses) >= len(octopuses.keys()): 45 | print("Part 2:", step + 1) 46 | break 47 | -------------------------------------------------------------------------------- /2021/12.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2021/12: Passage Pathing """ 3 | 4 | import sys 5 | import collections as cl 6 | 7 | cave = cl.defaultdict(set) 8 | for tunnel in sys.stdin.read().strip().split("\n"): 9 | e1, e2 = tunnel.split("-") 10 | 11 | cave[e1].add(e2) 12 | cave[e2].add(e1) 13 | 14 | def p1(cave, position="start", visited=set()): 15 | return sum( 16 | p1( 17 | cave, 18 | d, 19 | visited=visited | {d} 20 | ) if d != "end" else 1 21 | for d in cave[position] 22 | # From here, we can proceed to any cell that is not the start, 23 | # or a small cave that we've already visited. 24 | if d != "start" and (d not in visited if d.islower() else True) 25 | ) 26 | 27 | def p2(cave, position="start", visited=set(), explored=False): 28 | return sum( 29 | p2( 30 | cave, 31 | d, 32 | visited=visited | {d}, 33 | # If we've already explored a cell, or, if we're going to a small 34 | # cave we've already been to, then we can't explore any more. 35 | explored=explored or (d in visited if d.islower() else False) 36 | ) if d != "end" else 1 37 | for d in cave[position] 38 | if d != "start" and ( 39 | (d not in visited) or (not explored) 40 | if d.islower() else True 41 | ) 42 | ) 43 | 44 | print("Part 1:", p1(cave)) 45 | print("Part 2:", p2(cave)) 46 | -------------------------------------------------------------------------------- /2021/13.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2021/13: Transparent Origami """ 3 | 4 | import sys 5 | 6 | dots, instructions = sys.stdin.read().strip().split("\n\n") 7 | 8 | dots = set( 9 | tuple(int(c) for c in dot.split(",")) 10 | for dot in dots.split("\n") 11 | ) 12 | instructions = [ 13 | tuple(f(x) for x, f in zip(instruction.split()[-1].split("="), [str.strip, int])) 14 | for instruction in instructions.split("\n") 15 | ] 16 | 17 | for i, (axis, coordinate) in enumerate(instructions): 18 | dots = set( 19 | ( 20 | coordinate - abs(x - coordinate) if axis == "x" else x, 21 | coordinate - abs(y - coordinate) if axis == "y" else y 22 | ) 23 | for x, y in dots 24 | ) 25 | 26 | if i == 0: 27 | print("Part 1:", len(dots)) 28 | 29 | max_x = max(x for x, y in dots) 30 | max_y = max(y for x, y in dots) 31 | 32 | print("Part 2:", 33 | "\n" + "\n".join( 34 | "".join( 35 | "#" if (x, y) in dots else " " 36 | for x in range(max_x + 1) 37 | ) 38 | for y in range(max_y + 1) 39 | ) 40 | ) 41 | -------------------------------------------------------------------------------- /2021/14.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2021/14: Extended Polymerization """ 3 | 4 | import sys 5 | import collections as cl 6 | 7 | polymer, rules = sys.stdin.read().strip().split("\n\n") 8 | 9 | rules = { 10 | tuple(rule.split()[0]): rule.split()[2] 11 | for rule in rules.strip().split("\n") 12 | } 13 | 14 | pairs = cl.Counter( 15 | polymer[i:i+2] for i in range(len(polymer) - 1) 16 | ) 17 | for step in range(1, 40 + 1): 18 | _pairs = cl.Counter() 19 | for (l, r), n in pairs.items(): 20 | m = rules[l, r] 21 | _pairs[l + m] += n 22 | _pairs[m + r] += n 23 | 24 | pairs = _pairs 25 | 26 | if step == 10 or step == 40: 27 | elements = cl.Counter() 28 | for (l, r), n in pairs.items(): 29 | elements[l] += n 30 | elements[polymer[-1]] += 1 31 | 32 | print("Part {}:".format({10: "1", 40: "2"}[step]), 33 | max(elements.values()) - min(elements.values()) 34 | ) 35 | -------------------------------------------------------------------------------- /2021/15.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2021/15: Chiton """ 3 | 4 | import sys 5 | import networkx as nx 6 | 7 | risk = { 8 | (x, y): int(v) 9 | for y, ln in enumerate(sys.stdin.read().strip().split("\n")) 10 | for x, v in enumerate(ln) 11 | } 12 | w, h = max(x for x, y in risk.keys()) + 1, max(y for x, y in risk.keys()) + 1 13 | 14 | g = nx.DiGraph() 15 | for x, y in risk.keys(): 16 | for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: 17 | if (x + dx, y + dy) in risk.keys(): 18 | g.add_edge((x, y), (x + dx, y + dy), weight=risk[x + dx, y + dy]) 19 | 20 | print("Part 1:", 21 | nx.shortest_path_length(g, (0, 0), (w - 1, h - 1), weight="weight") 22 | ) 23 | 24 | g = nx.DiGraph() 25 | for x in range(5 * w): 26 | for y in range(5 * h): 27 | cost = (risk[x % w, y % h] - 1 + (x // w) + (y // h)) % 9 + 1 28 | 29 | for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: 30 | if 0 <= x + dx < 5 * w and 0 <= y + dy < 5 * h: 31 | g.add_edge((x + dx, y + dy), (x, y), weight=cost) 32 | 33 | print("Part 2:", 34 | nx.shortest_path_length(g, (0, 0), (5*w - 1, 5*h - 1), weight="weight") 35 | ) 36 | -------------------------------------------------------------------------------- /2021/16.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2021/16: Packet Decoder """ 3 | 4 | import sys 5 | import dataclasses as dc 6 | import functools as ft 7 | from operator import add, mul, gt, lt, eq 8 | 9 | message = ( 10 | (int(nibble, 16) >> s) & 1 11 | for nibble in sys.stdin.read().strip() 12 | for s in range(3, -1, -1) 13 | ) 14 | 15 | @dc.dataclass 16 | class Packet: 17 | version: ... 18 | tid: ... 19 | n: ... 20 | subpackets: ... 21 | 22 | @classmethod 23 | def parse(cls, message): 24 | def consume(n): 25 | return sum(next(message) << i for i in range(n - 1, -1, -1)) 26 | 27 | version = consume(3) 28 | tid = consume(3) 29 | 30 | n, subpackets = 0, [] 31 | if tid == 4: 32 | while consume(1): 33 | n = (n << 4) | consume(4) 34 | n = (n << 4) | consume(4) 35 | else: 36 | if consume(1): 37 | for _ in range(consume(11)): 38 | subpackets.append(Packet.parse(message)) 39 | else: 40 | chunk = ( 41 | consume(1) for _ in range(consume(15)) 42 | ) 43 | while True: 44 | try: 45 | subpackets.append(Packet.parse(chunk)) 46 | except RuntimeError: # "generator raised StopIteration" 47 | break 48 | 49 | return Packet(version=version, tid=tid, n=n, subpackets=subpackets) 50 | 51 | def checksum(self): 52 | return self.version + sum(subpacket.checksum() for subpacket in self.subpackets) 53 | 54 | def eval(self): 55 | if self.tid == 4: 56 | return self.n 57 | 58 | return ft.reduce( 59 | [add, mul, min, max, None, gt, lt, eq][self.tid], ( 60 | subpacket.eval() for subpacket in self.subpackets 61 | ) 62 | ) 63 | 64 | p = Packet.parse(message) 65 | 66 | print("Part 1:", p.checksum()) 67 | print("Part 2:", p.eval()) 68 | -------------------------------------------------------------------------------- /2021/17.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2021/17: Trick Shot """ 3 | 4 | import sys 5 | import re 6 | 7 | min_x, max_x, min_y, max_y = map( 8 | int, re.findall(r"-?\d+", sys.stdin.read()) 9 | ) 10 | 11 | print("Part 1:", 12 | -min_y * (-min_y - 1) // 2 13 | ) 14 | 15 | def launch(vx, vy): 16 | x, y = 0, 0 17 | while x <= max_x and y >= min_y: 18 | x = x + vx; y = y + vy 19 | if min_x <= x <= max_x and min_y <= y <= max_y: 20 | return True 21 | 22 | vx = max(0, vx - 1) 23 | vy = vy - 1 24 | 25 | return False 26 | 27 | print("Part 2:", sum( 28 | launch(vx, vy) 29 | for vx in range(int((2*min_x) ** 0.5), max_x + 1) 30 | for vy in range(min_y, -min_y) 31 | )) 32 | -------------------------------------------------------------------------------- /2021/18.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2021/18: Snailfish """ 3 | 4 | import sys 5 | import collections as cl 6 | import functools as ft 7 | 8 | Number = cl.namedtuple( 9 | "Number", 10 | [ 11 | "l", # = int | Number 12 | "r" # = int | Number 13 | ] 14 | ) 15 | 16 | numbers = list( 17 | eval(ln.replace("[", "Number(").replace("]", ")")) 18 | for ln in sys.stdin.read().strip().split("\n") 19 | ) 20 | 21 | def add_l(n, a): 22 | if isinstance(n, int): 23 | return n + a 24 | return Number(add_l(n.l, a), n.r) 25 | 26 | def add_r(n, a): 27 | if isinstance(n, int): 28 | return n + a 29 | return Number(n.l, add_r(n.r, a)) 30 | 31 | def explode(n, depth=0): 32 | if isinstance(n, Number): 33 | if depth == 4: 34 | assert \ 35 | isinstance(n.l, int) and isinstance(n.r, int), \ 36 | "Can't explode this pair." 37 | return True, (n.l, n.r), 0 38 | 39 | exploded, (l, r), child = explode(n.l, depth + 1) 40 | if exploded: 41 | return True, (l, None), Number(child, add_l(n.r, r) if r else n.r) 42 | 43 | exploded, (l, r), child = explode(n.r, depth + 1) 44 | if exploded: 45 | return True, (None, r), Number(add_r(n.l, l) if l else n.l, child) 46 | 47 | return False, (None, None), n 48 | 49 | def split(n): 50 | if isinstance(n, int): 51 | if n >= 10: 52 | return True, Number(n // 2, (n + 1) // 2) 53 | else: 54 | splitted, child = split(n.l) 55 | if splitted: 56 | return True, Number(child, n.r) 57 | 58 | splitted, child = split(n.r) 59 | return splitted, Number(n.l, child) 60 | 61 | return False, n 62 | 63 | def add(a, b): 64 | s = Number(a, b) 65 | while True: 66 | exploded, _, s = explode(s) 67 | if exploded: 68 | continue 69 | 70 | splitted, s = split(s) 71 | if not splitted: 72 | break 73 | 74 | return s 75 | 76 | def magnitude(n): 77 | if isinstance(n, int): 78 | return n 79 | return 3*magnitude(n.l) + 2*magnitude(n.r) 80 | 81 | print("Part 1:", magnitude(ft.reduce(add, numbers))) 82 | 83 | print("Part 2:", 84 | max( 85 | magnitude(add(n1, n2)) 86 | for i, n1 in enumerate(numbers) 87 | for j, n2 in enumerate(numbers) 88 | if i != j 89 | ) 90 | ) 91 | -------------------------------------------------------------------------------- /2021/19.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2021/19: Beacon Scanner """ 3 | 4 | import sys 5 | import itertools as it 6 | 7 | scans = [ 8 | [ 9 | tuple(int(coordinate) for coordinate in point.split(",")) 10 | for point in scanner.split("\n")[1:] 11 | ] 12 | for scanner in sys.stdin.read().strip().split("\n\n") 13 | ] 14 | 15 | fingerprints = [ [] for _ in range(len(scans)) ] 16 | for n, scan in enumerate(scans): 17 | for i, p1 in enumerate(scan): 18 | fingerprints[n].append( 19 | frozenset( 20 | tuple(sorted(abs(p1[l] - p2[l]) for l in range(3))) 21 | for p2 in scan 22 | ) 23 | ) 24 | 25 | def align(A, B): 26 | # Find the transformation (rotation and translation) that maps the points 27 | # in B to the corresponding points in A. 28 | # 29 | # This can be done easily with some linear algebra: 30 | # 31 | # 1. We construct three systems of equations that separately describe 32 | # the translation along the x-, y-, and z-axes. 33 | # 34 | # | b[0].x b[0].y b[0].z 1 | | rxx | | a[0].x | 35 | # | b[1].x b[1].y b[1].z 1 | \/ | rxy | -- | a[1].x | 36 | # | b[2].x b[2].y b[2].z 1 | /\ | rxx | -- | a[2].x | 37 | # | b[3].x b[3].y b[3].z 1 | | dx | | a[3].x | 38 | # 39 | # | b[0].x b[0].y b[0].z 1 | | ryx | | a[0].y | 40 | # | b[1].x b[1].y b[1].z 1 | \/ | ryy | -- | a[1].y | 41 | # | b[2].x b[2].y b[2].z 1 | /\ | ryx | -- | a[2].y | 42 | # | b[3].x b[3].y b[3].z 1 | | dy | | a[3].y | 43 | # 44 | # | b[0].x b[0].y b[0].z 1 | | rzx | | a[0].z | 45 | # | b[1].x b[1].y b[1].z 1 | \/ | rzy | -- | a[1].z | 46 | # | b[2].x b[2].y b[2].z 1 | /\ | rzz | -- | a[2].z | 47 | # | b[3].x b[3].y b[3].z 1 | | dx | | a[3].z | 48 | # 49 | # 2. Assuming that we have linear independence among the rows of the 50 | # left-hand matrix, we can solve these systems and and use their 51 | # solutions to construct the homogenous transformation matrix that 52 | # transforms B to A. 53 | # 54 | # | rxx rxy rxx dx | 55 | # | ryx ryy ryx dx | 56 | # | rzx rzy rzx dx | 57 | # | 0 0 0 1 | 58 | # 59 | # However, we don't need to "properly" solve these systems (via, say, 60 | # Gaussian elimination) due to additional restrictions that this puzzle 61 | # imposes (for example, only one of { rxx, rxy, rxx } can be non-zero and 62 | # this non-zero value must be +/- 1.) 63 | 64 | for (a1, b1), (a2, b2) in it.combinations( 65 | ( 66 | (p1, p2) 67 | for (i, p1), (j, p2) in it.product( 68 | enumerate(scans[A]), enumerate(scans[B]) 69 | ) 70 | if len(fingerprints[A][i] & fingerprints[B][j]) >= 12 71 | ), 2 72 | ): 73 | X = [] 74 | for d1 in range(3): 75 | x = [] 76 | d = a1[d1] - a2[d1] 77 | for d2 in range(3): 78 | if b2[d2] - b1[d2] == d: 79 | x.append((-1, d2, a1[d1] + b1[d2])) 80 | if b1[d2] - b2[d2] == d: 81 | x.append(( 1, d2, a1[d1] - b1[d2])) 82 | 83 | # More than one possible transformation? 84 | if len(x) != 1: 85 | break 86 | X.append(x[0]) 87 | else: 88 | return X[0] + X[1] + X[2] 89 | 90 | scanners = { 91 | 0: (0, 0, 0) 92 | } 93 | beacons = set( scans[0] ) 94 | 95 | while len(scanners.keys()) < len(scans): 96 | for n in range(len(scans)): 97 | if n in scanners: 98 | continue 99 | 100 | fingerprint = frozenset().union(*fingerprints[n]) 101 | for m in scanners.keys(): 102 | # Skip this reference scan if the scans provably do not overlap. 103 | if ( 104 | sum( 105 | d in fingerprint 106 | for d in it.chain.from_iterable(fingerprints[m]) 107 | ) < 66 108 | ): 109 | continue 110 | 111 | h = align(m, n) 112 | if h: 113 | sx, ax, dx, sy, ay, dy, sz, az, dz = h 114 | 115 | # Transform this scan. 116 | scans[n] = [ 117 | (sx*p[ax] + dx, sy*p[ay] + dy, sz*p[az] + dz) 118 | for p in scans[n] 119 | ] 120 | 121 | scanners[n] = (dx, dy, dz) 122 | beacons |= set(scans[n]) 123 | break 124 | 125 | print("Part 1:", len(beacons)) 126 | 127 | print("Part 2:", 128 | max( 129 | sum(abs(m[i] - n[i]) for i in range(3)) 130 | for m, n in it.combinations(scanners.values(), 2) 131 | ) 132 | ) 133 | -------------------------------------------------------------------------------- /2021/20.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2021/20: Trench Map """ 3 | 4 | import sys 5 | 6 | rule, image = sys.stdin.read().strip().split("\n\n") 7 | 8 | assert len(rule) == 512 9 | assert rule[0] == "#" 10 | assert rule[-1] == "." 11 | 12 | p = set( 13 | (x, y) 14 | for y, ln in enumerate(image.split()) 15 | for x, c in enumerate(ln) 16 | if c == "#" 17 | ) 18 | 19 | for r in range(1, 50 + 1): 20 | pp = set() 21 | 22 | # In odd-numbered steps, we only track the cells that turn off because an 23 | # infinite number turn on. In even-numbered steps, we only track the cells 24 | # that turn on because an infinite number turn off. 25 | t = "." if r % 2 else "#" 26 | 27 | min_x, *_, max_x = sorted(x for (x, y) in p) 28 | min_y, *_, max_y = sorted(y for (x, y) in p) 29 | for y in range(min_y - 1, max_y + 2): 30 | for x in range(min_x - 1, max_x + 2): 31 | n = sum( 32 | x*pow(2, i) 33 | for i, x in enumerate( 34 | (x + dx, y + dy) in p if r % 2 else (x + dx, y + dy) not in p 35 | for dy in [1, 0, -1] 36 | for dx in [1, 0, -1] 37 | ) 38 | ) 39 | 40 | if rule[n] == t: 41 | pp.add((x, y)) 42 | 43 | p = pp 44 | 45 | if r in { 2, 50 }: 46 | print("Part {}:".format({2: "1", 50: "2"}[r]), 47 | len(p) 48 | ) 49 | -------------------------------------------------------------------------------- /2021/21.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2021/21: Dirac Dice """ 3 | 4 | import sys 5 | import collections as cl 6 | import itertools as it 7 | import re 8 | 9 | _, p1, _, p2 = re.findall(r"\d+", sys.stdin.read()) 10 | p1, p2 = int(p1), int(p2) 11 | 12 | die = it.count() 13 | def roll3(): 14 | return sum(next(die) % 100 + 1 for _ in range(3)) 15 | 16 | _p1, _p2, s1, s2 = p1 - 1, p2 - 1, 0, 0 17 | while True: 18 | _p1 = (_p1 + roll3()) % 10 19 | s1 = s1 + _p1 + 1 20 | if s1 >= 1000: 21 | break 22 | 23 | _p2 = (_p2 + roll3()) % 10 24 | s2 = s2 + _p2 + 1 25 | if s2 >= 1000: 26 | break 27 | 28 | print("Part 1:", min(s1, s2) * next(die)) 29 | 30 | rolls = cl.Counter( 31 | sum(roll) for roll in it.product([1, 2, 3], repeat=3) 32 | ) 33 | 34 | memo = {} 35 | def play(p1, p2, s1=0, s2=0): 36 | if s1 >= 21 or s2 >= 21: 37 | return (s1 >= 21, s2 >= 21) 38 | 39 | if (p1, p2, s1, s2) not in memo: 40 | w1, w2 = 0, 0 41 | for roll, frequency in rolls.items(): 42 | _p1 = (p1 + roll) % 10 43 | _w1, _w2 = play(p2, _p1, s2, s1 + _p1 + 1) 44 | w1, w2 = w1 + _w2*frequency, w2 + _w1*frequency 45 | 46 | memo[p1, p2, s1, s2] = (w1, w2) 47 | 48 | return memo[p1, p2, s1, s2] 49 | 50 | print("Part 2:", max(play(p1 - 1, p2 - 1))) 51 | -------------------------------------------------------------------------------- /2021/22.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | """ 2021/22: Reactor Reboot """ 3 | 4 | import sys 5 | import collections as cl 6 | import itertools as it 7 | import re 8 | 9 | steps = [ 10 | (action == "on", tuple(map(int, bounds))) 11 | for action, *bounds in re.findall( 12 | r"(.*) x=(-?\d+)..(-?\d+),y=(-?\d+)..(-?\d+),z=(-?\d+)..(-?\d+)", sys.stdin.read() 13 | ) 14 | ] 15 | 16 | reactor = cl.defaultdict(bool) 17 | for action, bounds in steps: 18 | min_x, max_x, min_y, max_y, min_z, max_z = bounds 19 | 20 | assert min_x <= max_x 21 | assert min_y <= max_y 22 | assert min_z <= min_z 23 | 24 | for x, y, z in it.product( 25 | range(max(min_x, -50), min(max_x + 1, 50)), 26 | range(max(min_y, -50), min(max_y + 1, 50)), 27 | range(max(min_z, -50), min(max_z + 1, 50)) 28 | ): 29 | reactor[x, y, z] = action 30 | 31 | print("Part 1:", sum(reactor.values())) 32 | 33 | def volume(cuboid): 34 | min_x, max_x, min_y, max_y, min_z, max_z = cuboid 35 | 36 | return ( 37 | max(max_x - min_x + 1, 0) 38 | * max(max_y - min_y + 1, 0) 39 | * max(max_z - min_z + 1, 0) 40 | ) 41 | 42 | reactor = {} 43 | for action, bounds in steps: 44 | min_x, max_x, min_y, max_y, min_z, max_z = bounds 45 | 46 | reactions = cl.defaultdict(int) 47 | for (bmin_x, bmax_x, bmin_y, bmax_y, bmin_z, bmax_z), bsgn in reactor.items(): 48 | intersection = ( 49 | max(min_x, bmin_x), min(max_x, bmax_x), 50 | max(min_y, bmin_y), min(max_y, bmax_y), 51 | max(min_z, bmin_z), min(max_z, bmax_z) 52 | ) 53 | if volume(intersection) > 0: 54 | reactions[intersection] -= bsgn 55 | 56 | if action: 57 | reactions[bounds] += 1 58 | 59 | reactor = { 60 | k: reactor.get(k, 0) + reactions.get(k, 0) 61 | for k in it.chain.from_iterable((reactor.keys(), reactions.keys())) 62 | } 63 | 64 | print("Part 2:", 65 | sum( 66 | sgn * volume(bounds) 67 | for bounds, sgn in reactor.items() 68 | ) 69 | ) 70 | -------------------------------------------------------------------------------- /2021/23.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2021/23: Amphipod """ 3 | 4 | import sys 5 | import heapq as hq 6 | 7 | ROOM_X = { 8 | "A": 2, 9 | "B": 4, 10 | "C": 6, 11 | "D": 8 12 | } 13 | 14 | MOVE_COST = { 15 | "A": 1, 16 | "B": 10, 17 | "C": 100, 18 | "D": 1000 19 | } 20 | 21 | STOPPABLE = [0, 1, 3, 5, 7, 9, 10] 22 | 23 | def deadlocked(rooms, hallway): 24 | # Detect if a given state is "deadlocked" -- that is, cannot possibly lead 25 | # to the winning state. 26 | # 27 | # This doesn't catch all deadlocking states; just the ones that are simple 28 | # enough to hard-code. 29 | 30 | return ( 31 | # Blocking each other? 32 | ( 33 | (hallway[3] == "D" or hallway[5] == "D") 34 | and (hallway[5] == "A" or hallway[7] == "A") 35 | ) 36 | or (hallway[5] == "A" and hallway[3] == "C") 37 | or (hallway[5] == "D" and hallway[7] == "B") 38 | 39 | # Not enough room? 40 | or ( 41 | hallway[3] == "A" 42 | and sum(x is None for x in hallway[: 2]) < sum(x is not None and x != "A" for x in rooms[0]) 43 | ) 44 | or ( 45 | hallway[7] == "D" 46 | and sum(x is None for x in hallway[-2:]) < sum(x is not None and x != "D" for x in rooms[3]) 47 | ) 48 | ) 49 | 50 | def dijkstra(amphipods): 51 | Q = [ 52 | (0, 0, (amphipods, (None, None, None, None, None, None, None, None, None, None, None))) 53 | ] 54 | hq.heapify(Q) 55 | 56 | seen = set() 57 | while len(Q): 58 | cost, _, (rooms, hallway) = hq.heappop(Q) 59 | if all( 60 | all(amphipod == chr(i + 65) for amphipod in room) 61 | for i, room in enumerate(rooms) 62 | ): 63 | return cost 64 | 65 | # Already been here? 66 | if (rooms, hallway) in seen: 67 | continue 68 | seen.add((rooms, hallway)) 69 | 70 | # Move an amphipod from a room into the hallway. 71 | for i, room in enumerate(rooms): 72 | # We can only move the first amphipod in each room; the others are 73 | # stuck. 74 | try: 75 | j, amphipod = next((j, amphipod) for j, amphipod in enumerate(room) if amphipod) 76 | except StopIteration: 77 | continue 78 | 79 | # If this amphipod, and all others behind it, are in the correct 80 | # room, then don't move. 81 | if all(room[k] == chr(i + 65) for k in range(j, len(room))): 82 | continue 83 | 84 | # We can move to any non-junction cell in the hallway. 85 | for p in STOPPABLE: 86 | # Check if path is obstructed. 87 | p1, p2 = 2*i + 2, p 88 | if p1 > p2: 89 | p1, p2 = p2, p1 90 | 91 | if any(hallway[k] is not None for k in range(p1, p2 + 1)): 92 | continue 93 | 94 | _cost = cost + MOVE_COST[amphipod]*(j + p2 - p1 + 1) 95 | _rooms = tuple( 96 | tuple( 97 | None if (i_ == i and j_ == j) else amphipod 98 | for j_, amphipod in enumerate(room) 99 | ) 100 | for i_, room in enumerate(rooms) 101 | ) 102 | _hallway = tuple( 103 | amphipod if p_ == p else h 104 | for p_, h in enumerate(hallway) 105 | ) 106 | 107 | if not deadlocked(_rooms, _hallway): 108 | hq.heappush( 109 | Q, (_cost, id(_rooms), (_rooms, _hallway)) 110 | ) 111 | 112 | # Move an amphipod from the hallway into their room. 113 | for p in STOPPABLE: 114 | amphipod = hallway[p] 115 | if amphipod is None: 116 | continue 117 | 118 | # Check if path is obstructed. 119 | p1, p2 = ROOM_X[amphipod], p 120 | if p < ROOM_X[amphipod]: 121 | p1, p2 = p2 + 1, p1 + 1 122 | 123 | if all(hallway[k] is None for k in range(p1, p2)): 124 | i = ord(amphipod) - 65 125 | room = rooms[i] 126 | 127 | # Check if the room is safe to enter. 128 | if any(c is not None and c != amphipod for c in room): 129 | continue 130 | 131 | # Go as far in as possible! 132 | try: 133 | j = next(j for j, c in enumerate(room) if c is not None) - 1 134 | except StopIteration: 135 | j = len(room) - 1 136 | 137 | _cost = cost + MOVE_COST[amphipod]*(j + abs(ROOM_X[amphipod] - p) + 1) 138 | _rooms = tuple( 139 | tuple( 140 | amphipod if (k == i and l == j) else h 141 | for l, h in enumerate(room) 142 | ) 143 | for k, room in enumerate(rooms) 144 | ) 145 | _hallway = tuple( 146 | None if k == p else h 147 | for k, h in enumerate(hallway) 148 | ) 149 | 150 | if not deadlocked(_rooms, _hallway): 151 | hq.heappush( 152 | Q, (_cost, id(_rooms), (_rooms, _hallway)) 153 | ) 154 | 155 | burrow = [ 156 | list(ln) for ln in sys.stdin.read().strip().split("\n") 157 | ] 158 | 159 | rooms = tuple( 160 | (burrow[2][i], burrow[3][i]) 161 | for i in [3, 5, 7, 9] 162 | ) 163 | 164 | print("Part 1:", dijkstra(rooms)) 165 | 166 | print("Part 2:", 167 | dijkstra( 168 | tuple( 169 | (rooms[i][0], *fold, rooms[i][1]) 170 | for i, fold in enumerate(["DD", "CB", "BA", "AC"]) 171 | ) 172 | ) 173 | ) 174 | -------------------------------------------------------------------------------- /2021/24.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2021/24: Arithmetic Logic Unit """ 3 | 4 | import sys 5 | import re 6 | 7 | monad = [ 8 | tuple(map(int, x)) 9 | for x in re.findall( 10 | ( 11 | "inp w" "\n" 12 | "mul x 0" "\n" 13 | "add x z" "\n" 14 | "mod x 26" "\n" 15 | "div z (1|26)" "\n" 16 | "add x (-?\d+)" "\n" 17 | "eql x w" "\n" 18 | "eql x 0" "\n" 19 | "mul y 0" "\n" 20 | "add y 25" "\n" 21 | "mul y x" "\n" 22 | "add y 1" "\n" 23 | "mul z y" "\n" 24 | "mul y 0" "\n" 25 | "add y w" "\n" 26 | "add y (\d+)" "\n" 27 | "mul y x" "\n" 28 | "add z y" "\n?" 29 | ), 30 | sys.stdin.read() 31 | ) 32 | ] 33 | 34 | # Determine the constraints that our program imposes. 35 | constraints = [] 36 | 37 | stack = [] 38 | for i, (divisor, a, b) in enumerate(monad): 39 | if divisor == 1: 40 | assert a >= 10 41 | stack.append((i, b)) 42 | else: 43 | assert a <= 0 44 | _i, _b = stack.pop() 45 | constraints.append(((_i, i), _b + a)) 46 | 47 | assert len(stack) == 0 48 | 49 | def reverse(constraints, f): 50 | number = [ 51 | None for _ in range(14) 52 | ] 53 | for (a, b), d in constraints: 54 | number[a], number[b] = f( 55 | (i, i + d) 56 | for i in range(1, 10) 57 | if 0 < (i + d) < 10 58 | ) 59 | 60 | return "".join(str(n) for n in number) 61 | 62 | print("Part 1:", reverse(constraints, max)) 63 | print("Part 2:", reverse(constraints, min)) 64 | -------------------------------------------------------------------------------- /2021/25.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2021/25: Sea Cucumber """ 3 | 4 | import sys 5 | import itertools as it 6 | 7 | cucumbers = [ 8 | list(ln) for ln in sys.stdin.read().strip().split("\n") 9 | ] 10 | 11 | width, height = len(cucumbers[0]), len(cucumbers) 12 | 13 | for step in it.count(1): 14 | moves = sorted( 15 | (cucumbers[y][x], x, y) 16 | for y in range(height) 17 | for x in range(width) 18 | if ( 19 | # This is an east-facing cucumber that can move into an empty 20 | # space. 21 | (cucumbers[y][x] == ">" and cucumbers[y][(x + 1) % width] == ".") 22 | 23 | or 24 | # This is a south-facing cucumber that is moving into a cell 25 | # containing an east-facing cucumber that is about to get out 26 | # of the way. 27 | (cucumbers[y][x] == "v" and cucumbers[(y + 1) % height][x] == ">" and cucumbers[(y + 1) % height][(x + 1) % width] == ".") 28 | 29 | or 30 | # This is a south-facing cucumber that is moving into a cell 31 | # that will not be occupied by an east-facing cucumber. 32 | (cucumbers[y][x] == "v" and cucumbers[(y + 1) % height][x] == "." and cucumbers[(y + 1) % height][(x - 1) % width] != ">") 33 | ) 34 | ) 35 | 36 | if len(moves) == 0: 37 | print("Part 1:", step) 38 | break 39 | 40 | for c, x, y in moves: 41 | cucumbers[y][x] = "." 42 | 43 | if c == ">": 44 | cucumbers[y][(x + 1) % width] = c 45 | else: 46 | cucumbers[(y + 1) % height][x] = c 47 | 48 | print("Part 2:", "Remotely Start The Sleigh") 49 | -------------------------------------------------------------------------------- /2022/01.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2022/01: Calorie Counting """ 3 | 4 | import sys 5 | 6 | elves = sorted( 7 | sum(map(int, elf.split())) for elf in sys.stdin.read().split("\n\n") 8 | )[::-1] 9 | 10 | print("Part 1:", elves[0]) 11 | print("Part 2:", sum(elves[:3])) -------------------------------------------------------------------------------- /2022/02.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2022/02: Rock Paper Scissors """ 3 | 4 | import sys 5 | 6 | guide = [ 7 | ("ABC".index(ln[0]), "XYZ".index(ln[2])) 8 | for ln in sys.stdin.read().strip().split("\n") 9 | ] 10 | 11 | # Behold, magic formulas. 12 | print("Part 1:", sum(3*((2*p1 + p2 + 1) % 3) + p2 + 1 for p1, p2 in guide)) 13 | print("Part 2:", sum((p1 + p2 - 1) % 3 + 1 + 3*p2 for p1, p2 in guide)) 14 | -------------------------------------------------------------------------------- /2022/03.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2022/03: Rucksack Reorganization """ 3 | 4 | import string 5 | import sys 6 | 7 | rucksacks = sys.stdin.read().strip().split("\n") 8 | 9 | def priority(x): 10 | x, = x 11 | return string.ascii_letters.index(x) + 1 12 | 13 | print("Part 1:", sum( 14 | priority(set(rucksack[:len(rucksack) // 2]) & set(rucksack[len(rucksack) // 2:])) 15 | for rucksack in rucksacks 16 | )) 17 | print("Part 2:", sum( 18 | priority(set.intersection(*map(set, rucksacks[i:i + 3]))) 19 | for i in range(0, len(rucksacks), 3) 20 | )) -------------------------------------------------------------------------------- /2022/04.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2022/04: Camp Cleanup """ 3 | 4 | import re 5 | import sys 6 | 7 | f = lambda a, b, c, d: (set(range(a, b + 1)), set(range(c, d + 1))) 8 | assignments = [ 9 | f(*map(int, re.findall(r"\d+", assignment))) 10 | for assignment in sys.stdin.read().strip().split("\n") 11 | ] 12 | 13 | print("Part 1:", sum( 14 | X.issubset(Y) or Y.issubset(X) for X, Y in assignments 15 | )) 16 | print("Part 2:", sum( 17 | len(X.intersection(Y)) > 0 for X, Y in assignments 18 | )) 19 | -------------------------------------------------------------------------------- /2022/05.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2022/05: Supply Stacks """ 3 | 4 | import re 5 | import sys 6 | 7 | stacks, instructions = sys.stdin.read().split("\n\n") 8 | 9 | stacks = list( 10 | "".join(x).strip()[1:] 11 | for i, x in enumerate( 12 | zip(*map(list, stacks.split("\n")[::-1])) 13 | ) 14 | if i % 4 == 1 15 | ) 16 | instructions = [ 17 | tuple(map(int, re.findall(r"\d+", instruction))) 18 | for instruction in instructions.strip().split("\n") 19 | ] 20 | 21 | def cratemover(m9001 = False): 22 | _stacks = [ None ] + stacks[:] 23 | for N, a, b in instructions: 24 | _stacks[a], m = _stacks[a][:-N], _stacks[a][-N:] 25 | if not m9001: 26 | m = m[::-1] 27 | _stacks[b] = _stacks[b] + m 28 | 29 | return "".join(stack[-1] for stack in _stacks[1:]) 30 | 31 | print("Part 1:", cratemover()) 32 | print("Part 2:", cratemover(True)) 33 | -------------------------------------------------------------------------------- /2022/06.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2022/06: Tuning Trouble """ 3 | 4 | import sys 5 | 6 | signal = sys.stdin.read().strip() 7 | 8 | scan = lambda N: \ 9 | next(i for i in range(N, len(signal)) if len(set(signal[i - N:i])) == N) 10 | print("Part 1:", scan(4)) 11 | print("Part 2:", scan(14)) 12 | -------------------------------------------------------------------------------- /2022/07.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2022/07: No Space Left On Device """ 3 | 4 | import collections as cl 5 | import pathlib 6 | import sys 7 | 8 | T = sys.stdin.read().strip().split("\n") 9 | 10 | fs, cwd = cl.defaultdict(int), pathlib.Path("/") 11 | for t in T: 12 | if t.startswith("$ cd"): 13 | arg = t.split()[-1] 14 | cwd = { 15 | "/": pathlib.Path("/"), 16 | "..": cwd.parent, 17 | }.get(arg, cwd / arg) 18 | elif t[0].isdigit(): 19 | _cwd = cwd / t.split()[1] 20 | while _cwd != pathlib.Path("/"): 21 | _cwd = _cwd.parent 22 | fs[_cwd] += int(t.split()[0]) 23 | 24 | print("Part 1:", sum(size for size in fs.values() if size <= 100000)) 25 | print("Part 2:", min( 26 | size for size in fs.values() 27 | if size >= fs.get(pathlib.Path("/")) - 40000000 28 | )) 29 | -------------------------------------------------------------------------------- /2022/08.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2022/08: Treetop Tree House """ 3 | 4 | import itertools as it 5 | import math 6 | import sys 7 | 8 | trees = [ 9 | list(map(int, g)) 10 | for g in sys.stdin.read().strip().split("\n") 11 | ] 12 | w, h = len(trees[0]), len(trees) 13 | 14 | visible_n, best_scenic_score = 0, -math.inf 15 | for x, y in it.product(range(w), range(h)): 16 | visible, scenic_score = False, 1 17 | for dx, dy in [ 18 | (1, 0), (-1, 0), (0, 1), (0, -1) 19 | ]: 20 | x0, y0 = x, y 21 | for s0 in it.count(0): 22 | x0, y0 = x0 + dx, y0 + dy 23 | k = (0 <= x0 < w and 0 <= y0 < h) 24 | if k and trees[y0][x0] < trees[y][x]: 25 | continue 26 | 27 | visible, scenic_score = visible or not k, scenic_score * (s0 + k) 28 | break 29 | 30 | visible_n = visible_n + visible 31 | best_scenic_score = max(best_scenic_score, scenic_score) 32 | 33 | print("Part 1:", visible_n) 34 | print("Part 2:", best_scenic_score) -------------------------------------------------------------------------------- /2022/09.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2022/09: Rope Bridge """ 3 | 4 | import sys 5 | 6 | motions = sys.stdin.read().strip().split("\n") 7 | 8 | def sgn(x): 9 | return (x > 0) - (x < 0) 10 | 11 | part1, part2 = set(), set() 12 | knot = [ 13 | 0 + 0j for _ in range(10) 14 | ] 15 | for motion in motions: 16 | for _ in range(int(motion[1:])): 17 | for i in range(len(knot)): 18 | d = 0 19 | if i <= 0: 20 | d = {"R": 1, "L": -1, "U": 1j, "D": -1j}[motion[0]] 21 | else: 22 | k = knot[i - 1] - knot[i] 23 | if abs(k) >= 2: 24 | d = sgn(k.real) + sgn(k.imag) * 1j 25 | 26 | knot[i] = knot[i] + d 27 | 28 | if i == 1: part1.add(knot[i]) 29 | if i == 9: part2.add(knot[i]) 30 | 31 | print("Part 1:", len(part1)) 32 | print("Part 2:", len(part2)) 33 | -------------------------------------------------------------------------------- /2022/10.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2022/10: Cathode-Ray Tube """ 3 | 4 | import itertools as it 5 | import sys 6 | 7 | instructions = ( 8 | instruction.split() for instruction in sys.stdin.read().strip().split("\n") 9 | ) 10 | 11 | X, x = 1, None 12 | 13 | S = 0 14 | crt = [[" "] * 40 for _ in range(6)] 15 | for cycle in it.count(1): 16 | if cycle in (20, 60, 100, 140, 180, 220): 17 | S = S + cycle * X 18 | 19 | k = cycle - 1 20 | if abs((k % 40) - X) <= 1: 21 | crt[k // 40][k % 40] = "#" 22 | 23 | # Now, we can handle the instruction. 24 | if x is not None: 25 | X, x = X + x, None 26 | else: 27 | try: 28 | op, *args = next(instructions) 29 | except StopIteration: 30 | break 31 | 32 | x = int(args[0]) if op == "addx" else None 33 | 34 | print("Part 1:", S) 35 | print("Part 2:") 36 | print("\n".join("".join(ln) for ln in crt)) -------------------------------------------------------------------------------- /2022/11.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2022/11: Monkey in the Middle """ 3 | 4 | import collections as cl 5 | import copy 6 | import math 7 | import operator 8 | import sys 9 | 10 | def parse(expr): 11 | # A naive expression parser; just barely sufficient for the problem. 12 | lhs, op, rhs = expr.split() 13 | return ( 14 | lambda x: 15 | (operator.add if op == "+" else operator.mul) 16 | (int(lhs) if lhs.isdigit() else x, int(rhs) if rhs.isdigit() else x) 17 | ) 18 | 19 | class Monkey: 20 | def __init__(self, inventory, op, modulus, a, d): 21 | self.inventory = list(inventory) 22 | self.op = op 23 | self.modulus = modulus 24 | self.a = a 25 | self.d = d 26 | 27 | @classmethod 28 | def from_str(cls, S): 29 | S = S.split("\n") 30 | return cls( 31 | map(int, S[1][18:].split(", ")), 32 | parse(S[2][19:]), 33 | int(S[3][21:]), 34 | int(S[4][29:]), 35 | int(S[5][30:]) 36 | ) 37 | 38 | monkeys = [ 39 | Monkey.from_str(m) for m in sys.stdin.read().strip().split("\n\n") 40 | ] 41 | 42 | N = math.lcm(*(m.modulus for m in monkeys)) 43 | def play(monkeys, part = 1): 44 | activity = cl.Counter() 45 | for rnd in range([None, 20, 10000][part]): 46 | for i, monkey in enumerate(monkeys): 47 | activity[i] = activity[i] + len(monkey.inventory) 48 | while len(monkey.inventory) > 0: 49 | x = monkey.op(monkey.inventory.pop(0)) % N 50 | if part == 1: 51 | x //= 3 52 | 53 | k = monkey.a if (x % monkey.modulus == 0) else monkey.d 54 | monkeys[k].inventory.append(x) 55 | 56 | return activity.most_common()[0][1] * activity.most_common()[1][1] 57 | 58 | print("Part 1:", play(copy.deepcopy(monkeys))) 59 | print("Part 2:", play(monkeys, part = 2)) 60 | -------------------------------------------------------------------------------- /2022/12.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2022/12: Hill Climbing Algorithm """ 3 | 4 | import networkx as nx 5 | import sys 6 | 7 | heightmap = { 8 | complex(x, y): c 9 | for y, ln in enumerate(sys.stdin.read().strip().split("\n")) 10 | for x, c in enumerate(ln) 11 | } 12 | 13 | # Locate S and E; replace them with `a` and `z`. 14 | S = next(k for k, v in heightmap.items() if v == "S") 15 | E = next(k for k, v in heightmap.items() if v == "E") 16 | heightmap[S], heightmap[E] = "a", "z" 17 | 18 | G = nx.DiGraph() 19 | for xy, c in heightmap.items(): 20 | for d in (1, -1, 1j, -1j): 21 | if ord(heightmap.get(xy + d, "{")) <= ord(c) + 1: 22 | G.add_edge(xy, xy + d) 23 | 24 | H = nx.shortest_path_length(G, target = E) 25 | print("Part 1:", H.get(S)) 26 | print("Part 2:", min(v for k, v in H.items() if heightmap[k] == "a")) 27 | -------------------------------------------------------------------------------- /2022/13.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2022/13: Distress Signal """ 3 | 4 | import functools as ft 5 | import json 6 | import math 7 | import sys 8 | 9 | packets = [ 10 | json.loads(packet) 11 | for packet in sys.stdin.read().strip().split() 12 | ] 13 | 14 | def cmp(l, r): 15 | if isinstance(l, int) and isinstance(r, int): 16 | return (l > r) - (r > l) 17 | elif isinstance(l, list) and isinstance(r, list): 18 | for i in range(min(len(l), len(r))): 19 | k = cmp(l[i], r[i]) 20 | if k != 0: 21 | return k 22 | return cmp(len(l), len(r)) 23 | else: 24 | l = [ l ] if isinstance(l, int) else l 25 | r = [ r ] if isinstance(r, int) else r 26 | return cmp(l, r) 27 | 28 | print("Part 1:", sum( 29 | i // 2 + 1 30 | for i in range(0, len(packets), 2) 31 | if cmp(packets[i], packets[i + 1]) < 0 32 | )) 33 | 34 | packets.append([[2]]) 35 | packets.append([[6]]) 36 | packets.sort(key = ft.cmp_to_key(cmp)) 37 | 38 | print("Part 2:", math.prod( 39 | i for i, packet in enumerate(packets, 1) 40 | if packet in ([[2]], [[6]]) 41 | )) -------------------------------------------------------------------------------- /2022/14.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2022/14: Regolith Reservoir """ 3 | 4 | import itertools as it 5 | import sys 6 | 7 | paths = [ 8 | list(map(lambda xy: tuple(map(int, xy.split(","))), path.split(" -> "))) 9 | for path in sys.stdin.read().strip().split("\n") 10 | ] 11 | 12 | C = set() 13 | for (x, y), *path in paths: 14 | C.add((x, y)) 15 | for x0, y0 in path: 16 | while (x != x0) or (y != y0): 17 | x, y = x + (x0 > x) - (x > x0), y + (y0 > y) - (y > y0) 18 | C.add((x, y)) 19 | 20 | Y = max( 21 | max(map(lambda xy: xy[1], path)) + 2 22 | for path in paths 23 | ) 24 | for x in range(500 - Y - 1, 500 + Y + 1): 25 | C.add((x, Y)) 26 | 27 | # Now that we've identified the terrain, we can proceed to simulate the falling 28 | # sand particles. 29 | p1 = False 30 | for n in it.count(): 31 | x, y = 500, 0 32 | if (x, y) in C: 33 | print("Part 2:", n) 34 | break 35 | 36 | while True: 37 | if not p1 and y >= Y - 1: 38 | print("Part 1:", n) 39 | p1 = True 40 | 41 | for dx in (0, -1, 1): 42 | if (x + dx, y + 1) not in C: 43 | x, y = x + dx, y + 1 44 | break 45 | else: 46 | C.add((x, y)) 47 | break 48 | -------------------------------------------------------------------------------- /2022/15.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2022/15: Beacon Exclusion Zone """ 3 | 4 | import re 5 | import sys 6 | import z3 7 | 8 | observations = [ 9 | tuple(map(int, re.findall(r"-?\d+", observation))) 10 | for observation in sys.stdin.read().strip().split("\n") 11 | ] 12 | 13 | # Part 1 can be solved by identifying the intervals that each sensor covers 14 | # along y=2000000. 15 | X = [] 16 | for sx, sy, bx, by in observations: 17 | d = abs(sx - bx) + abs(sy - by) - abs(sy - 2000000) 18 | if d >= 0: 19 | X.append((sx - d, sx + d)) 20 | 21 | print("Part 1:", len( 22 | # At some point, remind me to re-write this using interval sets. 23 | set.union( 24 | *[set(range(a, b + 1)) for a, b in X] 25 | ) 26 | # Don't forget to exclude known beacon positions! 27 | - set(bx for *_, bx, by in observations if by == 2000000) 28 | )) 29 | 30 | # There are a number of very clever insights on Reddit regarding Part 2. I 31 | # particularily enjoyed u/bluepichu's. I, however, propose a much more brute 32 | # solution. 33 | s = z3.Solver() 34 | x, y = z3.Int("x"), z3.Int("y") 35 | 36 | s.add(0 <= x); s.add(x <= 4000000) 37 | s.add(0 <= y); s.add(y <= 4000000) 38 | 39 | def z3_abs(x): 40 | return z3.If(x >= 0, x, -x) 41 | 42 | for sx, sy, bx, by in observations: 43 | m = abs(sx - bx) + abs(sy - by) 44 | s.add(z3_abs(sx - x) + z3_abs(sy - y) > m) 45 | 46 | assert s.check() == z3.sat 47 | model = s.model() 48 | print("Part 2:", model[x].as_long() * 4000000 + model[y].as_long()) 49 | -------------------------------------------------------------------------------- /2022/16.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2022/16: Proboscidea Volcanium """ 3 | 4 | import collections as cl 5 | import re 6 | import sys 7 | 8 | valves = { 9 | valve: (int(flowrate), tunnels.split(", ")) 10 | for valve, flowrate, tunnels, *_ 11 | in map( 12 | lambda x: \ 13 | re.fullmatch(r"^Valve ([A-Z]{2}) has flow rate=(\d*); tunnels? leads? to valves? (([A-Z]{2})(, [A-Z]{2})*)$", x) 14 | .groups(), 15 | sys.stdin.read().strip().split("\n") 16 | ) 17 | } 18 | 19 | def p1(t = 30 - 1, p = "AA", state = {}, seen = {}): 20 | q = sum( 21 | v * valves[k][0] 22 | for k, v in state.items() 23 | if v is not None 24 | ) 25 | if not t: 26 | return q 27 | 28 | # If we can get to this state faster, don't bother exploring further! 29 | if seen.get((t, p), -1) >= q: 30 | return 0 31 | seen[t, p] = q 32 | 33 | # Otherwise, consider all possible moves from here. 34 | m = 0 35 | for p0 in [ p ] + valves[p][1]: 36 | if p == p0: 37 | if state.get(p) is None and valves[p][0] > 0: 38 | state[p] = t 39 | else: 40 | continue 41 | 42 | m = max(m, p1(t - 1, p0, state, seen)) 43 | 44 | if p == p0: 45 | state[p] = None 46 | 47 | return m 48 | 49 | # This is slow, and can probably be optimized quite a bit. 50 | def p2(t = 26 - 1, pA = "AA", pB = "AA", state = {}, seen = {}): 51 | q = sum( 52 | v * valves[k][0] 53 | for k, v in state.items() 54 | if v is not None 55 | ) 56 | if not t: 57 | return q 58 | 59 | # If we can get to this state faster, don't bother exploring further! 60 | if seen.get((t, pA, pB), -1) >= q: 61 | return 0 62 | seen[t, pA, pB] = q 63 | 64 | # Otherwise, consider all possible moves from here. 65 | m = 0 66 | for pA0 in [ pA ] + valves[pA][1]: 67 | if pA0 == pA: 68 | if state.get(pA) is None and valves[pA][0] > 0: 69 | state[pA] = t 70 | else: 71 | continue 72 | 73 | for pB0 in [ pB ] + valves[pB][1]: 74 | if pB0 == pB: 75 | if state.get(pB) is None and valves[pB][0] > 0: 76 | state[pB] = t 77 | else: 78 | continue 79 | 80 | m = max(m, p2(t - 1, pA0, pB0, state, seen)) 81 | 82 | if pB0 == pB: 83 | state[pB] = None 84 | 85 | if pA0 == pA: 86 | state[pA] = None 87 | 88 | return m 89 | 90 | print("Part 1:", p1()) 91 | print("Part 2:", p2()) -------------------------------------------------------------------------------- /2022/17.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2022/17: Pyroclastic Flow """ 3 | 4 | import itertools as it 5 | import sys 6 | 7 | jets = enumerate(it.cycle(sys.stdin.read().strip())) 8 | rocks = enumerate(it.cycle([ 9 | set([ 10 | 0 + 0j, 1 + 0j, 2 + 0j, 3 + 0j, 11 | ]), 12 | set([ 13 | 1 + 2j, 14 | 0 + 1j, 1 + 1j, 2 + 1j, 15 | 1 + 0j, 16 | ]), 17 | set([ 18 | 2 + 2j, 19 | 2 + 1j, 20 | 0 + 0j, 1 + 0j, 2 + 0j, 21 | ]), 22 | set([ 23 | 0 + 3j, 24 | 0 + 2j, 25 | 0 + 1j, 26 | 0 + 0j, 27 | ]), 28 | set([ 29 | 0 + 1j, 1 + 1j, 30 | 0 + 0j, 1 + 0j, 31 | ]) 32 | ])) 33 | 34 | def translate(X, d=0 + 0j): 35 | return set(x + d for x in X) 36 | 37 | tower = set( 38 | # Monkey-patch a "floor" - this eliminates an edge-case for the first 39 | # couple rocks dropped. 40 | complex(i, 0) for i in range(7) 41 | ) 42 | h = 0 43 | i, j = 0, 0 44 | 45 | _seen = {} 46 | while True: 47 | i, rock = next(rocks) 48 | if i == 2022: 49 | print("Part 1:", h) 50 | 51 | # To detect loops, hash the previous seven rows; this isn't 100% fool-proof 52 | # but is "good enough" for this puzzle. (A better way to do this would be 53 | # to hash the set of tiles that are directly "exposed to air." 54 | k = 0 55 | for y in range(h, h - 8, -1): 56 | for x in range(7): 57 | k = (k << 1) + int(complex(x, y) in tower) 58 | 59 | K = (i % 5, j % 10091, k) 60 | if K in _seen.keys(): 61 | i0, h0 = _seen.get(K) 62 | 63 | # Are we in a cycle, at the same index as one trillion? 64 | modulus = i - i0 65 | if (1000000000000 - i) % modulus <= 0: 66 | print("Part 2:", 67 | (1000000000000 - i) // modulus * (h - h0) + h 68 | ) 69 | break 70 | else: 71 | _seen[K] = (i, h) 72 | 73 | # Otherwise, tetris time! 74 | rock = translate(rock, complex(2, h + 4)) 75 | while True: 76 | j, jet = next(jets) 77 | jet = { ">": 1, "<": -1 }[jet] 78 | 79 | # Shift horizontally, if nothing collides? 80 | rock0 = translate(rock, jet) 81 | if all( 82 | (0 <= r.real < 7) and (r not in tower) 83 | for r in rock0 84 | ): 85 | rock = rock0 86 | 87 | # Shift vertically, if nothing collides? 88 | rock0 = translate(rock, -1j) 89 | if all(rock not in tower for rock in rock0): 90 | rock = rock0 91 | else: 92 | tower |= rock 93 | break 94 | 95 | h = max(h, max(int(r.imag) for r in rock)) 96 | -------------------------------------------------------------------------------- /2022/18.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2022/18: Boiling Boulders """ 3 | 4 | import sys 5 | 6 | droplets = set( 7 | tuple(map(int, droplet.split(","))) 8 | for droplet in sys.stdin.read().strip().split("\n") 9 | ) 10 | 11 | def neighbours(x, y, z): 12 | yield from ( 13 | (x + dx, y + dy, z + dz) 14 | for dx, dy, dz in [ 15 | # Can this be generated using itertools? 16 | (1, 0, 0), (-1, 0, 0), 17 | (0, 1, 0), ( 0, -1, 0), 18 | (0, 0, 1), ( 0, 0, -1), 19 | ] 20 | ) 21 | 22 | print("Part 1:", sum( 23 | 1 for droplet in droplets 24 | for droplet0 in neighbours(*droplet) 25 | if droplet0 not in droplets 26 | )) 27 | 28 | # For Part 2, we flood-fill starting from a point known to be outside the lava. 29 | xm, *_, xM = sorted(droplet[0] for droplet in droplets) 30 | ym, *_, yM = sorted(droplet[1] for droplet in droplets) 31 | zm, *_, zM = sorted(droplet[2] for droplet in droplets) 32 | 33 | S = set([ 34 | # These points, for certain, are outside the lava. 35 | (xm - 1, ym - 1, zm - 1), 36 | (xM + 1, yM + 1, zM + 1), 37 | ]) 38 | while True: 39 | S0 = set() 40 | for s in S: 41 | for s0 in neighbours(*s): 42 | x, y, z = s0 43 | if ( 44 | s0 not in droplets 45 | and xm - 1 <= x <= xM + 1 46 | and ym - 1 <= y <= yM + 1 47 | and zm - 1 <= z <= zM + 1 48 | ): 49 | S0.add(s0) 50 | 51 | if all(s0 in S for s0 in S0): 52 | break 53 | S |= S0 54 | 55 | print("Part 2:", sum( 56 | 1 for droplet in droplets 57 | for droplet0 in neighbours(*droplet) 58 | if droplet0 in S 59 | )) 60 | -------------------------------------------------------------------------------- /2022/19.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2022/19: Not Enough Minerals """ 3 | 4 | import collections as cl 5 | import re 6 | import sys 7 | 8 | blueprints = [ 9 | tuple(map(int, re.findall(r"\d+", blueprint))) 10 | for blueprint in sys.stdin.read().strip().split("\n") 11 | ] 12 | assert all(len(blueprint) == 7 for blueprint in blueprints) 13 | 14 | def solve(oo, co, Oo, Oc, go, gO, T = 24): 15 | Q = cl.deque() 16 | Q.append((0, (0, 0, 0, 0, 1, 0, 0, 0))) 17 | S = set() 18 | 19 | G = 0 20 | while len(Q) > 0: 21 | t, (o, c, O, g, No, Nc, NO, Ng) = Q.popleft() 22 | G = max(G, g) 23 | 24 | if t == T or (o, c, O, g, No, Nc, NO, Ng) in S: 25 | continue 26 | S.add((o, c, O, g, No, Nc, NO, Ng)) 27 | 28 | # Could we possibly beat the current best score? 29 | if (g + Ng*(T - t) + (T - t) * (T - t + 1) // 2) <= G: 30 | continue 31 | 32 | # I believe that the optimizations marked (*) are too aggressive in the 33 | # general case, but permissive enough to function on the Advent of Code 34 | # inputs. 35 | if ( 36 | o >= oo 37 | and No < max(oo, co, Oo, go) 38 | and o + (T - t) * No < (T - t) * max(oo, co, Oo, go) # (*) 39 | ): 40 | Q.append((t + 1, (o - oo + No, c + Nc, O + NO, g + Ng, No + 1, Nc, NO, Ng))) 41 | if ( 42 | o >= co 43 | and Nc < Oc 44 | and c + (T - t) * Nc < (T - t) * Oc # (*) 45 | ): 46 | Q.append((t + 1, (o - co + No, c + Nc, O + NO, g + Ng, No, Nc + 1, NO, Ng))) 47 | if ( 48 | o >= Oo and c >= Oc 49 | and NO < gO 50 | and O + (T - t) * NO < (T - t) * gO # (*) 51 | ): 52 | Q.append((t + 1, (o - Oo + No, c - Oc + Nc, O + NO, g + Ng, No, Nc, NO + 1, Ng))) 53 | if (o >= go and O >= gO): 54 | Q.append((t + 1, (o - go + No, c + Nc, O - gO + NO, g + Ng, No, Nc, NO, Ng + 1))) 55 | else: 56 | Q.append((t + 1, (o + No, c + Nc, O + NO, g + Ng, No, Nc, NO, Ng))) 57 | 58 | return G 59 | 60 | print("Part 1:", sum( 61 | i * solve(*blueprint, T = 24) 62 | for i, *blueprint in blueprints 63 | )) 64 | print("Part 2:", ( 65 | solve(*blueprints[0][1:], T = 32) 66 | * solve(*blueprints[1][1:], T = 32) 67 | * solve(*blueprints[2][1:], T = 32) 68 | )) 69 | -------------------------------------------------------------------------------- /2022/20.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2022/20: Grove Positioning System """ 3 | 4 | import collections as cl 5 | import sys 6 | 7 | message = list( 8 | enumerate(map(int, sys.stdin.read().strip().split())) 9 | ) 10 | for i, x in message * 1: 11 | j = message.index((i, x)) 12 | message.pop(j) 13 | message.insert((j + x) % len(message), (i, x)) 14 | 15 | i = next(i for i, (_, x) in enumerate(message) if x == 0) 16 | print("Part 1:", sum( 17 | message[(i + 1000*p) % len(message)][1] for p in (1, 2, 3) 18 | )) 19 | 20 | message = [ 21 | (i, x * 811589153) 22 | for i, x in sorted(message) # We need to restore the message! 23 | ] 24 | for i, x in message * 10: 25 | j = message.index((i, x)) 26 | message.pop(j) 27 | message.insert((j + x) % len(message), (i, x)) 28 | 29 | i = next(i for i, (_, x) in enumerate(message) if x == 0) 30 | print("Part 2:", sum( 31 | message[(i + 1000*p) % len(message)][1] for p in (1, 2, 3) 32 | )) 33 | -------------------------------------------------------------------------------- /2022/21.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2022/21: Monkey Math """ 3 | 4 | import operator 5 | import sys 6 | import z3 7 | 8 | monkeys = [ 9 | (monkey.split(": ")[0], monkey.split(": ")[1].split()) 10 | for monkey in sys.stdin.read().strip().split("\n") 11 | ] 12 | 13 | # Let's hand-roll Part 1, but we'll (ab)use z3 for Part 2. 14 | V = { 15 | a: int(b[0]) 16 | for a, b in monkeys 17 | if len(b) == 1 18 | } 19 | while "root" not in V.keys(): 20 | for a, b in monkeys: 21 | if a in V.keys(): 22 | continue 23 | 24 | l, op, r = b 25 | if (l in V.keys()) and (r in V.keys()): 26 | ops = { 27 | "+": operator.add, 28 | "-": operator.sub, 29 | "*": operator.mul, 30 | "/": operator.truediv 31 | } 32 | V[a] = int(ops.get(op)(V.get(l), V.get(r))) 33 | 34 | print("Part 1:", V.get("root")) 35 | 36 | s = z3.Solver() 37 | for a, b in monkeys: 38 | if len(b) == 1 and a != "humn": 39 | s.add(z3.Real(a) == int(b[0])) 40 | elif len(b) == 3: 41 | l, op, r = b 42 | if a != "root": 43 | s.add(z3.Real(a) == ops.get(op)(z3.Real(l), z3.Real(r))) 44 | else: 45 | s.add(z3.Real(l) == z3.Real(r)) 46 | 47 | assert s.check() == z3.sat 48 | model = s.model() 49 | print("Part 2:", model[z3.Real("humn")].as_long()) 50 | -------------------------------------------------------------------------------- /2022/23.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2022/23: Unstable Diffusion """ 3 | 4 | import collections as cl 5 | import itertools as it 6 | import sys 7 | 8 | # Since the y-axis is flipped, north is down. 9 | N, E = complex(0, -1), complex(1, 0) 10 | S, W = -N, -E 11 | P = [ 12 | (N, N + E, N + W), 13 | (S, S + E, S + W), 14 | (W, N + W, S + W), 15 | (E, N + E, S + E), 16 | ] 17 | 18 | elves = set( 19 | complex(x, y) 20 | for y, ln in enumerate(sys.stdin.read().strip().split("\n")) 21 | for x, c in enumerate(ln) 22 | if c == "#" 23 | ) 24 | for i in it.count(1): 25 | mv = {} 26 | for elf in elves: 27 | if all(elf + d not in elves for d in (N, N + E, E, S + E, S, S + W, W, N + W)): 28 | continue 29 | 30 | for p in P: 31 | if all(elf + d not in elves for d in p): 32 | mv[elf] = elf + p[0] 33 | break 34 | 35 | if len(mv) == 0: 36 | print("Part 2:", i) 37 | break 38 | 39 | mv0 = cl.Counter(mv.values()) 40 | for d, v in mv0.items(): 41 | if v == 1: 42 | elves.remove(next(k for k, v in mv.items() if v == d)) 43 | elves.add(d) 44 | 45 | P = P[1:] + P[:1] 46 | 47 | # Part 1: How much ground, after 10 rounds? 48 | if i == 10: 49 | mx, *_, Mx = sorted(int(elf.real) for elf in elves) 50 | my, *_, My = sorted(int(elf.imag) for elf in elves) 51 | print("Part 1:", (Mx - mx + 1) * (My - my + 1) - len(elves)) 52 | -------------------------------------------------------------------------------- /2022/25.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2022/25: Full of Hot Air """ 3 | 4 | import sys 5 | 6 | def f(n): 7 | return 5*f(n[:-1]) + "=-012".index(n[-1]) - 2 if n else 0 8 | 9 | def g(n): 10 | return g((n + 2) // 5) + "012=-"[n % 5] if n else "" 11 | 12 | print("Part 1:", g(sum(map(f, sys.stdin.read().strip().split("\n"))))) 13 | print("Part 2:", "Start The Blender") 14 | -------------------------------------------------------------------------------- /2023/01.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2023/01: Trebuchet?! """ 3 | 4 | import sys 5 | 6 | document = sys.stdin.read().strip() 7 | 8 | def calibrate(d): 9 | return sum( 10 | int((k := [c for c in d0 if c.isdigit()])[0] + k[-1]) 11 | for d0 in d.split("\n") 12 | ) 13 | 14 | print("Part 1:", calibrate(document)) 15 | print("Part 2:", 16 | calibrate( 17 | document.replace("one", "o1e") 18 | .replace("two", "t2o") 19 | .replace("three", "t3e") 20 | .replace("four", "f4r") 21 | .replace("five", "f5e") 22 | .replace("six", "s6x") 23 | .replace("seven", "s7n") 24 | .replace("eight", "e8t") 25 | .replace("nine", "n9e") 26 | ) 27 | ) 28 | -------------------------------------------------------------------------------- /2023/02.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2023/02: Cube Conundrum """ 3 | 4 | import math 5 | import sys 6 | 7 | games = { 8 | int(_id): [ 9 | { 10 | colour: int(n) 11 | for n, colour in map(lambda k: k.split(" "), draws) 12 | } 13 | for draws in map(lambda k: k.split(", "), game.strip().split("; ")) 14 | ] 15 | for _id, game in map(lambda k: k[5:].split(": "), sys.stdin.readlines()) 16 | } 17 | 18 | print("Part 1:", 19 | sum( 20 | _id for _id, game in games.items() 21 | if all( 22 | draw.get("red", 0) <= 12 23 | and draw.get("green", 0) <= 13 24 | and draw.get("blue", 0) <= 14 25 | for draw in game 26 | ) 27 | ) 28 | ) 29 | print("Part 2:", 30 | sum( 31 | max(draw.get("red", 0) for draw in game) 32 | * max(draw.get("green", 0) for draw in game) 33 | * max(draw.get("blue", 0) for draw in game) 34 | for _, game in games.items() 35 | ) 36 | ) 37 | -------------------------------------------------------------------------------- /2023/03.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2023/03: Gear Ratios """ 3 | 4 | import itertools as it 5 | import sys 6 | 7 | numbers, symbols = {}, {} 8 | for y, ln in enumerate(sys.stdin.readlines()): 9 | x = 0 10 | while x < len(ln.strip()): 11 | c = ln[x] 12 | if c.isdigit(): 13 | x0, n = x, int(c) 14 | while (c := ln[x + 1]).isdigit(): 15 | n = 10 * n + int(c) 16 | x = x + 1 17 | numbers[x0, y] = n 18 | elif c != ".": 19 | symbols[x, y] = c 20 | x = x + 1 21 | 22 | # Part numbers are adjacent to a symbol. We can be lazy with the bounds, since 23 | # no grid square is both a symbol and a digit. 24 | print("Part 1:", 25 | sum( 26 | number 27 | for (x, y), number in numbers.items() 28 | if any( 29 | (x + dx, y + dy) in symbols.keys() 30 | for dx in range(-1, len(str(number)) + 1) 31 | for dy in (-1, 0, 1) 32 | ) 33 | ) 34 | ) 35 | 36 | # Gears are a little more difficult to detect, but it's do-able in quadratic 37 | # time. 38 | t = 0 39 | for (x, y), symbol in symbols.items(): 40 | if symbol == "*": 41 | adjacent_numbers = [ 42 | number 43 | for (x0, y0), number in numbers.items() 44 | if ( 45 | x - len(str(number)) <= x0 <= x + 1 46 | and y - 1 <= y0 <= y + 1 47 | ) 48 | ] 49 | if len(adjacent_numbers) == 2: 50 | t = t + adjacent_numbers[0] * adjacent_numbers[1] 51 | print("Part 2:", t) 52 | -------------------------------------------------------------------------------- /2023/04.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2023/04: Scratchcards """ 3 | 4 | import sys 5 | 6 | # We'll assume that the cards are given in order, nothing skipped, etc. 7 | cards = [ 8 | tuple(map(lambda k: set(k.split()), card.split(": ")[1].split(" | "))) 9 | for card in sys.stdin.readlines() 10 | ] 11 | 12 | p1 = 0 13 | m = [1] * len(cards) 14 | for i, (ca, cb) in enumerate(cards): 15 | n = len(ca & cb) 16 | 17 | # The score is 2^(n - 1) if n > 0. This is a bit ugly, but we can compute 18 | # this without a branch using some bit-shifting. 19 | p1 = p1 + ((1 << n) >> 1) 20 | 21 | for j in range(i + 1, min(len(cards), i + n + 1)): 22 | m[j] += m[i] 23 | 24 | print("Part 1:", p1) 25 | print("Part 2:", sum(m)) 26 | -------------------------------------------------------------------------------- /2024/01.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2024/01: Historian Hysteria """ 3 | 4 | import sys 5 | 6 | left, right = zip(*( 7 | map(int, entry.split()) 8 | for entry in sys.stdin.read().strip().split("\n") 9 | )) 10 | 11 | print("Part 1:", 12 | sum(abs(l - r) for l, r in zip(sorted(left), sorted(right))) 13 | ) 14 | print("Part 2:", sum(l * right.count(l) for l in left)) 15 | -------------------------------------------------------------------------------- /2024/02.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2024/02: Red-Nosed Reports """ 3 | 4 | import sys 5 | 6 | reports = [ 7 | list(map(int, report.split())) 8 | for report in sys.stdin.read().strip().split("\n") 9 | ] 10 | 11 | def is_safe(report): 12 | is_incr = all(x1 < x2 for x1, x2 in zip(report, report[1:])) 13 | is_decr = all(x1 > x2 for x1, x2 in zip(report, report[1:])) 14 | return ( 15 | (is_incr or is_decr) 16 | and all(1 <= abs(x1 - x2) <= 3 for x1, x2 in zip(report, report[1:])) 17 | ) 18 | 19 | print("Part 1:", sum(is_safe(report) for report in reports)) 20 | print("Part 2:", 21 | sum( 22 | is_safe(report) or any(is_safe(report[:i] + report[i+1:]) for i in range(0, len(report))) 23 | for report in reports 24 | ) 25 | ) 26 | -------------------------------------------------------------------------------- /2024/03.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2024/03: Mull It Over """ 3 | 4 | import re 5 | import sys 6 | 7 | program = sys.stdin.read() 8 | 9 | p1, p2 = 0, 0 10 | s = True 11 | for o, arg1, arg2 in re.findall(r"(mul\((\d+),(\d+)\)|do\(\)|don't\(\))", program): 12 | if o == "do()": 13 | s = True 14 | elif o == "don't()": 15 | s = False 16 | else: 17 | p1 = p1 + int(arg1)*int(arg2) 18 | if s: 19 | p2 = p2 + int(arg1)*int(arg2) 20 | 21 | print("Part 1:", p1) 22 | print("Part 2:", p2) 23 | -------------------------------------------------------------------------------- /2024/04.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2024/04: Ceres Search """ 3 | 4 | import itertools as it 5 | import sys 6 | 7 | wordsearch = { 8 | (r, c): ch 9 | for r, line in enumerate(sys.stdin.read().strip().split("\n")) 10 | for c, ch in enumerate(line) 11 | } 12 | 13 | print("Part 1:", 14 | sum( 15 | (dr != 0 or dc != 0) and all(wordsearch.get((r + dr*i, c + dc*i)) == "XMAS"[i] for i in range(4)) 16 | for (r, c) in wordsearch.keys() 17 | for (dr, dc) in it.product((-1, 0, 1), repeat=2) 18 | ) 19 | ) 20 | print("Part 2:", 21 | sum( 22 | ch == "A" 23 | and { wordsearch.get((r - 1, c - 1)), wordsearch.get((r + 1, c + 1)) } == { "M", "S" } 24 | and { wordsearch.get((r + 1, c - 1)), wordsearch.get((r - 1, c + 1)) } == { "M", "S" } 25 | for (r, c), ch in wordsearch.items() 26 | ) 27 | ) 28 | -------------------------------------------------------------------------------- /2024/05.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2024/05: Print Queue """ 3 | 4 | import functools as ft 5 | import sys 6 | 7 | ordering_rules, updates = sys.stdin.read().strip().split("\n\n") 8 | ordering_rules = [ 9 | tuple(map(int, ordering_rule.split("|"))) 10 | for ordering_rule in ordering_rules.split("\n") 11 | ] 12 | updates = [ 13 | list(map(int, update.split(","))) 14 | for update in updates.split("\n") 15 | ] 16 | 17 | def cmp(x1, x2): 18 | return -1 if (x1, x2) in ordering_rules else 1 19 | 20 | print("Part 1:", 21 | sum( 22 | update[len(update) // 2] 23 | for update in updates 24 | if update == sorted(update, key=ft.cmp_to_key(cmp)) 25 | ) 26 | ) 27 | print("Part 2:", 28 | sum( 29 | sorted(update, key=ft.cmp_to_key(cmp))[len(update) // 2] 30 | for update in updates 31 | if update != sorted(update, key=ft.cmp_to_key(cmp)) 32 | ) 33 | ) 34 | -------------------------------------------------------------------------------- /2024/06.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2024/06: Guard Gallivant """ 3 | 4 | import sys 5 | 6 | lab = { 7 | (r, c): ch 8 | for r, line in enumerate(sys.stdin.read().strip().split("\n")) 9 | for c, ch in enumerate(line) 10 | } 11 | lab_obstructions = set((r, c) for (r, c), ch in lab.items() if ch == "#") 12 | 13 | def walk(r, c, lab_obstructions): 14 | dr, dc = -1, 0 15 | visited = set() 16 | seen = set() 17 | while True: 18 | if not lab.get((r, c)): 19 | return visited 20 | if (r, c, dr, dc) in seen: 21 | return True 22 | visited.add((r, c)) 23 | seen.add((r, c, dr, dc)) 24 | 25 | if (r + dr, c + dc) in lab_obstructions: 26 | dr, dc = dc, -dr 27 | else: 28 | r, c = r + dr, c + dc 29 | 30 | r0, c0 = next((r, c) for (r, c), ch in lab.items() if ch == "^") 31 | visited = walk(r0, c0, lab_obstructions) 32 | print("Part 1:", len(visited)) 33 | print("Part 2:", 34 | sum( 35 | walk(r0, c0, lab_obstructions | { (r, c) }) == True 36 | for r, c in visited 37 | if r != r0 or c != c0 38 | ) 39 | ) 40 | 41 | -------------------------------------------------------------------------------- /2024/07.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2024/07: Bridge Repair """ 3 | 4 | import re 5 | import sys 6 | 7 | equations = [ 8 | list(map(int, re.findall("\d+", equation))) 9 | for equation in sys.stdin.read().strip().split("\n") 10 | ] 11 | 12 | def test(X, xs, p2): 13 | if len(xs) == 1: 14 | return X == xs[0] 15 | else: 16 | x0, x1, xs = xs[0], xs[1], xs[2:] 17 | return ( 18 | test(X, [x0 + x1] + xs, p2) 19 | or test(X, [x0 * x1] + xs, p2) 20 | or (p2 and test(X, [int(str(x0) + str(x1))] + xs, p2)) 21 | ) 22 | 23 | print("Part 1:", 24 | sum( 25 | equation[0] for equation in equations 26 | if test(equation[0], equation[1:], False) 27 | ) 28 | ) 29 | print("Part 2:", 30 | sum( 31 | equation[0] for equation in equations 32 | if test(equation[0], equation[1:], True) 33 | ) 34 | ) 35 | -------------------------------------------------------------------------------- /2024/08.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2024/08: Resonant Collinearity """ 3 | 4 | import collections as cl 5 | import itertools as it 6 | import math 7 | import sys 8 | 9 | city = { 10 | (r, c): ch 11 | for r, line in enumerate(sys.stdin.read().strip().split("\n")) 12 | for c, ch in enumerate(line) 13 | } 14 | 15 | antennas = cl.defaultdict(set) 16 | for (r, c), ch in city.items(): 17 | if ch != ".": 18 | antennas[ch].add((r, c)) 19 | 20 | p1, p2 = set(), set() 21 | for ch, antennas in antennas.items(): 22 | for (a1_r, a1_c), (a2_r, a2_c) in it.combinations(antennas, 2): 23 | dr = a2_r - a1_r 24 | dc = a2_c - a1_c 25 | assert math.gcd(abs(dr), abs(dc)) == 1 26 | 27 | for sgn in { -1, 1 }: 28 | r0, c0 = (a1_r, a1_c) if sgn == -1 else (a2_r, a2_c) 29 | for i in it.count(0): 30 | r, c = r0 + sgn*i*dr, c0 + sgn*i*dc 31 | if (r, c) not in city.keys(): 32 | break 33 | if i == 1: 34 | p1.add((r, c)) 35 | p2.add((r, c)) 36 | 37 | print("Part 1:", len(p1)) 38 | print("Part 2:", len(p2)) 39 | -------------------------------------------------------------------------------- /2024/09.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2024/09: Disk Fragmenter """ 3 | 4 | import itertools as it 5 | import sys 6 | 7 | disk = [ 8 | (int(c), None if i % 2 == 1 else i // 2) 9 | for i, c in enumerate(sys.stdin.readline().strip()) 10 | ] 11 | 12 | # For Part 1, we can just traverse the filesystem with two pointers. 13 | filesystem = list(it.chain.from_iterable( 14 | [file_id]*length for (length, file_id) in disk 15 | )) 16 | 17 | p1 = 0 18 | i, j = 0, len(filesystem) 19 | while i < j: 20 | if filesystem[i] is not None: 21 | p1 = p1 + filesystem[i]*i 22 | else: 23 | # Decrement `j` until it is pointing at a file. 24 | j = j - 1 25 | while filesystem[j] is None: 26 | j = j - 1 27 | p1 = p1 + filesystem[j]*i 28 | i = i + 1 29 | 30 | print("Part 1:", p1) 31 | 32 | # For Part 2, we'll operate on the disk "map" and then compute the checksum. 33 | j = len(disk) - 1 34 | while j >= 0: 35 | if disk[j][1] is not None: 36 | size, file_id = disk[j] 37 | try: 38 | i, gap = next(filter( 39 | lambda t: t[0] < j and t[1][0] >= size and t[1][1] is None, 40 | enumerate(disk) 41 | )) 42 | disk[j] = (size, None) 43 | if gap[0] == size: 44 | disk[i] = (size, file_id) 45 | else: 46 | disk = disk[:i] + [ (size, file_id), (gap[0] - size, None) ] + disk[i + 1:] 47 | continue 48 | except StopIteration: # ...so, no sufficiently large gap. 49 | pass 50 | j = j - 1 51 | 52 | print("Part 2:", 53 | sum( 54 | i*file_id 55 | for i, file_id in enumerate(it.chain.from_iterable([file_id] * length for (length, file_id) in disk)) 56 | if file_id 57 | ) 58 | ) 59 | -------------------------------------------------------------------------------- /2024/10.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2024/10: Hoof It """ 3 | 4 | import networkx as nx 5 | import sys 6 | 7 | area = { 8 | (r, c): int(ht) 9 | for r, line in enumerate(sys.stdin.read().strip().split("\n")) 10 | for c, ht in enumerate(line) 11 | } 12 | 13 | G = nx.DiGraph( 14 | ((r, c), (r + dr, c + dc)) 15 | for (r, c), d in area.items() 16 | for (dr, dc) in [(-1, 0), (1, 0), (0, -1), (0, 1)] 17 | if area.get((r + dr, c + dc)) == d + 1 18 | ) 19 | 20 | trails = [ 21 | list(nx.all_simple_paths(G, r0, r9)) 22 | for r0 in filter(lambda n: area.get(n) == 0, G.nodes) 23 | for r9 in filter(lambda n: area.get(n) == 9, G.nodes) 24 | ] 25 | 26 | print("Part 1:", sum(map(any, trails))) 27 | print("Part 2:", sum(map(len, trails))) 28 | -------------------------------------------------------------------------------- /2024/11.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2024/11: Plutonian Pebbles """ 3 | 4 | import functools as ft 5 | import sys 6 | 7 | @ft.cache 8 | def blink(N, t): 9 | if t == 0: 10 | return 1 11 | else: 12 | s = str(N) 13 | if N == 0: 14 | return blink(1, t - 1) 15 | elif len(s) % 2 == 0: 16 | a, b = int(s[:len(s) // 2]), int(s[len(s) // 2:]) 17 | return blink(a, t - 1) + blink(b, t - 1) 18 | else: 19 | return blink(2024*N, t - 1) 20 | 21 | stones = [int(s) for s in sys.stdin.read().strip().split(" ")] 22 | print("Part 1:", sum(blink(s, 25) for s in stones)) 23 | print("Part 2:", sum(blink(s, 75) for s in stones)) 24 | -------------------------------------------------------------------------------- /2024/12.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2024/12: Garden Groups """ 3 | 4 | import sys 5 | 6 | farm = { 7 | (r, c): ch 8 | for r, line in enumerate(sys.stdin.read().strip().split("\n")) 9 | for c, ch in enumerate(line) 10 | } 11 | 12 | # Determine the connected components. 13 | regions = { p: set([ p ]) for p in farm.keys() } 14 | for (r, c), ch in farm.items(): 15 | for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]: 16 | if farm.get((r + dr, c + dc)) == ch: 17 | regions[r, c] |= regions[r + dr, c + dc] 18 | for p in regions[r, c]: 19 | regions[p] = regions[r, c] 20 | regions = set(map(tuple, regions.values())) 21 | 22 | def edges(region): 23 | return [ 24 | (r, c, dr, dc) 25 | for r, c in region 26 | for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)] 27 | if (r + dr, c + dc) not in region 28 | ] 29 | 30 | def sides(region): 31 | E = edges(region) 32 | return [ 33 | (r, c, dr, dc) 34 | for r, c, dr, dc in E 35 | if (r, c, -dc, dr) in E or (r + dr - dc, c + dc + dr, dc, -dr) in E 36 | ] 37 | 38 | print("Part 1:", sum(len(region) * len(edges(region)) for region in regions)) 39 | print("Part 2:", sum(len(region) * len(sides(region)) for region in regions)) 40 | -------------------------------------------------------------------------------- /2024/13.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2024/13: Claw Contraption """ 3 | 4 | import re 5 | import sys 6 | 7 | machines = [ 8 | tuple(map(int, re.findall(r"(\d+)", s))) 9 | for s in sys.stdin.read().strip().split("\n\n") 10 | ] 11 | 12 | def solve(dx1, dy1, dx2, dy2, gx, gy): 13 | # We'll use Cramer's rule... 14 | d = dx1*dy2 - dx2*dy1 15 | assert d != 0 16 | 17 | A = dy2 * gx - dx2 * gy 18 | B = -dy1 * gx + dx1 * gy 19 | if A % d == 0 and B % d == 0: 20 | return 3*(A // d) + B // d 21 | else: 22 | return 0 23 | 24 | print("Part 1:", sum(solve(*m) for m in machines)) 25 | print("Part 2:", sum(solve(*m[:4], m[4] + 10000000000000, m[5] + 10000000000000) for m in machines)) 26 | -------------------------------------------------------------------------------- /2024/14.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2024/14: Restroom Redoubt """ 3 | 4 | import re 5 | import statistics as stat 6 | import sys 7 | 8 | robots = [ 9 | tuple(map(int, re.findall(r"(-?\d+)", s))) 10 | for s in sys.stdin.read().strip().split("\n") 11 | ] 12 | 13 | def at(t): 14 | return [ 15 | ((x + t*dx) % 101, (y + t*dy) % 103) 16 | for (x, y, dx, dy) in robots 17 | ] 18 | 19 | print("Part 1:", 20 | sum(1 for (x, y) in at(100) if x > 50 and y > 51) 21 | * sum(1 for (x, y) in at(100) if x > 50 and y < 51) 22 | * sum(1 for (x, y) in at(100) if x < 50 and y > 51) 23 | * sum(1 for (x, y) in at(100) if x < 50 and y < 51) 24 | ) 25 | 26 | # We'll look at the time which minimises the variance of the robot positions 27 | # in each direction, and then use the Chinese Remainder Theorem to determine 28 | # when the overall variance is minimised (ie. when the robots are clustered). 29 | m = lambda i, mod: min( 30 | [ (t, stat.variance(p[i] for p in at(t))) for t in range(mod) ], 31 | key=lambda p: p[1], 32 | )[0] 33 | x_min = m(0, 101) 34 | y_min = m(1, 103) 35 | 36 | print("Part 2:", ((x_min * pow(103, -1, 101) * 103) + (y_min * pow(101, -1, 103) * 101)) % (101 * 103)) 37 | -------------------------------------------------------------------------------- /2024/15.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2024/15: Warehouse Woes """ 3 | 4 | import sys 5 | 6 | warehouse, moves = sys.stdin.read().strip().split("\n\n") 7 | moves = [ 8 | { "^": (-1, 0), "v": (1, 0), "<": (0, -1), ">": (0, 1) }[move] 9 | for move in moves.replace("\n", "") 10 | ] 11 | 12 | def push(W, r, c, dr, dc): 13 | if W[r + dr, c + dc] == ".": 14 | W[r, c], W[r + dr, c + dc] = W[r + dr, c + dc], W[r, c] 15 | elif W[r + dr, c + dc] == "#": 16 | raise "Cannot push wall." 17 | elif W[r + dr, c + dc] == "O": 18 | push(W, r + dr, c + dc, dr, dc) 19 | W[r, c], W[r + dr, c + dc] = W[r + dr, c + dc], W[r, c] 20 | elif W[r + dr, c + dc] == "[": 21 | push(W, r + dr, c + dc + 1, dr, dc) 22 | push(W, r + dr, c + dc , dr, dc) 23 | W[r, c], W[r + dr, c + dc] = W[r + dr, c + dc], W[r, c] 24 | elif W[r + dr, c + dc] == "]": 25 | push(W, r + dr, c + dc - 1, dr, dc) 26 | push(W, r + dr, c + dc , dr, dc) 27 | W[r, c], W[r + dr, c + dc] = W[r + dr, c + dc], W[r, c] 28 | 29 | def execute(warehouse, moves): 30 | warehouse = { 31 | (r, c): ch 32 | for r, line in enumerate(warehouse.split("\n")) 33 | for c, ch in enumerate(line) 34 | } 35 | (r0, c0), = ((r, c) for (r, c), ch in warehouse.items() if ch == "@") 36 | 37 | # We'll always operate on a copy of `warehouse`, rolling back if we can't 38 | # push. 39 | r, c = r0, c0 40 | for (dr, dc) in moves: 41 | warehouse0 = warehouse.copy() 42 | try: 43 | push(warehouse, r, c, dr, dc) 44 | r, c = r + dr, c + dc 45 | except: 46 | warehouse = warehouse0 47 | 48 | return sum(100*r + c for (r, c), ch in warehouse.items() if ch in "O[") 49 | 50 | print("Part 1:", execute(warehouse, moves)) 51 | print("Part 2:", 52 | execute( 53 | warehouse.replace("#", "##").replace(".", "..").replace("O", "[]").replace("@", "@."), 54 | moves, 55 | ) 56 | ) 57 | -------------------------------------------------------------------------------- /2024/16.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2024/16: Reindeer Maze """ 3 | 4 | import networkx as nx 5 | import sys 6 | 7 | maze = { 8 | (r, c): ch 9 | for r, line in enumerate(sys.stdin.read().strip().split("\n")) 10 | for c, ch in enumerate(line) 11 | } 12 | 13 | # Ah, we'll cheat... 14 | G = nx.DiGraph() 15 | for (r, c), ch in maze.items(): 16 | if maze.get((r, c)) == "#": 17 | continue 18 | 19 | for (dr, dc) in [(-1, 0), (1, 0), (0, -1), (0, 1)]: 20 | G.add_edge((r, c, dr, dc), (r, c, -dc, dr), weight=1000) 21 | if maze.get((r + dr, c + dc)) != "#": 22 | G.add_edge((r, c, dr, dc), (r + dr, c + dc, dr, dc), weight=1) 23 | 24 | S = next((r, c) for (r, c), ch in maze.items() if ch == "S") 25 | E = next((r, c) for (r, c), ch in maze.items() if ch == "E") 26 | 27 | for (dr, dc) in [(-1, 0), (1, 0), (0, -1), (0, 1)]: 28 | G.add_edge((*E, dr, dc), "end", weight=0) 29 | 30 | print("Part 1:", nx.shortest_path_length(G, (*S, 0, 1), "end", "weight")) 31 | print("Part 2:", len(set( 32 | (r, c) 33 | for path in nx.all_shortest_paths(G, (*S, 0, 1), "end", "weight") 34 | for (r, c, _, _) in path[:-1] 35 | ))) 36 | -------------------------------------------------------------------------------- /2024/17.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2024/17: Chronospatial Computer """ 3 | 4 | import re 5 | import sys 6 | from z3 import * 7 | 8 | A, B, C, *program = map(int, re.findall(r"\d+", sys.stdin.read())) 9 | 10 | # Handling of the combo operand isn't entirely correct, here. But it'll do. 11 | ip, out = 0, [] 12 | while ip < len(program): 13 | arg = program[ip + 1] 14 | match program[ip]: 15 | case 0: A >>= { 4: A, 5: B, 6: C }.get(arg, arg) 16 | case 1: B ^= arg 17 | case 2: B = { 4: A, 5: B, 6: C }.get(arg, arg) % 8 18 | case 3: ip = arg - 2 if A != 0 else ip 19 | case 4: B ^= C 20 | case 5: out.append({ 4: A, 5: B, 6: C }.get(arg, arg) % 8) 21 | case 6: B = A >> { 4: A, 5: B, 6: C }.get(arg, arg) 22 | case 7: C = A >> { 4: A, 5: B, 6: C }.get(arg, arg) 23 | ip = ip + 2 24 | print("Part 1:", ",".join(map(str, out))) 25 | 26 | # For Part 2, we'll use z3. This is currently hard-coded to my input; it would 27 | # be nice to generalise it at some point. 28 | o = Optimize() 29 | i = BitVec("i", 64) 30 | o.minimize(i) 31 | 32 | A, B, C = i, 0, 0 33 | for p in program: 34 | B = A % 8 35 | B ^= 5 36 | C = A >> B 37 | B ^= 6 38 | A >>= 3 39 | B ^= C 40 | o.add((B % 8) == p) 41 | o.add(A == 0) 42 | 43 | assert o.check() == sat 44 | print("Part 2:", o.model()[i]) 45 | -------------------------------------------------------------------------------- /2024/18.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2024/18: RAM Run """ 3 | 4 | import itertools as it 5 | import networkx as nx 6 | import sys 7 | 8 | coordinates = [ 9 | tuple(map(int, ln.split(","))) 10 | for ln in sys.stdin.read().strip().split("\n") 11 | ] 12 | 13 | G = nx.Graph() 14 | for x, y in it.product(range(71), repeat=2): 15 | for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: 16 | if 0 <= x + dx < 71 and 0 <= y + dy < 71: 17 | G.add_edge((x, y), (x + dx, y + dy)) 18 | 19 | # We'll assume that the problem is well-defined; that is, none of the first 20 | # 1024 coordinates break connectivity. 21 | p2 = False 22 | for i, coordinate in enumerate(coordinates): 23 | if i == 1024: 24 | print("Part 1:", nx.shortest_path_length(G, (0, 0), (70, 70))) 25 | p2 = True 26 | G.remove_node(coordinate) 27 | if p2 and not nx.has_path(G, (0, 0), (70, 70)): 28 | print("Part 2:", ",".join(map(str, coordinate))) 29 | break 30 | -------------------------------------------------------------------------------- /2024/19.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2024/19: Linen Layout """ 3 | 4 | import functools as ft 5 | import sys 6 | 7 | patterns, designs = sys.stdin.read().strip().split("\n\n") 8 | patterns = patterns.split(", ") 9 | designs = designs.split("\n") 10 | 11 | @ft.cache 12 | def solve(design): 13 | if len(design) == 0: 14 | return 1 15 | else: 16 | return sum(solve(design[len(pattern):]) for pattern in patterns if design.startswith(pattern)) 17 | 18 | solutions = [ 19 | solve(design) for design in designs 20 | ] 21 | print("Part 1:", sum(s > 0 for s in solutions)) 22 | print("Part 2:", sum(solutions)) 23 | -------------------------------------------------------------------------------- /2024/20.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2024/20: Race Condition """ 3 | 4 | import sys 5 | 6 | racetrack = { 7 | (r, c): ch 8 | for r, line in enumerate(sys.stdin.read().strip().split("\n")) 9 | for c, ch in enumerate(line) 10 | } 11 | (Sr, Sc), = ((r, c) for (r, c), ch in racetrack.items() if ch == "S") 12 | 13 | # Compute the distance from `S` to every road tile. 14 | distances, q = { (Sr, Sc): 0 }, [ (Sr, Sc) ] 15 | while len(q): 16 | (r, c) = q.pop(0) 17 | for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]: 18 | if racetrack.get((r + dr, c + dc)) != "#" and distances.get((r + dr, c + dc)) is None: 19 | distances[r + dr, c + dc] = distances[r, c] + 1 20 | q.append((r + dr, c + dc)) 21 | 22 | # Cheats are uniquely determined by their endpoints, so we can directly compute 23 | # the time saved by looking at the distances to the endpoints of the cheats 24 | # only. 25 | solve = lambda D: sum( 26 | t1 - t0 - d >= 100 27 | for (r0, c0), t0 in distances.items() 28 | for (r1, c1), t1 in distances.items() 29 | if t1 - t0 >= 100 and (d := abs(r1 - r0) + abs(c1 - c0)) <= D 30 | ) 31 | print("Part 1:", solve(2)) 32 | print("Part 2:", solve(20)) 33 | -------------------------------------------------------------------------------- /2024/21.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2024/21: Keypad Conundrum """ 3 | 4 | import itertools as it 5 | import functools as ft 6 | import sys 7 | 8 | NUMPAD = { 9 | '7': (0, 0), '8': (0, 1), '9': (0, 2), 10 | '4': (1, 0), '5': (1, 1), '6': (1, 2), 11 | '1': (2, 0), '2': (2, 1), '3': (2, 2), 12 | '0': (3, 1), 'A': (3, 2), 13 | } 14 | DIRPAD = { 15 | '^': (0, 1), 'A': (0, 2), 16 | '<': (1, 0), 'v': (1, 1), '>': (1, 2), 17 | } 18 | 19 | # "What's the number of button presses required on pad 0 to move from `b1` to 20 | # `b2` on pad `d` and press `b2` under the assumption that `b1` was just 21 | # pressed?" 22 | @ft.cache 23 | def move(d, b1, b2, num=False): 24 | if d == 0: 25 | return 1 26 | 27 | # The optimal path will always consist of only vertical motion followed by 28 | # only horizontal motion, or vice-versa. Hence, there are at most two paths 29 | # to consider between any two buttons on the pad. 30 | PAD = (NUMPAD if num else DIRPAD) 31 | r1, c1 = PAD[b1] 32 | r2, c2 = PAD[b2] 33 | dr, dc = r2 - r1, c2 - c1 34 | def paths(): 35 | if (r2, c1) in PAD.values(): yield ('v' if dr > 0 else '^')*abs(dr) + ('>' if dc > 0 else '<')*abs(dc) 36 | if (r1, c2) in PAD.values(): yield ('>' if dc > 0 else '<')*abs(dc) + ('v' if dr > 0 else '^')*abs(dr) 37 | 38 | return min( 39 | sum(move(d - 1, b1, b2) for b1, b2 in it.pairwise(f"A{path}A")) 40 | for path in paths() 41 | ) 42 | 43 | codes = sys.stdin.read().strip().split("\n") 44 | 45 | solve = lambda d: sum( 46 | sum(move(d + 1, b1, b2, num=True) for b1, b2 in it.pairwise(f"A{code}")) * int(code.removesuffix("A")) 47 | for code in codes 48 | ) 49 | print("Part 1:", solve(2)) 50 | print("Part 2:", solve(25)) 51 | -------------------------------------------------------------------------------- /2024/22.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2024/22: Monkey Market """ 3 | 4 | import collections as cl 5 | import itertools as it 6 | import sys 7 | 8 | def prng(S): 9 | while True: 10 | yield S 11 | S = (S ^ (S << 6)) & 0xffffff 12 | S = (S ^ (S >> 5)) & 0xffffff 13 | S = (S ^ (S << 11)) & 0xffffff 14 | 15 | assert next(it.islice(prng(1), 2000, 2001)) == 8685429 16 | 17 | sequences = [ 18 | list(it.islice(prng(int(s)), 0, 2001)) 19 | for s in sys.stdin.read().strip().split("\n") 20 | ] 21 | print("Part 1:", sum(sequence[-1] for sequence in sequences)) 22 | 23 | patterns = cl.defaultdict(int) 24 | for sequence in sequences: 25 | deltas = [ 26 | (s2 % 10) - (s1 % 10) for (s1, s2) in it.pairwise(sequence) 27 | ] 28 | 29 | # For each four-tuple, if we're yet to see it, record it. 30 | S = set() 31 | for i in range(len(sequence) - 4): 32 | pattern = tuple(deltas[i:i + 4]) 33 | if pattern not in S: 34 | patterns[pattern] = patterns[pattern] + sequence[i + 4] % 10 35 | S.add(pattern) 36 | 37 | print("Part 2:", max(patterns.values())) 38 | -------------------------------------------------------------------------------- /2024/23.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2024/23: LAN Party """ 3 | 4 | import itertools as it 5 | import networkx as nx 6 | import sys 7 | 8 | G = nx.Graph( 9 | ln.split("-") for ln in sys.stdin.read().strip().split("\n") 10 | ) 11 | 12 | Cs = list(nx.enumerate_all_cliques(G)) 13 | 14 | print("Part 1:", sum(len(C) == 3 and any(c.startswith("t") for c in C) for C in Cs)) 15 | 16 | assert len(Cs[-1]) > len(Cs[-2]) # unique maximal clique? 17 | print("Part 2:", ",".join(sorted(Cs[-1]))) 18 | -------------------------------------------------------------------------------- /2024/24.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2024/24: Crossed Wires """ 3 | 4 | import itertools as it 5 | import functools as ft 6 | import sys 7 | from z3 import * 8 | 9 | inputs, gates = sys.stdin.read().strip().split("\n\n") 10 | inputs = { 11 | i: True if c == "1" else False 12 | for i, c in map(lambda i: i.split(": "), inputs.split("\n")) 13 | } 14 | gates = { 15 | g: a.split() 16 | for a, g in map(lambda g: g.split(" -> "), gates.split("\n")) 17 | } 18 | 19 | def evaluator(inputs, swaps = {}): 20 | @ft.cache 21 | def _eval(register): 22 | if register in inputs.keys(): 23 | return inputs[register] 24 | else: 25 | if register in swaps.keys(): 26 | # The `swaps` dictionary contains both directions. 27 | register = swaps[register] 28 | 29 | arg1, op, arg2 = gates[register] 30 | match op: 31 | case "AND": return _eval(arg1) & _eval(arg2) 32 | case "OR" : return _eval(arg1) | _eval(arg2) 33 | case "XOR": return _eval(arg1) ^ _eval(arg2) 34 | 35 | return _eval 36 | 37 | e = evaluator(inputs) 38 | print("Part 1:", 39 | ft.reduce( 40 | lambda acc, g: acc + (1 << int(g.removeprefix("z"))) * e(g), 41 | filter(lambda g: g.startswith("z"), gates.keys()), 42 | 0 43 | ) 44 | ) 45 | 46 | # Identify the (least-significant) output bit whose definition is incorrect. We 47 | # can find this by asking z3, for each bit i, if there exists x, y, and z such 48 | # that x + y == z but z[i] != e(z[i]). 49 | def find_smallest_error(swaps = {}): 50 | x, y, z = BitVecs(["x", "y", "z"], 46) 51 | inputs_x = { 52 | f"x{i:02d}": Extract(i, i, x) for i in range(45) 53 | } 54 | inputs_y = { 55 | f"y{i:02d}": Extract(i, i, y) for i in range(45) 56 | } 57 | e = evaluator(inputs_x | inputs_y, swaps=swaps) 58 | 59 | for i in range(45): 60 | s = Solver() 61 | s.add(x + y == z) 62 | s.add(e(f"z{i:02d}") != Extract(i, i, z)) 63 | if s.check() == sat: 64 | return i 65 | else: 66 | # There exists no counterexample, ie. the derived expressions are 67 | # correct. 68 | print("Part 2:", ",".join(sorted(swaps.keys()))) 69 | exit() 70 | 71 | def descendants(register, swaps = {}): 72 | if register not in inputs.keys(): 73 | yield register 74 | if register in swaps.keys(): 75 | register = swaps[register] 76 | yield register 77 | 78 | arg1, _, arg2 = gates[register] 79 | yield from descendants(arg1) 80 | yield from descendants(arg2) 81 | 82 | # Identify a swap which increases the position of the least-significant 83 | # incorrect output bit. We can reduce the search space by noticing that the 84 | # registers that z[i0 - 1] depends on are necessarily correct, and that at 85 | # least one of the registers that z[i0] depends on is necessarily incorrect. 86 | def find_swap(i0, swaps = {}): 87 | is_correct = set(descendants(f"z{i0 - 1:02d}", swaps=swaps)) 88 | 89 | # The Advent of Code inputs always perform local swaps, so to optimise for 90 | # this, we'll begin by testing gates closer to z[i0] before working upward. 91 | tried = set() 92 | for i1 in range(i0 + 1, 46): 93 | for c1, c2 in it.product( 94 | set(descendants(f"z{i0:02d}", swaps=swaps)) - is_correct, 95 | set(descendants(f"z{i1:02d}", swaps=swaps)) - is_correct, 96 | ): 97 | if (c1, c2) in tried or c1 in descendants(c2, swaps=swaps): 98 | continue 99 | tried.add((c1, c2)) 100 | 101 | s = swaps | { c1: c2, c2: c1 } 102 | i = find_smallest_error(s) 103 | if i > i0: 104 | find_swap(i, s) 105 | 106 | find_swap(find_smallest_error()) 107 | -------------------------------------------------------------------------------- /2024/25.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 2024/25: Code Chronicle """ 3 | 4 | import itertools as it 5 | import sys 6 | 7 | lockkeys = sys.stdin.read().strip().split("\n\n") 8 | 9 | print("Part 1:", 10 | sum( 11 | not any(x1 == x2 == "#" for x1, x2 in zip(X1, X2)) 12 | for X1, X2 in it.combinations(lockkeys, 2) 13 | ) 14 | ) 15 | print("Part 2:", "Deliver The Chronicle") 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # advent-of-code :christmas_tree: 2 | 3 | I used to be quite competitive, but have found myself busier lately. 4 | 5 | - 2019: 282nd overall, with 189 points. 6 | - 2020: 61st overall, with 984 (902) points. 7 | - 2021: 66th overall, with 959 points. 8 | - 2022: 68th overall, with 896 points. 9 | 10 | #### santa 11 | 12 | `santa` is a command-line tool for managing Advent of Code puzzle-solving scripts. It handles downloading and [caching](https://www.reddit.com/r/adventofcode/wiki/faqs/automation#wiki_cache_your_inputs_after_initial_download) inputs, as well as providing each script with appropriate input (so long as each script declares its corresponding puzzle in a docstring). 13 | 14 | To see it in action, try: 15 | 16 | ```bash 17 | $ echo "YOUR_SECRET_TOKEN" > .TOKEN 18 | $ santa run 2015/01 19 | ``` 20 | 21 | It follows the automation guidelines listed on the [r/adventofcode Community Wiki](https://www.reddit.com/r/adventofcode/wiki/faqs/automation). In particular, 22 | 23 | - Once inputs are downloaded, they are cached locally in the `.cache` directory. 24 | - If you suspect your input is corrupted, you can manually request a fresh copy using `--invalidate-cached-input`. *This should almost never be necessary!* 25 | - When interacting with adventofcode.com, `santa` sets a `User-Agent` header that links back to this repository. 26 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2024.7.4 2 | charset-normalizer==3.3.2 3 | idna==3.7 4 | networkx==3.3 5 | requests==2.32.3 6 | urllib3==2.2.2 7 | z3-solver==4.13.0.0 8 | -------------------------------------------------------------------------------- /santa/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import datetime 3 | import os 4 | import pathlib 5 | import re 6 | import subprocess 7 | import sys 8 | import time 9 | 10 | ANSI_BG_RED = "\x1b[48;5;1m\x1b[38;5;16m" 11 | ANSI_BG_YELLOW = "\x1b[48;5;3m\x1b[38;5;16m" 12 | ANSI_BG_GREEN = "\x1b[48;5;2m\x1b[38;5;16m" 13 | ANSI_BG_BLUE = "\x1b[48;5;4m\x1b[38;5;16m" 14 | ANSI_CLEAR = "\x1b[0m" 15 | 16 | PREFIX_ERR = f"{ANSI_BG_RED} ERR! {ANSI_CLEAR}" 17 | PREFIX_WARN = f"{ANSI_BG_YELLOW} WARN {ANSI_CLEAR}" 18 | PREFIX_INFO = f"{ANSI_BG_BLUE} INFO {ANSI_CLEAR}" 19 | 20 | try: 21 | import requests 22 | except ModuleNotFoundError: 23 | print(PREFIX_ERR, 24 | f"Couldn't import module 'requests'.", 25 | f"(Have you activated the right virtualenv?)" 26 | ) 27 | sys.exit(1) 28 | 29 | parser = argparse.ArgumentParser(prog="santa", 30 | description="A little helper for solving Advent of Code puzzles.") 31 | subparsers = parser.add_subparsers(dest="command") 32 | 33 | subparser_new = subparsers.add_parser("new", help="create a script") 34 | subparser_new.add_argument("date") 35 | subparser_new.add_argument("name") 36 | 37 | subparser_run = subparsers.add_parser("run", help="run a script") 38 | subparser_run.add_argument("script", nargs="?", default=None) 39 | subparser_run.add_argument("--invalidate-cached-input", default=False, 40 | action="store_true", 41 | help="invalidate the cached input; fetch again from server.") 42 | subparser_run.add_argument("--test", default=False, action="store_true", 43 | help="pass '--test' to the script.") 44 | subparser_run.add_argument("--timeout", type=int, default=10, 45 | help="kill script after (default: 10) seconds.") 46 | 47 | def get_input(year, day, invalidate_cached_input=False): 48 | """ Fetch the input, downloading from the Advent of Code website if the 49 | input isn't in the cache. 50 | """ 51 | 52 | fp = pathlib.Path( 53 | f".cache/{year:04}/{day:02}/input" 54 | ).resolve() 55 | 56 | if not fp.is_file() or invalidate_cached_input: 57 | # Wait until after midnight, eastern. We don't want to spam the 58 | # servers before the puzzle unlocks! 59 | waiting = 0 60 | while True: 61 | est = datetime.timezone(datetime.timedelta(hours=-5)) 62 | delta = ( 63 | datetime.datetime(year, 12, day, tzinfo=est) - datetime.datetime.now(tz=est) 64 | ) 65 | 66 | if delta < datetime.timedelta(seconds=0): 67 | break 68 | 69 | # Determine the time remaining until unlock, and display a 70 | # formatted countdown timer. 71 | 72 | d = delta.days 73 | h = (delta.seconds // 3600) % 24 74 | m = (delta.seconds // 60) % 60 75 | s = (delta.seconds) % 60 76 | 77 | countdown = ( 78 | (f"{d}d" if d > 0 else "") 79 | + (f"{h:02}h" if h > 0 else "") 80 | + (f"{m:02}m" if m > 0 else "") 81 | + (f"{s:02}s") 82 | ) 83 | 84 | if waiting > 0: 85 | print("\x1b[A\x1b[K", end="") 86 | print(PREFIX_WARN, 87 | f"Puzzle hasn't unlocked; {countdown} remaining." 88 | ) 89 | 90 | waiting = waiting + 1 91 | time.sleep(1) 92 | 93 | print(PREFIX_INFO, "Downloading input from adventofcode.com...") 94 | 95 | # Read the session token from an environment variable, or, from 96 | # a secret file. 97 | _token = os.environ.get("AOC_TOKEN") 98 | if not _token: 99 | token_fps = [ 100 | pathlib.Path(".AOC_TOKEN"), 101 | pathlib.Path(".TOKEN"), 102 | pathlib.Path("~/.AOC_TOKEN").expanduser(), 103 | pathlib.Path("~/.TOKEN").expanduser() 104 | ] 105 | for token_fp in token_fps: 106 | if not token_fp.is_file(): 107 | continue 108 | 109 | print(PREFIX_INFO, f"Reading token from `{token_fp}`.") 110 | 111 | with token_fp.open(mode="r") as fh: 112 | _token = fh.read() 113 | 114 | break 115 | else: 116 | print(PREFIX_ERR, "Failed to locate token.") 117 | sys.exit(1) 118 | 119 | _token = _token.strip() 120 | 121 | # Fetch the input, using the token. 122 | response = requests.get( 123 | f"https://adventofcode.com/{year}/day/{day}/input", 124 | headers={ 125 | "Cookie": f"session={_token};", 126 | "User-Agent": "santa [github.com/hughcoleman/advent-of-code] by {}@{}.ca".format("htcolema", "uwaterloo") 127 | } 128 | ) 129 | 130 | assert response.status_code == 200 131 | _token = None # just a precaution 132 | 133 | # Cache the input. 134 | fp.parent.mkdir(parents=True, exist_ok=True) 135 | with fp.open(mode="wb") as fh: 136 | fh.write(response.content) 137 | 138 | with fp.open(mode="rb") as fh: 139 | inp_s = fh.read() 140 | 141 | return inp_s 142 | 143 | def run(fp, invalidate_cached_input=False, test=False, timeout=10): 144 | """ Run script `fp`. """ 145 | 146 | # Determine the year and day associated with this script. This information 147 | # is (should be) embedded in the """ docstring """ of the script, on line 148 | # 2. 149 | with fp.open(mode="r") as fh: 150 | fh.readline() # ignore the shebang; the docstring is on line 2. 151 | ln = fh.readline().strip() 152 | 153 | try: 154 | year, day, name = re.fullmatch( 155 | r"\"\"\"\s+\b(\d{4})\b.*\b(\d{1,2})\b[^\s]*\s*(.*)\s+\"\"\"", 156 | ln 157 | ).groups() 158 | if len(name) <= 0: 159 | name = None 160 | 161 | except: 162 | # If we can't extract the year and day from the docstring, attempt 163 | # to infer the year and day from the path to `fp`. 164 | year, day = re.fullmatch( 165 | r".*[aA].*[oO].*[cC].*\b(\d\d\d\d)\b.*\b(\d\d)\b.*", 166 | str( 167 | fp.resolve() 168 | ) 169 | ).groups() 170 | name = None 171 | 172 | print(PREFIX_WARN, 173 | f"Inferred year \"{year}\" and day \"{day}\" from path.", 174 | f"Is this what you want?" 175 | ) 176 | 177 | finally: 178 | year = int(year); assert 2015 <= year 179 | day = int(day ); assert 1 <= day <= 25 180 | 181 | # Print a little header. 182 | print( 183 | "---", 184 | f"{year:04}/{day:02}{f': {name}' if name else ''}", 185 | f"({fp.relative_to(pathlib.Path.cwd())})", 186 | "---" 187 | ) 188 | 189 | # Run the script. 190 | if sys.stdin.isatty(): 191 | inp_s = get_input(year, day, invalidate_cached_input) 192 | else: 193 | print(PREFIX_WARN, 194 | "Standard input isn't interactive; assuming input data is piped." 195 | ) 196 | inp_s = sys.stdin.read().encode() 197 | 198 | try: 199 | process = subprocess.run( 200 | # TODO: Properly "activate" the virtualenv. 201 | [ 202 | "python3", str(fp), "--test" if test else "" 203 | ], 204 | input=inp_s, 205 | stdout=sys.stdout, # we need to see the output! 206 | timeout=timeout 207 | ) 208 | except subprocess.TimeoutExpired: 209 | print(PREFIX_ERR, f"Timed out! (timeout was {args.timeout}s)") 210 | 211 | return 212 | 213 | if __name__ == "__main__": 214 | args = parser.parse_args() 215 | 216 | if args.command == "new": 217 | fp = pathlib.Path(f"{args.date}.py") 218 | if fp.is_file(): 219 | print(PREFIX_ERR, f"The file {fp} already exists.") 220 | sys.exit(1) 221 | 222 | # Create the file, and load it with some template content. 223 | fp.parent.mkdir(parents=True, exist_ok=True) 224 | with fp.open(mode="w") as fh: 225 | fh.write("#!/usr/bin/env python3\n") 226 | fh.write(f"\"\"\" {args.date}: {args.name} \"\"\"\n") 227 | fh.write("\n") 228 | fh.write("\n") 229 | 230 | fp.chmod(0o755) 231 | elif args.command == "run": 232 | # Locate the script. 233 | if args.script is None: 234 | run( 235 | pathlib.Path.cwd() / "main.py", 236 | invalidate_cached_input=args.invalidate_cached_input, 237 | test=args.test, 238 | timeout=args.timeout 239 | ) 240 | 241 | else: 242 | for p in sorted(pathlib.Path.cwd().glob(f"{args.script}*")): 243 | if p.is_dir(): 244 | # Run all scripts in the directory. 245 | for fp in sorted(p.glob("*.py")): 246 | run( 247 | fp, 248 | invalidate_cached_input=args.invalidate_cached_input, 249 | test=args.test, 250 | timeout=args.timeout 251 | ) 252 | elif p.is_file(): 253 | # Run script. 254 | run( 255 | p, 256 | invalidate_cached_input=args.invalidate_cached_input, 257 | test=args.test, 258 | timeout=args.timeout 259 | ) 260 | --------------------------------------------------------------------------------