├── 2015 ├── 10 │ └── solution.py ├── 11 │ └── solution.py ├── 12 │ └── solution.py ├── 13 │ └── solution.py ├── 14 │ └── solution.py ├── 15 │ └── solution.py ├── 16 │ └── solution.py ├── 17 │ └── solution.py ├── 18 │ └── solution.py ├── 19 │ └── solution.py ├── 20 │ └── solution.py ├── 21 │ └── solution.py ├── 22 │ └── solution.py ├── 23 │ └── solution.py ├── 24 │ └── solution.py ├── 25 │ └── solution.py ├── 01 │ └── solution.py ├── 02 │ └── solution.py ├── 03 │ └── solution.py ├── 04 │ └── solution.py ├── 05 │ └── solution.py ├── 06 │ └── solution.py ├── 07 │ └── solution.py ├── 08 │ └── solution.py └── 09 │ └── solution.py ├── 2016 ├── 10 │ └── solution.py ├── 11 │ └── solution.py ├── 12 │ └── solution.py ├── 13 │ └── solution.py ├── 14 │ └── solution.py ├── 15 │ └── solution.py ├── 16 │ └── solution.py ├── 17 │ └── solution.py ├── 18 │ └── solution.py ├── 19 │ └── solution.py ├── 20 │ └── solution.py ├── 21 │ └── solution.py ├── 22 │ └── solution.py ├── 23 │ └── solution.py ├── 24 │ └── solution.py ├── 25 │ └── solution.py ├── 01 │ └── solution.py ├── 02 │ └── solution.py ├── 03 │ └── solution.py ├── 04 │ └── solution.py ├── 05 │ └── solution.py ├── 06 │ └── solution.py ├── 07 │ └── solution.py ├── 08 │ └── solution.py └── 09 │ └── solution.py ├── 2017 ├── 10 │ └── solution.py ├── 11 │ └── solution.py ├── 12 │ └── solution.py ├── 13 │ └── solution.py ├── 14 │ └── solution.py ├── 15 │ └── solution.py ├── 16 │ └── solution.py ├── 17 │ └── solution.py ├── 18 │ └── solution.py ├── 19 │ └── solution.py ├── 20 │ └── solution.py ├── 21 │ └── solution.py ├── 22 │ └── solution.py ├── 23 │ └── solution.py ├── 24 │ └── solution.py ├── 25 │ └── solution.py ├── 01 │ └── solution.py ├── 02 │ └── solution.py ├── 03 │ └── solution.py ├── 04 │ └── solution.py ├── 05 │ └── solution.py ├── 06 │ └── solution.py ├── 07 │ └── solution.py ├── 08 │ └── solution.py └── 09 │ └── solution.py ├── 2018 ├── 10 │ └── solution.py ├── 11 │ └── solution.py ├── 12 │ └── solution.py ├── 13 │ └── solution.py ├── 14 │ └── solution.py ├── 15 │ └── solution.py ├── 16 │ └── solution.py ├── 17 │ └── solution.py ├── 18 │ └── solution.py ├── 19 │ └── solution.py ├── 20 │ └── solution.py ├── 21 │ └── solution.py ├── 22 │ └── solution.py ├── 23 │ └── solution.py ├── 24 │ └── solution.py ├── 25 │ └── solution.py ├── 01 │ └── solution.py ├── 02 │ └── solution.py ├── 03 │ └── solution.py ├── 04 │ └── solution.py ├── 05 │ └── solution.py ├── 06 │ └── solution.py ├── 07 │ └── solution.py ├── 08 │ └── solution.py └── 09 │ └── solution.py ├── 2019 ├── 10 │ └── solution.py ├── 11 │ └── solution.py ├── 12 │ └── solution.py ├── 13 │ └── solution.py ├── 14 │ └── solution.py ├── 15 │ └── solution.py ├── 16 │ └── solution.py ├── 17 │ └── solution.py ├── 18 │ └── solution.py ├── 19 │ └── solution.py ├── 20 │ └── solution.py ├── 21 │ └── solution.py ├── 22 │ └── solution.py ├── 23 │ └── solution.py ├── 24 │ └── solution.py ├── 25 │ └── solution.py ├── 01 │ └── solution.py ├── 02 │ └── solution.py ├── 03 │ └── solution.py ├── 04 │ └── solution.py ├── 05 │ └── solution.py ├── 06 │ └── solution.py ├── 07 │ └── solution.py ├── 08 │ └── solution.py ├── 09 │ └── solution.py └── intcode.py ├── 2020 ├── 10 │ └── solution.py ├── 11 │ └── solution.py ├── 12 │ └── solution.py ├── 13 │ └── solution.py ├── 14 │ └── solution.py ├── 15 │ └── solution.py ├── 16 │ └── solution.py ├── 17 │ └── solution.py ├── 18 │ └── solution.py ├── 19 │ └── solution.py ├── 20 │ └── solution.py ├── 21 │ └── solution.py ├── 22 │ └── solution.py ├── 23 │ ├── solution.cpp │ └── solution.py ├── 24 │ └── solution.py ├── 25 │ └── solution.py ├── 01 │ └── solution.py ├── 02 │ └── solution.py ├── 03 │ └── solution.py ├── 04 │ └── solution.py ├── 05 │ └── solution.py ├── 06 │ └── solution.py ├── 07 │ └── solution.py ├── 08 │ └── solution.py └── 09 │ └── solution.py ├── 2021 ├── 10 │ └── solution.py ├── 11 │ └── solution.py ├── 12 │ └── solution.py ├── 13 │ └── solution.py ├── 14 │ └── solution.py ├── 15 │ └── solution.py ├── 16 │ └── solution.py ├── 17 │ └── solution.py ├── 18 │ └── solution.py ├── 19 │ └── solution.py ├── 20 │ └── solution.py ├── 21 │ └── solution.py ├── 22 │ └── solution.py ├── 23 │ └── solution.py ├── 24 │ └── solution.py ├── 25 │ └── solution.py ├── 01 │ └── solution.py ├── 02 │ └── solution.py ├── 03 │ └── solution.py ├── 04 │ └── solution.py ├── 05 │ └── solution.py ├── 06 │ └── solution.py ├── 07 │ └── solution.py ├── 08 │ └── solution.py └── 09 │ └── solution.py ├── 2022 ├── 10 │ └── solution.py ├── 11 │ └── solution.py ├── 12 │ └── solution.py ├── 13 │ └── solution.py ├── 14 │ └── solution.py ├── 15 │ └── solution.py ├── 16 │ └── solution.py ├── 17 │ └── solution.py ├── 18 │ └── solution.py ├── 19 │ └── solution.py ├── 20 │ └── solution.py ├── 21 │ └── solution.py ├── 22 │ └── solution.py ├── 23 │ └── solution.py ├── 24 │ └── solution.py ├── 25 │ └── solution.py ├── 01 │ └── solution.py ├── 02 │ └── solution.py ├── 03 │ └── solution.py ├── 04 │ └── solution.py ├── 05 │ └── solution.py ├── 06 │ └── solution.py ├── 07 │ └── solution.py ├── 08 │ └── solution.py └── 09 │ └── solution.py ├── .gitignore ├── README.md ├── data └── letters.json ├── editor.bat ├── lib ├── aoc.py ├── channels.py ├── cyk.py ├── graph.py ├── graphics.py ├── grid.py ├── hex_coord.py ├── lazy_dict.py ├── math.py ├── ocr.py ├── parsing.py ├── performance.py ├── special_characters.py └── symbolic_math.py ├── requirements.txt ├── setup.bat ├── setup.py └── virtualenv_python.bat /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /2015/01/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def part1(s): 4 | answer = s.count('(') - s.count(')') 5 | 6 | lib.aoc.give_answer(2015, 1, 1, answer) 7 | 8 | def part2(s): 9 | floor = 0 10 | 11 | for idx, c in enumerate(s): 12 | if c == '(': 13 | floor += 1 14 | else: 15 | floor -= 1 16 | if floor < 0: 17 | answer = idx+1 18 | break 19 | 20 | lib.aoc.give_answer(2015, 1, 2, answer) 21 | 22 | INPUT = lib.aoc.get_input(2015, 1) 23 | part1(INPUT) 24 | part2(INPUT) 25 | -------------------------------------------------------------------------------- /2015/02/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def part1(s): 4 | answer = 0 5 | 6 | for line in s.splitlines(): 7 | x, y, z = list(map(int, line.split('x'))) 8 | sides = (x*y, x*z, y*z) 9 | slack = min(sides) 10 | answer += 2*sum(sides) + slack 11 | 12 | lib.aoc.give_answer(2015, 2, 1, answer) 13 | 14 | def part2(s): 15 | answer = 0 16 | 17 | for line in s.splitlines(): 18 | x, y, z = list(map(int, line.split('x'))) 19 | sides = (2*x+2*y, 2*x+2*z, 2*y+2*z) 20 | answer += min(sides) + x*y*z 21 | 22 | lib.aoc.give_answer(2015, 2, 2, answer) 23 | 24 | INPUT = lib.aoc.get_input(2015, 2) 25 | part1(INPUT) 26 | part2(INPUT) 27 | -------------------------------------------------------------------------------- /2015/03/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | MOVES = { 4 | '>': 1, 5 | '<': -1, 6 | '^': 1j, 7 | 'v': -1j 8 | } 9 | 10 | def part1(s): 11 | seen = {0} 12 | 13 | p = 0 14 | 15 | for c in s: 16 | p += MOVES[c] 17 | seen.add(p) 18 | 19 | answer = len(seen) 20 | 21 | lib.aoc.give_answer(2015, 3, 1, answer) 22 | 23 | def part2(s): 24 | seen = {0} 25 | 26 | a, b = 0, 0 27 | 28 | for c in s: 29 | a += MOVES[c] 30 | seen.add(a) 31 | a, b = b, a 32 | 33 | answer = len(seen) 34 | 35 | lib.aoc.give_answer(2015, 3, 2, answer) 36 | 37 | INPUT = lib.aoc.get_input(2015, 3) 38 | part1(INPUT) 39 | part2(INPUT) 40 | -------------------------------------------------------------------------------- /2015/04/solution.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | import lib.aoc 4 | 5 | def part1(s): 6 | answer = 0 7 | while hashlib.md5((s + str(answer)).encode()).hexdigest()[:5] != '0' * 5: 8 | answer += 1 9 | 10 | lib.aoc.give_answer(2015, 4, 1, answer) 11 | 12 | def part2(s): 13 | answer = 0 14 | while hashlib.md5((s + str(answer)).encode()).hexdigest()[:6] != '0' * 6: 15 | answer += 1 16 | 17 | lib.aoc.give_answer(2015, 4, 2, answer) 18 | 19 | INPUT = lib.aoc.get_input(2015, 4) 20 | part1(INPUT) 21 | part2(INPUT) 22 | -------------------------------------------------------------------------------- /2015/05/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def count_matching(s, *rules): 4 | answer = 0 5 | for line in s.splitlines(): 6 | if all(r(line) for r in rules): 7 | answer += 1 8 | return answer 9 | 10 | def rule1(s): 11 | return sum(1 12 | for c in s 13 | if c in 'aeiou') >= 3 14 | 15 | def rule2(s): 16 | return any(s[i-1] == s[i] 17 | for i in range(1, len(s))) 18 | 19 | def rule3(s): 20 | return not any(bad in s 21 | for bad in ('ab', 'cd', 'pq', 'xy')) 22 | 23 | def part1(s): 24 | answer = count_matching(s, rule1, rule2, rule3) 25 | 26 | lib.aoc.give_answer(2015, 5, 1, answer) 27 | 28 | def rule4(s): 29 | return any(s[idx-1:idx+1] in s[idx+1:] 30 | for idx in range(1, len(s))) 31 | 32 | def rule5(s): 33 | return any(s[i-2] == s[i] 34 | for i in range(2, len(s))) 35 | 36 | def part2(s): 37 | answer = count_matching(s, rule4, rule5) 38 | 39 | lib.aoc.give_answer(2015, 5, 2, answer) 40 | 41 | INPUT = lib.aoc.get_input(2015, 5) 42 | part1(INPUT) 43 | part2(INPUT) 44 | -------------------------------------------------------------------------------- /2015/06/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def parse_input(s): 4 | for line in s.splitlines(): 5 | parts = line.split() 6 | if parts[0] == 'turn': 7 | parts = parts[1:] 8 | op, start, _, end = parts 9 | x0,y0 = start.split(',') 10 | x1,y1 = end.split(',') 11 | yield op, (int(x0), int(y0)), (int(x1), int(y1)) 12 | 13 | def part1(s): 14 | states = {} 15 | for x in range(1000): 16 | for y in range(1000): 17 | states[x,y] = False 18 | 19 | for op, (x0, y0), (x1, y1) in parse_input(s): 20 | for x in range(x0, x1+1): 21 | for y in range(y0, y1+1): 22 | if op == 'on': 23 | states[x,y] = True 24 | elif op == 'off': 25 | states[x,y] = False 26 | else: 27 | states[x,y] = not states[x,y] 28 | 29 | answer = sum(states.values()) 30 | 31 | lib.aoc.give_answer(2015, 6, 1, answer) 32 | 33 | def part2(s): 34 | states = {} 35 | for x in range(1000): 36 | for y in range(1000): 37 | states[x,y] = 0 38 | 39 | for op, (x0, y0), (x1, y1) in parse_input(s): 40 | for x in range(x0, x1+1): 41 | for y in range(y0, y1+1): 42 | if op == 'on': 43 | states[x,y] += 1 44 | elif op == 'off': 45 | states[x,y] = max(states[x,y]-1, 0) 46 | else: 47 | states[x,y] += 2 48 | 49 | answer = sum(states.values()) 50 | 51 | lib.aoc.give_answer(2015, 6, 2, answer) 52 | 53 | INPUT = lib.aoc.get_input(2015, 6) 54 | part1(INPUT) 55 | part2(INPUT) 56 | -------------------------------------------------------------------------------- /2015/07/solution.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | import lib.aoc 4 | 5 | def parse_input(s): 6 | wires = {} 7 | for line in s.splitlines(): 8 | left, right = line.split(' -> ') 9 | left = left.split() 10 | wires[right] = left 11 | return wires 12 | 13 | def evaluate_wires(wires, target): 14 | @functools.cache 15 | def impl(target): 16 | try: 17 | return int(target) 18 | except: 19 | pass 20 | 21 | op = wires[target] 22 | if 1 == len(op): 23 | return impl(op[0]) 24 | 25 | if 2 == len(op): 26 | NOT, other = op 27 | assert(NOT == 'NOT') 28 | return 0xFFFF ^ impl(other) 29 | 30 | left, op, right = op 31 | left = impl(left) 32 | right = impl(right) 33 | 34 | if op == 'AND': 35 | return left & right 36 | elif op == 'OR': 37 | return left | right 38 | elif op == 'LSHIFT': 39 | return left << right 40 | elif op == 'RSHIFT': 41 | return left >> right 42 | else: 43 | assert(False) 44 | 45 | return impl(target) 46 | 47 | def part1(s): 48 | wires = parse_input(s) 49 | 50 | answer = evaluate_wires(wires, 'a') 51 | 52 | lib.aoc.give_answer(2015, 7, 1, answer) 53 | 54 | def part2(s): 55 | wires = parse_input(s) 56 | wires['b'] = [evaluate_wires(wires, 'a')] 57 | 58 | answer = evaluate_wires(wires, 'a') 59 | 60 | lib.aoc.give_answer(2015, 7, 2, answer) 61 | 62 | INPUT = lib.aoc.get_input(2015, 7) 63 | part1(INPUT) 64 | part2(INPUT) 65 | -------------------------------------------------------------------------------- /2015/08/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def decoded_savings(line): 4 | assert(line[0] == '"' == line[-1]) 5 | 6 | saved = 2 7 | line = line[1:-1] 8 | 9 | idx = 0 10 | while idx < len(line): 11 | if line[idx] == '\\': 12 | if line[idx+1] == 'x': 13 | idx += 4 14 | saved += 3 15 | continue 16 | else: 17 | idx += 2 18 | saved += 1 19 | continue 20 | 21 | idx += 1 22 | 23 | return saved 24 | 25 | def part1(s): 26 | answer = sum(map(decoded_savings, s.splitlines())) 27 | 28 | lib.aoc.give_answer(2015, 8, 1, answer) 29 | 30 | def part2(s): 31 | answer = sum(2 + line.count('"') + line.count('\\') 32 | for line in s.splitlines()) 33 | 34 | lib.aoc.give_answer(2015, 8, 2, answer) 35 | 36 | INPUT = lib.aoc.get_input(2015, 8) 37 | part1(INPUT) 38 | part2(INPUT) 39 | -------------------------------------------------------------------------------- /2015/09/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import lib.aoc 4 | 5 | def solve(s, optimize_fn): 6 | graph = collections.defaultdict(list) 7 | for line in s.splitlines(): 8 | a, _, b, _, dist = line.split() 9 | dist = int(dist) 10 | graph[a].append((b, dist)) 11 | graph[b].append((a, dist)) 12 | 13 | def impl(path, dist): 14 | if len(path) == len(graph): 15 | return dist 16 | 17 | return optimize_fn(impl(path + [neighbor], dist + ndist) 18 | for neighbor, ndist in graph[path[-1]] 19 | if neighbor not in path) 20 | 21 | return optimize_fn(impl([start], 0) 22 | for start in graph.keys()) 23 | 24 | def part1(s): 25 | answer = solve(s, min) 26 | 27 | lib.aoc.give_answer(2015, 9, 1, answer) 28 | 29 | def part2(s): 30 | answer = solve(s, max) 31 | 32 | lib.aoc.give_answer(2015, 9, 2, answer) 33 | 34 | INPUT = lib.aoc.get_input(2015, 9) 35 | part1(INPUT) 36 | part2(INPUT) 37 | -------------------------------------------------------------------------------- /2015/10/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def look_and_say(s, iterations): 4 | n = list(map(int, s)) 5 | 6 | for _ in range(iterations): 7 | out = [] 8 | 9 | last = None 10 | count = 0 11 | 12 | for c in n: 13 | if last != c: 14 | if last is not None: 15 | out += [count, last] 16 | last, count = c, 1 17 | continue 18 | 19 | count += 1 20 | 21 | out += [count, last] 22 | 23 | n = out 24 | 25 | return len(n) 26 | 27 | def part1(s): 28 | answer = look_and_say(s, 40) 29 | 30 | lib.aoc.give_answer(2015, 10, 1, answer) 31 | 32 | def part2(s): 33 | answer = look_and_say(s, 50) 34 | 35 | lib.aoc.give_answer(2015, 10, 2, answer) 36 | 37 | INPUT = lib.aoc.get_input(2015, 10) 38 | part1(INPUT) 39 | part2(INPUT) 40 | -------------------------------------------------------------------------------- /2015/11/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | LETTERS = 'abcdefghijklmnopqrstuvwxyz' 4 | BAD_CHARS = [LETTERS.index(c) for c in 'iol'] 5 | 6 | def next_legal_password(s): 7 | nums = list(map(LETTERS.index, s)) 8 | 9 | while True: 10 | for i in range(len(s)-1, -1, -1): 11 | nums[i] += 1 12 | if nums[i] == 26: 13 | nums[i] = 0 14 | else: 15 | break 16 | 17 | if any(c in nums for c in BAD_CHARS): 18 | continue 19 | 20 | if all(nums[i]+1 != nums[i+1] or nums[i]+2 != nums[i+2] 21 | for i in range(len(nums)-2)): 22 | continue 23 | 24 | for i in range(len(nums)-3): 25 | if nums[i] == nums[i+1]: 26 | for j in range(i+2, len(nums)-1): 27 | if nums[j] == nums[j+1]: 28 | return ''.join(LETTERS[n] for n in nums) 29 | break 30 | 31 | def part1(s): 32 | answer = next_legal_password(s) 33 | 34 | lib.aoc.give_answer(2015, 11, 1, answer) 35 | 36 | def part2(s): 37 | answer = next_legal_password(next_legal_password(s)) 38 | 39 | lib.aoc.give_answer(2015, 11, 2, answer) 40 | 41 | INPUT = lib.aoc.get_input(2015, 11) 42 | part1(INPUT) 43 | part2(INPUT) 44 | -------------------------------------------------------------------------------- /2015/12/solution.py: -------------------------------------------------------------------------------- 1 | import json 2 | import parse 3 | 4 | import lib.aoc 5 | 6 | def part1(s): 7 | answer = sum(map(lambda r:r[0], parse.findall('{:d}', s))) 8 | 9 | lib.aoc.give_answer(2015, 12, 1, answer) 10 | 11 | def sum_not_red(data): 12 | if isinstance(data, list): 13 | return sum(map(sum_not_red, data)) 14 | if isinstance(data, int): 15 | return data 16 | if isinstance(data, str): 17 | return 0 18 | assert(isinstance(data, dict)) 19 | if 'red' in data or 'red' in data.values(): 20 | return 0 21 | 22 | return sum(map(sum_not_red, data.values())) 23 | 24 | def part2(s): 25 | answer = sum_not_red(json.loads(s)) 26 | 27 | lib.aoc.give_answer(2015, 12, 2, answer) 28 | 29 | INPUT = lib.aoc.get_input(2015, 12) 30 | part1(INPUT) 31 | part2(INPUT) 32 | -------------------------------------------------------------------------------- /2015/13/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import lib.aoc 4 | 5 | def parse_input(s): 6 | pairs = collections.Counter() 7 | 8 | for line in s.splitlines(): 9 | a, _, change, n, _, _, _, _, _, _, b = line.split() 10 | assert(b[-1] == '.') 11 | b = b[:-1] 12 | n = int(n) 13 | if change == 'lose': 14 | n = -n 15 | else: 16 | assert(change == 'gain') 17 | pairs[min(a,b), max(a,b)] += n 18 | 19 | graph = collections.defaultdict(list) 20 | 21 | for (a,b), n in pairs.items(): 22 | graph[a].append((b, n)) 23 | graph[b].append((a, n)) 24 | 25 | return graph 26 | 27 | def best_path(graph): 28 | def impl(start, path, cost): 29 | if len(path) == len(graph) and path[-1] == start: 30 | yield cost 31 | return 32 | 33 | current = start if len(path) == 0 else path[-1] 34 | 35 | for neighbor, diff in graph[current]: 36 | if neighbor in path: 37 | continue 38 | 39 | yield from impl(start, path + [neighbor], cost + diff) 40 | 41 | return max(max(impl(start, [], 0)) 42 | for start in graph.keys()) 43 | 44 | def part1(s): 45 | graph = parse_input(s) 46 | answer = best_path(graph) 47 | 48 | lib.aoc.give_answer(2015, 13, 1, answer) 49 | 50 | def part2(s): 51 | graph = parse_input(s) 52 | 53 | assert('me' not in graph.keys()) 54 | for person in list(graph.keys()): 55 | graph['me'].append((person, 0)) 56 | graph[person].append(('me', 0)) 57 | 58 | answer = best_path(graph) 59 | 60 | lib.aoc.give_answer(2015, 13, 2, answer) 61 | 62 | INPUT = lib.aoc.get_input(2015, 13) 63 | part1(INPUT) 64 | part2(INPUT) 65 | -------------------------------------------------------------------------------- /2015/14/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def run_race(s, total_time): 4 | deer = [] 5 | for line in s.splitlines(): 6 | _, _, _, speed, _, _, duration, _, _, _, _, _, _, rest, _ = line.split() 7 | speed = int(speed) 8 | duration = int(duration) 9 | rest = int(rest) 10 | deer.append((speed, duration, rest)) 11 | 12 | dists = [0] * len(deer) 13 | scores = [0] * len(deer) 14 | cycle_time = [0] * len(deer) 15 | 16 | for _ in range(total_time): 17 | best = None 18 | best_i = [] 19 | for i, (speed, duration, rest) in enumerate(deer): 20 | if cycle_time[i] < duration: 21 | dists[i] += speed 22 | cycle_time[i] = (cycle_time[i] + 1) % (duration + rest) 23 | 24 | if best is None or best < dists[i]: 25 | best = dists[i] 26 | best_i = [i] 27 | elif best == dists[i]: 28 | best_i.append(i) 29 | 30 | for i in best_i: 31 | scores[i] += 1 32 | 33 | return dists, scores 34 | 35 | def part1(s): 36 | answer = max(run_race(s, 2503)[0]) 37 | 38 | lib.aoc.give_answer(2015, 14, 1, answer) 39 | 40 | def part2(s): 41 | answer = max(run_race(s, 2503)[1]) 42 | 43 | lib.aoc.give_answer(2015, 14, 2, answer) 44 | 45 | INPUT = lib.aoc.get_input(2015, 14) 46 | part1(INPUT) 47 | part2(INPUT) 48 | -------------------------------------------------------------------------------- /2015/15/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def get_possible_sums(target, nums): 4 | if nums == 1: 5 | return [(target,)] 6 | 7 | options = [] 8 | for n in range(target+1): 9 | for opt in get_possible_sums(target-n, nums-1): 10 | options.append((n,) + opt) 11 | return options 12 | 13 | def optimize(s, teaspoons, calorie_requirement=None): 14 | properties = [] 15 | all_calories = [] 16 | 17 | for line in s.splitlines(): 18 | _, _, cap, _, dur, _, flav, _, texture, _, cal = line.split() 19 | 20 | properties.append((int(cap[:-1]), 21 | int(dur[:-1]), 22 | int(flav[:-1]), 23 | int(texture[:-1]))) 24 | all_calories.append(int(cal)) 25 | 26 | best = 0 27 | 28 | for weights in get_possible_sums(teaspoons, len(properties)): 29 | prop_sums = [0] * len(properties) 30 | calories = 0 31 | 32 | for weight, props, cals in zip(weights, properties, all_calories): 33 | calories += weight * cals 34 | for i in range(len(prop_sums)): 35 | prop_sums[i] += weight * props[i] 36 | 37 | if calorie_requirement is not None and calories != calorie_requirement: 38 | continue 39 | 40 | score = 1 41 | for prop in prop_sums: 42 | score *= max(prop, 0) 43 | 44 | best = max(score, best) 45 | 46 | return best 47 | 48 | def part1(s): 49 | answer = optimize(s, 100) 50 | 51 | lib.aoc.give_answer(2015, 15, 1, answer) 52 | 53 | def part2(s): 54 | answer = optimize(s, 100, 500) 55 | 56 | lib.aoc.give_answer(2015, 15, 2, answer) 57 | 58 | INPUT = lib.aoc.get_input(2015, 15) 59 | part1(INPUT) 60 | part2(INPUT) 61 | -------------------------------------------------------------------------------- /2015/16/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def find_match(s, *checks): 4 | for line in s.splitlines(): 5 | _, n, a, ac, b, bc, c, cc = line.split() 6 | 7 | num, info = line.split(': ', maxsplit=1) 8 | 9 | things = {} 10 | for thing in info.split(', '): 11 | name, count = thing.split(': ') 12 | things[name] = int(count) 13 | 14 | if all(check(things) 15 | for check in checks): 16 | return int(num.split()[1]) 17 | 18 | EXPECTED_EQ = [('children', 3), 19 | ('samoyeds', 2), 20 | ('akitas', 0), 21 | ('vizslas', 0), 22 | ('cars', 2), 23 | ('perfumes', 1)] 24 | EXPECTED_GT = [('cats', 7), 25 | ('trees', 3)] 26 | EXPECTED_LT = [('pomeranians', 3), 27 | ('goldfish', 5)] 28 | 29 | def make_eq_check(expectations): 30 | def impl(things): 31 | return all(things.get(key, expected) == expected 32 | for key, expected in expectations) 33 | return impl 34 | 35 | def part1(s): 36 | answer = find_match(s, 37 | make_eq_check(EXPECTED_EQ + EXPECTED_GT + EXPECTED_LT)) 38 | 39 | lib.aoc.give_answer(2015, 16, 1, answer) 40 | 41 | def make_gt_check(expectations): 42 | def impl(things): 43 | return all(things.get(key, expected+1) > expected 44 | for key, expected in expectations) 45 | return impl 46 | 47 | def make_lt_check(expectations): 48 | def impl(things): 49 | return all(things.get(key, expected-1) < expected 50 | for key, expected in expectations) 51 | return impl 52 | 53 | def part2(s): 54 | answer = find_match(s, 55 | make_eq_check(EXPECTED_EQ), 56 | make_gt_check(EXPECTED_GT), 57 | make_lt_check(EXPECTED_LT)) 58 | 59 | lib.aoc.give_answer(2015, 16, 2, answer) 60 | 61 | INPUT = lib.aoc.get_input(2015, 16) 62 | part1(INPUT) 63 | part2(INPUT) 64 | -------------------------------------------------------------------------------- /2015/17/solution.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | import lib.aoc 4 | 5 | def count_matching_combinations(containers, n): 6 | return sum(1 7 | for combo in itertools.combinations(containers, n) 8 | if sum(combo) == 150) 9 | 10 | def part1(s): 11 | containers = list(map(int, s.splitlines())) 12 | 13 | answer = sum(count_matching_combinations(containers, n) 14 | for n in range(len(containers))) 15 | 16 | lib.aoc.give_answer(2015, 17, 1, answer) 17 | 18 | def part2(s): 19 | containers = list(map(int, s.splitlines())) 20 | 21 | for n in range(len(containers)): 22 | answer = count_matching_combinations(containers, n) 23 | if answer != 0: 24 | # This is the minimum number of containers required! 25 | break 26 | 27 | lib.aoc.give_answer(2015, 17, 2, answer) 28 | 29 | INPUT = lib.aoc.get_input(2015, 17) 30 | part1(INPUT) 31 | part2(INPUT) 32 | -------------------------------------------------------------------------------- /2015/18/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import lib.aoc 4 | import lib.grid 5 | 6 | def parse_grid(s): 7 | width = len(s.splitlines()[0]) 8 | height = len(s.splitlines()) 9 | 10 | on = set() 11 | 12 | for y, line in enumerate(s.splitlines()): 13 | for x, cell in enumerate(line): 14 | if cell == '#': 15 | on.add((x,y)) 16 | 17 | return on, width, height 18 | 19 | def step(on_cells, width, height): 20 | counts = collections.Counter() 21 | 22 | for x,y in on_cells: 23 | for dx in (-1, 0, 1): 24 | for dy in (-1, 0, 1): 25 | if dx == dy == 0: 26 | continue 27 | counts[x+dx,y+dy] += 1 28 | 29 | new_on = set() 30 | 31 | for (x,y), count in counts.items(): 32 | if 0 <= x < width and 0 <= y < height: 33 | if count == 3 or (count == 2 and (x,y) in on_cells): 34 | new_on.add((x,y)) 35 | 36 | return new_on 37 | 38 | def part1(s): 39 | cells, width, height = parse_grid(s) 40 | 41 | for _ in range(100): 42 | cells = step(cells, width, height) 43 | 44 | answer = len(cells) 45 | 46 | lib.aoc.give_answer(2015, 18, 1, answer) 47 | 48 | def part2(s): 49 | cells, width, height = parse_grid(s) 50 | 51 | always_on = {(0,0), 52 | (0,height-1), 53 | (width-1,height-1), 54 | (width-1,0)} 55 | cells |= always_on 56 | 57 | for _ in range(100): 58 | cells = step(cells, width, height) | always_on 59 | 60 | answer = len(cells) 61 | 62 | lib.aoc.give_answer(2015, 18, 2, answer) 63 | 64 | INPUT = lib.aoc.get_input(2015, 18) 65 | part1(INPUT) 66 | part2(INPUT) 67 | -------------------------------------------------------------------------------- /2015/19/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import lib.aoc 4 | import lib.cyk 5 | 6 | def parse_input(s): 7 | g0, molecule = s.split('\n\n') 8 | 9 | rules = collections.defaultdict(list) 10 | for line in g0.splitlines(): 11 | left, right = line.split(' => ') 12 | assert(len(left) <= len(right)) 13 | rules[left].append(right) 14 | 15 | return rules, molecule 16 | 17 | def part1(s): 18 | rules, molecule = parse_input(s) 19 | 20 | possibilities = set() 21 | 22 | for base, replacements in rules.items(): 23 | last_idx = 0 24 | while True: 25 | idx = molecule.find(base, last_idx) 26 | if idx == -1: 27 | break 28 | for repl in replacements: 29 | new = molecule[:idx] + repl + molecule[idx+len(base):] 30 | possibilities.add(new) 31 | last_idx = idx + len(base) 32 | 33 | answer = len(possibilities) 34 | 35 | lib.aoc.give_answer(2015, 19, 1, answer) 36 | 37 | def tokenize_by_element(molecule): 38 | # Weird special case... 39 | if molecule == 'e': 40 | return ['e'] 41 | 42 | # All elements are camel case 43 | indices = [i for i,c in enumerate(molecule) 44 | if c.isupper()] 45 | assert(len(indices) > 0 and indices[0] == 0) 46 | return [molecule[i1:i2] 47 | for i1, i2 in zip(indices, indices[1:] + [len(molecule)])] 48 | 49 | def to_grammar(rules): 50 | known_elements = set() 51 | 52 | for base, options in sorted(rules.items()): 53 | assert(len(tokenize_by_element(base)) == 1) 54 | known_elements.add(base) 55 | for opt in options: 56 | option_elements = tokenize_by_element(opt) 57 | known_elements.update(option_elements) 58 | yield lib.cyk.Symbol(base), list(map(lib.cyk.Symbol, 59 | option_elements)) 60 | 61 | for elem in known_elements: 62 | # Mark this free, we only use symbols for compatibility with lib.cyk! 63 | yield lib.cyk.Symbol(elem), elem, 0 64 | 65 | def part2(s): 66 | rules, molecule = parse_input(s) 67 | 68 | cnf = lib.cyk.CNFGrammar(to_grammar(rules), lib.cyk.Symbol('e')) 69 | 70 | answer = cnf.steps_to_generate(tokenize_by_element(molecule)) 71 | 72 | lib.aoc.give_answer(2015, 19, 2, answer) 73 | 74 | INPUT = lib.aoc.get_input(2015, 19) 75 | part1(INPUT) 76 | part2(INPUT) 77 | -------------------------------------------------------------------------------- /2015/20/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import lib.aoc 4 | 5 | def solve(s, target, present_mult, visit_limit=None): 6 | visiting_elves = collections.defaultdict(list) 7 | 8 | house = 0 9 | 10 | while True: 11 | house += 1 12 | 13 | visiting_elves[house].append(house) 14 | 15 | visiting = visiting_elves.pop(house) 16 | 17 | delivered = sum(visiting) * present_mult 18 | if delivered >= target: 19 | return house 20 | 21 | for elf in visiting: 22 | next_house = house + elf 23 | if visit_limit is not None and elf * visit_limit < next_house: 24 | continue 25 | visiting_elves[next_house].append(elf) 26 | 27 | def part1(s): 28 | answer = solve(s, int(s), 10) 29 | 30 | lib.aoc.give_answer(2015, 20, 1, answer) 31 | 32 | def part2(s): 33 | answer = solve(s, int(s), 11, 50) 34 | 35 | lib.aoc.give_answer(2015, 20, 2, answer) 36 | 37 | INPUT = lib.aoc.get_input(2015, 20) 38 | part1(INPUT) 39 | part2(INPUT) 40 | -------------------------------------------------------------------------------- /2015/23/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def run(s, a, b): 4 | instructions = [] 5 | 6 | for line in s.splitlines(): 7 | op, rest = line.split(maxsplit=1) 8 | rest = rest.split(', ') 9 | instructions.append((op, rest)) 10 | 11 | idx = 0 12 | 13 | while idx < len(instructions): 14 | op, args = instructions[idx] 15 | 16 | if op == 'jmp': 17 | idx += int(args[0]) 18 | continue 19 | 20 | val = a if args[0] == 'a' else b 21 | 22 | if op == 'jie': 23 | if val % 2 == 0: 24 | idx += int(args[1]) 25 | else: 26 | idx += 1 27 | continue 28 | if op == 'jio': 29 | if val == 1: 30 | idx += int(args[1]) 31 | else: 32 | idx += 1 33 | continue 34 | 35 | if op == 'hlf': 36 | val //= 2 37 | elif op == 'tpl': 38 | val *= 3 39 | elif op == 'inc': 40 | val += 1 41 | else: 42 | assert(False) 43 | 44 | if args[0] == 'a': 45 | a = val 46 | else: 47 | b = val 48 | 49 | idx += 1 50 | 51 | return a, b 52 | 53 | def part1(s): 54 | _, answer = run(s, 0, 0) 55 | 56 | lib.aoc.give_answer(2015, 23, 1, answer) 57 | 58 | def part2(s): 59 | _, answer = run(s, 1, 0) 60 | 61 | lib.aoc.give_answer(2015, 23, 2, answer) 62 | 63 | INPUT = lib.aoc.get_input(2015, 23) 64 | part1(INPUT) 65 | part2(INPUT) 66 | -------------------------------------------------------------------------------- /2015/24/solution.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | import lib.aoc 4 | 5 | def good_groupings(packages, target_sum, take): 6 | for group in itertools.combinations(packages, take): 7 | if sum(group) == target_sum: 8 | yield group # Just assume the rest can be split evenly 9 | 10 | def entanglement(group): 11 | val = 1 12 | for p in group: 13 | val *= p 14 | return val 15 | 16 | def part1(s): 17 | packages = list(map(int, s.splitlines())) 18 | 19 | take = 0 20 | while True: 21 | take += 1 22 | options = list(good_groupings(packages, sum(packages)//3, take)) 23 | if options: 24 | answer = min(map(entanglement, options)) 25 | break 26 | 27 | lib.aoc.give_answer(2015, 24, 1, answer) 28 | 29 | def part2(s): 30 | packages = list(map(int, s.splitlines())) 31 | 32 | take = 0 33 | while True: 34 | take += 1 35 | options = list(good_groupings(packages, sum(packages)//4, take)) 36 | if options: 37 | answer = min(map(entanglement, options)) 38 | break 39 | 40 | lib.aoc.give_answer(2015, 24, 2, answer) 41 | 42 | INPUT = lib.aoc.get_input(2015, 24) 43 | part1(INPUT) 44 | part2(INPUT) 45 | -------------------------------------------------------------------------------- /2015/25/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def part1(s): 4 | line = s.split() 5 | row = int(line[15][:-1]) 6 | col = int(line[17][:-1]) 7 | 8 | idx = col 9 | last_finished_col = col + row - 1 10 | idx += last_finished_col * (last_finished_col-1) // 2 11 | 12 | MOD = 33554393 13 | 14 | answer = (20151125 * pow(252533, idx-1, MOD)) % MOD 15 | 16 | lib.aoc.give_answer(2015, 25, 1, answer) 17 | 18 | def part2(s): 19 | print('There is no part two for Christmas!') 20 | 21 | INPUT = lib.aoc.get_input(2015, 25) 22 | part1(INPUT) 23 | part2(INPUT) 24 | -------------------------------------------------------------------------------- /2016/01/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def walk_directions(s): 4 | d = 1j 5 | p = 0 6 | 7 | for move in s.split(', '): 8 | if move[0] == 'R': 9 | d *= -1j 10 | else: 11 | d *= 1j 12 | 13 | for _ in range(int(move[1:])): 14 | p += d 15 | yield p 16 | 17 | def part1(s): 18 | p = list(walk_directions(s))[-1] 19 | 20 | answer = int(abs(p.real) + abs(p.imag)) 21 | 22 | lib.aoc.give_answer(2016, 1, 1, answer) 23 | 24 | def part2(s): 25 | seen = {0} 26 | 27 | for p in walk_directions(s): 28 | if p in seen: 29 | break 30 | seen.add(p) 31 | 32 | answer = int(abs(p.real) + abs(p.imag)) 33 | 34 | lib.aoc.give_answer(2016, 1, 2, answer) 35 | 36 | INPUT = lib.aoc.get_input(2016, 1) 37 | part1(INPUT) 38 | part2(INPUT) 39 | -------------------------------------------------------------------------------- /2016/02/solution.py: -------------------------------------------------------------------------------- 1 | import string 2 | 3 | import lib.aoc 4 | 5 | def map_key(directions): 6 | x, y = 1, 1 7 | 8 | for d in directions: 9 | if d == 'U': 10 | y = max(y-1, 0) 11 | elif d == 'D': 12 | y = min(y+1, 2) 13 | elif d == 'L': 14 | x = max(x-1, 0) 15 | elif d == 'R': 16 | x = min(x+1, 2) 17 | 18 | return str(3*y + x + 1) 19 | 20 | def part1(s): 21 | answer = int(''.join(map(map_key, s.splitlines()))) 22 | 23 | lib.aoc.give_answer(2016, 2, 1, answer) 24 | 25 | KEYLIST = string.digits + string.ascii_uppercase 26 | 27 | def map_diamond_key(directions): 28 | x, y = -2, 0 # Start on 5 29 | 30 | for d in directions: 31 | if d == 'U': 32 | nx, ny = x, y-1 33 | elif d == 'D': 34 | nx, ny = x, y+1 35 | elif d == 'L': 36 | nx, ny = x-1, y 37 | elif d == 'R': 38 | nx, ny = x+1, y 39 | 40 | if abs(nx) + abs(ny) <= 2: 41 | x, y = nx, ny 42 | 43 | offset = 1 44 | for skipped_y in range(-2, y): 45 | num_in_row = 2 * (2 - abs(skipped_y)) + 1 46 | offset += num_in_row 47 | 48 | max_x_for_row = (2 - abs(y)) 49 | offset += x + max_x_for_row 50 | 51 | return KEYLIST[offset] 52 | 53 | def part2(s): 54 | answer = ''.join(map(map_diamond_key, s.splitlines())) 55 | 56 | lib.aoc.give_answer(2016, 2, 2, answer) 57 | 58 | INPUT = lib.aoc.get_input(2016, 2) 59 | part1(INPUT) 60 | part2(INPUT) 61 | -------------------------------------------------------------------------------- /2016/03/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def count_possible_triangles(tris): 4 | count = 0 5 | 6 | for a, b, c in tris: 7 | maxlen = max(a, b, c) 8 | if a+b+c - maxlen > maxlen: 9 | count += 1 10 | 11 | return count 12 | 13 | def part1(s): 14 | tris = [list(map(int, line.split())) 15 | for line in s.splitlines()] 16 | 17 | answer = count_possible_triangles(tris) 18 | 19 | lib.aoc.give_answer(2016, 3, 1, answer) 20 | 21 | def column_tris(s): 22 | lines = s.splitlines() 23 | while lines: 24 | a = lines.pop(0).split() 25 | b = lines.pop(0).split() 26 | c = lines.pop(0).split() 27 | 28 | yield int(a.pop(0)), int(b.pop(0)), int(c.pop(0)) 29 | yield int(a.pop(0)), int(b.pop(0)), int(c.pop(0)) 30 | yield int(a.pop(0)), int(b.pop(0)), int(c.pop(0)) 31 | 32 | def part2(s): 33 | answer = count_possible_triangles(column_tris(s)) 34 | 35 | lib.aoc.give_answer(2016, 3, 2, answer) 36 | 37 | INPUT = lib.aoc.get_input(2016, 3) 38 | part1(INPUT) 39 | part2(INPUT) 40 | -------------------------------------------------------------------------------- /2016/04/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import string 3 | 4 | import lib.aoc 5 | 6 | def real_rooms(s): 7 | for line in s.splitlines(): 8 | assert(line[-7] == '[') 9 | assert(line[-1] == ']') 10 | checksum = line[-6:-1] 11 | 12 | room = line[:-7] 13 | idx = room.rfind('-') 14 | sector = int(room[idx+1:]) 15 | room = room[:idx] 16 | 17 | counts = collections.Counter(room.replace('-', '')) 18 | counts = sorted(counts.most_common(), 19 | key=lambda p: (-p[1],p[0]))[:5] 20 | real_checksum = ''.join(c[0] for c in counts) 21 | if checksum != real_checksum: 22 | continue 23 | 24 | decoded = '' 25 | for c in room: 26 | if c == '-': 27 | decoded += ' ' 28 | continue 29 | idx = string.ascii_lowercase.index(c) 30 | idx = (idx + sector) % 26 31 | c = string.ascii_lowercase[idx] 32 | decoded += c 33 | 34 | yield decoded, int(sector) 35 | 36 | def part1(s): 37 | answer = sum(sector for room, sector in real_rooms(s)) 38 | 39 | lib.aoc.give_answer(2016, 4, 1, answer) 40 | 41 | def part2(s): 42 | candidates = list(sector 43 | for room, sector in real_rooms(s) 44 | if 'north' in room) 45 | assert(len(candidates) == 1) 46 | answer = candidates[0] 47 | 48 | lib.aoc.give_answer(2016, 4, 2, answer) 49 | 50 | INPUT = lib.aoc.get_input(2016, 4) 51 | part1(INPUT) 52 | part2(INPUT) 53 | -------------------------------------------------------------------------------- /2016/05/solution.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | import lib.aoc 4 | 5 | def interesting_hashes(s): 6 | idx = 0 7 | while True: 8 | test = s + str(idx) 9 | h = hashlib.md5(test.encode()).hexdigest() 10 | if h[:5] == '00000': 11 | yield h 12 | idx += 1 13 | 14 | def part1(s): 15 | answer = '' 16 | 17 | for h in interesting_hashes(s): 18 | answer += h[5] 19 | if len(answer) == 8: 20 | break 21 | 22 | lib.aoc.give_answer(2016, 5, 1, answer) 23 | 24 | def part2(s): 25 | answer = [None] * 8 26 | known = 0 27 | 28 | for h in interesting_hashes(s): 29 | pos = h[5] 30 | if pos not in '01234567': 31 | continue 32 | pos = int(pos) 33 | if answer[pos] is not None: 34 | continue 35 | answer[pos] = h[6] 36 | known += 1 37 | if known == 8: 38 | break 39 | 40 | answer = ''.join(answer) 41 | 42 | lib.aoc.give_answer(2016, 5, 2, answer) 43 | 44 | INPUT = lib.aoc.get_input(2016, 5) 45 | part1(INPUT) 46 | part2(INPUT) 47 | -------------------------------------------------------------------------------- /2016/06/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import lib.aoc 4 | 5 | def part1(s): 6 | columns = collections.defaultdict(collections.Counter) 7 | 8 | for line in s.splitlines(): 9 | for idx, c in enumerate(line): 10 | columns[idx][c] += 1 11 | 12 | answer = '' 13 | 14 | for idx, c in sorted(columns.items()): 15 | answer += c.most_common(1)[0][0] 16 | 17 | lib.aoc.give_answer(2016, 6, 1, answer) 18 | 19 | def part2(s): 20 | columns = collections.defaultdict(collections.Counter) 21 | 22 | for line in s.splitlines(): 23 | for idx, c in enumerate(line): 24 | columns[idx][c] += 1 25 | 26 | answer = '' 27 | 28 | for idx, c in sorted(columns.items()): 29 | answer += c.most_common()[-1][0] 30 | 31 | lib.aoc.give_answer(2016, 6, 2, answer) 32 | 33 | INPUT = lib.aoc.get_input(2016, 6) 34 | part1(INPUT) 35 | part2(INPUT) 36 | -------------------------------------------------------------------------------- /2016/07/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def split_ip(ip): 4 | supernets = [] 5 | hypernets = [] 6 | 7 | while True: 8 | idx = ip.find('[') 9 | if idx == -1: 10 | assert(-1 == ip.find(']')) 11 | supernets.append(ip) 12 | break 13 | 14 | end_idx = ip.find(']') 15 | assert(end_idx > idx) 16 | 17 | supernets.append(ip[:idx]) 18 | hyper = ip[idx+1:end_idx] 19 | ip = ip[end_idx+1:] 20 | assert(hyper.find('[') == -1) 21 | hypernets.append(hyper) 22 | 23 | return supernets, hypernets 24 | 25 | def has_abba(sequence): 26 | for a, b, c, d in zip(sequence, 27 | sequence[1:], 28 | sequence[2:], 29 | sequence[3:]): 30 | if a == d and b == c and a != b: 31 | return True 32 | 33 | return False 34 | 35 | def supports_tls(ip): 36 | supernets, hypernets = split_ip(ip) 37 | 38 | if any(filter(has_abba, hypernets)): 39 | return False 40 | 41 | return any(filter(has_abba, supernets)) 42 | 43 | def part1(s): 44 | answer = len(list(filter(supports_tls, s.splitlines()))) 45 | 46 | lib.aoc.give_answer(2016, 7, 1, answer) 47 | 48 | def supports_ssl(ip): 49 | supernets, hypernets = split_ip(ip) 50 | 51 | for sequence in supernets: 52 | for a, b, c in zip(sequence, 53 | sequence[1:], 54 | sequence[2:]): 55 | if a == c and a != b: 56 | bab = b + a + b 57 | if any(bab in hyper_s 58 | for hyper_s in hypernets): 59 | return True 60 | 61 | return False 62 | 63 | def part2(s): 64 | answer = len(list(filter(supports_ssl, s.splitlines()))) 65 | 66 | lib.aoc.give_answer(2016, 7, 2, answer) 67 | 68 | INPUT = lib.aoc.get_input(2016, 7) 69 | part1(INPUT) 70 | part2(INPUT) 71 | -------------------------------------------------------------------------------- /2016/08/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.ocr 3 | 4 | def apply_instructions(s): 5 | on_pixels = set() 6 | 7 | for inst in s.splitlines(): 8 | parts = inst.split() 9 | if parts[0] == 'rect': 10 | w, h = parts[1].split('x') 11 | w = int(w) 12 | h = int(h) 13 | for x in range(w): 14 | for y in range(h): 15 | on_pixels.add((x, y)) 16 | continue 17 | 18 | n = int(parts[2][2:]) 19 | by = int(parts[4]) 20 | if parts[1] == 'row': 21 | new_on = set() 22 | for x, y in on_pixels: 23 | if y == n: 24 | x = (x + by) % 50 25 | new_on.add((x, y)) 26 | on_pixels = new_on 27 | else: 28 | assert(parts[1] == 'column') 29 | new_on = set() 30 | for x, y in on_pixels: 31 | if x == n: 32 | y = (y + by) % 6 33 | new_on.add((x, y)) 34 | on_pixels = new_on 35 | 36 | return on_pixels 37 | 38 | def part1(s): 39 | answer = len(apply_instructions(s)) 40 | 41 | lib.aoc.give_answer(2016, 8, 1, answer) 42 | 43 | def part2(s): 44 | answer = lib.ocr.parse_coord_set(apply_instructions(s)) 45 | 46 | lib.aoc.give_answer(2016, 8, 2, answer) 47 | 48 | INPUT = lib.aoc.get_input(2016, 8) 49 | part1(INPUT) 50 | part2(INPUT) 51 | -------------------------------------------------------------------------------- /2016/09/solution.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | import lib.aoc 4 | 5 | def decompress(s): 6 | out = '' 7 | 8 | while s: 9 | idx = s.find('(') 10 | if idx == -1: 11 | assert(')' not in s) 12 | out += s 13 | s = '' 14 | continue 15 | end_idx = s.find(')') 16 | assert(end_idx > idx) 17 | 18 | out += s[:idx] 19 | marker = s[idx+1:end_idx] 20 | s = s[end_idx+1:] 21 | 22 | l, c = marker.split('x') 23 | l = int(l) 24 | c = int(c) 25 | 26 | take = s[:l] 27 | s = s[l:] 28 | for _ in range(c): 29 | out += take 30 | 31 | return out 32 | 33 | def part1(s): 34 | answer = len(decompress(s)) 35 | 36 | lib.aoc.give_answer(2016, 9, 1, answer) 37 | 38 | @functools.cache 39 | def decompress_2_length(s): 40 | length = 0 41 | 42 | while s: 43 | idx = s.find('(') 44 | if idx == -1: 45 | assert(')' not in s) 46 | length += len(s) 47 | s = '' 48 | continue 49 | 50 | end_idx = s.find(')') 51 | assert(end_idx > idx) 52 | 53 | length += idx 54 | marker = s[idx+1:end_idx] 55 | s = s[end_idx+1:] 56 | 57 | l, c = marker.split('x') 58 | l = int(l) 59 | c = int(c) 60 | 61 | take = s[:l] 62 | s = s[l:] 63 | 64 | length += c * decompress_2_length(take) 65 | 66 | return length 67 | 68 | def part2(s): 69 | answer = decompress_2_length(s) 70 | 71 | lib.aoc.give_answer(2016, 9, 2, answer) 72 | 73 | INPUT = lib.aoc.get_input(2016, 9) 74 | part1(INPUT) 75 | part2(INPUT) 76 | -------------------------------------------------------------------------------- /2016/10/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import lib.aoc 4 | 5 | def parse_input(s): 6 | bots = collections.defaultdict(list) 7 | instructions = collections.defaultdict(list) 8 | 9 | for line in s.splitlines(): 10 | parts = line.split() 11 | if 'bot' == parts[0]: 12 | low_out = parts[5] == 'output' 13 | low_target = int(parts[6]) 14 | high_out = parts[10] == 'output' 15 | high_target = int(parts[11]) 16 | instructions[int(parts[1])].append((low_out, low_target, 17 | high_out, high_target)) 18 | else: 19 | assert(parts[0] == 'value') 20 | val = int(parts[1]) 21 | target = int(parts[5]) 22 | bots[target].append(val) 23 | 24 | return bots, instructions 25 | 26 | def part1(s): 27 | bots, instructions = parse_input(s) 28 | 29 | while True: 30 | for bot, values in bots.items(): 31 | if len(values) == 2: 32 | break 33 | 34 | assert(len(values) == 2) 35 | low, high = min(values), max(values) 36 | if low == 17 and high == 61: 37 | answer = bot 38 | break 39 | 40 | del bots[bot] 41 | low_out, low_target, high_out, high_target = instructions[bot].pop(0) 42 | 43 | if not low_out: 44 | bots[low_target].append(low) 45 | if not high_out: 46 | bots[high_target].append(high) 47 | 48 | lib.aoc.give_answer(2016, 10, 1, answer) 49 | 50 | def part2(s): 51 | bots, instructions = parse_input(s) 52 | outputs = {} 53 | 54 | while True: 55 | done = True 56 | for bot, values in bots.items(): 57 | if len(values) == 2: 58 | done = False 59 | break 60 | 61 | if done: 62 | break 63 | 64 | low, high = min(values), max(values) 65 | 66 | del bots[bot] 67 | low_out, low_target, high_out, high_target = instructions[bot].pop(0) 68 | 69 | if low_out: 70 | outputs[low_target] = low 71 | else: 72 | bots[low_target].append(low) 73 | if high_out: 74 | outputs[high_target] = high 75 | else: 76 | bots[high_target].append(high) 77 | 78 | answer = outputs[0] * outputs[1] * outputs[2] 79 | 80 | lib.aoc.give_answer(2016, 10, 2, answer) 81 | 82 | INPUT = lib.aoc.get_input(2016, 10) 83 | part1(INPUT) 84 | part2(INPUT) 85 | -------------------------------------------------------------------------------- /2016/12/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def parse_instructions(s): 4 | instructions = [] 5 | 6 | for line in s.splitlines(): 7 | parts = line.split() 8 | for i in range(1, len(parts)): 9 | if parts[i] not in 'abcd': 10 | parts[i] = int(parts[i]) 11 | instructions.append(parts) 12 | 13 | return instructions 14 | 15 | def run_code(instructions, registers): 16 | def get_val(val): 17 | if isinstance(val, str): 18 | val = registers[val] 19 | return val 20 | 21 | idx = 0 22 | while idx < len(instructions): 23 | inst = instructions[idx] 24 | 25 | if inst[0] == 'jnz': 26 | if get_val(inst[1]) != 0: 27 | idx += inst[2] 28 | else: 29 | idx += 1 30 | continue 31 | 32 | if inst[0] == 'inc': 33 | registers[inst[1]] += 1 34 | elif inst[0] == 'dec': 35 | registers[inst[1]] -= 1 36 | elif inst[0] == 'cpy': 37 | registers[inst[2]] = get_val(inst[1]) 38 | else: 39 | assert(False) 40 | idx += 1 41 | 42 | return registers 43 | 44 | def part1(s): 45 | registers = { 46 | name: 0 47 | for name in 'abcd' 48 | } 49 | 50 | run_code(parse_instructions(s), registers) 51 | 52 | answer = registers['a'] 53 | 54 | lib.aoc.give_answer(2016, 12, 1, answer) 55 | 56 | def part2(s): 57 | registers = { 58 | name: 0 59 | for name in 'abd' 60 | } 61 | registers['c'] = 1 62 | 63 | run_code(parse_instructions(s), registers) 64 | 65 | answer = registers['a'] 66 | 67 | lib.aoc.give_answer(2016, 12, 2, answer) 68 | 69 | INPUT = lib.aoc.get_input(2016, 12) 70 | part1(INPUT) 71 | part2(INPUT) 72 | -------------------------------------------------------------------------------- /2016/13/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.graph 3 | import lib.lazy_dict 4 | 5 | def parse_graph(s): 6 | n = int(s) 7 | 8 | def grid_cell_fn(coord): 9 | x, y = coord 10 | ones = bin(x*x + 3*x + 2*x*y + y + y*y + n).count('1') 11 | return ones % 2 == 0 12 | 13 | grid = lib.lazy_dict.make_lazy_dict(grid_cell_fn) 14 | 15 | def neighbor_fn(c): 16 | x, y = c 17 | if x > 0 and grid[x-1, y]: 18 | yield (x-1, y), 1 19 | if y > 0 and grid[x, y-1]: 20 | yield (x, y-1), 1 21 | if grid[x+1, y]: 22 | yield (x+1, y), 1 23 | if grid[x, y+1]: 24 | yield (x, y+1), 1 25 | 26 | return lib.graph.make_lazy_graph(neighbor_fn) 27 | 28 | def part1(s): 29 | graph = parse_graph(s) 30 | 31 | answer = lib.graph.dijkstra_length(graph, 32 | (1, 1), 33 | (31, 39)) 34 | 35 | lib.aoc.give_answer(2016, 13, 1, answer) 36 | 37 | def part2(s): 38 | graph = parse_graph(s) 39 | 40 | answer = len(list(lib.graph.all_reachable(graph, (1, 1), 50))) 41 | answer += 1 # all_reachable excludes the start location 42 | 43 | lib.aoc.give_answer(2016, 13, 2, answer) 44 | 45 | INPUT = lib.aoc.get_input(2016, 13) 46 | part1(INPUT) 47 | part2(INPUT) 48 | -------------------------------------------------------------------------------- /2016/14/solution.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | import lib.aoc 4 | 5 | def gen_hashes(s): 6 | idx = 0 7 | while True: 8 | idx += 1 9 | h = hashlib.md5((s + str(idx)).encode()).hexdigest() 10 | yield h 11 | 12 | def gen_keys(hashes): 13 | window = [] 14 | 15 | for _ in range(1000): 16 | window.append(next(hashes)) 17 | 18 | idx = 0 19 | while True: 20 | idx += 1 21 | h = window.pop(0) 22 | window.append(next(hashes)) 23 | 24 | triple = None 25 | for j in range(len(h)-2): 26 | if h[j] == h[j+1] == h[j+2]: 27 | triple = h[j] 28 | break 29 | 30 | if triple is None: 31 | continue 32 | 33 | target = triple * 5 34 | if any(target in test_h 35 | for test_h in window): 36 | yield h, idx 37 | 38 | def part1(s): 39 | keys = gen_keys(gen_hashes(s)) 40 | 41 | for _ in range(64): 42 | _, answer = next(keys) 43 | 44 | lib.aoc.give_answer(2016, 14, 1, answer) 45 | 46 | def gen_superhashes(s): 47 | idx = 0 48 | while True: 49 | idx += 1 50 | h = hashlib.md5((s + str(idx)).encode()).hexdigest() 51 | for _ in range(2016): 52 | h = hashlib.md5(h.encode()).hexdigest() 53 | yield h 54 | 55 | def part2(s): 56 | keys = gen_keys(gen_superhashes(s)) 57 | 58 | for _ in range(64): 59 | h, answer = next(keys) 60 | 61 | lib.aoc.give_answer(2016, 14, 2, answer) 62 | 63 | INPUT = lib.aoc.get_input(2016, 14) 64 | part1(INPUT) 65 | part2(INPUT) 66 | -------------------------------------------------------------------------------- /2016/15/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.math 3 | 4 | def parse_congruencies(s): 5 | for idx, line in enumerate(s.splitlines()): 6 | parts = line.split() 7 | yield int(parts[3]), int(parts[11][:-1]) + idx + 1 8 | 9 | def part1(s): 10 | answer = lib.math.offset_chinese_remainder(parse_congruencies(s)) 11 | 12 | lib.aoc.give_answer(2016, 15, 1, answer) 13 | 14 | def part2(s): 15 | s += '\nDisc n+1 has 11 positions; at time=0, it is at position 0.' 16 | 17 | answer = lib.math.offset_chinese_remainder(parse_congruencies(s)) 18 | 19 | lib.aoc.give_answer(2016, 15, 2, answer) 20 | 21 | INPUT = lib.aoc.get_input(2016, 15) 22 | part1(INPUT) 23 | part2(INPUT) 24 | -------------------------------------------------------------------------------- /2016/16/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def solve(s, disk): 4 | # Expand 5 | while len(s) < disk: 6 | s = s + '0' + s[::-1].translate(str.maketrans('01', '10')) 7 | 8 | s = s[:disk] 9 | 10 | # Checksum 11 | while len(s) % 2 == 0: 12 | s = ''.join('1' if a == b else '0' 13 | for a, b in zip(s[::2], s[1::2])) 14 | 15 | return s 16 | 17 | def part1(s): 18 | answer = solve(s, 272) 19 | 20 | lib.aoc.give_answer(2016, 16, 1, answer) 21 | 22 | def part2(s): 23 | answer = solve(s, 35651584) 24 | 25 | lib.aoc.give_answer(2016, 16, 2, answer) 26 | 27 | INPUT = lib.aoc.get_input(2016, 16) 28 | part1(INPUT) 29 | part2(INPUT) 30 | -------------------------------------------------------------------------------- /2016/17/solution.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | import lib.aoc 4 | 5 | def generate_all_paths(s, width, height): 6 | OPEN_DOOR = 'bcdef' 7 | 8 | states = [(1, 1, '')] 9 | 10 | while True: 11 | new_states = [] 12 | 13 | for x, y, path in states: 14 | if x == width and y == height: 15 | yield path 16 | continue 17 | 18 | h = hashlib.md5((s + path).encode()).hexdigest() 19 | up, down, left, right = h[:4] 20 | 21 | if x > 1 and left in OPEN_DOOR: 22 | new_states.append((x-1, y, path+'L')) 23 | if x < width and right in OPEN_DOOR: 24 | new_states.append((x+1, y, path+'R')) 25 | if y > 1 and up in OPEN_DOOR: 26 | new_states.append((x, y-1, path+'U')) 27 | if y < height and down in OPEN_DOOR: 28 | new_states.append((x, y+1, path+'D')) 29 | 30 | states = new_states 31 | 32 | if len(states) == 0: 33 | return 34 | 35 | def part1(s): 36 | answer = min(generate_all_paths(s, 4, 4), key=len) 37 | 38 | lib.aoc.give_answer(2016, 17, 1, answer) 39 | 40 | def part2(s): 41 | answer = max(map(len, generate_all_paths(s, 4, 4))) 42 | 43 | lib.aoc.give_answer(2016, 17, 2, answer) 44 | 45 | INPUT = lib.aoc.get_input(2016, 17) 46 | part1(INPUT) 47 | part2(INPUT) 48 | -------------------------------------------------------------------------------- /2016/18/solution.py: -------------------------------------------------------------------------------- 1 | import gmpy2 2 | 3 | import lib.aoc 4 | 5 | def solve(s, total_rows): 6 | row = 0 7 | mask = 0 8 | 9 | # Convert the row into a binary number with 1's marking traps 10 | for c in s: 11 | row <<= 1 12 | mask <<= 1 13 | mask += 1 14 | if c == '^': 15 | row += 1 16 | 17 | # Count trap tile bits 18 | # TODO: Maybe row.bit_count() with Python 3.10? 19 | trap_tiles = gmpy2.popcount(row) 20 | 21 | for _ in range(total_rows-1): 22 | # Each iteration only cares about how the left and right sides interact 23 | # A tile is a trap iff the previous left and right tiles are opposites 24 | # Since safe tiles are 0, this is a simple XOR operation on shifted 25 | # rows, with a mask to limit the width of the rows 26 | left = row >> 1 27 | right = row << 1 28 | row = (left ^ right) & mask 29 | 30 | trap_tiles += gmpy2.popcount(row) 31 | 32 | # We care about safe tiles :) 33 | return len(s) * total_rows - trap_tiles 34 | 35 | def part1(s): 36 | answer = solve(s, 40) 37 | 38 | lib.aoc.give_answer(2016, 18, 1, answer) 39 | 40 | def part2(s): 41 | answer = solve(s, 400000) 42 | 43 | lib.aoc.give_answer(2016, 18, 2, answer) 44 | 45 | INPUT = lib.aoc.get_input(2016, 18) 46 | part1(INPUT) 47 | part2(INPUT) 48 | -------------------------------------------------------------------------------- /2016/20/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def parse_input(s): 4 | for line in s.splitlines(): 5 | a, b = line.split('-') 6 | yield int(a), int(b) 7 | 8 | def part1(s): 9 | answer = 0 10 | for low, high in sorted(parse_input(s)): 11 | if answer < low: 12 | break 13 | answer = max(answer, high+1) 14 | 15 | lib.aoc.give_answer(2016, 20, 1, answer) 16 | 17 | def part2(s): 18 | answer = 0 19 | maybe_allowed = 0 20 | 21 | for low, high in sorted(parse_input(s)): 22 | if maybe_allowed < low: 23 | answer += low - maybe_allowed 24 | maybe_allowed = max(maybe_allowed, high+1) 25 | 26 | answer += 4294967296 - maybe_allowed 27 | 28 | lib.aoc.give_answer(2016, 20, 2, answer) 29 | 30 | INPUT = lib.aoc.get_input(2016, 20) 31 | part1(INPUT) 32 | part2(INPUT) 33 | -------------------------------------------------------------------------------- /2016/24/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.graph 3 | import lib.grid 4 | 5 | def get_point_of_interest_graph(s): 6 | grid = lib.grid.FixedGrid.parse(s) 7 | 8 | poi_graph = {} 9 | 10 | for start, v in grid.items(): 11 | if v not in '#.': 12 | destinations = [] 13 | 14 | def neighbor_fn(pos): 15 | if pos != start and grid[pos] != '.': 16 | return 17 | for n in grid.neighbors(*pos): 18 | nv = grid[n] 19 | if nv == '#': 20 | continue 21 | yield n, 1 22 | 23 | for pos, dist in lib.graph.all_reachable(lib.graph.make_lazy_graph(neighbor_fn), 24 | start): 25 | if grid[pos] == '.': 26 | continue 27 | destinations.append((int(grid[pos]), dist)) 28 | 29 | poi_graph[int(v)] = destinations 30 | 31 | return poi_graph 32 | 33 | def solve(s, required_end_node=None): 34 | poi_graph = get_point_of_interest_graph(s) 35 | 36 | def neighbor_fn(state): 37 | pos, seen = state 38 | for neighbor, dist in poi_graph[pos]: 39 | new_state = (neighbor, tuple(sorted(set(seen + (neighbor,))))) 40 | yield new_state, dist 41 | 42 | graph = lib.graph.make_lazy_graph(neighbor_fn) 43 | 44 | ALL_POINTS_OF_INTEREST = tuple(sorted(poi_graph.keys())) 45 | 46 | START = (0, (0,)) 47 | END = (0, ALL_POINTS_OF_INTEREST) 48 | 49 | def end_fn(state): 50 | pos, seen = state 51 | if required_end_node is not None and required_end_node != pos: 52 | return False 53 | return seen == ALL_POINTS_OF_INTEREST 54 | 55 | return lib.graph.dijkstra_length_fuzzy_end(graph, START, end_fn) 56 | 57 | def part1(s): 58 | answer = solve(s) 59 | 60 | lib.aoc.give_answer(2016, 24, 1, answer) 61 | 62 | def part2(s): 63 | answer = solve(s, 0) 64 | 65 | lib.aoc.give_answer(2016, 24, 2, answer) 66 | 67 | INPUT = lib.aoc.get_input(2016, 24) 68 | part1(INPUT) 69 | part2(INPUT) 70 | -------------------------------------------------------------------------------- /2017/01/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def part1(s): 4 | nums = list(map(int, s)) 5 | 6 | answer = 0 7 | 8 | for a, b in zip(nums, nums[1:] + [nums[0]]): 9 | if a == b: 10 | answer += a 11 | 12 | lib.aoc.give_answer(2017, 1, 1, answer) 13 | 14 | def part2(s): 15 | nums = list(map(int, s)) 16 | 17 | rotated = nums[len(nums)//2:] + nums[:len(nums)//2] 18 | 19 | answer = 0 20 | 21 | for a, b in zip(nums, rotated): 22 | if a == b: 23 | answer += a 24 | 25 | lib.aoc.give_answer(2017, 1, 2, answer) 26 | 27 | INPUT = lib.aoc.get_input(2017, 1) 28 | part1(INPUT) 29 | part2(INPUT) 30 | -------------------------------------------------------------------------------- /2017/02/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def part1(s): 4 | answer = 0 5 | 6 | for line in s.splitlines(): 7 | nums = list(map(int, line.split())) 8 | 9 | answer += max(nums) - min(nums) 10 | 11 | lib.aoc.give_answer(2017, 2, 1, answer) 12 | 13 | def part2(s): 14 | answer = 0 15 | 16 | for line in s.splitlines(): 17 | nums = list(map(int, line.split())) 18 | 19 | for a in nums: 20 | for b in nums: 21 | if a % b == 0 and a != b: 22 | answer += a // b 23 | 24 | lib.aoc.give_answer(2017, 2, 2, answer) 25 | 26 | INPUT = lib.aoc.get_input(2017, 2) 27 | part1(INPUT) 28 | part2(INPUT) 29 | -------------------------------------------------------------------------------- /2017/03/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def gen_coords(): 4 | x, y = 0, 0 5 | dist = 1 6 | 7 | yield x, y 8 | 9 | while True: 10 | for _ in range(dist): 11 | x += 1 12 | yield x, y 13 | for _ in range(dist): 14 | y += 1 15 | yield x, y 16 | dist += 1 17 | for _ in range(dist): 18 | x -= 1 19 | yield x, y 20 | for _ in range(dist): 21 | y -= 1 22 | yield x, y 23 | dist += 1 24 | 25 | def part1(s): 26 | n = int(s) 27 | 28 | for idx, (x, y) in enumerate(gen_coords()): 29 | if idx+1 == n: 30 | answer = abs(x) + abs(y) 31 | break 32 | 33 | lib.aoc.give_answer(2017, 3, 1, answer) 34 | 35 | def part2(s): 36 | n = int(s) 37 | 38 | written = { 39 | (0, 0): 1 40 | } 41 | 42 | for x, y in gen_coords(): 43 | val = sum(written.get((x+dx, y+dy), 0) 44 | for dx in (-1, 0, 1) 45 | for dy in (-1, 0, 1)) 46 | written[x, y] = val 47 | if val > n: 48 | answer = val 49 | break 50 | 51 | lib.aoc.give_answer(2017, 3, 2, answer) 52 | 53 | INPUT = lib.aoc.get_input(2017, 3) 54 | part1(INPUT) 55 | part2(INPUT) 56 | -------------------------------------------------------------------------------- /2017/04/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def parse_input(s): 4 | for line in s.splitlines(): 5 | yield line.split() 6 | 7 | def part1(s): 8 | answer = 0 9 | 10 | for passphrase in parse_input(s): 11 | if len(passphrase) == len(set(passphrase)): 12 | answer += 1 13 | 14 | lib.aoc.give_answer(2017, 4, 1, answer) 15 | 16 | def part2(s): 17 | answer = 0 18 | 19 | for passphrase in parse_input(s): 20 | passphrase = [tuple(sorted(part)) 21 | for part in passphrase] 22 | if len(passphrase) == len(set(passphrase)): 23 | answer += 1 24 | 25 | lib.aoc.give_answer(2017, 4, 2, answer) 26 | 27 | INPUT = lib.aoc.get_input(2017, 4) 28 | part1(INPUT) 29 | part2(INPUT) 30 | -------------------------------------------------------------------------------- /2017/05/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def part1(s): 4 | nums = list(map(int, s.splitlines())) 5 | 6 | answer = 0 7 | idx = 0 8 | while idx < len(nums): 9 | answer += 1 10 | jump = nums[idx] 11 | nums[idx] += 1 12 | idx += jump 13 | 14 | lib.aoc.give_answer(2017, 5, 1, answer) 15 | 16 | def part2(s): 17 | nums = list(map(int, s.splitlines())) 18 | 19 | answer = 0 20 | idx = 0 21 | while idx < len(nums): 22 | answer += 1 23 | jump = nums[idx] 24 | if jump >= 3: 25 | nums[idx] -= 1 26 | else: 27 | nums[idx] += 1 28 | idx += jump 29 | 30 | lib.aoc.give_answer(2017, 5, 2, answer) 31 | 32 | INPUT = lib.aoc.get_input(2017, 5) 33 | part1(INPUT) 34 | part2(INPUT) 35 | -------------------------------------------------------------------------------- /2017/06/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def step(nums): 4 | idx = nums.index(max(nums)) 5 | new = list(nums) 6 | new[idx] = 0 7 | for i in range(nums[idx]): 8 | new[(idx+i+1) % len(nums)] += 1 9 | return tuple(new) 10 | 11 | def part1(s): 12 | nums = tuple(map(int, s.split())) 13 | 14 | seen = {nums} 15 | 16 | while True: 17 | nums = step(nums) 18 | if nums in seen: 19 | answer = len(seen) 20 | break 21 | seen.add(nums) 22 | 23 | lib.aoc.give_answer(2017, 6, 1, answer) 24 | 25 | def part2(s): 26 | nums = tuple(map(int, s.split())) 27 | 28 | seen_steps = { 29 | nums: 0 30 | } 31 | 32 | while True: 33 | nums = step(nums) 34 | if nums in seen_steps: 35 | answer = len(seen_steps) - seen_steps[nums] 36 | break 37 | seen_steps[nums] = len(seen_steps) 38 | 39 | lib.aoc.give_answer(2017, 6, 2, answer) 40 | 41 | INPUT = lib.aoc.get_input(2017, 6) 42 | part1(INPUT) 43 | part2(INPUT) 44 | -------------------------------------------------------------------------------- /2017/07/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import lib.aoc 4 | 5 | def parse_tree(s): 6 | tree = {} 7 | referenced = set() 8 | 9 | for line in s.splitlines(): 10 | parts = line.split(' -> ') 11 | name, weight = parts[0].split() 12 | assert(weight[0] == '(' and weight[-1] == ')') 13 | weight = int(weight[1:-1]) 14 | if len(parts) == 2: 15 | held = parts[1].split(', ') 16 | else: 17 | held = [] 18 | tree[name] = (weight, held) 19 | referenced.update(held) 20 | 21 | root = set(tree.keys()) - referenced 22 | assert(len(root) == 1) 23 | return list(root)[0], tree 24 | 25 | def part1(s): 26 | answer, _ = parse_tree(s) 27 | 28 | lib.aoc.give_answer(2017, 7, 1, answer) 29 | 30 | def part2(s): 31 | root, tree = parse_tree(s) 32 | 33 | fixes = {} 34 | 35 | def fix_recursive(name): 36 | weight, held = tree[name] 37 | if len(held) == 0: 38 | return weight 39 | 40 | sub_weights = list(map(fix_recursive, held)) 41 | 42 | weight_counts = collections.Counter(sub_weights) 43 | counts = weight_counts.most_common() 44 | if len(counts) != 1: 45 | assert(len(counts) == 2) 46 | good_weight = counts[0][0] 47 | bad_weight = counts[1][0] 48 | assert(counts[1][1] == 1) 49 | idx = sub_weights.index(bad_weight) 50 | diff = good_weight - bad_weight 51 | changed_node = held[idx] 52 | fixes[changed_node] = tree[changed_node][0] + diff 53 | sub_weights[idx] = good_weight 54 | 55 | this_weight = weight + sum(sub_weights) 56 | return this_weight 57 | 58 | fix_recursive(root) 59 | 60 | assert(len(fixes) == 1) 61 | 62 | answer = list(fixes.values())[0] 63 | 64 | lib.aoc.give_answer(2017, 7, 2, answer) 65 | 66 | INPUT = lib.aoc.get_input(2017, 7) 67 | part1(INPUT) 68 | part2(INPUT) 69 | -------------------------------------------------------------------------------- /2017/08/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def parse_input(s): 4 | for line in s.splitlines(): 5 | var1, op, val, _, var2, cond, target = line.split() 6 | val = int(val) 7 | target = int(target) 8 | yield var1, op, val, var2, cond, target 9 | 10 | CONDITIONS = { 11 | '>': lambda a, b: a > b, 12 | '<': lambda a, b: a < b, 13 | '>=': lambda a, b: a >= b, 14 | '<=': lambda a, b: a <= b, 15 | '==': lambda a, b: a == b, 16 | '!=': lambda a, b: a != b, 17 | } 18 | 19 | OPERATIONS = { 20 | 'inc': lambda a, b: a + b, 21 | 'dec': lambda a, b: a - b, 22 | } 23 | 24 | def part1(s): 25 | registers = {} 26 | 27 | for var1, op, val, var2, cond, target in parse_input(s): 28 | if CONDITIONS[cond](registers.get(var2, 0), target): 29 | val = OPERATIONS[op](registers.get(var1, 0), val) 30 | registers[var1] = val 31 | 32 | answer = max(registers.values()) 33 | 34 | lib.aoc.give_answer(2017, 8, 1, answer) 35 | 36 | def part2(s): 37 | registers = {} 38 | 39 | answer = 0 40 | 41 | for var1, op, val, var2, cond, target in parse_input(s): 42 | if CONDITIONS[cond](registers.get(var2, 0), target): 43 | val = OPERATIONS[op](registers.get(var1, 0), val) 44 | answer = max(answer, val) 45 | registers[var1] = val 46 | 47 | lib.aoc.give_answer(2017, 8, 2, answer) 48 | 49 | INPUT = lib.aoc.get_input(2017, 8) 50 | part1(INPUT) 51 | part2(INPUT) 52 | -------------------------------------------------------------------------------- /2017/09/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def solve(s): 4 | assert(s[0] == '{' and s[-1] == '}') 5 | 6 | score = 0 7 | depth = 0 8 | 9 | ignoring = False 10 | garbage = False 11 | discarded = 0 12 | 13 | for c in s: 14 | if ignoring: 15 | assert(garbage) 16 | ignoring = False 17 | continue 18 | if garbage: 19 | if c == '>': 20 | garbage = False 21 | continue 22 | if c == '!': 23 | ignoring = True 24 | continue 25 | discarded += 1 26 | continue 27 | if c == '<': 28 | garbage = True 29 | continue 30 | if c == ',': 31 | continue 32 | if c == '{': 33 | depth += 1 34 | continue 35 | if c == '}': 36 | score += depth 37 | depth -= 1 38 | continue 39 | assert(False) 40 | 41 | assert(depth == 0) 42 | 43 | return score, discarded 44 | 45 | def part1(s): 46 | answer, _ = solve(s) 47 | 48 | lib.aoc.give_answer(2017, 9, 1, answer) 49 | 50 | def part2(s): 51 | _, answer = solve(s) 52 | 53 | lib.aoc.give_answer(2017, 9, 2, answer) 54 | 55 | INPUT = lib.aoc.get_input(2017, 9) 56 | part1(INPUT) 57 | part2(INPUT) 58 | -------------------------------------------------------------------------------- /2017/10/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def part1(s): 4 | nums = list(range(256)) 5 | 6 | pos, skip = 0, 0 7 | 8 | for l in map(int, s.split(',')): 9 | if pos+l < 256: 10 | # Simple reverse 11 | nums[pos:pos+l] = nums[pos:pos+l][::-1] 12 | else: 13 | # Complex reverse (loop around) 14 | to_rev = nums[pos:] + nums[:(pos+l) & 0xFF] 15 | to_rev = to_rev[::-1] 16 | nums[pos:] = to_rev[:256-pos] 17 | nums[:(pos+l) & 0xFF] = to_rev[256-pos:] 18 | 19 | pos = (pos + l + skip) & 0xFF 20 | skip = (skip+1) & 0xFF 21 | 22 | answer = nums[0] * nums[1] 23 | 24 | lib.aoc.give_answer(2017, 10, 1, answer) 25 | 26 | def knot_hash(s): 27 | nums = list(range(256)) 28 | lengths = list(map(ord, s)) + [17, 31, 73, 47, 23] 29 | 30 | pos, skip = 0, 0 31 | 32 | for _ in range(64): 33 | for l in lengths: 34 | if pos+l < 256: 35 | # Simple reverse 36 | nums[pos:pos+l] = nums[pos:pos+l][::-1] 37 | else: 38 | # Complex reverse (loop around) 39 | to_rev = nums[pos:] + nums[:(pos+l) & 0xFF] 40 | to_rev = to_rev[::-1] 41 | nums[pos:] = to_rev[:256-pos] 42 | nums[:(pos+l) & 0xFF] = to_rev[256-pos:] 43 | 44 | pos = (pos + l + skip) & 0xFF 45 | skip = (skip+1) & 0xFF 46 | 47 | res = 0 48 | for i in range(0, 256, 16): 49 | val = 0 50 | for v in nums[i:i+16]: 51 | val = val ^ v 52 | res = (res << 8) + val 53 | 54 | return res 55 | 56 | def part2(s): 57 | answer = hex(knot_hash(s))[2:].zfill(32) 58 | 59 | lib.aoc.give_answer(2017, 10, 2, answer) 60 | 61 | INPUT = lib.aoc.get_input(2017, 10) 62 | part1(INPUT) 63 | part2(INPUT) 64 | -------------------------------------------------------------------------------- /2017/11/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.hex_coord 3 | 4 | def part1(s): 5 | pos = lib.hex_coord.NSHexCoord() 6 | 7 | for d in s.split(','): 8 | pos = pos.move(d) 9 | 10 | answer = pos.steps_to(lib.hex_coord.NSHexCoord()) 11 | 12 | lib.aoc.give_answer(2017, 11, 1, answer) 13 | 14 | def part2(s): 15 | answer = 0 16 | pos = lib.hex_coord.NSHexCoord() 17 | 18 | for d in s.split(','): 19 | pos = pos.move(d) 20 | answer = max(answer, pos.steps_to(lib.hex_coord.NSHexCoord())) 21 | 22 | lib.aoc.give_answer(2017, 11, 2, answer) 23 | 24 | INPUT = lib.aoc.get_input(2017, 11) 25 | part1(INPUT) 26 | part2(INPUT) 27 | -------------------------------------------------------------------------------- /2017/12/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.graph 3 | 4 | def parse_graph(s): 5 | graph = {} 6 | 7 | for line in s.splitlines(): 8 | node, neighbors = line.split(' <-> ') 9 | graph[int(node)] = list(map(int, neighbors.split(','))) 10 | 11 | return graph 12 | 13 | def part1(s): 14 | graph = lib.graph.to_distance_graph(parse_graph(s)) 15 | 16 | answer = 1 + len(list(lib.graph.all_reachable(graph, 0))) 17 | 18 | lib.aoc.give_answer(2017, 12, 1, answer) 19 | 20 | def part2(s): 21 | graph = parse_graph(s) 22 | unreached = set(graph.keys()) # Extract before converting to a lazy graph 23 | 24 | graph = lib.graph.to_distance_graph(graph) 25 | 26 | answer = 0 27 | 28 | while unreached: 29 | answer += 1 30 | start = list(unreached)[0] 31 | group = lib.graph.all_reachable(graph, start) 32 | unreached.remove(start) 33 | unreached -= set(lib.graph.node_dist_list_to_nodes(group)) 34 | 35 | lib.aoc.give_answer(2017, 12, 2, answer) 36 | 37 | INPUT = lib.aoc.get_input(2017, 12) 38 | part1(INPUT) 39 | part2(INPUT) 40 | -------------------------------------------------------------------------------- /2017/13/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.math 3 | 4 | def parse_input(s): 5 | for line in s.splitlines(): 6 | depth, layers = tuple(map(int, line.split(':'))) 7 | cycle = 2 * (layers - 1) 8 | if cycle == 0: 9 | cycle = 1 10 | yield depth, layers, cycle 11 | 12 | def part1(s): 13 | answer = 0 14 | 15 | for depth, layers, cycle in parse_input(s): 16 | if depth % cycle == 0: 17 | # Caught! 18 | answer += depth * layers 19 | 20 | lib.aoc.give_answer(2017, 13, 1, answer) 21 | 22 | def part2(s): 23 | incongruencies = [(cycle, depth % cycle) 24 | for depth, _, cycle in parse_input(s)] 25 | 26 | answer = lib.math.offset_chinese_remainder_incongruence(incongruencies) 27 | 28 | lib.aoc.give_answer(2017, 13, 2, answer) 29 | 30 | INPUT = lib.aoc.get_input(2017, 13) 31 | part1(INPUT) 32 | part2(INPUT) 33 | -------------------------------------------------------------------------------- /2017/14/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def knot_hash(s): 4 | nums = list(range(256)) 5 | lengths = list(map(ord, s)) + [17, 31, 73, 47, 23] 6 | 7 | pos, skip = 0, 0 8 | 9 | for _ in range(64): 10 | for l in lengths: 11 | if pos+l < 256: 12 | # Simple reverse 13 | nums[pos:pos+l] = nums[pos:pos+l][::-1] 14 | else: 15 | # Complex reverse (loop around) 16 | to_rev = nums[pos:] + nums[:(pos+l) & 0xFF] 17 | to_rev = to_rev[::-1] 18 | nums[pos:] = to_rev[:256-pos] 19 | nums[:(pos+l) & 0xFF] = to_rev[256-pos:] 20 | 21 | pos = (pos + l + skip) & 0xFF 22 | skip = (skip+1) & 0xFF 23 | 24 | res = 0 25 | for i in range(0, 256, 16): 26 | val = 0 27 | for v in nums[i:i+16]: 28 | val = val ^ v 29 | res = (res << 8) + val 30 | 31 | return res 32 | 33 | def part1(s): 34 | rows = [knot_hash(f'{s}-{row_n}') for row_n in range(128)] 35 | 36 | answer = 0 37 | for row in rows: 38 | answer += bin(row).count('1') 39 | 40 | lib.aoc.give_answer(2017, 14, 1, answer) 41 | 42 | def part2(s): 43 | rows = [knot_hash(f'{s}-{row_n}') for row_n in range(128)] 44 | 45 | on_positions = set() 46 | 47 | for y, row in enumerate(rows): 48 | for x, c in enumerate(bin(row)[2:].zfill(128)): 49 | if c == '1': 50 | on_positions.add((x, y)) 51 | 52 | answer = 0 53 | while on_positions: 54 | answer += 1 55 | 56 | to_process = [list(on_positions)[0]] 57 | on_positions.remove(to_process[0]) 58 | 59 | while to_process: 60 | x, y = to_process.pop(-1) 61 | for n in [(x-1, y), 62 | (x+1, y), 63 | (x, y-1), 64 | (x, y+1)]: 65 | if n in on_positions: 66 | on_positions.remove(n) 67 | to_process.append(n) 68 | 69 | lib.aoc.give_answer(2017, 14, 2, answer) 70 | 71 | INPUT = lib.aoc.get_input(2017, 14) 72 | part1(INPUT) 73 | part2(INPUT) 74 | -------------------------------------------------------------------------------- /2017/15/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def sequence_generator(val, fact, mod): 4 | while True: 5 | val = (val * fact) % mod 6 | yield val 7 | 8 | def parse_sequences(s): 9 | a, b = s.splitlines() 10 | a = int(a.split()[-1]) 11 | b = int(b.split()[-1]) 12 | 13 | MOD = 2147483647 14 | 15 | return sequence_generator(a, 16807, MOD), sequence_generator(b, 48271, MOD) 16 | 17 | def judge(a, b, to_check): 18 | BIT_MASK = 0xFFFF # 16 bits 19 | 20 | matched = 0 21 | 22 | for _ in range(to_check): 23 | if next(a) & BIT_MASK == next(b) & BIT_MASK: 24 | matched += 1 25 | 26 | return matched 27 | 28 | def part1(s): 29 | a, b = parse_sequences(s) 30 | answer = judge(a, b, 40000000) 31 | 32 | lib.aoc.give_answer(2017, 15, 1, answer) 33 | 34 | def part2(s): 35 | a, b = parse_sequences(s) 36 | 37 | a = filter(lambda val: val % 4 == 0, a) 38 | b = filter(lambda val: val % 8 == 0, b) 39 | 40 | answer = judge(a, b, 5000000) 41 | 42 | lib.aoc.give_answer(2017, 15, 2, answer) 43 | 44 | INPUT = lib.aoc.get_input(2017, 15) 45 | part1(INPUT) 46 | part2(INPUT) 47 | -------------------------------------------------------------------------------- /2017/16/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def dance(order, s): 4 | for inst in s.split(','): 5 | if inst[0] == 's': 6 | x = int(inst[1:]) 7 | order = order[-x:] + order[:-x] 8 | elif inst[0] == 'x': 9 | a, b = sorted(map(int, inst[1:].split('/'))) 10 | order = order[:a] + order[b] + order[a+1:b] + order[a] + order[b+1:] 11 | elif inst[0] == 'p': 12 | a = inst[1] 13 | assert(inst[2] == '/') 14 | b = inst[3] 15 | order = order.translate(str.maketrans(a+b, b+a)) 16 | else: 17 | assert(False) 18 | 19 | return order 20 | 21 | def part1(s): 22 | answer = dance('abcdefghijklmnop', s) 23 | 24 | lib.aoc.give_answer(2017, 16, 1, answer) 25 | 26 | def part2(s): 27 | answer = 'abcdefghijklmnop' 28 | 29 | order_to_iter = { 30 | answer: 0 31 | } 32 | 33 | while True: 34 | answer = dance(answer, s) 35 | if answer in order_to_iter: 36 | break 37 | 38 | order_to_iter[answer] = len(order_to_iter) 39 | 40 | iteration = len(order_to_iter) 41 | cycle = iteration - order_to_iter[answer] 42 | remaining = (1000000000 - iteration) % cycle 43 | 44 | for _ in range(remaining): 45 | answer = dance(answer, s) 46 | 47 | lib.aoc.give_answer(2017, 16, 2, answer) 48 | 49 | INPUT = lib.aoc.get_input(2017, 16) 50 | part1(INPUT) 51 | part2(INPUT) 52 | -------------------------------------------------------------------------------- /2017/17/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def part1(s): 4 | steps = int(s) 5 | buffer = [0] 6 | 7 | pos = 0 8 | 9 | for n in range(1, 2018): 10 | pos = (pos + steps) % len(buffer) 11 | buffer.insert(pos+1, n) 12 | pos += 1 13 | 14 | answer = buffer[(buffer.index(2017)+1) % len(buffer)] 15 | 16 | lib.aoc.give_answer(2017, 17, 1, answer) 17 | 18 | def part2(s): 19 | steps = int(s) 20 | 21 | pos = 0 22 | length = 1 23 | 24 | n = 1 25 | to_insert = 50000000-1 26 | 27 | while to_insert > 0: 28 | # See if we can do a quick jump-ahead. We only care about insertions 29 | # which land on 0, so we can skip iterations which don't loop around 30 | skip_ahead = (length - 1 - pos) // (steps + 1) 31 | if skip_ahead: 32 | # Insert the chunks so long as they're still pending 33 | skip_ahead = min(skip_ahead, to_insert) 34 | 35 | pos += steps * skip_ahead + skip_ahead 36 | length += skip_ahead 37 | 38 | to_insert -= skip_ahead 39 | n += skip_ahead 40 | continue 41 | 42 | pos = (pos + steps) % length + 1 43 | length += 1 44 | 45 | # If the insert location is 1 then it's the new "after 0" value 46 | if pos == 1: 47 | answer = n 48 | 49 | to_insert -= 1 50 | n += 1 51 | 52 | lib.aoc.give_answer(2017, 17, 2, answer) 53 | 54 | INPUT = lib.aoc.get_input(2017, 17) 55 | part1(INPUT) 56 | part2(INPUT) 57 | -------------------------------------------------------------------------------- /2017/19/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.grid 3 | 4 | def solve(s): 5 | grid = lib.grid.FixedGrid.parse(s) 6 | 7 | pos = None 8 | 9 | for x in range(grid.width): 10 | if grid[x,0] == '|': 11 | pos = (x,0) 12 | break 13 | 14 | assert(pos is not None) 15 | x, y = pos 16 | dx, dy = 0, 1 17 | 18 | seen_letters = '' 19 | steps = 1 # Count the first step onto the board 20 | 21 | while True: 22 | cur = grid[x,y] 23 | if cur == '+': 24 | nx, ny = x+dx, y+dy 25 | if grid[nx,ny] not in '-|': 26 | # Forced turn 27 | good_turn = False 28 | for cand in [(dy, dx), 29 | (-dy, -dx)]: 30 | dx, dy = cand 31 | nx, ny = x+dx, y+dy 32 | if grid[nx,ny] != ' ': 33 | good_turn = True 34 | break 35 | assert(good_turn) 36 | continue 37 | 38 | x, y = x+dx, y+dy 39 | val = grid[x,y] 40 | if val == ' ': 41 | # Reached the end 42 | break 43 | 44 | steps += 1 45 | 46 | if val not in '-|+': 47 | # Letter, accumulate it 48 | seen_letters += val 49 | 50 | return seen_letters, steps 51 | 52 | def part1(s): 53 | answer, _ = solve(s) 54 | 55 | lib.aoc.give_answer(2017, 19, 1, answer) 56 | 57 | def part2(s): 58 | _, answer = solve(s) 59 | 60 | lib.aoc.give_answer(2017, 19, 2, answer) 61 | 62 | INPUT = lib.aoc.get_input(2017, 19) 63 | part1(INPUT) 64 | part2(INPUT) 65 | -------------------------------------------------------------------------------- /2017/21/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | # Stolen from 2020 day 20 4 | def get_all_symmetries(pattern): 5 | grid = [list(line) for line in pattern.split('/')] 6 | 7 | candidates = [] 8 | 9 | # Generate all rotations 10 | for _ in range(4): 11 | last, grid = grid, [l[:] for l in grid] 12 | 13 | for x in range(len(last)): 14 | for y in range(len(last[x])): 15 | grid[x][y] = last[len(grid[x])-y-1][x] 16 | 17 | # Append both the grid and its vertical mirror to the candidate list 18 | # Other mirrorings will be generated automatically by the 4 rotations 19 | candidates.append(grid) 20 | candidates.append(grid[::-1]) 21 | 22 | candidates = ['/'.join(''.join(line) for line in grid) 23 | for grid in candidates] 24 | 25 | return sorted(set(candidates)) 26 | 27 | def solve(s, iterations): 28 | transforms = {} 29 | 30 | for line in s.splitlines(): 31 | before, after = line.split(' => ') 32 | for before in get_all_symmetries(before): 33 | transforms[before] = after 34 | 35 | grid = ['.#.', 36 | '..#', 37 | '###'] 38 | 39 | for _ in range(iterations): 40 | if len(grid) % 2 == 0: 41 | run = 2 42 | else: 43 | run = 3 44 | 45 | next_grid = [] 46 | 47 | for y in range(0, len(grid), run): 48 | row_chunks = [] 49 | for x in range(0, len(grid), run): 50 | block = '/'.join(grid[y+off][x:x+run] 51 | for off in range(run)) 52 | 53 | row_chunks.append(transforms[block].split('/')) 54 | 55 | while row_chunks[0]: 56 | row = '' 57 | for chunk in row_chunks: 58 | row += chunk.pop(0) 59 | next_grid.append(row) 60 | 61 | grid = next_grid 62 | 63 | return '/'.join(grid).count('#') 64 | 65 | def part1(s): 66 | answer = solve(s, 5) 67 | 68 | lib.aoc.give_answer(2017, 21, 1, answer) 69 | 70 | def part2(s): 71 | answer = solve(s, 18) 72 | 73 | lib.aoc.give_answer(2017, 21, 2, answer) 74 | 75 | INPUT = lib.aoc.get_input(2017, 21) 76 | part1(INPUT) 77 | part2(INPUT) 78 | -------------------------------------------------------------------------------- /2017/22/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.grid 3 | 4 | def part1(s): 5 | grid = lib.grid.FixedGrid.parse(s) 6 | 7 | x, y = (grid.width)//2, (grid.height)//2 8 | d = grid.to_dict() 9 | 10 | dx, dy = 0, -1 11 | 12 | answer = 0 13 | 14 | for step in range(10000): 15 | if d.get((x,y), '.') == '.': 16 | dx, dy = dy, -dx # Turn left 17 | d[x,y] = '#' 18 | answer += 1 19 | else: 20 | dx, dy = -dy, dx # Turn right 21 | d[x,y] = '.' 22 | 23 | x += dx 24 | y += dy 25 | 26 | lib.aoc.give_answer(2017, 22, 1, answer) 27 | 28 | def part2(s): 29 | grid = lib.grid.FixedGrid.parse(s) 30 | 31 | x, y = (grid.width)//2, (grid.height)//2 32 | d = grid.to_dict() 33 | 34 | dx, dy = 0, -1 35 | 36 | answer = 0 37 | 38 | for step in range(10000000): 39 | state = d.get((x,y), '.') 40 | if state == '.': # Clean 41 | dx, dy = dy, -dx # Turn left 42 | elif state == 'W': 43 | pass 44 | elif state == '#': # Infected 45 | dx, dy = -dy, dx # Turn right 46 | elif state == 'F': 47 | dx, dy = -dx, -dy 48 | else: 49 | assert(False) 50 | 51 | if state == '.': # Clean 52 | d[x,y] = 'W' 53 | elif state == 'W': 54 | d[x,y] = '#' 55 | answer += 1 56 | elif state == '#': # Infected 57 | d[x,y] = 'F' 58 | elif state == 'F': 59 | d[x,y] = '.' 60 | else: 61 | assert(False) 62 | 63 | x += dx 64 | y += dy 65 | 66 | lib.aoc.give_answer(2017, 22, 2, answer) 67 | 68 | INPUT = lib.aoc.get_input(2017, 22) 69 | part1(INPUT) 70 | part2(INPUT) 71 | -------------------------------------------------------------------------------- /2017/24/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import functools 3 | 4 | import lib.aoc 5 | 6 | def solve(s, maximize_length): 7 | parts = [] 8 | connections = collections.defaultdict(set) 9 | 10 | for idx, line in enumerate(s.splitlines()): 11 | a, b = line.split('/') 12 | a, b = int(a), int(b) 13 | connections[a].add(idx) 14 | connections[b].add(idx) 15 | parts.append((a, b)) 16 | 17 | @functools.cache 18 | def strongest(port, used): 19 | candidates = connections[port] - set(used) 20 | 21 | if maximize_length: 22 | best = (0, 0) 23 | else: 24 | best = 0 25 | 26 | for idx in candidates: 27 | a, b = parts[idx] 28 | if a == port: 29 | other = b 30 | else: 31 | other = a 32 | assert(b == port) 33 | 34 | strength = strongest(other, tuple(sorted(used + (idx,)))) 35 | 36 | if maximize_length: 37 | length, strength = strength 38 | best = max(best, (length+1, a + b + strength)) 39 | else: 40 | best = max(best, a + b + strength) 41 | 42 | return best 43 | 44 | best = strongest(0, tuple()) 45 | if maximize_length: 46 | best = best[1] 47 | 48 | return best 49 | 50 | def part1(s): 51 | answer = solve(s, maximize_length=False) 52 | 53 | lib.aoc.give_answer(2017, 24, 1, answer) 54 | 55 | def part2(s): 56 | answer = solve(s, maximize_length=True) 57 | 58 | lib.aoc.give_answer(2017, 24, 2, answer) 59 | 60 | INPUT = lib.aoc.get_input(2017, 24) 61 | part1(INPUT) 62 | part2(INPUT) 63 | -------------------------------------------------------------------------------- /2017/25/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def parse_action(lines): 4 | if lines[0] == ' - Write the value 1.': 5 | write = 1 6 | else: 7 | assert(lines[0] == ' - Write the value 0.') 8 | write = 0 9 | 10 | if lines[1] == ' - Move one slot to the left.': 11 | move = -1 12 | else: 13 | assert(lines[1] == ' - Move one slot to the right.') 14 | move = 1 15 | 16 | next_state = lines[2].split()[-1][:-1] 17 | 18 | return write, move, next_state 19 | 20 | def parse_input(s): 21 | groups = s.split('\n\n') 22 | setup = groups[0].splitlines() 23 | 24 | start_state = setup[0].split()[-1][:-1] 25 | steps = int(setup[1].split()[5]) 26 | 27 | states = {} 28 | 29 | for desc in groups[1:]: 30 | lines = desc.splitlines() 31 | 32 | name = lines[0].split()[-1][:-1] 33 | assert(' If the current value is 0:' == lines[1]) 34 | zero_action = parse_action(lines[2:5]) 35 | assert(' If the current value is 1:' == lines[5]) 36 | one_action = parse_action(lines[6:9]) 37 | 38 | states[name] = (zero_action, one_action) 39 | 40 | return start_state, steps, states 41 | 42 | def part1(s): 43 | start_state, steps, states = parse_input(s) 44 | 45 | tape = {} 46 | cursor = 0 47 | 48 | s = start_state 49 | 50 | for _ in range(steps): 51 | val = tape.get(cursor, 0) 52 | write, move, next_state = states[s][val] 53 | tape[cursor] = write 54 | cursor += move 55 | s = next_state 56 | 57 | answer = sum(tape.values()) 58 | 59 | lib.aoc.give_answer(2017, 25, 1, answer) 60 | 61 | def part2(s): 62 | print('There is no part two for Christmas!') 63 | 64 | INPUT = lib.aoc.get_input(2017, 25) 65 | part1(INPUT) 66 | part2(INPUT) 67 | -------------------------------------------------------------------------------- /2018/01/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def part1(s): 4 | answer = sum(map(int, s.splitlines())) 5 | 6 | lib.aoc.give_answer(2018, 1, 1, answer) 7 | 8 | def part2(s): 9 | changes = list(map(int, s.splitlines())) 10 | 11 | f = 0 12 | seen = set() 13 | 14 | while True: 15 | done = False 16 | 17 | for d in changes: 18 | f += d 19 | if f in seen: 20 | done = True 21 | break 22 | seen.add(f) 23 | 24 | if done: 25 | break 26 | 27 | answer = f 28 | 29 | lib.aoc.give_answer(2018, 1, 2, answer) 30 | 31 | INPUT = lib.aoc.get_input(2018, 1) 32 | part1(INPUT) 33 | part2(INPUT) 34 | -------------------------------------------------------------------------------- /2018/02/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import lib.aoc 4 | 5 | def part1(s): 6 | twice = 0 7 | thrice = 0 8 | 9 | for line in s.splitlines(): 10 | c = collections.Counter(line) 11 | if 3 in c.values(): 12 | thrice += 1 13 | if 2 in c.values(): 14 | twice += 1 15 | 16 | answer = twice * thrice 17 | 18 | lib.aoc.give_answer(2018, 2, 1, answer) 19 | 20 | def part2(s): 21 | ids = list(s.splitlines()) 22 | 23 | for idx, a in enumerate(ids): 24 | for b in ids[idx+1:]: 25 | diffs = 0 26 | for c1, c2 in zip(a, b): 27 | if c1 != c2: 28 | diffs += 1 29 | if diffs == 1: 30 | for idx, (c1, c2) in enumerate(zip(a, b)): 31 | if c1 != c2: 32 | answer = a[:idx] + a[idx+1:] 33 | 34 | lib.aoc.give_answer(2018, 2, 2, answer) 35 | 36 | INPUT = lib.aoc.get_input(2018, 2) 37 | part1(INPUT) 38 | part2(INPUT) 39 | -------------------------------------------------------------------------------- /2018/03/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import lib.aoc 4 | 5 | def parse_input(s): 6 | for line in s.splitlines(): 7 | num, _, start, dims = line.split() 8 | num = int(num[1:]) 9 | x_start, y_start = list(map(int, start[:-1].split(','))) 10 | w, h = list(map(int, dims.split('x'))) 11 | 12 | coords = [(x, y) 13 | for x in range(x_start, x_start+w) 14 | for y in range(y_start, y_start+h)] 15 | 16 | yield num, coords 17 | 18 | def part1(s): 19 | hits = collections.Counter() 20 | 21 | for num, coords in parse_input(s): 22 | for c in coords: 23 | hits[c] += 1 24 | 25 | answer = sum(1 for claims in hits.values() 26 | if claims > 1) 27 | 28 | lib.aoc.give_answer(2018, 3, 1, answer) 29 | 30 | def part2(s): 31 | hits = collections.Counter() 32 | 33 | for num, coords in parse_input(s): 34 | for c in coords: 35 | hits[c] += 1 36 | 37 | for num, coords in parse_input(s): 38 | if all(hits[c] == 1 for c in coords): 39 | answer = num 40 | break 41 | 42 | lib.aoc.give_answer(2018, 3, 2, answer) 43 | 44 | INPUT = lib.aoc.get_input(2018, 3) 45 | part1(INPUT) 46 | part2(INPUT) 47 | -------------------------------------------------------------------------------- /2018/04/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import lib.aoc 4 | 5 | def parse_sleep_record(s): 6 | guard_sleep_record = collections.defaultdict(collections.Counter) 7 | 8 | duty = None 9 | fell_asleep = None 10 | 11 | for line in sorted(s.splitlines()): 12 | date, time, act = line.split(maxsplit=2) 13 | date = date[1:] 14 | 15 | hour, minute = list(map(int, time[:-1].split(':'))) 16 | 17 | if act.startswith('Guard'): 18 | assert(fell_asleep is None) 19 | _, n, _, _ = act.split() 20 | duty = int(n[1:]) 21 | continue 22 | assert(hour == 0) 23 | if act == 'falls asleep': 24 | assert(fell_asleep is None) 25 | fell_asleep = (date, minute) 26 | continue 27 | if act == 'wakes up': 28 | assert(fell_asleep is not None) 29 | assert(date == fell_asleep[0]) 30 | for m in range(fell_asleep[1], minute): 31 | guard_sleep_record[duty][m] += 1 32 | fell_asleep = None 33 | continue 34 | assert(False) 35 | 36 | return guard_sleep_record 37 | 38 | def part1(s): 39 | guard_sleep_record = parse_sleep_record(s) 40 | 41 | guard, sleep_record = max(guard_sleep_record.items(), 42 | key=lambda pair: sum(pair[1].values())) 43 | (minute, _), = sleep_record.most_common(1) 44 | 45 | answer = guard * minute 46 | 47 | lib.aoc.give_answer(2018, 4, 1, answer) 48 | 49 | def part2(s): 50 | guard_sleep_record = parse_sleep_record(s) 51 | 52 | best = (0, 0, 0) 53 | 54 | for guard, sleep_record in guard_sleep_record.items(): 55 | (minute, times_slept), = sleep_record.most_common(1) 56 | best = max(best, (times_slept, guard, minute)) 57 | 58 | _, guard, minute = best 59 | 60 | answer = guard * minute 61 | 62 | lib.aoc.give_answer(2018, 4, 2, answer) 63 | 64 | INPUT = lib.aoc.get_input(2018, 4) 65 | part1(INPUT) 66 | part2(INPUT) 67 | -------------------------------------------------------------------------------- /2018/05/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def full_reduce(s): 4 | stack = [] 5 | 6 | for unit in s: 7 | if stack: 8 | if stack[-1].swapcase() == unit: 9 | # Reaction 10 | stack.pop(-1) 11 | continue 12 | 13 | stack.append(unit) 14 | 15 | return ''.join(stack) 16 | 17 | def part1(s): 18 | answer = len(full_reduce(s)) 19 | 20 | lib.aoc.give_answer(2018, 5, 1, answer) 21 | 22 | def part2(s): 23 | answer = len(s) 24 | 25 | for t in 'abcdefghijklmnopqrstuvwxyz': 26 | cand = full_reduce(s.replace(t, '').replace(t.upper(), '')) 27 | answer = min(answer, len(cand)) 28 | 29 | lib.aoc.give_answer(2018, 5, 2, answer) 30 | 31 | INPUT = lib.aoc.get_input(2018, 5) 32 | part1(INPUT) 33 | part2(INPUT) 34 | -------------------------------------------------------------------------------- /2018/07/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import lib.aoc 4 | 5 | def parse_input(s): 6 | dep_graph = collections.defaultdict(set) 7 | all_steps = set() 8 | 9 | for line in s.splitlines(): 10 | _, a, _, _, _, _, _, b, _, _ = line.split() 11 | dep_graph[b].add(a) 12 | all_steps.add(b) 13 | all_steps.add(a) 14 | 15 | return dep_graph, sorted(all_steps) 16 | 17 | def best_next_step(all_steps, dep_graph, steps_started, steps_done): 18 | for step in all_steps: 19 | if step in steps_started: 20 | continue 21 | if len(dep_graph[step] - steps_done) == 0: 22 | # We can do this now! 23 | return step 24 | 25 | def part1(s): 26 | dep_graph, all_steps = parse_input(s) 27 | 28 | done = set() 29 | order = [] 30 | 31 | while len(order) < len(all_steps): 32 | step = best_next_step(all_steps, dep_graph, done, done) 33 | order.append(step) 34 | done.add(step) 35 | 36 | answer = ''.join(order) 37 | 38 | lib.aoc.give_answer(2018, 7, 1, answer) 39 | 40 | def part2(s): 41 | dep_graph, all_steps = parse_input(s) 42 | 43 | done = set() 44 | started = set() 45 | workers = [None] * 5 46 | 47 | answer = -1 # Start on second 0 48 | 49 | while len(done) < len(all_steps): 50 | answer += 1 51 | for idx, processing in enumerate(workers): 52 | if processing is None: 53 | continue 54 | step, remaining = processing 55 | remaining -= 1 56 | if remaining == 0: 57 | done.add(step) 58 | workers[idx] = None 59 | else: 60 | workers[idx] = (step, remaining) 61 | 62 | for idx, processing in enumerate(workers): 63 | if processing is not None: 64 | continue 65 | 66 | step = best_next_step(all_steps, dep_graph, started, done) 67 | if step is None: 68 | # No worker can start something new right now 69 | break 70 | 71 | workers[idx] = (step, 60 + ord(step) - ord('A') + 1) 72 | started.add(step) 73 | 74 | lib.aoc.give_answer(2018, 7, 2, answer) 75 | 76 | INPUT = lib.aoc.get_input(2018, 7) 77 | part1(INPUT) 78 | part2(INPUT) 79 | -------------------------------------------------------------------------------- /2018/08/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def parse_tree(s): 4 | def impl(packet): 5 | num_children = packet[0] 6 | num_metadata = packet[1] 7 | 8 | packet = packet[2:] 9 | 10 | children = [] 11 | while num_children > 0: 12 | child, packet = impl(packet) 13 | children.append(child) 14 | num_children -= 1 15 | 16 | metadata = packet[:num_metadata] 17 | packet = packet[num_metadata:] 18 | 19 | return (children, metadata), packet 20 | 21 | packet = list(map(int, s.split())) 22 | 23 | root, rest = impl(packet) 24 | assert(len(rest) == 0) 25 | return root 26 | 27 | def part1(s): 28 | root = parse_tree(s) 29 | 30 | def sum_metadata(node): 31 | children, metadata = node 32 | return sum(map(sum_metadata, children)) + sum(metadata) 33 | 34 | answer = sum_metadata(root) 35 | 36 | lib.aoc.give_answer(2018, 8, 1, answer) 37 | 38 | def part2(s): 39 | root = parse_tree(s) 40 | 41 | def evaluate(node): 42 | children, metadata = node 43 | 44 | if children: 45 | counts = [0] * len(children) 46 | 47 | for idx in metadata: 48 | if idx == 0: 49 | continue 50 | idx -= 1 51 | if idx >= len(children): 52 | continue 53 | counts[idx] += 1 54 | 55 | value = 0 56 | 57 | for c, child in zip(counts, children): 58 | if c == 0: 59 | continue 60 | 61 | value += c * evaluate(child) 62 | 63 | return value 64 | 65 | # No children 66 | return sum(metadata) 67 | 68 | answer = evaluate(root) 69 | 70 | lib.aoc.give_answer(2018, 8, 2, answer) 71 | 72 | INPUT = lib.aoc.get_input(2018, 8) 73 | part1(INPUT) 74 | part2(INPUT) 75 | -------------------------------------------------------------------------------- /2018/09/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import lib.aoc 4 | 5 | def play(players, last_marble): 6 | scores = [0] * players 7 | 8 | marbles = collections.deque([0]) 9 | 10 | player = 0 11 | 12 | for m in range(1, last_marble+1): 13 | if m % 23 == 0: 14 | scores[player] += m 15 | marbles.rotate(7) 16 | scores[player] += marbles.popleft() 17 | else: 18 | marbles.rotate(-2) 19 | marbles.appendleft(m) 20 | 21 | player = (player + 1) % players 22 | 23 | return scores 24 | 25 | def part1(s): 26 | players, _, _, _, _, _, last_marble, _ = s.split() 27 | 28 | answer = max(play(int(players), int(last_marble))) 29 | 30 | lib.aoc.give_answer(2018, 9, 1, answer) 31 | 32 | def part2(s): 33 | players, _, _, _, _, _, last_marble, _ = s.split() 34 | 35 | answer = max(play(int(players), 100 * int(last_marble))) 36 | 37 | lib.aoc.give_answer(2018, 9, 2, answer) 38 | 39 | INPUT = lib.aoc.get_input(2018, 9) 40 | part1(INPUT) 41 | part2(INPUT) 42 | -------------------------------------------------------------------------------- /2018/10/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.ocr 3 | 4 | def parse(s): 5 | points = [] 6 | velocities = [] 7 | 8 | for line in s.splitlines(): 9 | line = line.replace('=<', ' ').replace('>', ' ').replace(',', '') 10 | _, px, py, _, vx, vy = line.split() 11 | 12 | points.append((int(px), int(py))) 13 | velocities.append((int(vx), int(vy))) 14 | 15 | steps = 0 16 | 17 | while True: 18 | min_y = min(y for x,y in points) 19 | max_y = max(y for x,y in points) 20 | 21 | if max_y - min_y < 10: 22 | # Assume that the valid position is relatively short 23 | return lib.ocr.parse_coord_set(points), steps 24 | 25 | for idx, ((px, py), (vx, vy)) in enumerate(zip(points, velocities)): 26 | points[idx] = (px+vx, py+vy) 27 | steps += 1 28 | 29 | def part1(s): 30 | answer, _ = parse(s) 31 | 32 | lib.aoc.give_answer(2018, 10, 1, answer) 33 | 34 | def part2(s): 35 | _, answer = parse(s) 36 | 37 | lib.aoc.give_answer(2018, 10, 2, answer) 38 | 39 | INPUT = lib.aoc.get_input(2018, 10) 40 | part1(INPUT) 41 | part2(INPUT) 42 | -------------------------------------------------------------------------------- /2018/12/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def solve(s, generations): 4 | groups = s.split('\n\n') 5 | 6 | pots = groups[0].split()[2] 7 | start = pots.index('#') 8 | pots = pots.strip('.') 9 | 10 | transitions = {} 11 | 12 | for line in groups[1].splitlines(): 13 | a, b = line.split(' => ') 14 | transitions[a] = b 15 | 16 | assert(transitions['.....'] == '.') 17 | 18 | memory = {} 19 | sequence = [] 20 | 21 | for gen_num in range(generations): 22 | sequence.append((start, pots)) 23 | if pots in memory: 24 | gen_seen, prev_start = memory[pots] 25 | # Jump ahead! 26 | 27 | repeat_every = gen_num - gen_seen 28 | shift_per = start - prev_start 29 | 30 | remaining = generations - gen_num 31 | jump_by = remaining // repeat_every 32 | start += jump_by * shift_per 33 | 34 | remaining = remaining % repeat_every 35 | next_start, pots = sequence[gen_seen + remaining] 36 | 37 | start += next_start - prev_start 38 | break 39 | 40 | memory[pots] = (gen_num, start) 41 | 42 | # The added padding shifts start to the left, though 43 | # we'll likely shift back at the end 44 | start -= 2 45 | pots = '....' + pots + '....' 46 | 47 | pots = ''.join(transitions[pots[i:i+5]] 48 | for i in range(len(pots)-4)) 49 | 50 | start += pots.index('#') 51 | pots = pots.strip('.') 52 | 53 | return sum(start + idx 54 | for idx, c in enumerate(pots) 55 | if c == '#') 56 | 57 | def part1(s): 58 | answer = solve(s, 20) 59 | 60 | lib.aoc.give_answer(2018, 12, 1, answer) 61 | 62 | def part2(s): 63 | answer = solve(s, 50000000000) 64 | 65 | lib.aoc.give_answer(2018, 12, 2, answer) 66 | 67 | INPUT = lib.aoc.get_input(2018, 12) 68 | part1(INPUT) 69 | part2(INPUT) 70 | -------------------------------------------------------------------------------- /2018/14/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def part1(s): 4 | n = int(s) 5 | 6 | recipes = [3, 7] 7 | e0, e1 = 0, 1 8 | 9 | while len(recipes) < n + 10: 10 | combined = recipes[e0] + recipes[e1] 11 | if combined > 9: 12 | recipes.append(1) 13 | recipes.append(combined % 10) 14 | 15 | e0 = (e0 + 1 + recipes[e0]) % len(recipes) 16 | e1 = (e1 + 1 + recipes[e1]) % len(recipes) 17 | 18 | answer = ''.join(map(str, recipes[n:n+10])) 19 | 20 | lib.aoc.give_answer(2018, 14, 1, answer) 21 | 22 | def part2(s): 23 | target = list(map(int, s)) 24 | most_recent = [-1] * len(target) 25 | most_recent = most_recent[2:] + [3, 7] 26 | 27 | recipes = [3, 7] 28 | e0, e1 = 0, 1 29 | 30 | while True: 31 | combined = recipes[e0] + recipes[e1] 32 | if combined > 9: 33 | recipes.append(1) 34 | most_recent = most_recent[1:] + [1] 35 | if most_recent == target: 36 | break 37 | recipes.append(combined % 10) 38 | most_recent = most_recent[1:] + [recipes[-1]] 39 | if most_recent == target: 40 | break 41 | 42 | e0 = (e0 + 1 + recipes[e0]) % len(recipes) 43 | e1 = (e1 + 1 + recipes[e1]) % len(recipes) 44 | 45 | answer = len(recipes) - len(target) 46 | 47 | lib.aoc.give_answer(2018, 14, 2, answer) 48 | 49 | INPUT = lib.aoc.get_input(2018, 14) 50 | part1(INPUT) 51 | part2(INPUT) 52 | -------------------------------------------------------------------------------- /2018/18/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.grid 3 | 4 | def step(grid): 5 | start_state = grid.to_dict() 6 | 7 | for (x, y), val in grid.items(): 8 | ground = 0 9 | trees = 0 10 | lumber = 0 11 | for n in grid.neighbors(x, y, diagonals=True): 12 | nval = start_state[n] 13 | if nval == '.': 14 | ground += 1 15 | elif nval == '|': 16 | trees += 1 17 | elif nval == '#': 18 | lumber += 1 19 | else: 20 | assert(False) 21 | if val == '.': 22 | if trees >= 3: 23 | grid[x,y] = '|' 24 | elif val == '|': 25 | if lumber >= 3: 26 | grid[x,y] = '#' 27 | elif val == '#': 28 | if lumber < 1 or trees < 1: 29 | grid[x,y] = '.' 30 | else: 31 | assert(False) 32 | 33 | def part1(s): 34 | grid = lib.grid.FixedGrid.parse(s) 35 | 36 | for _ in range(10): 37 | step(grid) 38 | 39 | wooded = sum(1 for c, val in grid.items() 40 | if val == '|') 41 | lumber = sum(1 for c, val in grid.items() 42 | if val == '#') 43 | 44 | answer = wooded * lumber 45 | 46 | lib.aoc.give_answer(2018, 18, 1, answer) 47 | 48 | def part2(s): 49 | TARGET = 1000000000 50 | 51 | grid = lib.grid.FixedGrid.parse(s) 52 | 53 | seen = { 54 | grid.as_str(''): 0 55 | } 56 | order = [grid.as_str('')] 57 | 58 | step_num = 0 59 | while step_num < TARGET: 60 | step_num += 1 61 | step(grid) 62 | key = grid.as_str('') 63 | if key in seen: 64 | last_step = seen[key] 65 | jump_by = step_num - last_step 66 | remaining = TARGET - step_num 67 | final_idx = last_step + (remaining % jump_by) 68 | final_key = order[final_idx] 69 | break 70 | 71 | seen[key] = step_num 72 | order.append(key) 73 | 74 | wooded = final_key.count('|') 75 | lumber = final_key.count('#') 76 | 77 | answer = wooded * lumber 78 | 79 | lib.aoc.give_answer(2018, 18, 2, answer) 80 | 81 | INPUT = lib.aoc.get_input(2018, 18) 82 | part1(INPUT) 83 | part2(INPUT) 84 | -------------------------------------------------------------------------------- /2018/20/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import lib.aoc 4 | import lib.graph 5 | 6 | def contruct_map(s): 7 | assert(s[0] == '^') 8 | assert(s[-1] == '$') 9 | 10 | MOVES = { 11 | 'N': lambda x,y: (x, y-1), 12 | 'S': lambda x,y: (x, y+1), 13 | 'E': lambda x,y: (x+1, y), 14 | 'W': lambda x,y: (x-1, y), 15 | } 16 | 17 | m = collections.defaultdict(set) 18 | 19 | stack = [] 20 | base_positions = None 21 | positions = {(0, 0)} 22 | 23 | for c in s[1:-1]: 24 | if c in MOVES: 25 | new_positions = set() 26 | for x, y in positions: 27 | n = MOVES[c](x, y) 28 | m[x,y].add((n, 1)) 29 | m[n].add(((x,y), 1)) 30 | new_positions.add(n) 31 | positions = new_positions 32 | elif c == '(': 33 | stack.append((set(), base_positions)) 34 | base_positions = positions 35 | elif c == '|': 36 | # Propagate positions back up the stack 37 | stack[-1][0].update(positions) 38 | # New set of options 39 | positions = base_positions 40 | elif c == ')': 41 | # Propagate positions back up the stack 42 | stack[-1][0].update(positions) 43 | # Pop back up 44 | positions, base_positions = stack.pop(-1) 45 | else: 46 | assert(False) 47 | 48 | assert(len(stack) == 0) 49 | 50 | return m 51 | 52 | def part1(s): 53 | _, answer = lib.graph.longest_minimal_path_length(contruct_map(s), (0, 0)) 54 | 55 | lib.aoc.give_answer(2018, 20, 1, answer) 56 | 57 | def part2(s): 58 | answer = sum(1 59 | for dest, dist 60 | in lib.graph.all_reachable(contruct_map(s), (0, 0)) 61 | if dist >= 1000) 62 | 63 | lib.aoc.give_answer(2018, 20, 2, answer) 64 | 65 | INPUT = lib.aoc.get_input(2018, 20) 66 | part1(INPUT) 67 | part2(INPUT) 68 | -------------------------------------------------------------------------------- /2018/22/solution.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | import lib.aoc 4 | import lib.graph 5 | import lib.lazy_dict 6 | 7 | def parse_cave(s): 8 | depth, target = s.splitlines() 9 | depth = int(depth.split()[1]) 10 | tx, ty = target.split()[1].split(',') 11 | tx, ty = int(tx), int(ty) 12 | 13 | MOD = 20183 14 | 15 | @functools.cache 16 | def erosion_level(x, y): 17 | if (x, y) in [(0, 0), (tx, ty)]: 18 | return depth % MOD 19 | if y == 0: 20 | return (x * 16807 + depth) % MOD 21 | if x == 0: 22 | return (y * 48271 + depth) % MOD 23 | return (erosion_level(x-1, y) * erosion_level(x, y-1) + depth) % MOD 24 | 25 | def risk_level(coord): 26 | x, y = coord 27 | return erosion_level(x, y) % 3 28 | 29 | return lib.lazy_dict.make_lazy_dict(risk_level), tx, ty 30 | 31 | def part1(s): 32 | cave, tx, ty = parse_cave(s) 33 | 34 | answer = sum(cave[x, y] 35 | for x in range(tx+1) 36 | for y in range(ty+1)) 37 | 38 | lib.aoc.give_answer(2018, 22, 1, answer) 39 | 40 | TORCH = 0 41 | CLIMBING = 1 42 | NEITHER = 2 43 | 44 | VALID_TOOLS = { 45 | 0: (TORCH, CLIMBING), # Rocky 46 | 1: (CLIMBING, NEITHER), # Wet 47 | 2: (TORCH, NEITHER), # Narrow 48 | } 49 | 50 | def part2(s): 51 | cave, tx, ty = parse_cave(s) 52 | 53 | def neighbor_fn(state): 54 | x, y, tool = state 55 | this_risk = cave[x,y] 56 | 57 | for other_tool in VALID_TOOLS[this_risk]: 58 | if tool != other_tool: 59 | yield (x, y, other_tool), 7 60 | 61 | for nx, ny in [(x-1, y), 62 | (x+1, y), 63 | (x, y+1), 64 | (x, y-1)]: 65 | if nx < 0 or ny < 0: 66 | continue 67 | if tool not in VALID_TOOLS[cave[nx, ny]]: 68 | continue 69 | yield (nx, ny, tool), 1 70 | 71 | graph = lib.graph.make_lazy_graph(neighbor_fn) 72 | 73 | answer = lib.graph.dijkstra_length(graph, 74 | (0, 0, TORCH), 75 | (tx, ty, TORCH)) 76 | 77 | lib.aoc.give_answer(2018, 22, 2, answer) 78 | 79 | INPUT = lib.aoc.get_input(2018, 22) 80 | part1(INPUT) 81 | part2(INPUT) 82 | -------------------------------------------------------------------------------- /2018/25/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import lib.aoc 4 | 5 | def parse_input(s): 6 | for line in s.splitlines(): 7 | yield tuple(map(int, line.split(','))) 8 | 9 | def part1(s): 10 | data = list(parse_input(s)) 11 | 12 | links = collections.defaultdict(set) 13 | 14 | for i, (x, y, z, w) in enumerate(data): 15 | for j, (x2, y2, z2, w2) in enumerate(data): 16 | dist = abs(x-x2) + abs(y-y2) + abs(z-z2) + abs(w-w2) 17 | if dist <= 3: 18 | links[i].add(j) 19 | 20 | answer = 0 21 | handled = set() 22 | 23 | for i in range(len(data)): 24 | if i in handled: 25 | continue 26 | 27 | answer += 1 28 | 29 | to_handle = [i] 30 | while to_handle: 31 | i = to_handle.pop(-1) 32 | handled.add(i) 33 | 34 | for j in links[i]: 35 | if j not in handled: 36 | to_handle.append(j) 37 | 38 | lib.aoc.give_answer(2018, 25, 1, answer) 39 | 40 | def part2(s): 41 | print('There is no part two for Christmas!') 42 | 43 | INPUT = lib.aoc.get_input(2018, 25) 44 | part1(INPUT) 45 | part2(INPUT) 46 | -------------------------------------------------------------------------------- /2019/01/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def part1(s): 4 | answer = sum(max(0, m // 3 - 2) 5 | for m in map(int, s.splitlines())) 6 | 7 | lib.aoc.give_answer(2019, 1, 1, answer) 8 | 9 | def part2(s): 10 | def recursive_fuel_cost(m): 11 | fuel = max(0, m // 3 - 2) 12 | if fuel: 13 | fuel += recursive_fuel_cost(fuel) 14 | return fuel 15 | 16 | answer = sum(map(recursive_fuel_cost, map(int, s.splitlines()))) 17 | 18 | lib.aoc.give_answer(2019, 1, 2, answer) 19 | 20 | INPUT = lib.aoc.get_input(2019, 1) 21 | part1(INPUT) 22 | part2(INPUT) 23 | -------------------------------------------------------------------------------- /2019/02/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | intcode = __import__('2019.intcode').intcode 4 | 5 | def part1(s): 6 | p = intcode.Program(s) 7 | 8 | p.memory[1] = 12 9 | p.memory[2] = 2 10 | 11 | _, out = p.run() 12 | out.wait_for_close() 13 | 14 | answer = p.memory[0] 15 | 16 | lib.aoc.give_answer(2019, 2, 1, answer) 17 | 18 | def part2(s): 19 | answer = None 20 | 21 | for noun in range(100): 22 | for verb in range(100): 23 | p = intcode.Program(s) 24 | 25 | p.memory[1] = noun 26 | p.memory[2] = verb 27 | 28 | _, out = p.run() 29 | out.wait_for_close() 30 | 31 | if p.memory[0] == 19690720: 32 | answer = 100 * noun + verb 33 | break 34 | 35 | if answer is not None: 36 | break 37 | 38 | lib.aoc.give_answer(2019, 2, 2, answer) 39 | 40 | INPUT = lib.aoc.get_input(2019, 2) 41 | part1(INPUT) 42 | part2(INPUT) 43 | -------------------------------------------------------------------------------- /2019/03/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def parse_wire(instructions): 4 | wire = {} 5 | 6 | x, y = 0, 0 7 | 8 | steps = 0 9 | for inst in instructions.split(','): 10 | direct = inst[0] 11 | count = int(inst[1:]) 12 | 13 | if direct == 'U': 14 | dx, dy = 0, 1 15 | elif direct == 'D': 16 | dx, dy = 0, -1 17 | elif direct == 'L': 18 | dx, dy = -1, 0 19 | elif direct == 'R': 20 | dx, dy = 1, 0 21 | else: 22 | assert(False) 23 | 24 | for _ in range(count): 25 | x, y = x+dx, y+dy 26 | steps += 1 27 | 28 | wire[x,y] = wire.get((x, y), steps) 29 | 30 | return wire 31 | 32 | def part1(s): 33 | w1, w2 = s.splitlines() 34 | 35 | w1 = parse_wire(w1) 36 | w2 = parse_wire(w2) 37 | 38 | answer = min(abs(x) + abs(y) 39 | for (x, y) in w1 40 | if (x, y) in w2) 41 | 42 | lib.aoc.give_answer(2019, 3, 1, answer) 43 | 44 | def part2(s): 45 | w1, w2 = s.splitlines() 46 | 47 | w1 = parse_wire(w1) 48 | w2 = parse_wire(w2) 49 | 50 | answer = min(w1[c] + w2[c] 51 | for c in w1 52 | if c in w2) 53 | 54 | lib.aoc.give_answer(2019, 3, 2, answer) 55 | 56 | INPUT = lib.aoc.get_input(2019, 3) 57 | part1(INPUT) 58 | part2(INPUT) 59 | -------------------------------------------------------------------------------- /2019/04/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def valid_password(p): 4 | p = str(p) 5 | 6 | if len(p) != 6: 7 | return False 8 | 9 | doubled = False 10 | for a, b in zip(p, p[1:]): 11 | if a == b: 12 | doubled = True 13 | continue 14 | 15 | if int(a) > int(b): 16 | return False 17 | 18 | return doubled 19 | 20 | def part1(s): 21 | a, b = list(map(int, s.split('-'))) 22 | 23 | answer = sum(map(valid_password, range(a, b+1))) 24 | 25 | lib.aoc.give_answer(2019, 4, 1, answer) 26 | 27 | def valid_password2(p): 28 | p = list(map(int, str(p))) 29 | 30 | if len(p) != 6: 31 | return False 32 | 33 | double = False 34 | last = p[0] 35 | last_start = 0 36 | for idx, d in enumerate(p[1:]): 37 | idx += 1 38 | 39 | if d < last: 40 | return False 41 | if d > last: 42 | if idx - last_start == 2: 43 | double = True 44 | last = d 45 | last_start = idx 46 | 47 | if len(p) - last_start == 2: 48 | double = True 49 | 50 | return double 51 | 52 | def part2(s): 53 | a, b = list(map(int, s.split('-'))) 54 | 55 | answer = sum(map(valid_password2, range(a, b+1))) 56 | 57 | lib.aoc.give_answer(2019, 4, 2, answer) 58 | 59 | INPUT = lib.aoc.get_input(2019, 4) 60 | part1(INPUT) 61 | part2(INPUT) 62 | -------------------------------------------------------------------------------- /2019/05/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | intcode = __import__('2019.intcode').intcode 4 | 5 | def part1(s): 6 | in_chan, out_chan = intcode.Program(s).run() 7 | 8 | in_chan.send(1) 9 | answer = list(out_chan)[-1] 10 | 11 | lib.aoc.give_answer(2019, 5, 1, answer) 12 | 13 | def part2(s): 14 | in_chan, out_chan = intcode.Program(s).run() 15 | 16 | in_chan.send(5) 17 | answer = out_chan.recv() 18 | 19 | lib.aoc.give_answer(2019, 5, 2, answer) 20 | 21 | INPUT = lib.aoc.get_input(2019, 5) 22 | part1(INPUT) 23 | part2(INPUT) 24 | -------------------------------------------------------------------------------- /2019/06/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def parse_orbits(s): 4 | orbits = {} 5 | 6 | for line in s.splitlines(): 7 | a, b = line.split(')') 8 | assert(b not in orbits) 9 | orbits[b] = a 10 | 11 | return orbits 12 | 13 | def part1(s): 14 | orbits = parse_orbits(s) 15 | 16 | def orbit_count(obj): 17 | orbiting = orbits.get(obj) 18 | if orbiting is None: 19 | return 0 20 | 21 | return 1 + orbit_count(orbiting) 22 | 23 | answer = sum(map(orbit_count, orbits)) 24 | 25 | lib.aoc.give_answer(2019, 6, 1, answer) 26 | 27 | def get_orbit_list(orbits, obj): 28 | orbiting = orbits.get(obj) 29 | if orbiting is None: 30 | return [] 31 | 32 | return [orbiting] + get_orbit_list(orbits, orbiting) 33 | 34 | def part2(s): 35 | orbits = parse_orbits(s) 36 | 37 | san_orbits = get_orbit_list(orbits, 'SAN') 38 | you_orbits = get_orbit_list(orbits, 'YOU') 39 | 40 | for idx, obj in enumerate(you_orbits): 41 | if obj in san_orbits: 42 | # Each step in the orbit lists represents a single jump 43 | answer = idx + san_orbits.index(obj) 44 | break 45 | 46 | lib.aoc.give_answer(2019, 6, 2, answer) 47 | 48 | INPUT = lib.aoc.get_input(2019, 6) 49 | part1(INPUT) 50 | part2(INPUT) 51 | -------------------------------------------------------------------------------- /2019/07/solution.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | import lib.aoc 4 | 5 | intcode = __import__('2019.intcode').intcode 6 | 7 | def part1(s): 8 | answer = 0 9 | 10 | for phases in itertools.permutations(range(5)): 11 | signal = 0 12 | for p in phases: 13 | in_chan, out_chan = intcode.Program(s).run() 14 | in_chan.send(p) 15 | in_chan.send(signal) 16 | signal = out_chan.recv() 17 | answer = max(answer, signal) 18 | 19 | lib.aoc.give_answer(2019, 7, 1, answer) 20 | 21 | def part2(s): 22 | answer = 0 23 | 24 | for phases in itertools.permutations(range(5, 10)): 25 | first_in = None 26 | last_out = None 27 | 28 | for p in phases: 29 | in_chan, out_chan = intcode.Program(s).run(in_chan=last_out) 30 | # The previous program won't output until we give it the input 31 | # signal. There's no chance of synchronization problems here. 32 | in_chan.send(p) 33 | if first_in is None: 34 | first_in = in_chan 35 | last_out = out_chan 36 | 37 | # Seed the chain 38 | first_in.send(0) 39 | 40 | for signal in last_out: 41 | first_in.send(signal) 42 | 43 | # This is the final signal 44 | answer = max(answer, signal) 45 | 46 | lib.aoc.give_answer(2019, 7, 2, answer) 47 | 48 | INPUT = lib.aoc.get_input(2019, 7) 49 | part1(INPUT) 50 | part2(INPUT) 51 | -------------------------------------------------------------------------------- /2019/08/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import lib.aoc 4 | import lib.ocr 5 | 6 | def partition(nums, stride): 7 | assert(len(nums) % stride == 0) 8 | 9 | out = [] 10 | 11 | for i in range(0, len(nums), stride): 12 | out.append(nums[i:i+stride]) 13 | 14 | return out 15 | 16 | WIDTH = 25 17 | HEIGHT = 6 18 | LAYER_LENGTH = WIDTH*HEIGHT 19 | 20 | def part1(s): 21 | best = (LAYER_LENGTH, 0) 22 | 23 | for layer in partition(list(map(int, s)), LAYER_LENGTH): 24 | c = collections.Counter(layer) 25 | key = (c[0], c[1] * c[2]) 26 | best = min(best, key) 27 | 28 | answer = best[1] 29 | 30 | lib.aoc.give_answer(2019, 8, 1, answer) 31 | 32 | def part2(s): 33 | composite = [2] * LAYER_LENGTH 34 | 35 | for layer in partition(list(map(int, s)), LAYER_LENGTH): 36 | for i, (image_cell, layer_cell) in enumerate(zip(composite, layer)): 37 | if image_cell == 2: 38 | composite[i] = layer_cell 39 | 40 | assert(2 not in composite) 41 | 42 | image = partition(composite, WIDTH) 43 | 44 | white_cells = [(x, y) 45 | for y, row in enumerate(image) 46 | for x, cell in enumerate(row) 47 | if cell == 1] 48 | 49 | answer = lib.ocr.parse_coord_set(white_cells) 50 | 51 | lib.aoc.give_answer(2019, 8, 2, answer) 52 | 53 | INPUT = lib.aoc.get_input(2019, 8) 54 | part1(INPUT) 55 | part2(INPUT) 56 | -------------------------------------------------------------------------------- /2019/09/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | intcode = __import__('2019.intcode').intcode 4 | 5 | def part1(s): 6 | in_chan, out_chan = intcode.Program(s).run() 7 | 8 | in_chan.send(1) 9 | answer = list(out_chan)[-1] 10 | 11 | lib.aoc.give_answer(2019, 9, 1, answer) 12 | 13 | def part2(s): 14 | in_chan, out_chan = intcode.Program(s).run() 15 | 16 | in_chan.send(2) 17 | answer = list(out_chan)[-1] 18 | 19 | lib.aoc.give_answer(2019, 9, 2, answer) 20 | 21 | INPUT = lib.aoc.get_input(2019, 9) 22 | part1(INPUT) 23 | part2(INPUT) 24 | -------------------------------------------------------------------------------- /2019/11/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.math 3 | import lib.ocr 4 | 5 | intcode = __import__('2019.intcode').intcode 6 | 7 | def get_painted_grid(s, first_panel): 8 | in_chan, out_chan = intcode.Program(s).run() 9 | 10 | grid = {} 11 | 12 | # NOTE: Low y is negative 13 | x, y = 0, 0 14 | dx, dy = 0, -1 15 | 16 | # Initial panel 17 | in_chan.send(first_panel) 18 | 19 | for color in out_chan: 20 | turn = out_chan.recv() 21 | 22 | grid[x,y] = color 23 | 24 | if turn == 0: 25 | # Turn left 26 | dx, dy = dy, -dx 27 | else: 28 | # Turn right 29 | dx, dy = -dy, dx 30 | 31 | x += dx 32 | y += dy 33 | 34 | # New panel 35 | in_chan.send(grid.get((x, y), 0)) 36 | 37 | return grid 38 | 39 | def part1(s): 40 | answer = len(get_painted_grid(s, 0)) 41 | 42 | lib.aoc.give_answer(2019, 11, 1, answer) 43 | 44 | def part2(s): 45 | answer = lib.ocr.parse_dict(get_painted_grid(s, 1), 1) 46 | 47 | lib.aoc.give_answer(2019, 11, 2, answer) 48 | 49 | INPUT = lib.aoc.get_input(2019, 11) 50 | part1(INPUT) 51 | part2(INPUT) 52 | -------------------------------------------------------------------------------- /2019/12/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.math 3 | 4 | def parse_moons(s): 5 | for c in '': 6 | s = s.replace(c, '') 7 | 8 | for line in s.splitlines(): 9 | x, y, z = list(map(int, line.split(','))) 10 | yield ([x, y, z], [0, 0, 0]) 11 | 12 | def step(moons): 13 | # Apply gravity to velocities 14 | for pos, vel in moons: 15 | for other_pos, _ in moons: 16 | for i, (p0, p1) in enumerate(zip(pos, other_pos)): 17 | if p0 < p1: 18 | vel[i] += 1 19 | elif p0 > p1: 20 | vel[i] -= 1 21 | 22 | # Apply velocities to positions 23 | for pos, vel in moons: 24 | for i, v in enumerate(vel): 25 | pos[i] += v 26 | 27 | def energy(moon): 28 | pos, vel = moon 29 | 30 | return sum(map(abs, pos)) * sum(map(abs, vel)) 31 | 32 | def part1(s): 33 | moons = list(parse_moons(s)) 34 | 35 | for _ in range(1000): 36 | step(moons) 37 | 38 | answer = sum(map(energy, moons)) 39 | 40 | lib.aoc.give_answer(2019, 12, 1, answer) 41 | 42 | def get_cycle_congruence(moons, comp_idx): 43 | # Isolate the component in question 44 | moons = [([p[comp_idx]], [v[comp_idx]]) 45 | for p, v in moons] 46 | 47 | def state_key(): 48 | return tuple((p[0], v[0]) for p, v in moons) 49 | 50 | step_num = 0 51 | states = {state_key(): step_num} 52 | 53 | while True: 54 | step_num += 1 55 | step(moons) 56 | 57 | key = state_key() 58 | last_seen = states.get(key) 59 | if last_seen is not None: 60 | return step_num - last_seen, last_seen 61 | 62 | states[key] = step_num 63 | 64 | def part2(s): 65 | moons = list(parse_moons(s)) 66 | 67 | answer = lib.math.chinese_remainder([get_cycle_congruence(moons, 0), 68 | get_cycle_congruence(moons, 1), 69 | get_cycle_congruence(moons, 2)], 70 | 1) 71 | 72 | lib.aoc.give_answer(2019, 12, 2, answer) 73 | 74 | INPUT = lib.aoc.get_input(2019, 12) 75 | part1(INPUT) 76 | part2(INPUT) 77 | -------------------------------------------------------------------------------- /2019/13/solution.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | import lib.aoc 4 | import lib.channels 5 | 6 | intcode = __import__('2019.intcode').intcode 7 | 8 | def part1(s): 9 | in_chan, out_chan = intcode.Program(s).run() 10 | 11 | screen = {} 12 | 13 | for x in out_chan: 14 | y = out_chan.recv() 15 | tile = out_chan.recv() 16 | 17 | screen[x,y] = tile 18 | 19 | answer = sum(1 for tile in screen.values() 20 | if tile == 2) 21 | 22 | lib.aoc.give_answer(2019, 13, 1, answer) 23 | 24 | def part2(s): 25 | p = intcode.Program(s) 26 | p.memory[0] = 2 # Hack in some quarters! 27 | 28 | # Set up the channels here so that we can detect "deadlock" correctly 29 | in_chan = lib.channels.SyncChannel() 30 | out_chan = lib.channels.BufferedChannel() 31 | 32 | screen = {} 33 | 34 | # Each time there's an input request it's a "deadlock" until we 35 | # provide a move. But wait for the full screen to be read first! 36 | input_requests = lib.channels.detect_deadlock_events(2, in_chan, out_chan) 37 | def move_thread(): 38 | for event in input_requests: 39 | # We need to input an action! 40 | paddle = None 41 | ball = None 42 | for (x, y), tile in screen.items(): 43 | if tile == 3: 44 | paddle = x 45 | elif tile == 4: 46 | ball = x 47 | 48 | # Just move the paddle towards the ball. Simple AI 49 | if paddle == ball: 50 | in_chan.send(0) # Neutral 51 | elif paddle < ball: 52 | in_chan.send(1) # Right 53 | else: 54 | in_chan.send(-1) # Left 55 | 56 | threading.Thread(target=move_thread).start() 57 | 58 | p.run(in_chan, out_chan) 59 | 60 | score = None 61 | 62 | for x in out_chan: 63 | y = out_chan.recv() 64 | tile = out_chan.recv() 65 | 66 | if x == -1 and y == 0: 67 | score = tile 68 | else: 69 | screen[x,y] = tile 70 | 71 | # Tell the move thread to close 72 | input_requests.close() 73 | answer = score 74 | 75 | lib.aoc.give_answer(2019, 13, 2, answer) 76 | 77 | INPUT = lib.aoc.get_input(2019, 13) 78 | part1(INPUT) 79 | part2(INPUT) 80 | -------------------------------------------------------------------------------- /2019/16/solution.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | import lib.aoc 4 | 5 | def phase(nums): 6 | out_nums = [] 7 | 8 | for idx, n in enumerate(nums): 9 | repeat = idx+1 10 | val = 0 11 | 12 | offset = idx 13 | while offset < len(nums): 14 | val += sum(nums[offset:offset+repeat]) 15 | offset += 2*repeat 16 | val -= sum(nums[offset:offset+repeat]) 17 | offset += 2*repeat 18 | 19 | out_nums.append(abs(val) % 10) 20 | 21 | return out_nums 22 | 23 | def part1(s): 24 | nums = list(map(int, s)) 25 | 26 | for _ in range(100): 27 | nums = phase(nums) 28 | 29 | answer = ''.join(map(str, nums[:8])) 30 | 31 | lib.aoc.give_answer(2019, 16, 1, answer) 32 | 33 | def part2(s): 34 | nums = list(map(int, s)) 35 | offset = int(s[:7]) 36 | 37 | tot_length = len(nums) * 10000 38 | # Assuming the offset is far enough in then we can discard the *vast* 39 | # majority of the repeat list and compute each phase via partial sums, 40 | # summing from the right 41 | assert(offset * 2 - 1 >= tot_length) 42 | 43 | remain_length = tot_length - offset 44 | repeat_times = (remain_length + len(nums) - 1) // len(nums) 45 | reverse_nums = (nums[::-1] * repeat_times)[:remain_length] 46 | 47 | for _ in range(100): 48 | reverse_nums = [n%10 for n in itertools.accumulate(reverse_nums)] 49 | 50 | answer = ''.join(map(str, reverse_nums[-8:]))[::-1] 51 | 52 | lib.aoc.give_answer(2019, 16, 2, answer) 53 | 54 | INPUT = lib.aoc.get_input(2019, 16) 55 | part1(INPUT) 56 | part2(INPUT) 57 | -------------------------------------------------------------------------------- /2019/19/solution.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | import lib.aoc 4 | 5 | intcode = __import__('2019.intcode').intcode 6 | 7 | def part1(s): 8 | p = intcode.Program(s) 9 | 10 | answer = 0 11 | 12 | for x in range(50): 13 | for y in range(50): 14 | in_chan, out_chan = p.clone().run() 15 | in_chan.send(x) 16 | in_chan.send(y) 17 | 18 | if out_chan.recv() == 1: 19 | answer += 1 20 | 21 | lib.aoc.give_answer(2019, 19, 1, answer) 22 | 23 | def part2(s): 24 | p = intcode.Program(s) 25 | 26 | def is_affected(x, y): 27 | in_chan, out_chan = p.clone().run() 28 | in_chan.send(x) 29 | in_chan.send(y) 30 | 31 | return out_chan.recv() == 1 32 | 33 | x, y = 0, 0 34 | 35 | WIDTH = 100 36 | HEIGHT = 100 37 | 38 | while True: 39 | # Walk down until the right side of the box is affected 40 | while not is_affected(x+WIDTH-1, y): 41 | y += 1 42 | 43 | # Walk right until the left side of the box is affected 44 | while not is_affected(x, y+HEIGHT-1): 45 | x += 1 46 | 47 | if not is_affected(x+WIDTH-1, y): 48 | # The right side isn't affected anymore! 49 | continue 50 | 51 | # Sanity check the full square 52 | assert(all(is_affected(x+dx, y+dy) 53 | for dx in range(WIDTH) 54 | for dy in range(HEIGHT))) 55 | break 56 | 57 | answer = 10000 * x + y 58 | 59 | lib.aoc.give_answer(2019, 19, 2, answer) 60 | 61 | INPUT = lib.aoc.get_input(2019, 19) 62 | part1(INPUT) 63 | part2(INPUT) 64 | -------------------------------------------------------------------------------- /2019/21/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | intcode = __import__('2019.intcode').intcode 4 | 5 | def run_springdroid(s, springscript): 6 | in_chan, out_chan = intcode.Program(s).run() 7 | 8 | for c in springscript: 9 | in_chan.send(ord(c)) 10 | 11 | for c in out_chan: 12 | if c < 256: 13 | print(chr(c), end='') 14 | else: 15 | return c 16 | 17 | def part1(s): 18 | answer = run_springdroid(s, '''NOT A J 19 | OR B T 20 | AND C T 21 | NOT T T 22 | AND D T 23 | OR T J 24 | WALK 25 | ''') 26 | 27 | lib.aoc.give_answer(2019, 21, 1, answer) 28 | 29 | def part2(s): 30 | answer = run_springdroid(s, '''OR E J 31 | AND I J 32 | OR H J 33 | AND D J 34 | OR B T 35 | AND C T 36 | NOT T T 37 | AND T J 38 | NOT A T 39 | OR T J 40 | RUN 41 | ''') 42 | 43 | lib.aoc.give_answer(2019, 21, 2, answer) 44 | 45 | INPUT = lib.aoc.get_input(2019, 21) 46 | part1(INPUT) 47 | part2(INPUT) 48 | -------------------------------------------------------------------------------- /2020/01/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def part1(s): 4 | nums = list(map(int, s.split())) 5 | for a in nums: 6 | for b in nums: 7 | if a + b == 2020: 8 | answer = a * b 9 | lib.aoc.give_answer(2020, 1, 1, answer) 10 | return 11 | 12 | def part2(s): 13 | nums = list(map(int, s.split())) 14 | for a in nums: 15 | for b in nums: 16 | if a + b >= 2020: 17 | continue 18 | for c in nums: 19 | if a + b + c == 2020: 20 | answer = a * b * c 21 | lib.aoc.give_answer(2020, 1, 2, answer) 22 | return 23 | 24 | INPUT = lib.aoc.get_input(2020, 1) 25 | part1(INPUT) 26 | part2(INPUT) 27 | -------------------------------------------------------------------------------- /2020/02/solution.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import lib.aoc 4 | 5 | def count_valid_password(s, validator): 6 | valid_count = 0 7 | for m in re.finditer('(\d+)\-(\d+) (\w): (\w+)', s): 8 | n1, n2, c, password = m.groups() 9 | if validator(int(n1), int(n2), c, password.strip()): 10 | valid_count += 1 11 | return valid_count 12 | 13 | def part1(s): 14 | def validator(mint, maxt, c, password): 15 | return mint <= password.count(c) <= maxt 16 | answer = count_valid_password(s, validator) 17 | lib.aoc.give_answer(2020, 2, 1, answer) 18 | 19 | def part2(s): 20 | def validator(a, b, c, password): 21 | return (password[a-1] == c) ^ (password[b-1] == c) 22 | answer = count_valid_password(s, validator) 23 | lib.aoc.give_answer(2020, 2, 2, answer) 24 | 25 | INPUT = lib.aoc.get_input(2020, 2) 26 | part1(INPUT) 27 | part2(INPUT) 28 | -------------------------------------------------------------------------------- /2020/03/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def count_tree_hits(s, right, down): 4 | answer = 0 5 | x = 0 6 | for line in s.splitlines()[::down]: 7 | if line[x % len(line)] == '#': 8 | answer += 1 9 | x += right 10 | return answer 11 | 12 | def part1(s): 13 | answer = count_tree_hits(s, 3, 1) 14 | lib.aoc.give_answer(2020, 3, 1, answer) 15 | 16 | def part2(s): 17 | answer = 1 18 | for right, down in ((1,1), 19 | (3,1), 20 | (5,1), 21 | (7,1), 22 | (1,2)): 23 | answer *= count_tree_hits(s, right, down) 24 | lib.aoc.give_answer(2020, 3, 2, answer) 25 | 26 | INPUT = lib.aoc.get_input(2020, 3) 27 | part1(INPUT) 28 | part2(INPUT) 29 | -------------------------------------------------------------------------------- /2020/04/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def parse_passports(s): 4 | for record in s.split('\n\n'): 5 | passport = {} 6 | for part in record.split(): 7 | f, val = part.split(':') 8 | passport[f] = val 9 | yield passport 10 | 11 | def part1(s): 12 | answer = 0 13 | FIELDS = {'byr', 'iyr', 'eyr', 'hgt', 'hcl', 'ecl', 'pid', 'cid'} 14 | for passport in parse_passports(s): 15 | seen = set(passport.keys()) 16 | if 'cid' not in seen: 17 | seen.add('cid') 18 | if seen == FIELDS: 19 | answer += 1 20 | lib.aoc.give_answer(2020, 4, 1, answer) 21 | 22 | def validate_hgt(hgt): 23 | if hgt is None: 24 | return False 25 | if hgt[-2:] == 'cm': 26 | return 150 <= int(hgt[:-2]) <= 193 27 | if hgt[-2:] == 'in': 28 | return 59 <= int(hgt[:-2]) <= 76 29 | return False 30 | 31 | def validate_hcl(hcl): 32 | return (hcl is not None and 33 | hcl[0] == '#' and 34 | all(c in '0123456789abcdef' for c in hcl[1:])) 35 | 36 | def validate_pid(pid): 37 | return (pid is not None and 38 | len(pid) == 9 and 39 | all(c in '0123456789' for c in pid)) 40 | 41 | def part2(s): 42 | answer = 0 43 | for passport in parse_passports(s): 44 | if (1920 <= int(passport.get('byr', 0)) <= 2002 and 45 | 2010 <= int(passport.get('iyr', 0)) <= 2020 and 46 | 2020 <= int(passport.get('eyr', 0)) <= 2030 and 47 | validate_hgt(passport.get('hgt')) and 48 | validate_hcl(passport.get('hcl')) and 49 | passport.get('ecl') in ('amb', 50 | 'blu', 51 | 'brn', 52 | 'gry', 53 | 'grn', 54 | 'hzl', 55 | 'oth') and 56 | validate_pid(passport.get('pid'))): 57 | answer += 1 58 | lib.aoc.give_answer(2020, 4, 2, answer) 59 | 60 | INPUT = lib.aoc.get_input(2020, 4) 61 | part1(INPUT) 62 | part2(INPUT) 63 | -------------------------------------------------------------------------------- /2020/05/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def iter_seats(s): 4 | for seat in s.splitlines(): 5 | yield int(seat.translate(str.maketrans('FBLR', '0101')), 2) 6 | 7 | def part1(s): 8 | answer = max(iter_seats(s)) 9 | lib.aoc.give_answer(2020, 5, 1, answer) 10 | 11 | def part2(s): 12 | seats = set(iter_seats(s)) 13 | answer = [s for s in range(min(seats), max(seats)) if s not in seats][0] 14 | lib.aoc.give_answer(2020, 5, 2, answer) 15 | 16 | INPUT = lib.aoc.get_input(2020, 5) 17 | part1(INPUT) 18 | part2(INPUT) 19 | -------------------------------------------------------------------------------- /2020/06/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def part1(s): 4 | groups = s.split('\n\n') 5 | answer = 0 6 | for g in groups: 7 | g = ''.join(g.split()) 8 | answer += len(set(g)) 9 | lib.aoc.give_answer(2020, 6, 1, answer) 10 | 11 | def part2(s): 12 | groups = s.split('\n\n') 13 | answer = 0 14 | for g in groups: 15 | people = g.split() 16 | all_yes = set(people[0]) 17 | for p in people[1:]: 18 | all_yes &= set(p) 19 | answer += len(set(all_yes)) 20 | lib.aoc.give_answer(2020, 6, 2, answer) 21 | 22 | INPUT = lib.aoc.get_input(2020, 6) 23 | part1(INPUT) 24 | part2(INPUT) 25 | -------------------------------------------------------------------------------- /2020/08/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def run(program, return_accumulator_on_looping=False): 4 | acc = 0 5 | idx = 0 6 | seen = set() 7 | while True: 8 | if idx == len(program): 9 | return acc 10 | if idx in seen: 11 | if return_accumulator_on_looping: 12 | return acc 13 | return None 14 | seen.add(idx) 15 | op, val = program[idx].split() 16 | val = int(val) 17 | if op == 'acc': 18 | acc = acc + val 19 | idx += 1 20 | continue 21 | if op == 'jmp': 22 | idx += val 23 | continue 24 | if op == 'nop': 25 | idx += 1 26 | continue 27 | assert(False) 28 | 29 | def part1(s): 30 | program = list(s.splitlines()) 31 | answer = run(program, True) 32 | lib.aoc.give_answer(2020, 8, 1, answer) 33 | 34 | def part2(s): 35 | program = list(s.splitlines()) 36 | for idx, inst in enumerate(program): 37 | if inst.startswith('nop'): 38 | program[idx] = inst.replace('nop', 'jmp') 39 | answer = run(program) 40 | program[idx] = inst 41 | elif inst.startswith('jmp'): 42 | program[idx] = inst.replace('jmp', 'nop') 43 | answer = run(program) 44 | program[idx] = inst 45 | else: 46 | continue 47 | if answer is not None: 48 | break 49 | assert(answer is not None) 50 | lib.aoc.give_answer(2020, 8, 2, answer) 51 | 52 | INPUT = lib.aoc.get_input(2020, 8) 53 | 54 | part1(INPUT) 55 | part2(INPUT) 56 | -------------------------------------------------------------------------------- /2020/09/solution.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | import lib.aoc 4 | 5 | def get_invalid_num(nums): 6 | for idx, n in enumerate(nums[25:]): 7 | options = set(nums[idx:idx+25]) 8 | if not any(n-v in options 9 | for v 10 | in options): 11 | return n 12 | 13 | def part1(s): 14 | nums = list(map(int, s.split())) 15 | answer = get_invalid_num(nums) 16 | lib.aoc.give_answer(2020, 9, 1, answer) 17 | 18 | def part2(s): 19 | nums = list(map(int, s.split())) 20 | target = get_invalid_num(nums) 21 | sums = list(itertools.accumulate(nums)) 22 | for idx, a in enumerate(sums): 23 | for idx2, b in enumerate(sums[idx+1:]): 24 | diff = b - a 25 | if diff > target: 26 | break 27 | if diff == target: 28 | the_nums = nums[idx+1:idx+idx2+2] 29 | assert(sum(the_nums) == target) 30 | answer = min(the_nums) + max(the_nums) 31 | lib.aoc.give_answer(2020, 9, 2, answer) 32 | return 33 | 34 | INPUT = lib.aoc.get_input(2020, 9) 35 | 36 | part1(INPUT) 37 | part2(INPUT) 38 | -------------------------------------------------------------------------------- /2020/10/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import lib.aoc 4 | 5 | def part1(s): 6 | nums = list(map(int, s.split())) 7 | nums += [max(nums) + 3, 0] 8 | nums = sorted(nums) 9 | last = nums[0] 10 | diff_counts = collections.defaultdict(int) 11 | one_diffs = 0 12 | three_diffs = 0 13 | for n in nums[1:]: 14 | diff_counts[n-last] += 1 15 | last = n 16 | assert(max(diff_counts.keys()) <= 3) 17 | answer = diff_counts[1] * diff_counts[3] 18 | lib.aoc.give_answer(2020, 10, 1, answer) 19 | 20 | def part2(s): 21 | nums = list(map(int, s.split())) 22 | nums += [max(nums) + 3, 0] 23 | nums = sorted(nums) 24 | arrangements = [1] 25 | for idx in range(1, len(nums)): 26 | count = sum(arrangements[i] 27 | for i in range(idx) 28 | if nums[idx] - nums[i] in (1, 2, 3)) 29 | arrangements.append(count) 30 | answer = arrangements[-1] 31 | lib.aoc.give_answer(2020, 10, 2, answer) 32 | 33 | INPUT = lib.aoc.get_input(2020, 10) 34 | 35 | part1(INPUT) 36 | part2(INPUT) 37 | -------------------------------------------------------------------------------- /2020/12/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def part1(s): 4 | dx, dy = 1, 0 5 | x = 0 6 | y = 0 7 | for act in s.splitlines(): 8 | t = act[0] 9 | n = int(act[1:]) 10 | if t == 'N': 11 | y += n 12 | if t == 'S': 13 | y -= n 14 | if t == 'E': 15 | x += n 16 | if t == 'W': 17 | x -= n 18 | if t == 'L': 19 | for _ in range(n//90): 20 | dx, dy = -dy, dx 21 | if t == 'R': 22 | for _ in range(n//90): 23 | dx, dy = dy, -dx 24 | if t == 'F': 25 | x += dx*n 26 | y += dy*n 27 | answer = abs(x) + abs(y) 28 | lib.aoc.give_answer(2020, 12, 1, answer) 29 | 30 | def part2(s): 31 | sx, sy = 0, 0 32 | x = 10 33 | y = 1 34 | for act in s.splitlines(): 35 | t = act[0] 36 | n = int(act[1:]) 37 | if t == 'N': 38 | y += n 39 | if t == 'S': 40 | y -= n 41 | if t == 'E': 42 | x += n 43 | if t == 'W': 44 | x -= n 45 | if t == 'L': 46 | for _ in range(n//90): 47 | x, y = -y, x 48 | if t == 'R': 49 | for _ in range(n//90): 50 | x, y = y, -x 51 | if t == 'F': 52 | sx += x*n 53 | sy += y*n 54 | answer = abs(sx) + abs(sy) 55 | lib.aoc.give_answer(2020, 12, 2, answer) 56 | 57 | INPUT = lib.aoc.get_input(2020, 12) 58 | 59 | part1(INPUT) 60 | part2(INPUT) 61 | -------------------------------------------------------------------------------- /2020/13/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.math 3 | 4 | def parse_notes(s): 5 | timestamp, rest = s.splitlines() 6 | rest = rest.split(',') 7 | busses = [int(b) for b in rest if b != 'x'] 8 | offsets = [idx for idx,b in enumerate(rest) if b != 'x'] 9 | return int(timestamp), busses, offsets 10 | 11 | def part1(s): 12 | timestamp, busses, _ = parse_notes(s) 13 | available_times = [] 14 | for b in busses: 15 | time_available = ((timestamp+b-1) // b) * b 16 | available_times.append((time_available, b)) 17 | best_time, best_bus = min(available_times) 18 | answer = (best_time-timestamp) * best_bus 19 | lib.aoc.give_answer(2020, 13, 1, answer) 20 | 21 | def part2(s): 22 | _, busses, offsets = parse_notes(s) 23 | answer = lib.math.offset_chinese_remainder(zip(busses, offsets)) 24 | lib.aoc.give_answer(2020, 13, 2, answer) 25 | 26 | INPUT = lib.aoc.get_input(2020, 13) 27 | 28 | part1(INPUT) 29 | part2(INPUT) 30 | -------------------------------------------------------------------------------- /2020/14/solution.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import lib.aoc 4 | 5 | def parse_program(prog): 6 | for line in prog.splitlines(): 7 | m = re.fullmatch('mask = ([X01]+)', line) 8 | if m is not None: 9 | yield 'mask', m.group(1) 10 | continue 11 | m = re.fullmatch('mem\[(\d+)\] = (\d+)', line) 12 | if m is not None: 13 | yield 'mem', int(m.group(1)), int(m.group(2)) 14 | continue 15 | assert(False) 16 | 17 | def part1(s): 18 | mask = '' 19 | memory = {} 20 | for op in parse_program(s): 21 | if op[0] == 'mask': 22 | mask = op[1] 23 | elif op[0] == 'mem': 24 | _, addr, val = op 25 | # Apply zeros in mask 26 | val &= int(mask.replace('X', '1'), 2) 27 | # Apply ones in mask 28 | val |= int(mask.replace('X', '0'), 2) 29 | memory[addr] = val 30 | answer = sum(memory.values()) 31 | lib.aoc.give_answer(2020, 14, 1, answer) 32 | 33 | def all_addresses(addr, addr_mask): 34 | # Apply ones in mask 35 | addr |= int(addr_mask.replace('X', '0'), 2) 36 | candidates = [addr] 37 | mask = 1 38 | for c in addr_mask[::-1]: 39 | if c == 'X': 40 | candidates = [cand & (~mask) for cand in candidates] 41 | candidates += [cand | mask for cand in candidates] 42 | mask <<= 1 43 | return candidates 44 | 45 | def part2(s): 46 | mask = '' 47 | memory = {} 48 | for op in parse_program(s): 49 | if op[0] == 'mask': 50 | mask = op[1] 51 | elif op[0] == 'mem': 52 | _, addr, val = op 53 | for a in all_addresses(addr, mask): 54 | memory[a] = val 55 | answer = sum(memory.values()) 56 | lib.aoc.give_answer(2020, 14, 2, answer) 57 | 58 | INPUT = lib.aoc.get_input(2020, 14) 59 | 60 | part1(INPUT) 61 | part2(INPUT) 62 | -------------------------------------------------------------------------------- /2020/15/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def get_nth_number_in_game(seed, desired_turn): 4 | spoken_on = {n:turn 5 | for turn, n 6 | in enumerate(map(int, 7 | seed.split(',')))} 8 | n = 0 # The seed numbers are all unique 9 | for turn in range(len(spoken_on), desired_turn-1): 10 | # Get the number of turns since the number was spoken (or 0 if it's new) 11 | n_next = turn - spoken_on.get(n, turn) 12 | spoken_on[n] = turn 13 | n = n_next 14 | return n 15 | 16 | def part1(s): 17 | answer = get_nth_number_in_game(s, 2020) 18 | lib.aoc.give_answer(2020, 15, 1, answer) 19 | 20 | def part2(s): 21 | answer = get_nth_number_in_game(s, 30000000) 22 | lib.aoc.give_answer(2020, 15, 2, answer) 23 | 24 | INPUT = lib.aoc.get_input(2020, 15) 25 | 26 | part1(INPUT) 27 | part2(INPUT) 28 | -------------------------------------------------------------------------------- /2020/16/solution.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import lib.aoc 4 | 5 | def parse_input(s): 6 | raw_rules, my_ticket, nearby = s.split('\n\n') 7 | 8 | rules = {} 9 | for rule in raw_rules.splitlines(): 10 | m = re.match('(.*): (\d+)\-(\d+) or (\d+)\-(\d+)', rule) 11 | name = m.group(1) 12 | range_1 = range(int(m.group(2)), int(m.group(3))+1) 13 | range_2 = range(int(m.group(4)), int(m.group(5))+1) 14 | rules[name] = set(range_1) | set(range_2) 15 | 16 | my_ticket = list(map(int, my_ticket.splitlines()[1].split(','))) 17 | 18 | nearby = [list(map(int, l.split(','))) for l in nearby.splitlines()[1:]] 19 | 20 | return rules, my_ticket, nearby 21 | 22 | def part1(s): 23 | rules, _, nearby = parse_input(s) 24 | 25 | answer = 0 26 | for ticket in nearby: 27 | for n in ticket: 28 | if not any(n in nums for nums in rules.values()): 29 | answer += n 30 | 31 | lib.aoc.give_answer(2020, 16, 1, answer) 32 | 33 | def part2(s): 34 | rules, my_ticket, nearby = parse_input(s) 35 | 36 | # Filter to the good nearby tickets 37 | nearby = [ticket for ticket in nearby 38 | if all(any(n in nums for nums in rules.values()) 39 | for n in ticket)] 40 | 41 | rule_order_candidates = [] 42 | for vals in zip(*nearby): 43 | candidates = set() 44 | for name, valid_nums in rules.items(): 45 | if all(v in valid_nums for v in vals): 46 | candidates.add(name) 47 | rule_order_candidates.append(candidates) 48 | 49 | rule_order = [None] * len(rule_order_candidates) 50 | for _ in range(len(rule_order)): 51 | for idx, candidates in enumerate(rule_order_candidates): 52 | if rule_order[idx] is None and 1 == len(candidates): 53 | (rule_order[idx],) = candidates 54 | for j, old_candidates in enumerate(rule_order_candidates): 55 | rule_order_candidates[j] = old_candidates - candidates 56 | break 57 | assert(None not in rule_order) 58 | 59 | answer = 1 60 | for name, val in zip(rule_order, my_ticket): 61 | if name.startswith('departure'): 62 | answer *= val 63 | 64 | lib.aoc.give_answer(2020, 16, 2, answer) 65 | 66 | INPUT = lib.aoc.get_input(2020, 16) 67 | 68 | part1(INPUT) 69 | part2(INPUT) 70 | -------------------------------------------------------------------------------- /2020/17/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import itertools 3 | 4 | import lib.aoc 5 | 6 | def neighbor_coords(coord): 7 | neighbors = itertools.product(*((c-1, c, c+1) 8 | for c in coord)) 9 | return (n for n in neighbors if n != coord) 10 | 11 | def step(prev_active): 12 | active_counts = collections.defaultdict(int) 13 | for coord in prev_active: 14 | for neighbor in neighbor_coords(coord): 15 | active_counts[neighbor] += 1 16 | new_active = [] 17 | for coord in prev_active: 18 | count = active_counts.pop(coord, 0) 19 | if count in (2, 3): 20 | new_active.append(coord) 21 | for coord, count in active_counts.items(): 22 | if count == 3: 23 | new_active.append(coord) 24 | return new_active 25 | 26 | def parse_active_coords(s, coord_suffix): 27 | return active_coords 28 | 29 | def run_simulation(s, dims, steps): 30 | active_coords = [] 31 | for y, line in enumerate(s.splitlines()): 32 | for x, c in enumerate(line): 33 | if c == '#': 34 | active_coords.append((x, y) + (0,) * (dims - 2)) 35 | for _ in range(steps): 36 | active_coords = step(active_coords) 37 | return len(active_coords) 38 | 39 | def part1(s): 40 | answer = run_simulation(s, dims=3, steps=6) 41 | lib.aoc.give_answer(2020, 17, 1, answer) 42 | 43 | def part2(s): 44 | answer = run_simulation(s, dims=4, steps=6) 45 | lib.aoc.give_answer(2020, 17, 2, answer) 46 | 47 | INPUT = lib.aoc.get_input(2020, 17) 48 | 49 | part1(INPUT) 50 | part2(INPUT) 51 | -------------------------------------------------------------------------------- /2020/18/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.parsing 3 | 4 | def special_eval(expression): 5 | def merger(parts): 6 | while len(parts) > 1: 7 | a, op, b = parts[:3] 8 | parts[:3] = [eval(f'{a} {op} {b}')] 9 | return parts[0] 10 | return lib.parsing.eval_parenthesized_expression(expression, merger) 11 | 12 | def part1(s): 13 | answer = sum(map(special_eval, s.splitlines())) 14 | lib.aoc.give_answer(2020, 18, 1, answer) 15 | 16 | def advanced_special_eval(expression): 17 | def merger(parts): 18 | for op in '+*': 19 | while op in parts: 20 | idx = parts.index(op) 21 | a, op, b = parts[idx-1:idx+2] 22 | parts[idx-1:idx+2] = [eval(f'{a} {op} {b}')] 23 | return parts[0] 24 | return lib.parsing.eval_parenthesized_expression(expression, merger) 25 | 26 | def part2(s): 27 | answer = sum(map(advanced_special_eval, s.splitlines())) 28 | lib.aoc.give_answer(2020, 18, 2, answer) 29 | 30 | INPUT = lib.aoc.get_input(2020, 18) 31 | 32 | part1(INPUT) 33 | part2(INPUT) 34 | -------------------------------------------------------------------------------- /2020/19/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.cyk 3 | 4 | def solve(s): 5 | rule_list, messages = s.split('\n\n') 6 | 7 | rules = [] 8 | for line in rule_list.splitlines(): 9 | num, options = line.split(': ') 10 | 11 | if options[0] == '"': 12 | # Single character 13 | assert(options[-1] == '"' and len(options) == 3) 14 | rules.append((lib.cyk.Symbol(num), options[1])) 15 | continue 16 | 17 | for opt in options.split(' | '): 18 | rules.append((lib.cyk.Symbol(num), 19 | [lib.cyk.Symbol(part) for part in opt.split()])) 20 | 21 | cnf = lib.cyk.CNFGrammar(rules, lib.cyk.Symbol('0')) 22 | 23 | return len(list(filter(cnf.matches, messages.splitlines()))) 24 | 25 | def part1(s): 26 | answer = solve(s) 27 | 28 | lib.aoc.give_answer(2020, 19, 1, answer) 29 | 30 | def part2(s): 31 | # Replace rules 8 and 11 32 | s = s.replace('\n8: 42\n', 33 | '\n8: 42 | 42 8\n') 34 | s = s.replace('\n11: 42 31\n', 35 | '\n11: 42 31 | 42 11 31\n') 36 | 37 | answer = solve(s) 38 | 39 | lib.aoc.give_answer(2020, 19, 2, answer) 40 | 41 | INPUT = lib.aoc.get_input(2020, 19) 42 | 43 | part1(INPUT) 44 | part2(INPUT) 45 | -------------------------------------------------------------------------------- /2020/21/solution.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import lib.aoc 4 | 5 | def parse_foods(s): 6 | foods = [] 7 | all_ingredients = set() 8 | all_allergens = set() 9 | for line in s.splitlines(): 10 | m = re.fullmatch('([a-zA-Z ]+) \(contains ([a-zA-Z ,]+)\)', line) 11 | ingredients = set(m.group(1).split()) 12 | allergens = set(m.group(2).split(', ')) 13 | all_ingredients |= ingredients 14 | all_allergens |= allergens 15 | foods.append((ingredients, allergens)) 16 | return foods, all_ingredients, all_allergens 17 | 18 | def find_allergen_containing_ingredients(foods, all_ingredients, all_allergens): 19 | allergen_to_ingredients = {allergen:set(all_ingredients) 20 | for allergen in all_allergens} 21 | 22 | for ingredients, allergens in foods: 23 | for allergen in allergens: 24 | allergen_to_ingredients[allergen] &= ingredients 25 | 26 | isolated_ingredients = set() 27 | while len(isolated_ingredients) < len(all_allergens): 28 | for ingredients in allergen_to_ingredients.values(): 29 | if len(ingredients) == 1: 30 | isolated_ingredients |= ingredients 31 | for ingredients in allergen_to_ingredients.values(): 32 | if len(ingredients) > 1: 33 | ingredients -= isolated_ingredients 34 | 35 | return {allergen:list(ingredients)[0] 36 | for allergen,ingredients in allergen_to_ingredients.items()} 37 | 38 | def part1(s): 39 | foods, all_ingredients, all_allergens = parse_foods(s) 40 | allergen_to_ingredient = find_allergen_containing_ingredients(foods, 41 | all_ingredients, 42 | all_allergens) 43 | 44 | bad_ingredients = set(allergen_to_ingredient.values()) 45 | 46 | answer = 0 47 | for ingredients, allergens in foods: 48 | answer += len(ingredients - bad_ingredients) 49 | lib.aoc.give_answer(2020, 21, 1, answer) 50 | 51 | def part2(s): 52 | allergen_to_ingredient = find_allergen_containing_ingredients(*parse_foods(s)) 53 | 54 | answer = ','.join(ingredient 55 | for _,ingredient 56 | in sorted(allergen_to_ingredient.items())) 57 | 58 | lib.aoc.give_answer(2020, 21, 2, answer) 59 | 60 | INPUT = lib.aoc.get_input(2020, 21) 61 | 62 | part1(INPUT) 63 | part2(INPUT) 64 | -------------------------------------------------------------------------------- /2020/22/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def iter_game(p1, p2): 4 | c1, c2 = p1[0], p2[0] 5 | p1, p2 = p1[1:], p2[1:] 6 | if c1 > c2: 7 | p1.append(c1) 8 | p1.append(c2) 9 | else: 10 | p2.append(c2) 11 | p2.append(c1) 12 | return p1, p2 13 | 14 | def score(deck): 15 | deck = deck[::-1] 16 | tot = 0 17 | for idx, card in enumerate(deck): 18 | tot += (idx+1) * card 19 | return tot 20 | 21 | def part1(s): 22 | p1, p2 = s.split('\n\n') 23 | p1 = list(map(int, p1.splitlines()[1:])) 24 | p2 = list(map(int, p2.splitlines()[1:])) 25 | while len(p1) > 0 and len(p2) > 0: 26 | p1, p2 = iter_game(p1, p2) 27 | answer = score(p1 + p2) 28 | lib.aoc.give_answer(2020, 22, 1, answer) 29 | 30 | def play_recursive(p1, p2): 31 | seen = set() 32 | while len(p1) > 0 and len(p2) > 0: 33 | state = (tuple(p1), tuple(p2)) 34 | if state in seen: 35 | # Player 1 wins! 36 | return p1, [] 37 | seen.add(state) 38 | c1, c2 = p1[0], p2[0] 39 | p1, p2 = p1[1:], p2[1:] 40 | if len(p1) >= c1 and len(p2) >= c2: 41 | win1, win2 = play_recursive(list(p1[:c1]), list(p2[:c2])) 42 | if win1: 43 | p1.append(c1) 44 | p1.append(c2) 45 | else: 46 | p2.append(c2) 47 | p2.append(c1) 48 | continue 49 | if c1 > c2: 50 | p1.append(c1) 51 | p1.append(c2) 52 | else: 53 | p2.append(c2) 54 | p2.append(c1) 55 | return p1, p2 56 | 57 | def part2(s): 58 | p1, p2 = s.split('\n\n') 59 | p1 = list(map(int, p1.splitlines()[1:])) 60 | p2 = list(map(int, p2.splitlines()[1:])) 61 | p1, p2 = play_recursive(p1, p2) 62 | answer = score(p1 + p2) 63 | lib.aoc.give_answer(2020, 22, 2, answer) 64 | 65 | INPUT = lib.aoc.get_input(2020, 22) 66 | 67 | part1(INPUT) 68 | part2(INPUT) 69 | -------------------------------------------------------------------------------- /2020/23/solution.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | using namespace std; 5 | 6 | struct Link { 7 | Link *next = nullptr; 8 | long long num = 0; 9 | }; 10 | 11 | constexpr long long seed[] = {}; 12 | constexpr long long NUM_CUPS = 1000000; 13 | constexpr long long TURNS = 10000000; 14 | 15 | void do_turn(Link *current, std::vector &links, long long turn) { 16 | Link *pulled[3]; 17 | long long taboo[3]; 18 | { 19 | Link *next_pulled = current->next; 20 | for (int i = 0; i < 3; i++) 21 | { 22 | pulled[i] = next_pulled; 23 | taboo[i] = next_pulled->num; 24 | next_pulled = next_pulled->next; 25 | } 26 | current->next = next_pulled; 27 | } 28 | long long dest = current->num - 1; 29 | if (dest < 1) 30 | dest = NUM_CUPS; 31 | while (dest == taboo[0] || dest == taboo[1] || dest == taboo[2]) 32 | { 33 | dest--; 34 | if (dest < 1) 35 | dest = NUM_CUPS; 36 | } 37 | auto dest_link = &links[dest]; 38 | 39 | pulled[2]->next = dest_link->next; 40 | dest_link->next = pulled[0]; 41 | } 42 | 43 | void debug_sequence(Link *start) { 44 | Link *cur = start; 45 | for (;;) { 46 | std::cout << cur->num << ' '; 47 | cur = cur->next; 48 | if (cur == nullptr) { 49 | std::cout << "nullptr" << std::endl; 50 | return; 51 | } 52 | if (cur == start) { 53 | std::cout << std::endl; 54 | return; 55 | } 56 | } 57 | } 58 | 59 | int main() { 60 | std::vector links(NUM_CUPS+1); 61 | for (long long i = 0; i < NUM_CUPS+1; i++) { 62 | links[i].num = i; 63 | links[i].next = &links[(i+1)%(NUM_CUPS+1)]; 64 | } 65 | for (long long idx = 0; idx < 8; idx++) { 66 | links[seed[idx]].next = &links[seed[idx+1]]; 67 | } 68 | links[seed[8]].next = &links[10]; 69 | links[NUM_CUPS].next = &links[seed[0]]; 70 | links[0].next = nullptr; 71 | links[0].num = -1; 72 | 73 | Link *current = &links[seed[0]]; 74 | for (long long i = 0; i < TURNS; i++) { 75 | do_turn(current, links, i); 76 | current = current->next; 77 | } 78 | const auto one = &links[1]; 79 | const auto a = one->next; 80 | const auto b = a->next; 81 | 82 | std::cout << a->num << ", " << b->num << ", " << static_cast(a->num) * static_cast(b->num) << '\n'; 83 | 84 | return 0; 85 | } 86 | -------------------------------------------------------------------------------- /2020/23/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def play_cups(seed, cup_count, turns): 4 | assert(sorted(seed) == list(range(1, max(seed)+1))) 5 | 6 | cups = [-1] + list(range(2, cup_count+1)) + [1] 7 | for cup, next_cup in zip(seed, seed[1:]): 8 | cups[cup] = next_cup 9 | if len(seed) < cup_count: 10 | cups[seed[-1]] = max(seed)+1 11 | cups[-1] = seed[0] 12 | else: 13 | cups[seed[-1]] = seed[0] 14 | 15 | min_cup = min(seed) 16 | current = seed[0] 17 | for _ in range(turns): 18 | p1 = cups[current] 19 | p2 = cups[p1] 20 | p3 = cups[p2] 21 | cups[current] = cups[p3] 22 | dest = current-1 23 | while True: 24 | if dest < min_cup: 25 | dest = cup_count 26 | if dest not in [p1, p2, p3]: 27 | break 28 | dest -= 1 29 | cups[p3] = cups[dest] 30 | cups[dest] = p1 31 | current = cups[current] 32 | return cups 33 | 34 | def part1(s): 35 | seed = list(map(int, s)) 36 | cups = play_cups(seed, len(seed), 100) 37 | order = [] 38 | current = cups[1] 39 | while current != 1: 40 | order.append(current) 41 | current = cups[current] 42 | answer = ''.join(map(str, order)) 43 | lib.aoc.give_answer(2020, 23, 1, answer) 44 | 45 | def part2(s): 46 | cups = play_cups(list(map(int, s)), 1000000, 10000000) 47 | a = cups[1] 48 | b = cups[a] 49 | answer = a*b 50 | lib.aoc.give_answer(2020, 23, 2, answer) 51 | 52 | INPUT = lib.aoc.get_input(2020, 23) 53 | 54 | part1(INPUT) 55 | part2(INPUT) 56 | -------------------------------------------------------------------------------- /2020/24/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import re 3 | 4 | import lib.aoc 5 | import lib.hex_coord 6 | 7 | def get_initially_active_tiles(move_list): 8 | active_tiles = set() 9 | for moves in move_list.splitlines(): 10 | pos = lib.hex_coord.EWHexCoord() 11 | for move in re.findall('e|w|se|sw|nw|ne', moves): 12 | pos = pos.move(move) 13 | if pos in active_tiles: 14 | active_tiles.remove(pos) 15 | else: 16 | active_tiles.add(pos) 17 | return active_tiles 18 | 19 | def part1(s): 20 | answer = len(get_initially_active_tiles(s)) 21 | lib.aoc.give_answer(2020, 24, 1, answer) 22 | 23 | def iterate(active_tiles): 24 | neighbor_counts = collections.defaultdict(int) 25 | for pos in active_tiles: 26 | for n in pos.neighbors: 27 | neighbor_counts[n] += 1 28 | new_active = {pos 29 | for pos in active_tiles 30 | if neighbor_counts.pop(pos, 0) in (1, 2)} 31 | new_active |= {pos 32 | for pos, count in neighbor_counts.items() 33 | if count == 2} 34 | return new_active 35 | 36 | def part2(s): 37 | active_tiles = get_initially_active_tiles(s) 38 | for _ in range(100): 39 | active_tiles = iterate(active_tiles) 40 | answer = len(active_tiles) 41 | lib.aoc.give_answer(2020, 24, 2, answer) 42 | 43 | INPUT = lib.aoc.get_input(2020, 24) 44 | 45 | part1(INPUT) 46 | part2(INPUT) 47 | -------------------------------------------------------------------------------- /2020/25/solution.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | import lib.aoc 4 | 5 | def part1(s): 6 | a, b = map(int, s.splitlines()) 7 | 8 | cracking_val = 1 9 | mod = 20201227 10 | for exp in itertools.count(): 11 | if cracking_val == a: 12 | base = b 13 | break 14 | if cracking_val == b: 15 | base = a 16 | break 17 | cracking_val = (cracking_val * 7) % mod 18 | 19 | answer = pow(base, exp, mod) 20 | lib.aoc.give_answer(2020, 25, 1, answer) 21 | 22 | def part2(s): 23 | print('There is no part two for Christmas!') 24 | 25 | INPUT = lib.aoc.get_input(2020, 25) 26 | 27 | part1(INPUT) 28 | part2(INPUT) 29 | -------------------------------------------------------------------------------- /2021/01/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def part1(s): 4 | nums = list(map(int, s.split())) 5 | answer = 0 6 | for i in range(len(nums)-1): 7 | if nums[i] < nums[i+1]: 8 | answer += 1 9 | 10 | lib.aoc.give_answer(2021, 1, 1, answer) 11 | 12 | def part2(s): 13 | nums = list(map(int, s.split())) 14 | answer = 0 15 | for i in range(len(nums)-3): 16 | if nums[i] < nums[i+3]: 17 | answer += 1 18 | 19 | lib.aoc.give_answer(2021, 1, 2, answer) 20 | 21 | INPUT = lib.aoc.get_input(2021, 1) 22 | part1(INPUT) 23 | part2(INPUT) 24 | -------------------------------------------------------------------------------- /2021/02/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def part1(s): 4 | x, d = 0, 0 5 | for line in s.split('\n'): 6 | act, n = line.split() 7 | n = int(n) 8 | if act == 'forward': 9 | x += n 10 | elif act == 'down': 11 | d += n 12 | elif act == 'up': 13 | d -= n 14 | 15 | answer = x*d 16 | 17 | lib.aoc.give_answer(2021, 2, 1, answer) 18 | 19 | def part2(s): 20 | x, d = 0, 0 21 | aim = 0 22 | for line in s.split('\n'): 23 | act, n = line.split() 24 | n = int(n) 25 | if act == 'forward': 26 | x += n 27 | d += aim*n 28 | elif act == 'down': 29 | aim += n 30 | elif act == 'up': 31 | aim -= n 32 | 33 | answer = x*d 34 | 35 | lib.aoc.give_answer(2021, 2, 2, answer) 36 | 37 | INPUT = lib.aoc.get_input(2021, 2) 38 | part1(INPUT) 39 | part2(INPUT) 40 | -------------------------------------------------------------------------------- /2021/03/solution.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | 3 | import lib.aoc 4 | 5 | def parse(s): 6 | nums = [int(n, 2) 7 | for n in s.split()] 8 | bits = len(s.split()[0]) 9 | masks = [2**b 10 | for b in range(bits-1, -1, -1)] 11 | return nums, masks 12 | 13 | def most_least(nums, mask): 14 | return Counter(n & mask for n in nums).most_common(2) 15 | 16 | def part1(s): 17 | nums, masks = parse(s) 18 | 19 | gamma, epsilon = 0, 0 20 | 21 | for m in masks: 22 | most, least = most_least(nums, m) 23 | gamma |= most[0] 24 | epsilon |= least[0] 25 | 26 | answer = gamma * epsilon 27 | 28 | lib.aoc.give_answer(2021, 3, 1, answer) 29 | 30 | def part2(s): 31 | nums, masks = parse(s) 32 | 33 | oxy_cands, co2_cands = nums, nums 34 | 35 | for m in masks: 36 | if len(oxy_cands) > 1: 37 | most, least = most_least(oxy_cands, m) 38 | target = most[0] 39 | if most[1] == least[1]: 40 | target = m 41 | oxy_cands = [n for n in oxy_cands if n & m == target] 42 | if len(co2_cands) > 1: 43 | most, least = most_least(co2_cands, m) 44 | target = least[0] 45 | if most[1] == least[1]: 46 | target = 0 47 | co2_cands = [n for n in co2_cands if n & m == target] 48 | 49 | answer = oxy_cands[0]*co2_cands[0] 50 | 51 | lib.aoc.give_answer(2021, 3, 2, answer) 52 | 53 | INPUT = lib.aoc.get_input(2021, 3) 54 | part1(INPUT) 55 | part2(INPUT) 56 | -------------------------------------------------------------------------------- /2021/04/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.grid 3 | 4 | class Board: 5 | def __init__(self, lines): 6 | board = lib.grid.FixedGrid.parse(lines, 7 | linesplit_fn=lambda line: line.split(), 8 | value_fn=int) 9 | 10 | assert(board.width == 5 and board.height == 5) 11 | 12 | self._nums = {val: c 13 | for c, val in board.items()} 14 | self._seen = set() 15 | 16 | def mark(self, n): 17 | coord = self._nums.get(n) 18 | if coord is not None: 19 | self._seen.add(coord) 20 | 21 | @property 22 | def winning(self): 23 | for x in range(5): 24 | if all((x, y) in self._seen 25 | for y in range(5)): 26 | return True 27 | for y in range(5): 28 | if all((x, y) in self._seen 29 | for x in range(5)): 30 | return True 31 | return False 32 | 33 | def score(self, last_n): 34 | t = 0 35 | for val, coord in self._nums.items(): 36 | if coord in self._seen: 37 | continue 38 | t += val 39 | return t * last_n 40 | 41 | def parse(s): 42 | groups = s.split('\n\n') 43 | nums = list(map(int, groups[0].split(','))) 44 | groups = groups[1:] 45 | 46 | boards = list(map(Board, groups)) 47 | 48 | return nums, boards 49 | 50 | def find_winning_score(boards, nums): 51 | for idx, n in enumerate(nums): 52 | for b in boards: 53 | b.mark(n) 54 | for b in boards: 55 | if b.winning: 56 | return b.score(n), nums[idx+1:] 57 | 58 | def part1(s): 59 | nums, boards = parse(s) 60 | 61 | answer, _ = find_winning_score(boards, nums) 62 | 63 | lib.aoc.give_answer(2021, 4, 1, answer) 64 | 65 | def part2(s): 66 | nums, boards = parse(s) 67 | 68 | while True: 69 | answer, nums = find_winning_score(boards, nums) 70 | boards = [b for b in boards if not b.winning] 71 | if 0 == len(boards): 72 | break 73 | 74 | lib.aoc.give_answer(2021, 4, 2, answer) 75 | 76 | INPUT = lib.aoc.get_input(2021, 4) 77 | part1(INPUT) 78 | part2(INPUT) 79 | -------------------------------------------------------------------------------- /2021/05/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import parse 3 | 4 | import lib.aoc 5 | 6 | def parse_lines(s): 7 | return parse.findall('{:d},{:d} -> {:d},{:d}', s) 8 | 9 | def count_coord_hits(lines): 10 | coord_hits = collections.Counter() 11 | 12 | for x, y, x1, y1 in lines: 13 | # Any diagonals are 45 degrees so dx/dy are simple! 14 | dx = 1 if x1 > x else -1 if x1 < x else 0 15 | dy = 1 if y1 > y else -1 if y1 < y else 0 16 | 17 | coord_hits[x,y] += 1 18 | while x != x1 or y != y1: 19 | x += dx 20 | y += dy 21 | coord_hits[x,y] += 1 22 | 23 | return sum(hits > 1 for hits in coord_hits.values()) 24 | 25 | def part1(s): 26 | answer = count_coord_hits(filter(lambda l: l[0] == l[2] or l[1] == l[3], 27 | parse_lines(s))) 28 | lib.aoc.give_answer(2021, 5, 1, answer) 29 | 30 | def part2(s): 31 | answer = count_coord_hits(parse_lines(s)) 32 | lib.aoc.give_answer(2021, 5, 2, answer) 33 | 34 | INPUT = lib.aoc.get_input(2021, 5) 35 | part1(INPUT) 36 | part2(INPUT) 37 | -------------------------------------------------------------------------------- /2021/06/solution.py: -------------------------------------------------------------------------------- 1 | import gmpy2 2 | import numpy 3 | 4 | import lib.aoc 5 | 6 | def solve(s, iterations): 7 | pop = [0] * 9 8 | for t in map(int, s.split(',')): 9 | pop[t] += 1 10 | 11 | # Matrices are faster (due to fast exponentiation 12 | # and doing the work down in C!) 13 | m = [ 14 | [0, 1, 0, 0, 0, 0, 0, 0, 0], 15 | [0, 0, 1, 0, 0, 0, 0, 0, 0], 16 | [0, 0, 0, 1, 0, 0, 0, 0, 0], 17 | [0, 0, 0, 0, 1, 0, 0, 0, 0], 18 | [0, 0, 0, 0, 0, 1, 0, 0, 0], 19 | [0, 0, 0, 0, 0, 0, 1, 0, 0], 20 | [1, 0, 0, 0, 0, 0, 0, 1, 0], 21 | [0, 0, 0, 0, 0, 0, 0, 0, 1], 22 | [1, 0, 0, 0, 0, 0, 0, 0, 0], 23 | ] 24 | # Also, Python's int is slower than gmpy2.mpz for some reason 25 | m = numpy.matrix([list(map(gmpy2.mpz, row)) for row in m]) 26 | m = m ** iterations 27 | 28 | return (m * numpy.matrix(pop).reshape((9,1))).sum() 29 | 30 | def part1(s): 31 | answer = solve(s, 80) 32 | lib.aoc.give_answer(2021, 6, 1, answer) 33 | 34 | def part2(s): 35 | answer = solve(s, 256) 36 | lib.aoc.give_answer(2021, 6, 2, answer) 37 | 38 | INPUT = lib.aoc.get_input(2021, 6) 39 | part1(INPUT) 40 | part2(INPUT) 41 | -------------------------------------------------------------------------------- /2021/07/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.math 3 | 4 | def part1(s): 5 | positions = list(map(int, s.split(','))) 6 | 7 | def value_fn(target): 8 | return sum(abs(p-target) for p in positions) 9 | 10 | target = lib.math.find_continuous_curve_minimum(range(min(positions), 11 | max(positions)+1), 12 | value_fn) 13 | answer = value_fn(target) 14 | 15 | lib.aoc.give_answer(2021, 7, 1, answer) 16 | 17 | def tri_num(n): 18 | return n*(n+1)//2 19 | 20 | def part2(s): 21 | positions = list(map(int, s.split(','))) 22 | 23 | def value_fn(target): 24 | return sum(tri_num(abs(p-target)) for p in positions) 25 | 26 | target = lib.math.find_continuous_curve_minimum(range(min(positions), 27 | max(positions)+1), 28 | value_fn) 29 | answer = value_fn(target) 30 | 31 | lib.aoc.give_answer(2021, 7, 2, answer) 32 | 33 | INPUT = lib.aoc.get_input(2021, 7) 34 | part1(INPUT) 35 | part2(INPUT) 36 | -------------------------------------------------------------------------------- /2021/08/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import itertools 3 | 4 | import lib.aoc 5 | 6 | def parse(s): 7 | for line in s.splitlines(): 8 | inputs, outputs = line.split(' | ') 9 | yield inputs.split(), outputs.split() 10 | 11 | def part1(s): 12 | answer = sum(len(pattern) in (2, 3, 4, 7) 13 | for _, outputs in parse(s) 14 | for pattern in outputs) 15 | 16 | lib.aoc.give_answer(2021, 8, 1, answer) 17 | 18 | def part2(s): 19 | VALID_PATTERNS = { 20 | 'abcefg':0, 21 | 'cf':1, 22 | 'acdeg':2, 23 | 'acdfg':3, 24 | 'bcdf':4, 25 | 'abdfg':5, 26 | 'abdefg':6, 27 | 'acf':7, 28 | 'abcdefg':8, 29 | 'abcdfg':9 30 | } 31 | 32 | pattern_to_options = collections.defaultdict(set) 33 | mapping_options = [] 34 | 35 | for order in itertools.permutations('abcdefg'): 36 | m = dict(zip('abcdefg', order)) 37 | m_rev = dict(zip(order, 'abcdefg')) 38 | 39 | idx = len(mapping_options) 40 | mapping_options.append(m) 41 | 42 | for real_pattern in VALID_PATTERNS.keys(): 43 | mangled_pattern = ''.join(sorted(m_rev[c] 44 | for c in real_pattern)) 45 | pattern_to_options[mangled_pattern].add(idx) 46 | 47 | answer = 0 48 | for inputs, outputs in parse(s): 49 | candidates = set(range(len(mapping_options))) 50 | for pattern in inputs + outputs: 51 | pattern = ''.join(sorted(pattern)) 52 | candidates &= pattern_to_options[pattern] 53 | 54 | assert(len(candidates) == 1) 55 | mapping = mapping_options[list(candidates)[0]] 56 | 57 | out = 0 58 | for n in outputs: 59 | new_n = ''.join(sorted(mapping[c] for c in n)) 60 | out = out * 10 + VALID_PATTERNS[new_n] 61 | answer += out 62 | 63 | lib.aoc.give_answer(2021, 8, 2, answer) 64 | 65 | INPUT = lib.aoc.get_input(2021, 8) 66 | part1(INPUT) 67 | part2(INPUT) 68 | -------------------------------------------------------------------------------- /2021/09/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.grid 3 | 4 | def part1(s): 5 | grid = lib.grid.FixedGrid.parse(s, value_fn=int) 6 | 7 | answer = 0 8 | 9 | for c, val in grid.items(): 10 | x, y = c 11 | if all(grid[n] > val 12 | for n in grid.neighbors(x, y)): 13 | answer += val+1 14 | 15 | lib.aoc.give_answer(2021, 9, 1, answer) 16 | 17 | def count_basin_size(grid, c): 18 | basin = {c} 19 | to_handle = [c] 20 | 21 | while to_handle: 22 | x, y = to_handle.pop(0) 23 | 24 | for n in grid.neighbors(x, y): 25 | if n in basin: 26 | continue 27 | nval = grid[n] 28 | if nval != 9: 29 | basin.add(n) 30 | to_handle.append(n) 31 | 32 | return len(basin) 33 | 34 | def part2(s): 35 | grid = lib.grid.FixedGrid.parse(s, value_fn=int) 36 | 37 | basin_sizes = [] 38 | 39 | for c, val in grid.items(): 40 | x, y = c 41 | if all(grid[n] > val 42 | for n in grid.neighbors(x, y)): 43 | basin_sizes.append(count_basin_size(grid, c)) 44 | 45 | a, b, c = sorted(basin_sizes)[-3:] 46 | answer = a*b*c 47 | 48 | lib.aoc.give_answer(2021, 9, 2, answer) 49 | 50 | INPUT = lib.aoc.get_input(2021, 9) 51 | part1(INPUT) 52 | part2(INPUT) 53 | -------------------------------------------------------------------------------- /2021/10/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | OPENINGS = '([{<' 4 | CLOSINGS = ')]}>' 5 | INVALID_SCORES = [3, 57, 1197, 25137] 6 | 7 | def validate_line(line): 8 | seen = [] 9 | for c in line: 10 | if c in OPENINGS: 11 | seen.append(c) 12 | continue 13 | close_idx = CLOSINGS.index(c) 14 | if seen[-1] == OPENINGS[close_idx]: 15 | seen.pop(-1) 16 | continue 17 | return INVALID_SCORES[close_idx], seen 18 | 19 | assert(len(seen)) # We shouldn't have any valid lines 20 | return None, seen 21 | 22 | def part1(s): 23 | answer = sum(score 24 | for score, _ in map(validate_line, s.splitlines()) 25 | if score is not None) 26 | 27 | lib.aoc.give_answer(2021, 10, 1, answer) 28 | 29 | def autocomplete_score(to_finish): 30 | score = 0 31 | for c in to_finish[::-1]: 32 | # Each closing score is actually its index plus 1 33 | score = score * 5 + OPENINGS.index(c) + 1 34 | return score 35 | 36 | def part2(s): 37 | auto_scores = sorted(autocomplete_score(to_finish) 38 | for score, to_finish in map(validate_line, 39 | s.splitlines()) 40 | if score is None) 41 | 42 | assert(len(auto_scores)%2 == 1) 43 | 44 | answer = auto_scores[len(auto_scores)//2] 45 | 46 | lib.aoc.give_answer(2021, 10, 2, answer) 47 | 48 | INPUT = lib.aoc.get_input(2021, 10) 49 | part1(INPUT) 50 | part2(INPUT) 51 | -------------------------------------------------------------------------------- /2021/11/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.grid 3 | 4 | def iterate(grid): 5 | flashed = set() 6 | 7 | for c, val in grid.items(): 8 | grid[c] = val+1 9 | if val+1 > 9: 10 | flashed.add(c) 11 | 12 | to_handle = list(flashed) 13 | while to_handle: 14 | x, y = to_handle.pop(0) 15 | for n in grid.neighbors(x, y, diagonals=True): 16 | if n in flashed: 17 | continue 18 | grid[n] += 1 19 | if grid[n] > 9: 20 | flashed.add(n) 21 | to_handle.append(n) 22 | 23 | for c in flashed: 24 | grid[c] = 0 25 | 26 | return len(flashed) 27 | 28 | def part1(s): 29 | grid = lib.grid.FixedGrid.parse(s, value_fn=int) 30 | 31 | answer = sum(iterate(grid) 32 | for _ in range(100)) 33 | 34 | lib.aoc.give_answer(2021, 11, 1, answer) 35 | 36 | def part2(s): 37 | grid = lib.grid.FixedGrid.parse(s, value_fn=int) 38 | 39 | answer = 1 40 | while iterate(grid) != grid.area: 41 | answer += 1 42 | 43 | lib.aoc.give_answer(2021, 11, 2, answer) 44 | 45 | INPUT = lib.aoc.get_input(2021, 11) 46 | part1(INPUT) 47 | part2(INPUT) 48 | -------------------------------------------------------------------------------- /2021/12/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import lib.aoc 4 | 5 | def solve(s, allow_second_visit=False): 6 | graph = collections.defaultdict(list) 7 | for line in s.splitlines(): 8 | a, b = line.split('-') 9 | graph[a].append(b) 10 | graph[b].append(a) 11 | 12 | def gen_paths(pos, seen, allow_second_visit): 13 | for target in graph[pos]: 14 | if target == 'end': 15 | yield 1 16 | continue 17 | if target == 'start': 18 | continue 19 | if target.islower() and target in seen: 20 | if allow_second_visit: 21 | yield from gen_paths(target, seen, False) 22 | continue 23 | yield from gen_paths(target, seen | {target}, allow_second_visit) 24 | 25 | return sum(gen_paths('start', set(), allow_second_visit)) 26 | 27 | def part1(s): 28 | answer = solve(s) 29 | lib.aoc.give_answer(2021, 12, 1, answer) 30 | 31 | def part2(s): 32 | answer = solve(s, True) 33 | lib.aoc.give_answer(2021, 12, 2, answer) 34 | 35 | INPUT = lib.aoc.get_input(2021, 12) 36 | part1(INPUT) 37 | part2(INPUT) 38 | -------------------------------------------------------------------------------- /2021/13/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.ocr 3 | 4 | def parse(s): 5 | groups = s.split('\n\n') 6 | 7 | pairs = {tuple(map(int, line.split(','))) 8 | for line in groups[0].splitlines()} 9 | folds = [] 10 | for line in groups[1].splitlines(): 11 | axis, n = line.split()[2].split('=') 12 | folds.append((axis, int(n))) 13 | 14 | return pairs, folds 15 | 16 | def do_fold(dots, fold): 17 | axis, n = fold 18 | 19 | def reflect(c): 20 | assert(c != n) 21 | return min(c, 2*n-c) 22 | 23 | if axis == 'x': 24 | return {(reflect(x), y) 25 | for x, y in dots} 26 | elif axis == 'y': 27 | return {(x, reflect(y)) 28 | for x, y in dots} 29 | 30 | def part1(s): 31 | dots, folds = parse(s) 32 | 33 | dots = do_fold(dots, folds[0]) 34 | 35 | answer = len(dots) 36 | 37 | lib.aoc.give_answer(2021, 13, 1, answer) 38 | 39 | def part2(s): 40 | dots, folds = parse(s) 41 | 42 | for fold in folds: 43 | dots = do_fold(dots, fold) 44 | 45 | answer = lib.ocr.parse_coord_set(dots) 46 | 47 | lib.aoc.give_answer(2021, 13, 2, answer) 48 | 49 | INPUT = lib.aoc.get_input(2021, 13) 50 | part1(INPUT) 51 | part2(INPUT) 52 | -------------------------------------------------------------------------------- /2021/14/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import lib.aoc 4 | 5 | def solve(s, iterations): 6 | groups = s.split('\n\n') 7 | s = groups[0] 8 | 9 | rules = {} 10 | for line in groups[1].split('\n'): 11 | left, right = line.split(' -> ') 12 | rules[left] = right 13 | 14 | counts = {pair: collections.Counter(pair) 15 | for pair in rules.keys()} 16 | 17 | for _ in range(iterations): 18 | new_counts = {} 19 | 20 | for pair, insert in rules.items(): 21 | a, b = pair 22 | 23 | c = counts[a+insert] + counts[insert+b] 24 | c[insert] -= 1 25 | 26 | new_counts[pair] = c 27 | counts = new_counts 28 | 29 | c = collections.Counter() 30 | for i in range(len(s)-1): 31 | c += counts[s[i:i+2]] 32 | c -= collections.Counter(s[1:-1]) # Eliminate double-counts 33 | 34 | most = c.most_common()[0] 35 | least = c.most_common()[-1] 36 | 37 | return most[1] - least[1] 38 | 39 | def part1(s): 40 | answer = solve(s, 10) 41 | 42 | lib.aoc.give_answer(2021, 14, 1, answer) 43 | 44 | def part2(s): 45 | answer = solve(s, 40) 46 | 47 | lib.aoc.give_answer(2021, 14, 2, answer) 48 | 49 | INPUT = lib.aoc.get_input(2021, 14) 50 | part1(INPUT) 51 | part2(INPUT) 52 | -------------------------------------------------------------------------------- /2021/15/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.graph 3 | import lib.grid 4 | 5 | def solve(grid): 6 | graph = {} 7 | 8 | for c, val in grid.items(): 9 | x, y = c 10 | graph[c] = [(n, grid[n]) 11 | for n in grid.neighbors(x, y)] 12 | 13 | X = grid.width-1 14 | Y = grid.height-1 15 | 16 | return lib.graph.dijkstra_length(graph, (0, 0), (X, Y)) 17 | 18 | def part1(s): 19 | grid = lib.grid.FixedGrid.parse(s, value_fn=int) 20 | answer = solve(grid) 21 | 22 | lib.aoc.give_answer(2021, 15, 1, answer) 23 | 24 | def part2(s): 25 | grid = lib.grid.FixedGrid.parse(s, value_fn=int) 26 | 27 | # TODO: Is there a way to do this less horribly? This feels so hacky! 28 | d = grid.to_dict() 29 | for xadj in range(5): 30 | for yadj in range(5): 31 | adj = xadj + yadj 32 | if adj == 0: 33 | continue 34 | 35 | xshift = grid.width * xadj 36 | yshift = grid.height * yadj 37 | 38 | for c, val in grid.items(): 39 | x, y = c 40 | newc = x + xshift, y + yshift 41 | d[newc] = (val + adj - 1) % 9 + 1 42 | grid = lib.grid.FixedGrid.from_dict(d) 43 | 44 | answer = solve(grid) 45 | 46 | lib.aoc.give_answer(2021, 15, 2, answer) 47 | 48 | INPUT = lib.aoc.get_input(2021, 15) 49 | part1(INPUT) 50 | part2(INPUT) 51 | -------------------------------------------------------------------------------- /2021/16/solution.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import operator 3 | 4 | import lib.aoc 5 | 6 | def parse_bits_impl(bits): 7 | def pull_raw_bits(n): 8 | nonlocal bits 9 | val = bits[:n] 10 | bits = bits[n:] 11 | return val 12 | def pull_bits(n): 13 | return int(pull_raw_bits(n), 2) 14 | 15 | v = pull_bits(3) 16 | t = pull_bits(3) 17 | if t == 4: 18 | # Literal 19 | payload = 0 20 | while True: 21 | n = pull_bits(5) 22 | payload = (payload << 4) + (n & 0xF) 23 | if not n & 0x10: 24 | break 25 | else: 26 | # Operator 27 | payload = [] 28 | 29 | if pull_bits(1): 30 | count = pull_bits(11) 31 | 32 | for _ in range(count): 33 | s, bits = parse_bits_impl(bits) 34 | payload.append(s) 35 | else: 36 | sub_len = pull_bits(15) 37 | sub_bits = pull_raw_bits(sub_len) 38 | 39 | while sub_bits: 40 | s, sub_bits = parse_bits_impl(sub_bits) 41 | payload.append(s) 42 | 43 | packet = (v, t, payload) 44 | return packet, bits 45 | 46 | def parse(s): 47 | bits = bin(int(s, 16))[2:].zfill(len(s)*4) 48 | packet, _ = parse_bits_impl(bits) 49 | return packet 50 | 51 | def sum_versions(packet): 52 | v, t, payload = packet 53 | if t != 4: 54 | v += sum(map(sum_versions, payload)) 55 | return v 56 | 57 | def part1(s): 58 | answer = sum_versions(parse(s)) 59 | 60 | lib.aoc.give_answer(2021, 16, 1, answer) 61 | 62 | OPERATORS = [ 63 | sum, 64 | lambda vals: functools.reduce(operator.mul, vals), 65 | min, 66 | max, 67 | lambda val: val, 68 | lambda vals: 1 if vals[0] > vals[1] else 0, 69 | lambda vals: 1 if vals[0] < vals[1] else 0, 70 | lambda vals: 1 if vals[0] == vals[1] else 0, 71 | ] 72 | 73 | def eval_packet(p): 74 | _, t, payload = p 75 | if isinstance(payload, list): 76 | payload = list(map(eval_packet, payload)) 77 | return OPERATORS[t](payload) 78 | 79 | def part2(s): 80 | answer = eval_packet(parse(s)) 81 | 82 | lib.aoc.give_answer(2021, 16, 2, answer) 83 | 84 | INPUT = lib.aoc.get_input(2021, 16) 85 | part1(INPUT) 86 | part2(INPUT) 87 | -------------------------------------------------------------------------------- /2021/18/solution.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import lib.aoc 4 | 5 | def explode_num(num): 6 | def add_to_rightmost(num, val): 7 | if isinstance(num, int): 8 | return num + val 9 | a, b = num 10 | return [a, add_to_rightmost(b, val)] 11 | 12 | # Returns left_val, new_num, right_val 13 | def impl(num, depth, right_val): 14 | if isinstance(num, int): 15 | return 0, num+right_val, 0 16 | 17 | a, b = num 18 | if depth == 4: 19 | return a+right_val, 0, b 20 | 21 | left_val, a, right_val = impl(a, depth+1, right_val) 22 | left_val_b, b, right_val = impl(b, depth+1, right_val) 23 | if left_val_b > 0: 24 | a = add_to_rightmost(a, left_val_b) 25 | 26 | return left_val, [a, b], right_val 27 | 28 | _, new_num, _ = impl(num, 0, 0) 29 | return new_num 30 | 31 | def split_num(num): 32 | if isinstance(num, int): 33 | if num > 9: 34 | d, m = divmod(num, 2) 35 | return [d, d + m] 36 | return None 37 | 38 | a, b = num 39 | 40 | new_a = split_num(a) 41 | if new_a is not None: 42 | return [new_a, b] 43 | 44 | new_b = split_num(b) 45 | if new_b is not None: 46 | return [a, new_b] 47 | 48 | return None 49 | 50 | def reduce_num(num): 51 | while True: 52 | num = explode_num(num) 53 | new_num = split_num(num) 54 | if new_num is None: 55 | return num 56 | 57 | num = new_num 58 | 59 | def magnitude(num): 60 | if isinstance(num, int): 61 | return num 62 | 63 | a, b = num 64 | return 3*magnitude(a) + 2*magnitude(b) 65 | 66 | def part1(s): 67 | nums = list(map(reduce_num, map(json.loads, s.splitlines()))) 68 | 69 | n = nums[0] 70 | for item in nums[1:]: 71 | n = reduce_num([n, item]) 72 | 73 | answer = magnitude(n) 74 | 75 | lib.aoc.give_answer(2021, 18, 1, answer) 76 | 77 | def part2(s): 78 | nums = list(map(reduce_num, map(json.loads, s.splitlines()))) 79 | 80 | answer = max(magnitude(reduce_num([a, b])) 81 | for ia, a in enumerate(nums) 82 | for ib, b in enumerate(nums) 83 | if ia != ib) 84 | 85 | lib.aoc.give_answer(2021, 18, 2, answer) 86 | 87 | INPUT = lib.aoc.get_input(2021, 18) 88 | part1(INPUT) 89 | part2(INPUT) 90 | -------------------------------------------------------------------------------- /2021/20/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.grid 3 | 4 | def run(s, iterations): 5 | s = s.translate(str.maketrans('.#', '01')) 6 | 7 | algo, im = s.split('\n\n') 8 | 9 | if algo[0] == '1' == algo[511]: 10 | # Once the infinite void turns on it stays on! Oh no! 11 | if iterations > 0: 12 | return 'infinity' 13 | # We must not be running any iterations...carry on 14 | elif algo[0] == '1' and algo[511] == '0': 15 | # The infinite void blinks. If we run an odd number of iterations 16 | # then it'll be on and we'll have an infinite number of lights on! 17 | if iterations % 2 == 1: 18 | return 'infinity' 19 | 20 | im = lib.grid.FixedGrid.parse(im) 21 | xrange = range(im.width) 22 | yrange = range(im.height) 23 | im = im.to_dict() 24 | 25 | # The infinite void starts out off 26 | default = '0' 27 | 28 | for _ in range(iterations): 29 | new_im = {} 30 | 31 | xrange = range(xrange[0]-1, xrange[-1]+2) 32 | yrange = range(yrange[0]-1, yrange[-1]+2) 33 | for x in xrange: 34 | for y in yrange: 35 | idx = ''.join(im.get((x+dx, y+dy), default) 36 | for dy in (-1, 0, 1) 37 | for dx in (-1, 0, 1)) 38 | idx = int(idx, 2) 39 | new_im[x,y] = algo[idx] 40 | 41 | # Figure out new out of bounds default 42 | default = algo[int(default * 9, 2)] 43 | 44 | im = new_im 45 | 46 | assert(default == '0') 47 | 48 | return sum(map(int, im.values())) 49 | 50 | def part1(s): 51 | answer = run(s, 2) 52 | lib.aoc.give_answer(2021, 20, 1, answer) 53 | 54 | def part2(s): 55 | answer = run(s, 50) 56 | lib.aoc.give_answer(2021, 20, 2, answer) 57 | 58 | INPUT = lib.aoc.get_input(2021, 20) 59 | part1(INPUT) 60 | part2(INPUT) 61 | -------------------------------------------------------------------------------- /2021/22/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def cuboid_intersect(base, other): 4 | return tuple(range(max(b.start, o.start), 5 | min(b.stop, o.stop)) 6 | for b, o in zip(base, other)) 7 | 8 | def cuboid_volume(cuboid): 9 | x, y, z = tuple(map(len, cuboid)) 10 | return x * y * z 11 | 12 | def unique_cuboid_volume(cuboid, rest): 13 | conflicts = [] 14 | 15 | for act, other in rest: 16 | intersection = cuboid_intersect(cuboid, other) 17 | if cuboid_volume(intersection) == 0: 18 | continue 19 | 20 | conflicts.append((act, intersection)) 21 | 22 | volume = cuboid_volume(cuboid) 23 | volume -= sum(unique_cuboid_volume(conflict, conflicts[idx+1:]) 24 | for idx, (_, conflict) in enumerate(conflicts)) 25 | 26 | return volume 27 | 28 | def parse_range(s): 29 | c0, c1 = s[2:].split('..') 30 | return range(int(c0), int(c1)+1) 31 | 32 | def solve(s, area_of_interest=None): 33 | steps = [] 34 | for line in s.splitlines(): 35 | act, cuboid = line.split() 36 | cuboid = tuple(map(parse_range, cuboid.split(','))) 37 | if area_of_interest is not None: 38 | cuboid = cuboid_intersect(cuboid, area_of_interest) 39 | steps.append((act, cuboid)) 40 | 41 | return sum(unique_cuboid_volume(cuboid, steps[idx+1:]) 42 | for idx, (act, cuboid) in enumerate(steps) 43 | if act == 'on') 44 | 45 | def part1(s): 46 | answer = solve(s, (range(-50, 51), range(-50, 51), range(-50, 51))) 47 | 48 | lib.aoc.give_answer(2021, 22, 1, answer) 49 | 50 | def part2(s): 51 | answer = solve(s) 52 | 53 | lib.aoc.give_answer(2021, 22, 2, answer) 54 | 55 | INPUT = lib.aoc.get_input(2021, 22) 56 | part1(INPUT) 57 | part2(INPUT) 58 | -------------------------------------------------------------------------------- /2021/25/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.grid 3 | 4 | def step(eastward, southward, width, height): 5 | moved = False 6 | 7 | new_east = set() 8 | for x,y in eastward: 9 | new_c = ((x+1) % width, y) 10 | if new_c in eastward or new_c in southward: 11 | # Can't move 12 | new_east.add((x,y)) 13 | else: 14 | new_east.add(new_c) 15 | moved = True 16 | 17 | eastward = new_east 18 | 19 | new_south = set() 20 | for x,y in southward: 21 | new_c = (x, (y + 1) % height) 22 | if new_c in eastward or new_c in southward: 23 | # Can't move 24 | new_south.add((x,y)) 25 | else: 26 | new_south.add(new_c) 27 | moved = True 28 | 29 | southward = new_south 30 | 31 | return eastward, southward, moved 32 | 33 | def part1(s): 34 | grid = lib.grid.FixedGrid.parse(s) 35 | 36 | eastward = {c for c,v in grid.items() if v == '>'} 37 | southward = {c for c,v in grid.items() if v == 'v'} 38 | width = grid.width 39 | height = grid.height 40 | 41 | answer = 0 42 | while True: 43 | answer += 1 44 | eastward, southward, moved = step(eastward, southward, width, height) 45 | 46 | if not moved: 47 | break 48 | 49 | lib.aoc.give_answer(2021, 25, 1, answer) 50 | 51 | def part2(s): 52 | print('There is no part two for Christmas!') 53 | 54 | INPUT = lib.aoc.get_input(2021, 25) 55 | part1(INPUT) 56 | part2(INPUT) 57 | -------------------------------------------------------------------------------- /2022/01/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def parse_input(s): 4 | for elf in s.split('\n\n'): 5 | yield sum(map(int, elf.splitlines())) 6 | 7 | def part1(s): 8 | answer = max(parse_input(s)) 9 | 10 | lib.aoc.give_answer(2022, 1, 1, answer) 11 | 12 | def part2(s): 13 | answer = sum(sorted(parse_input(s))[-3:]) 14 | 15 | lib.aoc.give_answer(2022, 1, 2, answer) 16 | 17 | INPUT = lib.aoc.get_input(2022, 1) 18 | part1(INPUT) 19 | part2(INPUT) 20 | -------------------------------------------------------------------------------- /2022/02/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def part1(s): 4 | SCORES = { 5 | 'A X': 4, 'A Y': 8, 'A Z': 3, 6 | 'B X': 1, 'B Y': 5, 'B Z': 9, 7 | 'C X': 7, 'C Y': 2, 'C Z': 6, 8 | } 9 | 10 | answer = sum(map(SCORES.get, s.splitlines())) 11 | 12 | lib.aoc.give_answer(2022, 2, 1, answer) 13 | 14 | def part2(s): 15 | SCORES = { 16 | 'A X': 3, 'A Y': 4, 'A Z': 8, 17 | 'B X': 1, 'B Y': 5, 'B Z': 9, 18 | 'C X': 2, 'C Y': 6, 'C Z': 7, 19 | } 20 | 21 | answer = sum(map(SCORES.get, s.splitlines())) 22 | 23 | lib.aoc.give_answer(2022, 2, 2, answer) 24 | 25 | INPUT = lib.aoc.get_input(2022, 2) 26 | part1(INPUT) 27 | part2(INPUT) 28 | -------------------------------------------------------------------------------- /2022/03/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def score_shared_item(*groups): 4 | shared = set(groups[0]) 5 | for s in groups[1:]: 6 | shared &= set(s) 7 | 8 | shared = list(shared)[0] 9 | if shared.islower(): 10 | return ord(shared) - ord('a') + 1 11 | else: 12 | assert(shared.isupper()) 13 | return ord(shared) - ord('A') + 27 14 | 15 | def part1(s): 16 | answer = sum(score_shared_item(sack[:len(sack)//2], 17 | sack[len(sack)//2:]) 18 | for sack in s.splitlines()) 19 | 20 | lib.aoc.give_answer(2022, 3, 1, answer) 21 | 22 | def part2(s): 23 | sacks = s.splitlines() 24 | 25 | answer = sum(score_shared_item(a, b, c) 26 | for (a, b, c) in (sacks[i:i+3] 27 | for i in range(0, len(sacks), 3))) 28 | 29 | lib.aoc.give_answer(2022, 3, 2, answer) 30 | 31 | INPUT = lib.aoc.get_input(2022, 3) 32 | part1(INPUT) 33 | part2(INPUT) 34 | -------------------------------------------------------------------------------- /2022/04/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def parse_input(s): 4 | for line in s.splitlines(): 5 | a, b = line.split(',') 6 | 7 | yield tuple(map(int, a.split('-'))), tuple(map(int, b.split('-'))) 8 | 9 | def part1(s): 10 | answer = sum(1 11 | for (a0, a1), (b0, b1) in parse_input(s) 12 | if a0 <= b0 <= b1 <= a1 or b0 <= a0 <= a1 <= b1) 13 | 14 | lib.aoc.give_answer(2022, 4, 1, answer) 15 | 16 | def part2(s): 17 | answer = sum(1 18 | for (a0, a1), (b0, b1) in parse_input(s) 19 | if a0 <= b1 and a1 >= b0) 20 | 21 | lib.aoc.give_answer(2022, 4, 2, answer) 22 | 23 | INPUT = lib.aoc.get_input(2022, 4) 24 | part1(INPUT) 25 | part2(INPUT) 26 | -------------------------------------------------------------------------------- /2022/05/solution.py: -------------------------------------------------------------------------------- 1 | import parse 2 | 3 | import lib.aoc 4 | 5 | def solve(s, move_handler): 6 | stacks, moves = s.split('\n\n') 7 | 8 | stack_lines = stacks.splitlines() 9 | 10 | stacks = [[] for _ in range(len(stack_lines.pop().split()))] 11 | 12 | for row in stack_lines[::-1]: 13 | for i, entry in enumerate(row[1::4]): 14 | if entry != ' ': 15 | stacks[i].append(entry) 16 | 17 | for n, source, target in parse.findall('move {:d} from {:d} to {:d}', moves): 18 | move_handler(stacks, n, source-1, target-1) 19 | 20 | return ''.join(s[-1] for s in stacks) 21 | 22 | def part1(s): 23 | def move_blocks(stacks, n, source, target): 24 | for _ in range(n): 25 | stacks[target].append(stacks[source].pop(-1)) 26 | 27 | answer = solve(s, move_blocks) 28 | 29 | lib.aoc.give_answer(2022, 5, 1, answer) 30 | 31 | def part2(s): 32 | def move_blocks(stacks, n, source, target): 33 | moved = stacks[source][-n:] 34 | stacks[source] = stacks[source][:-n] 35 | stacks[target] += moved 36 | 37 | answer = solve(s, move_blocks) 38 | 39 | lib.aoc.give_answer(2022, 5, 2, answer) 40 | 41 | INPUT = lib.aoc.get_input(2022, 5) 42 | part1(INPUT) 43 | part2(INPUT) 44 | -------------------------------------------------------------------------------- /2022/06/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def part1(s): 4 | for i in range(len(s)): 5 | if len(set(s[i:i+4])) == 4: 6 | answer = i+4 7 | break 8 | 9 | lib.aoc.give_answer(2022, 6, 1, answer) 10 | 11 | def part2(s): 12 | for i in range(len(s)): 13 | if len(set(s[i:i+14])) == 14: 14 | answer = i+14 15 | break 16 | 17 | lib.aoc.give_answer(2022, 6, 2, answer) 18 | 19 | INPUT = lib.aoc.get_input(2022, 6) 20 | part1(INPUT) 21 | part2(INPUT) 22 | -------------------------------------------------------------------------------- /2022/07/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import lib.aoc 4 | 5 | def get_folder_sizes(s): 6 | folders = collections.defaultdict(int) 7 | 8 | cwd = [] 9 | 10 | for line in s.splitlines(): 11 | parts = line.split() 12 | if parts[0] == '$': 13 | if parts[1] == 'cd': 14 | if parts[2] == '..': 15 | cwd.pop() 16 | elif parts[2] == '/': 17 | # Special handling to avoid double slash 18 | cwd = [''] 19 | else: 20 | cwd.append(parts[2]) 21 | elif parts[1] == 'ls': 22 | pass 23 | else: 24 | assert(False) 25 | elif parts[0] == 'dir': 26 | pass # Handled implicitly when adding up sizes 27 | else: 28 | size = int(parts[0]) 29 | name = '' 30 | for fold in cwd: 31 | if name != '/': 32 | # Special handling to avoid double slash 33 | name += '/' 34 | name += fold 35 | folders[name] += size 36 | 37 | return folders 38 | 39 | def part1(s): 40 | answer = sum(size 41 | for size in get_folder_sizes(s).values() 42 | if size <= 100000) 43 | 44 | lib.aoc.give_answer(2022, 7, 1, answer) 45 | 46 | def part2(s): 47 | folders = get_folder_sizes(s) 48 | 49 | TO_FREE = 30000000 - (70000000 - folders['/']) 50 | 51 | answer = min(size 52 | for size in folders.values() 53 | if size >= TO_FREE) 54 | 55 | lib.aoc.give_answer(2022, 7, 2, answer) 56 | 57 | INPUT = lib.aoc.get_input(2022, 7) 58 | part1(INPUT) 59 | part2(INPUT) 60 | -------------------------------------------------------------------------------- /2022/08/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.grid 3 | 4 | def get_tree_stats(s): 5 | grid = lib.grid.FixedGrid.parse(s, value_fn=int) 6 | 7 | for (x, y), self_height in grid.items(): 8 | obscured = True 9 | distances = [] 10 | 11 | for dx, dy in [(-1, 0), 12 | (1, 0), 13 | (0, -1), 14 | (0, 1)]: 15 | nx, ny = x, y 16 | num_trees_visible = 0 17 | while True: 18 | nx, ny = nx+dx, ny+dy 19 | if (nx, ny) not in grid: 20 | obscured = False 21 | break 22 | num_trees_visible += 1 23 | if grid[nx, ny] >= self_height: 24 | break # Obscured 25 | distances.append(num_trees_visible) 26 | 27 | yield distances, obscured 28 | 29 | def part1(s): 30 | answer = sum(1 31 | for _, obscured in get_tree_stats(s) 32 | if not obscured) 33 | 34 | lib.aoc.give_answer(2022, 8, 1, answer) 35 | 36 | def part2(s): 37 | def score(distances): 38 | t = 1 39 | for dist in distances: 40 | t *= dist 41 | return t 42 | 43 | answer = max(score(distances) 44 | for distances, _ in get_tree_stats(s)) 45 | 46 | lib.aoc.give_answer(2022, 8, 2, answer) 47 | 48 | INPUT = lib.aoc.get_input(2022, 8) 49 | part1(INPUT) 50 | part2(INPUT) 51 | -------------------------------------------------------------------------------- /2022/09/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def head_positions(s): 4 | x, y = 0, 0 5 | yield x, y 6 | 7 | for line in s.splitlines(): 8 | d, c = line.split() 9 | 10 | if d == 'U': dx, dy = 0, -1 11 | elif d == 'D': dx, dy = 0, 1 12 | elif d == 'L': dx, dy = -1, 0 13 | elif d == 'R': dx, dy = 1, 0 14 | 15 | for _ in range(int(c)): 16 | x, y = x+dx, y+dy 17 | yield x, y 18 | 19 | def next_segment_positions(positions): 20 | x, y = 0, 0 21 | yield x, y 22 | 23 | for prev_x, prev_y in positions: 24 | dx, dy = prev_x-x, prev_y-y 25 | 26 | if max(abs(dx), abs(dy)) == 2: 27 | # This segment needs to move to catch up! 28 | if dx != 0: 29 | x += dx // abs(dx) 30 | if dy != 0: 31 | y += dy // abs(dy) 32 | yield x, y 33 | 34 | def part1(s): 35 | answer = len(set(next_segment_positions(head_positions(s)))) 36 | 37 | lib.aoc.give_answer(2022, 9, 1, answer) 38 | 39 | def part2(s): 40 | rope = head_positions(s) 41 | for _ in range(9): 42 | rope = next_segment_positions(rope) 43 | answer = len(set(rope)) 44 | 45 | lib.aoc.give_answer(2022, 9, 2, answer) 46 | 47 | INPUT = lib.aoc.get_input(2022, 9) 48 | part1(INPUT) 49 | part2(INPUT) 50 | -------------------------------------------------------------------------------- /2022/10/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.ocr 3 | 4 | def register_per_cycle(s): 5 | val_per_cycle = [] 6 | 7 | x = 1 8 | for line in s.splitlines(): 9 | inst = line.split() 10 | if inst[0] == 'noop': 11 | # Single cycle instruction 12 | val_per_cycle += [x] 13 | elif inst[0] == 'addx': 14 | # Two cycle instruction 15 | val_per_cycle += [x, x] 16 | x += int(inst[1]) 17 | else: 18 | assert(False) 19 | 20 | return val_per_cycle 21 | 22 | def part1(s): 23 | x_per_cycle = register_per_cycle(s) 24 | 25 | answer = sum(cycle * x_per_cycle[cycle-1] 26 | for cycle in (20, 60, 100, 140, 180, 220)) 27 | 28 | lib.aoc.give_answer(2022, 10, 1, answer) 29 | 30 | def part2(s): 31 | lit_pixels = {(cycle % 40, cycle // 40) 32 | for cycle, pos in enumerate(register_per_cycle(s)) 33 | if cycle % 40 in (pos-1, pos, pos+1)} 34 | 35 | answer = lib.ocr.parse_coord_set(lit_pixels) 36 | 37 | lib.aoc.give_answer(2022, 10, 2, answer) 38 | 39 | INPUT = lib.aoc.get_input(2022, 10) 40 | part1(INPUT) 41 | part2(INPUT) 42 | -------------------------------------------------------------------------------- /2022/12/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | import lib.graph 3 | import lib.grid 4 | 5 | def parse_graph_for_reverse_search(s): 6 | grid = lib.grid.FixedGrid.parse(s) 7 | 8 | start = None 9 | end = None 10 | 11 | for c, val in grid.items(): 12 | if val == 'S': 13 | grid[c] = 'a' 14 | start = c 15 | elif val == 'E': 16 | grid[c] = 'z' 17 | end = c 18 | 19 | def neighbor_fn(pos): 20 | self = grid[pos] 21 | for n in grid.neighbors(*pos): 22 | if ord(grid[n]) - ord(self) <= 1: 23 | yield n, 1 24 | 25 | graph = lib.graph.make_lazy_graph(neighbor_fn) 26 | 27 | return grid, graph, start, end 28 | 29 | def part1(s): 30 | _, graph, start, end = parse_graph_for_reverse_search(s) 31 | answer = lib.graph.dijkstra_length(graph, start, end) 32 | lib.aoc.give_answer(2022, 12, 1, answer) 33 | 34 | def part2(s): 35 | grid, graph, start, end = parse_graph_for_reverse_search(s) 36 | start = [c for c,val in grid.items() if val == 'a'] 37 | answer = lib.graph.dijkstra_length(graph, start, end) 38 | lib.aoc.give_answer(2022, 12, 2, answer) 39 | 40 | INPUT = lib.aoc.get_input(2022, 12) 41 | part1(INPUT) 42 | part2(INPUT) 43 | -------------------------------------------------------------------------------- /2022/13/solution.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import json 3 | 4 | import lib.aoc 5 | 6 | def parse_packets_as_flat_list(s): 7 | return map(json.loads, s.replace('\n\n', '\n').splitlines()) 8 | 9 | def packet_cmp(a, b): 10 | for aval, bval in zip(a, b): 11 | if isinstance(aval, int): 12 | if isinstance(bval, int): 13 | if aval < bval: 14 | return -1 15 | if aval > bval: 16 | return 1 17 | continue 18 | # bval is a list, make aval a list to match for subsequent checks 19 | aval = [aval] 20 | 21 | # aval is a list 22 | if isinstance(bval, int): 23 | bval = [bval] 24 | 25 | subcmp = packet_cmp(aval, bval) 26 | if subcmp != 0: 27 | return subcmp 28 | 29 | if len(a) < len(b): 30 | return -1 31 | if len(b) < len(a): 32 | return 1 33 | return 0 34 | 35 | def part1(s): 36 | packets = list(parse_packets_as_flat_list(s)) 37 | 38 | answer = sum(i+1 39 | for i, (a, b) 40 | in enumerate(zip(packets[::2], packets[1::2])) 41 | if packet_cmp(a, b) == -1) 42 | 43 | lib.aoc.give_answer(2022, 13, 1, answer) 44 | 45 | def part2(s): 46 | packets = list(parse_packets_as_flat_list(s)) + [[[2]], [[6]]] 47 | packets.sort(key=functools.cmp_to_key(packet_cmp)) 48 | 49 | answer = (packets.index([[2]]) + 1) * (packets.index([[6]]) + 1) 50 | 51 | lib.aoc.give_answer(2022, 13, 2, answer) 52 | 53 | INPUT = lib.aoc.get_input(2022, 13) 54 | part1(INPUT) 55 | part2(INPUT) 56 | -------------------------------------------------------------------------------- /2022/14/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def parse_cave(s): 4 | cave = set() 5 | 6 | for line in s.splitlines(): 7 | parts = [tuple(map(int, p.split(','))) 8 | for p in line.split(' -> ')] 9 | x, y = parts.pop(0) 10 | for x2, y2 in parts: 11 | if x == x2: 12 | dy = -1 if y2 < y else 1 13 | cave.update((x, y) for y in range(y, y2+dy, dy)) 14 | y = y2 15 | else: 16 | dx = -1 if x2 < x else 1 17 | cave.update((x, y) for x in range(x, x2+dx, dx)) 18 | x = x2 19 | 20 | return cave 21 | 22 | def solve(cave, in_x, in_y): 23 | max_y = max(y for x,y in cave) 24 | initial_count = len(cave) 25 | 26 | def settle(x, y): 27 | if y > max_y: # Part 1 exit condition 28 | return False 29 | if (x, y) in cave: 30 | return True 31 | # Will short circuit if any cannot settle 32 | if settle(x, y+1) and settle(x-1, y+1) and settle(x+1, y+1): 33 | cave.add((x, y)) 34 | return True 35 | return False 36 | 37 | settle(in_x, in_y) 38 | 39 | return len(cave) - initial_count 40 | 41 | def part1(s): 42 | answer = solve(parse_cave(s), 500, 0) 43 | lib.aoc.give_answer(2022, 14, 1, answer) 44 | 45 | def part2(s): 46 | cave = parse_cave(s) 47 | 48 | # The sand can only go so far so we don't need an infinite floor 49 | floor = max(y for x,y in cave) + 2 50 | cave.update((x, floor) for x in range(500-floor, 500+floor+1)) 51 | 52 | answer = solve(cave, 500, 0) 53 | lib.aoc.give_answer(2022, 14, 2, answer) 54 | 55 | INPUT = lib.aoc.get_input(2022, 14) 56 | part1(INPUT) 57 | part2(INPUT) 58 | -------------------------------------------------------------------------------- /2022/18/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def parse_lava(s): 4 | return set(tuple(map(int, line.split(','))) 5 | for line in s.splitlines()) 6 | 7 | def neighbors(x, y, z): 8 | return {(x-1, y, z), (x+1, y, z), 9 | (x, y-1, z), (x, y+1, z), 10 | (x, y, z-1), (x, y, z+1)} 11 | 12 | def part1(s): 13 | lava = parse_lava(s) 14 | 15 | answer = sum(len(neighbors(x, y, z) - lava) 16 | for x, y, z in lava) 17 | 18 | lib.aoc.give_answer(2022, 18, 1, answer) 19 | 20 | def part2(s): 21 | lava = parse_lava(s) 22 | 23 | min_x = min(x for x,y,z in lava) 24 | max_x = max(x for x,y,z in lava) 25 | min_y = min(y for x,y,z in lava) 26 | max_y = max(y for x,y,z in lava) 27 | min_z = min(z for x,y,z in lava) 28 | max_z = max(z for x,y,z in lava) 29 | 30 | x_range = range(min_x-1, max_x+2) 31 | y_range = range(min_y-1, max_y+2) 32 | z_range = range(min_z-1, max_z+2) 33 | 34 | outside_air = {(min_x-1, min_y-1, min_z-1)} 35 | to_handle = [(min_x-1, min_y-1, min_z-1)] 36 | 37 | # Flood fill the exterior to find which voxels are air 38 | while to_handle: 39 | x, y, z = to_handle.pop() 40 | if x not in x_range or y not in y_range or z not in z_range: 41 | continue 42 | newly_found_air = neighbors(x, y, z) - outside_air - lava 43 | outside_air.update(newly_found_air) 44 | to_handle.extend(newly_found_air) 45 | 46 | answer = sum(len((neighbors(x, y, z) & outside_air) - lava) 47 | for x, y, z in lava) 48 | 49 | lib.aoc.give_answer(2022, 18, 2, answer) 50 | 51 | INPUT = lib.aoc.get_input(2022, 18) 52 | part1(INPUT) 53 | part2(INPUT) 54 | -------------------------------------------------------------------------------- /2022/20/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import lib.aoc 4 | 5 | def solve(s, times=1, num_mult=1): 6 | class Wrapper: 7 | def __init__(self, n): 8 | self.n = n 9 | 10 | nums = collections.deque(map(Wrapper, map(int, s.split()))) 11 | order = list(nums) 12 | 13 | # Multiplier for part 2 14 | for n in nums: 15 | n.n *= num_mult 16 | 17 | for _ in range(times): 18 | for n in order: 19 | idx = nums.index(n) 20 | nums.rotate(-idx) 21 | nums.popleft() 22 | nums.rotate(-n.n) 23 | nums.insert(0, n) 24 | 25 | nums = [n.n for n in nums] 26 | 27 | zero_idx = nums.index(0) 28 | 29 | return sum(nums[(zero_idx + off) % len(nums)] 30 | for off in (1000, 2000, 3000)) 31 | 32 | def part1(s): 33 | answer = solve(s) 34 | 35 | lib.aoc.give_answer(2022, 20, 1, answer) 36 | 37 | def part2(s): 38 | answer = solve(s, 10, num_mult=811589153) 39 | 40 | lib.aoc.give_answer(2022, 20, 2, answer) 41 | 42 | INPUT = lib.aoc.get_input(2022, 20) 43 | part1(INPUT) 44 | part2(INPUT) 45 | -------------------------------------------------------------------------------- /2022/21/solution.py: -------------------------------------------------------------------------------- 1 | import sympy 2 | 3 | import lib.aoc 4 | 5 | def parse_monkeys(s): 6 | barrel = {} 7 | for line in s.splitlines(): 8 | monkey, job = line.split(': ') 9 | barrel[monkey] = job 10 | return barrel 11 | 12 | def to_expr(barrel, monkey): 13 | job = barrel[monkey].split() 14 | if len(job) == 1: 15 | return job[0] 16 | left, op, right = job 17 | return f'({to_expr(barrel, left)}) {op} ({to_expr(barrel, right)})' 18 | 19 | def part1(s): 20 | answer = sympy.parse_expr(to_expr(parse_monkeys(s), 'root')) 21 | 22 | lib.aoc.give_answer(2022, 21, 1, answer) 23 | 24 | def part2(s): 25 | barrel = parse_monkeys(s) 26 | 27 | barrel['humn'] = 'humn' 28 | left, _, right = barrel['root'].split() 29 | 30 | left = sympy.parse_expr(to_expr(barrel, left)) 31 | right = sympy.parse_expr(to_expr(barrel, right)) 32 | 33 | answer = sympy.solve(sympy.Eq(left, right))[0] 34 | 35 | lib.aoc.give_answer(2022, 21, 2, answer) 36 | 37 | INPUT = lib.aoc.get_input(2022, 21) 38 | part1(INPUT) 39 | part2(INPUT) 40 | -------------------------------------------------------------------------------- /2022/23/solution.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import lib.aoc 4 | import lib.grid 5 | 6 | class Grove: 7 | def __init__(self, s): 8 | self.elves = {c for c,v in lib.grid.FixedGrid.parse(s).items() 9 | if v == '#'} 10 | self.search_order = [ 11 | [(-1, -1), (0, -1), (1, -1)], # North 12 | [(-1, 1), (0, 1), (1, 1)], # South 13 | [(-1, -1), (-1, 0), (-1, 1)], # West 14 | [(1, -1), (1, 0), (1, 1)], # East 15 | ] 16 | 17 | def step(self): 18 | targets = collections.defaultdict(list) 19 | 20 | for x, y in self.elves: 21 | if not any((tx, ty) in self.elves 22 | for tx in (x-1, x, x+1) 23 | for ty in (y-1, y, y+1) 24 | if tx != x or ty != y): 25 | # No neighbor elves 26 | continue 27 | 28 | for search in self.search_order: 29 | if not any((x+dx, y+dy) in self.elves 30 | for dx, dy in search): 31 | dx, dy = search[1] # The target is the middle of the search 32 | targets[x+dx, y+dy].append((x, y)) 33 | break 34 | 35 | self.search_order = self.search_order[1:] + self.search_order[:1] 36 | 37 | moves = 0 38 | 39 | for (tx, ty), elves in targets.items(): 40 | if len(elves) > 1: 41 | continue 42 | self.elves.add((tx, ty)) 43 | self.elves.remove(elves[0]) 44 | moves += 1 45 | 46 | return moves > 0 47 | 48 | def part1(s): 49 | grove = Grove(s) 50 | 51 | for _ in range(10): 52 | grove.step() 53 | 54 | min_x = min(x for x, y in grove.elves) 55 | max_x = max(x for x, y in grove.elves) 56 | 57 | min_y = min(y for x, y in grove.elves) 58 | max_y = max(y for x, y in grove.elves) 59 | 60 | answer = (max_x - min_x + 1) * (max_y - min_y + 1) - len(grove.elves) 61 | 62 | lib.aoc.give_answer(2022, 23, 1, answer) 63 | 64 | def part2(s): 65 | grove = Grove(s) 66 | 67 | rounds_with_movement = 0 68 | while grove.step(): 69 | rounds_with_movement += 1 70 | 71 | # First round *without* movement 72 | answer = rounds_with_movement + 1 73 | 74 | lib.aoc.give_answer(2022, 23, 2, answer) 75 | 76 | INPUT = lib.aoc.get_input(2022, 23) 77 | part1(INPUT) 78 | part2(INPUT) 79 | -------------------------------------------------------------------------------- /2022/25/solution.py: -------------------------------------------------------------------------------- 1 | import lib.aoc 2 | 3 | def parse_snafu(num): 4 | total = 0 5 | for c in num: 6 | total = total * 5 + ('=-012'.index(c) - 2) 7 | 8 | return total 9 | 10 | def to_snafu(num): 11 | output = '' 12 | 13 | while num != 0: 14 | # Offsetting the number at this place makes conversion simple 15 | num, place = divmod(num + 2, 5) 16 | output += '=-012'[place] 17 | 18 | return output[::-1] 19 | 20 | def part1(s): 21 | answer = to_snafu(sum(map(parse_snafu, s.splitlines()))) 22 | 23 | lib.aoc.give_answer(2022, 25, 1, answer) 24 | 25 | def part2(s): 26 | print('There is no part two for Christmas!') 27 | 28 | INPUT = lib.aoc.get_input(2022, 25) 29 | part1(INPUT) 30 | part2(INPUT) 31 | -------------------------------------------------------------------------------- /data/letters.json: -------------------------------------------------------------------------------- 1 | {"3x6": {"3a497": "I"}, "3x8": {"e92497": "I"}, "4x6": {"11171f": "F", "117997": "P", "691196": "C", "69888c": "J", "699996": "O", "699999": "U", "78611e": "S", "799797": "B", "955359": "K", "957997": "R", "999f99": "H", "99f996": "A", "e9d196": "G", "f11111": "L", "f1171f": "E", "f1248f": "Z"}, "5x5": {"1f8c63f": "\u25a1"}, "5x6": {"8422a31": "Y"}, "5x8": {"8c631fc631": "H"}, "6x10": {"391450410410438": "J", "4104105f04107f": "F", "4104105f86185f": "P", "7a104104104185e": "C", "7e186185f86185f": "B", "8512450c3149461": "K", "86145125f86185f": "R", "86149230c492861": "X", "86186187f861861": "H", "861861fe186148c": "A", "871c69a659638e1": "N", "bb1861e4104185e": "G", "fc1041041041041": "L", "fc104105f04107f": "E", "fc104210842083f": "Z"}} -------------------------------------------------------------------------------- /editor.bat: -------------------------------------------------------------------------------- 1 | virtualenv_python.bat -m idlelib %* 2 | -------------------------------------------------------------------------------- /lib/lazy_dict.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | class _LazyDict(collections.defaultdict): 4 | def __init__(self, fn): 5 | super().__init__() 6 | self.fn = fn 7 | 8 | def __missing__(self, key): 9 | val = self.fn(key) 10 | self[key] = val 11 | return val 12 | 13 | def make_lazy_dict(fn): 14 | return _LazyDict(fn) 15 | -------------------------------------------------------------------------------- /lib/performance.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import time 3 | 4 | def timed(f): 5 | @functools.wraps(f) 6 | def impl(*args, **kwargs): 7 | t = time.perf_counter() 8 | res = f(*args, **kwargs) 9 | t = time.perf_counter() - t 10 | print(f'{f.__name__} took {t} seconds') 11 | return res 12 | return impl 13 | -------------------------------------------------------------------------------- /lib/special_characters.py: -------------------------------------------------------------------------------- 1 | FULL_BLOCK = '█' 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | gmpy2 2 | jsonplus 3 | numpy 4 | parse 5 | python-dateutil 6 | requests 7 | sympy 8 | z3 9 | z3-solver 10 | -------------------------------------------------------------------------------- /setup.bat: -------------------------------------------------------------------------------- 1 | virtualenv_python.bat setup.py 2 | -------------------------------------------------------------------------------- /virtualenv_python.bat: -------------------------------------------------------------------------------- 1 | "%HOMEDRIVE%%HOMEPATH%\Envs\advent-of-code\Scripts\python" %* 2 | --------------------------------------------------------------------------------