├── 2015 ├── 01.py ├── 02.py ├── 03.py ├── 04.py ├── 05.py ├── 06.py ├── 07.py ├── 08.py ├── 09.py ├── 10.py ├── 11.py ├── 12.py ├── 13.py ├── 14.py ├── 15.py ├── 16.py ├── 17.py ├── 18.py ├── 19.py ├── 20.py ├── 21.py ├── 22.py ├── 23.py ├── 24.py └── 25.py ├── 2016 ├── 01.py ├── 02.py ├── 03.py ├── 04.py ├── 05.py ├── 06.py ├── 07.py ├── 08.py ├── 09.py ├── 10.py ├── 11.py ├── 12.py ├── 13.py ├── 14.py ├── 15.py ├── 16.py ├── 17.py ├── 18.py ├── 19.py ├── 20.py ├── 21.py ├── 22.py ├── 23.py ├── 24.py └── 25.py ├── 2017 ├── 01.py ├── 02.py ├── 03.py ├── 04.py ├── 05.py ├── 06.py ├── 07.py ├── 08.py ├── 09.py ├── 10.py ├── 11.py ├── 12.py ├── 13.py ├── 14.py ├── 15.py ├── 16.py ├── 17.py ├── 18.py ├── 19.py ├── 20.py ├── 21.py ├── 22.py ├── 23.py ├── 24.py └── 25.py ├── 2018 ├── 01.py ├── 02.py ├── 03.py ├── 04.py ├── 05.py ├── 06.py ├── 07.py ├── 08.py ├── 09.py ├── 10.py ├── 11.py ├── 12.py ├── 13.py └── 14.py ├── 2019 ├── 01.py ├── 02.py ├── 03.py ├── 04.py ├── 05.py ├── 06.py ├── 07.py ├── 08.py ├── 09.py ├── 10.py ├── 11.py ├── 12.py ├── 13.py ├── 14.py ├── 15.py ├── 16.py └── 17.py ├── 2020 ├── 01.js ├── 01.py ├── 02.py ├── 03.py ├── 04.py ├── 05.py ├── 06.py ├── 07.py ├── 08.py ├── 09.py ├── 10.py ├── 11.py ├── 12.py ├── 13.py ├── 14.py ├── 15.py ├── 16.py ├── 17.py ├── 18.py ├── 19.py ├── 20.py ├── 21.py ├── 22.py ├── 23.py ├── 24.py ├── 25.py ├── bootcode │ └── __init__.py └── tools │ └── __init__.py ├── 2021 ├── 01.py ├── 02.py ├── 03.py ├── 04.py ├── 05.py ├── 06.py ├── 07.py ├── 08.py ├── 09.py ├── 10.py ├── 11.py ├── 12.py ├── 13.py ├── 14.py ├── 15.py ├── 16.py ├── 17.py ├── 18.py ├── 19.py ├── 20.py ├── 21.py ├── 22.py ├── 23.py ├── 24.py └── 25.py ├── 2022 ├── 01.py ├── 02.py ├── 03.py ├── 04.py ├── 05.py ├── 06.py ├── 07.py ├── 08.py ├── 09.py ├── 10.py ├── 11.py ├── 12.py ├── 13.py ├── 14.py ├── 15.py └── 16.py ├── 2023 ├── 01.py ├── 02.py ├── 03.py ├── 04.py ├── 05.py ├── 06.py ├── 07.py ├── 08.py ├── 09.py ├── 10.py ├── 11.py ├── 12.py ├── 13.py ├── 14.py ├── 15.py ├── 16.py ├── 17.py ├── 18.py ├── 19.py ├── 20.py ├── 21.py ├── 22.py ├── 23.py ├── 24.py └── 25.py ├── 2024 ├── 01.py ├── 02.py ├── 03.py ├── 04.py ├── 05.py ├── 06.py ├── 07.py ├── 08.py ├── 09.py ├── 10.py ├── 11.py ├── 12.py ├── 13.py ├── 14.py ├── 15.py ├── 16.py ├── 17.py ├── 18.py ├── 19.py ├── 20.py ├── 21.py ├── 22.py ├── 23.py ├── 24.py └── 25.py ├── README.md ├── poetry.lock └── pyproject.toml /2015/01.py: -------------------------------------------------------------------------------- 1 | with open("01.txt", "r") as file: 2 | raw_data = file.read().strip() 3 | 4 | 5 | def part_one(data): 6 | return sum(1 - 2 * (c != "(") for c in data) 7 | 8 | 9 | def part_two(data): 10 | res = 0 11 | for i, c in enumerate(data): 12 | res += 1 - 2 * (c != "(") 13 | if res == -1: 14 | return i + 1 15 | 16 | 17 | print(f"Part 1: {part_one(raw_data)}") # 74 18 | print(f"Part 2: {part_two(raw_data)}") # 1795 19 | -------------------------------------------------------------------------------- /2015/02.py: -------------------------------------------------------------------------------- 1 | with open("02.txt", "r") as file: 2 | raw_data = file.read().strip() 3 | 4 | 5 | def parse_input(raw_data): 6 | return [tuple(map(int, line.split("x"))) for line in raw_data.split("\n")] 7 | 8 | 9 | data = parse_input(raw_data) 10 | 11 | 12 | def get_wrapping_area(dims): 13 | l, w, h = dims 14 | a, b, c = l * w, l * h, w * h 15 | return 2 * (a + b + c) + min(a, b, c) 16 | 17 | 18 | def get_ribbon_len(dims): 19 | a, b, c = sorted(dims) 20 | return 2 * (a + b) + (a * b * c) 21 | 22 | 23 | def part_one(data): 24 | return sum(get_wrapping_area(dims) for dims in data) 25 | 26 | 27 | def part_two(data): 28 | return sum(get_ribbon_len(dims) for dims in data) 29 | 30 | 31 | print(f"Part 1: {part_one(data)}") # 1588178 32 | print(f"Part 2: {part_two(data)}") # 3783758 33 | -------------------------------------------------------------------------------- /2015/03.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | with open("03.txt", "r") as file: 4 | raw_data = file.read().strip() 5 | 6 | DIRS = { 7 | "^": 0 + 1j, 8 | ">": 1 + 0j, 9 | "v": 0 - 1j, 10 | "<": -1 - 0j, 11 | } 12 | 13 | 14 | def part_one(data): 15 | points = defaultdict(int) 16 | cur = 0 + 0j 17 | points[cur.real, cur.imag] += 1 18 | for c in data: 19 | cur += DIRS[c] 20 | points[cur.real, cur.imag] += 1 21 | return len(points) 22 | 23 | 24 | def part_two(data): 25 | points = [defaultdict(int), defaultdict(int)] 26 | bots = [0 + 0j, 0 + 0j] 27 | i = 0 28 | cur = bots[i] 29 | cur_points = points[i] 30 | cur_points[cur.real, cur.imag] += 1 31 | for c in data: 32 | cur = bots[i] 33 | cur_points = points[i] 34 | cur += DIRS[c] 35 | bots[i] = cur 36 | cur_points[cur.real, cur.imag] += 1 37 | i = 1 - i 38 | a, b = points 39 | return len(set(a.keys()) | set(b.keys())) 40 | 41 | 42 | print(f"Part 1: {part_one(raw_data)}") # 2572 43 | print(f"Part 2: {part_two(raw_data)}") # 2631 44 | -------------------------------------------------------------------------------- /2015/04.py: -------------------------------------------------------------------------------- 1 | from hashlib import md5 2 | 3 | BASE = "yzbqklnj" 4 | 5 | 6 | def bruce_force(n): 7 | i = 0 8 | while True: 9 | cur = BASE + str(i) 10 | cur = cur.encode("utf-8") 11 | if md5(cur).hexdigest().startswith("0" * n): 12 | return i 13 | i += 1 14 | 15 | 16 | print(f"Part 1: {bruce_force(5)}") # 282749 17 | print(f"Part 2: {bruce_force(6)}") # 9962624 18 | -------------------------------------------------------------------------------- /2015/05.py: -------------------------------------------------------------------------------- 1 | import re 2 | from itertools import groupby 3 | 4 | with open("05.txt", "r") as file: 5 | raw_data = file.read().strip() 6 | 7 | data = raw_data.split("\n") 8 | 9 | 10 | def is_nice(s): 11 | check1 = sum(c in "aeiou" for c in s) >= 3 12 | check2 = check3 = False 13 | for _, grp in groupby(s): 14 | if len(list(grp)) >= 2: 15 | check2 = True 16 | break 17 | check3 = len(re.findall(r"ab|cd|pq|xy", s)) == 0 18 | return check1 & check2 & check3 19 | 20 | 21 | def is_nice_2(s): 22 | check1 = any(s[i : i + 2] in s[:i] for i in range(len(s) - 1)) 23 | check2 = any(a == b for a, b in zip(s, s[2:])) 24 | return check1 & check2 25 | 26 | 27 | def part_one(data): 28 | return sum(map(is_nice, data)) 29 | 30 | 31 | def part_two(data): 32 | return sum(map(is_nice_2, data)) 33 | 34 | 35 | print(f"Part 1: {part_one(data)}") # 255 36 | print(f"Part 2: {part_two(data)}") # 55 37 | -------------------------------------------------------------------------------- /2015/06.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import defaultdict 3 | 4 | with open("06.txt", "r") as file: 5 | raw_data = file.read().strip().split("\n") 6 | 7 | 8 | def parse_input(raw_data): 9 | res = [] 10 | for line in raw_data: 11 | x, a, b, c, d = re.findall(r"(\w+) (\d+),(\d+) through (\d+),(\d+)", line)[0] 12 | res.append([x, tuple(map(int, (a, b, c, d)))]) 13 | return res 14 | 15 | 16 | data = parse_input(raw_data) 17 | 18 | 19 | def trigger_lights(grid, ops, cmd): 20 | inst, (a, b, c, d) = cmd 21 | if a > c: 22 | a, c = c, a 23 | if b > d: 24 | b, d = d, b 25 | f = ops[inst] 26 | for i in range(a, c + 1): 27 | for j in range(b, d + 1): 28 | cur = grid[(i, j)] 29 | grid[(i, j)] = f(cur) 30 | return grid 31 | 32 | 33 | def part_one(data): 34 | ops = { 35 | "on": lambda x: 1, 36 | "off": lambda x: 0, 37 | "toggle": lambda x: 1 - x, 38 | } 39 | grid = defaultdict(int) 40 | for cmd in data: 41 | grid = trigger_lights(grid, ops, cmd) 42 | return sum(grid.values()) 43 | 44 | 45 | def part_two(data): 46 | ops = { 47 | "on": lambda x: x + 1, 48 | "off": lambda x: max(0, x - 1), 49 | "toggle": lambda x: x + 2, 50 | } 51 | grid = defaultdict(int) 52 | for cmd in data: 53 | grid = trigger_lights(grid, ops, cmd) 54 | return sum(grid.values()) 55 | 56 | 57 | print(f"Part 1: {part_one(data)}") # 377891 58 | print(f"Part 2: {part_two(data)}") # 14110788 59 | -------------------------------------------------------------------------------- /2015/07.py: -------------------------------------------------------------------------------- 1 | from functools import cache 2 | from operator import and_, lshift, or_, rshift 3 | 4 | with open("07.txt", "r") as file: 5 | raw_data = file.read().strip().split("\n") 6 | 7 | 8 | def parse_input(raw_data): 9 | return {(s := line.split(" -> "))[1]: s[0].split(" ") for line in raw_data} 10 | 11 | 12 | data = parse_input(raw_data) 13 | 14 | OPS = { 15 | "AND": and_, 16 | "OR": or_, 17 | "RSHIFT": rshift, 18 | "LSHIFT": lshift, 19 | } 20 | 21 | 22 | @cache 23 | def dp(wire): 24 | if wire.isdigit(): 25 | return int(wire) 26 | left = data[wire] 27 | if len(left) == 1: 28 | return dp(left[0]) 29 | elif len(left) == 2: 30 | return ~dp(left[1]) & 0xFFFF 31 | return OPS[left[1]](dp(left[0]), dp(left[2])) 32 | 33 | 34 | def part_one(): 35 | res = dp("a") 36 | dp.cache_clear() 37 | return res 38 | 39 | 40 | def part_two(): 41 | data["b"] = ["3176"] 42 | res = dp("a") 43 | dp.cache_clear() 44 | return res 45 | 46 | 47 | print(f"Part 1: {part_one()}") # 3176 48 | print(f"Part 2: {part_two()}") # 14710 49 | -------------------------------------------------------------------------------- /2015/08.py: -------------------------------------------------------------------------------- 1 | with open("08.txt", "r") as file: 2 | raw_data = file.read().strip().split("\n") 3 | 4 | DATA = raw_data 5 | 6 | 7 | def part_one(): 8 | return sum(map(lambda x: len(x) - len(eval(x)), DATA)) 9 | 10 | 11 | def part_two(): 12 | return sum(map(lambda x: x.count(r'"') + x.count("\\") + 2, DATA)) 13 | 14 | 15 | print(f"Part 1: {part_one()}") # 1342 16 | print(f"Part 2: {part_two()}") # 2074 17 | -------------------------------------------------------------------------------- /2015/09.py: -------------------------------------------------------------------------------- 1 | import re 2 | from itertools import permutations 3 | 4 | with open("09.txt", "r") as file: 5 | raw_data = file.read().strip().split("\n") 6 | 7 | 8 | def parse_input(raw_data): 9 | res = [] 10 | for line in raw_data: 11 | u, v, w = re.findall(r"(\w+) to (\w+) = (\d+)", line)[0] 12 | res.append((u, v, int(w))) 13 | return res 14 | 15 | 16 | DATA = parse_input(raw_data) 17 | 18 | 19 | def build_graph(data): 20 | g = dict() 21 | nodes = set() 22 | for u, v, w in data: 23 | g[(u, v)] = w 24 | g[(v, u)] = w 25 | nodes.add(u) 26 | nodes.add(v) 27 | return g, nodes 28 | 29 | 30 | G, NODES = build_graph(DATA) 31 | 32 | 33 | def part_one(): 34 | res = float("inf") 35 | for path in permutations(NODES): 36 | cur = 0 37 | for edge in zip(path, path[1:]): 38 | cur += G[edge] 39 | res = min(res, cur) 40 | return res 41 | 42 | 43 | def part_two(): 44 | res = 0 45 | for path in permutations(NODES): 46 | cur = 0 47 | for edge in zip(path, path[1:]): 48 | cur += G[edge] 49 | res = max(res, cur) 50 | return res 51 | 52 | 53 | print(f"Part 1: {part_one()}") # 117 54 | print(f"Part 2: {part_two()}") # 909 55 | -------------------------------------------------------------------------------- /2015/10.py: -------------------------------------------------------------------------------- 1 | from itertools import groupby 2 | 3 | INPUT = "1321131112" 4 | 5 | 6 | def look_and_say(s, reps): 7 | for _ in range(reps): 8 | cur = [] 9 | for c, grp in groupby(s): 10 | cur.append(str(len(list(grp))) + c) 11 | s = "".join(cur) 12 | return len(s) 13 | 14 | 15 | def part_one(): 16 | return look_and_say(INPUT, 40) 17 | 18 | 19 | def part_two(): 20 | return look_and_say(INPUT, 50) 21 | 22 | 23 | print(f"Part 1: {part_one()}") # 492982 24 | print(f"Part 2: {part_two()}") # 6989950 25 | -------------------------------------------------------------------------------- /2015/11.py: -------------------------------------------------------------------------------- 1 | from itertools import groupby 2 | 3 | INPUT = "cqjxjnds" 4 | 5 | 6 | def has_straight(s): 7 | nums = list(map(ord, s)) 8 | for x, y, z in zip(nums, nums[1:], nums[2:]): 9 | if x == y - 1 == z - 2: 10 | return True 11 | return False 12 | 13 | 14 | def no_invalids(s): 15 | return not any(c in "iol" for c in s) 16 | 17 | 18 | def has_two_pairs(s): 19 | cur = None 20 | cnt = 0 21 | for c, grp in groupby(s): 22 | if c != cur and len(list(grp)) == 2: 23 | cur = c 24 | cnt += 1 25 | return cnt >= 2 26 | 27 | 28 | def next_pw(s): 29 | p = list(s) 30 | while True: 31 | i = -1 32 | while p[i] == "z": 33 | p[i] = "a" 34 | i -= 1 35 | p[i] = chr(ord(p[i]) + 1) 36 | yield "".join(p) 37 | 38 | 39 | def next_valid_pw(s): 40 | while True: 41 | s = next(next_pw(s)) 42 | if has_straight(s) and no_invalids(s) and has_two_pairs(s): 43 | yield s 44 | 45 | 46 | def part_one(): 47 | return next(next_valid_pw(INPUT)) 48 | 49 | 50 | def part_two(): 51 | res = next_valid_pw(INPUT) 52 | next(res) 53 | return next(res) 54 | 55 | 56 | print(f"Part 1: {part_one()}") # cqjxxyzz 57 | print(f"Part 2: {part_two()}") # cqkaabcc 58 | -------------------------------------------------------------------------------- /2015/12.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | 4 | with open("12.txt", "r") as file: 5 | raw_data = file.read().strip() 6 | 7 | 8 | def parse_json(j): 9 | match j: 10 | case dict(): 11 | return 0 if "red" in j.values() else sum(map(parse_json, j.values())) 12 | case list(): 13 | return sum(map(parse_json, j)) 14 | case int(): 15 | return j 16 | case _: 17 | return 0 18 | 19 | 20 | def part_one(): 21 | return sum(map(int, re.findall(r"(-?\d+)", raw_data))) 22 | 23 | 24 | def part_two(): 25 | j = json.loads(raw_data) 26 | return parse_json(j) 27 | 28 | 29 | print(f"Part 1: {part_one()}") # 111754 30 | print(f"Part 2: {part_two()}") # 65402 31 | -------------------------------------------------------------------------------- /2015/13.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import defaultdict 3 | from itertools import permutations 4 | 5 | with open("13.txt", "r") as file: 6 | raw_data = file.read().strip() 7 | 8 | 9 | def parse_input(raw_data): 10 | g = defaultdict(int) 11 | happy = lambda sgn, w: int(w) * (1 - 2 * (sgn == "lose")) 12 | nodes = set() 13 | for u, sgn, w, v in re.findall( 14 | r"(\w+) would (\w+) (\d+) happiness units by sitting next to (\w+)", raw_data 15 | ): 16 | g[u, v] += happy(sgn, w) 17 | g[v, u] += happy(sgn, w) 18 | nodes.update((u, v)) 19 | return g, nodes 20 | 21 | 22 | def brute_force(g, nodes): 23 | res = 0 24 | for path in permutations(nodes): 25 | path = list(path) 26 | cur = 0 27 | for u, v in zip(path, path[1:] + [path[0]]): 28 | cur += g[u, v] 29 | res = max(res, cur) 30 | return res 31 | 32 | 33 | def part_one(): 34 | g, nodes = parse_input(raw_data) 35 | return brute_force(g, nodes) 36 | 37 | 38 | def part_two(): 39 | g, nodes = parse_input(raw_data) 40 | nodes.add("me") 41 | return brute_force(g, nodes) 42 | 43 | 44 | print(f"Part 1: {part_one()}") # 664 45 | print(f"Part 2: {part_two()}") # 640 46 | -------------------------------------------------------------------------------- /2015/14.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | with open("14.txt", "r") as file: 4 | data = file.read().strip().split("\n") 5 | 6 | 7 | def parse_input(data): 8 | return [tuple(map(int, re.findall(r"(\d+)", line))) for line in data] 9 | 10 | 11 | def calc_dist(velo, time, rest, elapsed): 12 | n, rm = divmod(elapsed, time + rest) 13 | res = (n * time + min(time, rm)) * velo 14 | return res 15 | 16 | 17 | def part_one(): 18 | reindeer = parse_input(data) 19 | return max(calc_dist(*rd, 2503) for rd in reindeer) 20 | 21 | 22 | def part_two(): 23 | reindeer = parse_input(data) 24 | scores = [0] * len(reindeer) 25 | for i in range(1, 2504): 26 | cur = [calc_dist(*rd, i) for rd in reindeer] 27 | high = max(cur) 28 | for i, v in enumerate(cur): 29 | scores[i] += high == v 30 | return max(scores) 31 | 32 | 33 | print(f"Part 1: {part_one()}") # 2660 34 | print(f"Part 2: {part_two()}") # 1256 35 | -------------------------------------------------------------------------------- /2015/15.py: -------------------------------------------------------------------------------- 1 | import re 2 | from math import prod 3 | 4 | with open("15.txt", "r") as file: 5 | data = file.read().strip().split("\n") 6 | 7 | 8 | def parse_input(data): 9 | return [tuple(map(int, re.findall(r"(-?\d+)", line))) for line in data] 10 | 11 | 12 | INGS = parse_input(data) 13 | 14 | 15 | def partitions(n, r): 16 | if r == 1: 17 | yield (n,) 18 | return 19 | for i in range(n + 1): 20 | for j in partitions(n - i, r - 1): 21 | yield i, *j 22 | 23 | 24 | def calc_score(p, ings): 25 | cur = [] 26 | for i in range(len(ings)): 27 | x = sum(a[i] * b for a, b in zip(ings, p)) 28 | cur.append(x if x > 0 else 0) 29 | return prod(cur) 30 | 31 | 32 | def part_one(): 33 | res = 0 34 | for p in partitions(100, len(INGS)): 35 | res = max(res, calc_score(p, INGS)) 36 | return res 37 | 38 | 39 | def part_two(): 40 | res = 0 41 | for p in partitions(100, len(INGS)): 42 | if sum(a[4] * b for a, b in zip(INGS, p)) != 500: 43 | continue 44 | res = max(res, calc_score(p, INGS)) 45 | return res 46 | 47 | 48 | print(f"Part 1: {part_one()}") # 222870 49 | print(f"Part 2: {part_two()}") # 117936 50 | -------------------------------------------------------------------------------- /2015/16.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import Counter 3 | 4 | with open("16.txt", "r") as file: 5 | data = file.read().strip().split("\n") 6 | 7 | 8 | def parse_line(s): 9 | return Counter({k: int(v) for k, v in re.findall(r"(\w+): (\d+)", s)}) 10 | 11 | 12 | def parse_input(data): 13 | return {i: parse_line(v) for i, v in enumerate(data, start=1)} 14 | 15 | 16 | TAPE = parse_line( 17 | """children: 3 18 | cats: 7 19 | samoyeds: 2 20 | pomeranians: 3 21 | akitas: 0 22 | vizslas: 0 23 | goldfish: 5 24 | trees: 3 25 | cars: 2 26 | perfumes: 1""" 27 | ) 28 | 29 | SUES = parse_input(data) 30 | 31 | 32 | def check_sue_1(i, tape, sues): 33 | sue = sues[i] 34 | same = Counter({k: tape[k] for k in tape.keys() & sue.keys()}) 35 | return same == sue 36 | 37 | 38 | def check_sue_2(i, tape, sues): 39 | sue = sues[i] 40 | same = Counter({k: tape[k] for k in tape.keys() & sue.keys()}) 41 | check = ( 42 | lambda k, v: v < sue[k] 43 | if k in ("cats", "trees") 44 | else v > sue[k] 45 | if k in ("pomeranians", "goldfish") 46 | else v == sue[k] 47 | ) 48 | for k, v in same.items(): 49 | if not check(k, v): 50 | return False 51 | return True 52 | 53 | 54 | def part_one(): 55 | for i in range(1, 501): 56 | if check_sue_1(i, TAPE, SUES): 57 | return i 58 | 59 | 60 | def part_two(): 61 | for i in range(1, 501): 62 | if check_sue_2(i, TAPE, SUES): 63 | return i 64 | 65 | 66 | print(f"Part 1: {part_one()}") # 373 67 | print(f"Part 2: {part_two()}") # 260 68 | -------------------------------------------------------------------------------- /2015/17.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | with open("17.txt", "r") as file: 4 | data = file.read() 5 | 6 | NUMS = tuple(map(int, re.findall(r"(\d+)", data))) 7 | 8 | 9 | def recur(bins, rem, used=0): 10 | if rem == 0: 11 | res.append(used) 12 | if rem > 0 and bins: 13 | recur(bins[1:], rem, used) 14 | recur(bins[1:], rem - bins[0], used + 1) 15 | 16 | 17 | res = [] 18 | recur(NUMS, 150) 19 | 20 | 21 | def part_one(): 22 | return len(res) 23 | 24 | 25 | def part_two(): 26 | lo = min(res) 27 | return sum(x == lo for x in res) 28 | 29 | 30 | print(f"Part 1: {part_one()}") # 1304 31 | print(f"Part 2: {part_two()}") # 18 32 | -------------------------------------------------------------------------------- /2015/18.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from itertools import product 3 | 4 | with open("18.txt", "r") as file: 5 | data = file.read().strip() 6 | 7 | LIGHTS = [list(map(lambda x: int(x == "#"), line)) for line in data.split("\n")] 8 | 9 | 10 | def find_on(grid): 11 | on = set() 12 | for i in range(len(grid)): 13 | for j in range(len(grid[0])): 14 | if grid[i][j]: 15 | on.add((i, j)) 16 | return on 17 | 18 | 19 | def find_neighbors(points): 20 | neis = defaultdict(int) 21 | for p in points: 22 | for delta in filter(any, product([-1, 0, 1], repeat=2)): 23 | ni, nj = (x + dx for x, dx in zip(p, delta)) 24 | if 0 <= ni < 100 and 0 <= nj < 100: 25 | neis[ni, nj] += 1 26 | return neis 27 | 28 | 29 | def step(points): 30 | new_on = set() 31 | neis = find_neighbors(points) 32 | for k, v in neis.items(): 33 | if v == 3 or (v == 2 and k in points): 34 | new_on.add(k) 35 | return new_on 36 | 37 | 38 | def part_one(): 39 | on = find_on(LIGHTS) 40 | for _ in range(100): 41 | on = step(on) 42 | return len(on) 43 | 44 | 45 | def part_two(): 46 | on = find_on(LIGHTS) 47 | on.update(((0, 0), (0, 99), (99, 0), (99, 99))) 48 | for _ in range(100): 49 | on = step(on) 50 | on.update(((0, 0), (0, 99), (99, 0), (99, 99))) 51 | return len(on) 52 | 53 | 54 | print(f"Part 1: {part_one()}") # 821 55 | print(f"Part 2: {part_two()}") # 886 56 | -------------------------------------------------------------------------------- /2015/19.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import defaultdict 3 | 4 | with open("19.txt", "r") as file: 5 | data = file.read().strip() 6 | 7 | MOLECULE = data.split("\n")[-1] 8 | REPLACEMENTS = defaultdict(list) 9 | 10 | for k, v in re.findall(r"(\w+) => (\w+)", data): 11 | REPLACEMENTS[k].append(v) 12 | 13 | 14 | def part_one(): 15 | res = set() 16 | for k, vals in REPLACEMENTS.items(): 17 | n = len(k) 18 | i = MOLECULE.find(k) 19 | while i != -1: 20 | for v in vals: 21 | res.add(MOLECULE[:i] + v + MOLECULE[i + n :]) 22 | i = MOLECULE.find(k, i + n) 23 | return len(res) 24 | 25 | 26 | def part_two(): 27 | elements = sum(c.isupper() for c in MOLECULE) 28 | a, b = MOLECULE.count("Rn"), MOLECULE.count("Y") 29 | return elements - 2 * (a + b) - 1 30 | 31 | 32 | print(f"Part 1: {part_one()}") # 509 33 | print(f"Part 2: {part_two()}") # 195 34 | -------------------------------------------------------------------------------- /2015/20.py: -------------------------------------------------------------------------------- 1 | from itertools import count 2 | 3 | INPUT = 36000000 4 | 5 | 6 | def solve(target, upper): 7 | dp = [1] * target 8 | for i in count(2): 9 | for j in range(min(target // i, upper)): 10 | dp[i * j] += i 11 | if dp[i] >= target: 12 | return i 13 | 14 | 15 | print(f"Part 1: {solve(INPUT//10, INPUT//10)}") # 831600 16 | print(f"Part 2: {solve(INPUT//11, 50)}") # 884520 17 | -------------------------------------------------------------------------------- /2015/21.py: -------------------------------------------------------------------------------- 1 | import re 2 | from itertools import combinations, product 3 | 4 | with open("21.txt", "r") as file: 5 | data = file.read().strip() 6 | 7 | WEAPONS = [ 8 | (8, 4, 0), 9 | (10, 5, 0), 10 | (25, 6, 0), 11 | (40, 7, 0), 12 | (74, 8, 0), 13 | ] 14 | 15 | ARMOR = [ 16 | (13, 0, 1), 17 | (31, 0, 2), 18 | (53, 0, 3), 19 | (75, 0, 4), 20 | (102, 0, 5), 21 | (0, 0, 0), 22 | ] 23 | 24 | RINGS = [ 25 | (25, 1, 0), 26 | (50, 2, 0), 27 | (100, 3, 0), 28 | (20, 0, 1), 29 | (40, 0, 2), 30 | (80, 0, 3), 31 | (0, 0, 0), 32 | (0, 0, 0), 33 | ] 34 | 35 | BOSS_HP, BOSS_DAMAGE, BOSS_ARMOR = map(int, re.findall(r"(\d+)", data)) 36 | 37 | 38 | def attack(weapon_dmg, rings, boss_armor): 39 | return max(1, weapon_dmg + sum(rings) - boss_armor) 40 | 41 | 42 | def defend(armor, rings, boss_dmg): 43 | return max(1, boss_dmg - armor - sum(rings)) 44 | 45 | 46 | def part_one(): 47 | return min( 48 | weapon[0] + armor[0] + sum(r[0] for r in rings) 49 | for weapon, armor, rings in product(WEAPONS, ARMOR, combinations(RINGS, 2)) 50 | if (BOSS_HP // attack(weapon[1], (r[1] for r in rings), BOSS_ARMOR)) 51 | <= (100 // defend(armor[2], (r[2] for r in rings), BOSS_DAMAGE)) 52 | ) 53 | 54 | 55 | def part_two(): 56 | return max( 57 | weapon[0] + armor[0] + sum(r[0] for r in rings) 58 | for weapon, armor, rings in product(WEAPONS, ARMOR, combinations(RINGS, 2)) 59 | if (BOSS_HP // attack(weapon[1], (r[1] for r in rings), BOSS_ARMOR)) 60 | > (100 // defend(armor[2], (r[2] for r in rings), BOSS_DAMAGE)) 61 | ) 62 | 63 | 64 | print(f"Part 1: {part_one()}") # 91 65 | print(f"Part 2: {part_two()}") # 158 66 | -------------------------------------------------------------------------------- /2015/23.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | with open("23.txt", "r") as file: 4 | data = file.read().strip() 5 | 6 | INSTRUCTIONS = [re.split(r"[ ,]+", line.strip()) for line in data.split("\n")] 7 | 8 | 9 | class CPU: 10 | def __init__(self, instructions, reg_a=0): 11 | self.registers = {"a": reg_a, "b": 0} 12 | self.pos = 0 13 | self.instructions = instructions 14 | 15 | def hlf(self, r): 16 | self.registers[r] >>= 1 17 | self.pos += 1 18 | 19 | def tpl(self, r): 20 | self.registers[r] *= 3 21 | self.pos += 1 22 | 23 | def inc(self, r): 24 | self.registers[r] += 1 25 | self.pos += 1 26 | 27 | def jmp(self, offset): 28 | self.pos += int(offset) 29 | 30 | def jie(self, r, offset): 31 | self.pos += int(offset) if not self.registers[r] & 1 else 1 32 | 33 | def jio(self, r, offset): 34 | self.pos += int(offset) if self.registers[r] == 1 else 1 35 | 36 | def step(self): 37 | op, *args = self.instructions[self.pos] 38 | getattr(self, op)(*args) 39 | 40 | def run(self): 41 | while 0 <= self.pos < len(self.instructions): 42 | self.step() 43 | return self.registers["b"] 44 | 45 | 46 | def part_one(): 47 | cpu = CPU(INSTRUCTIONS) 48 | return cpu.run() 49 | 50 | 51 | def part_two(): 52 | cpu = CPU(INSTRUCTIONS, 1) 53 | return cpu.run() 54 | 55 | 56 | print(f"Part 1: {part_one()}") # 307 57 | print(f"Part 2: {part_two()}") # 160 58 | -------------------------------------------------------------------------------- /2015/24.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | with open("24.txt", "r") as file: 4 | data = file.read().strip() 5 | 6 | PACKAGES = sorted(map(int, re.findall(r"(\d+)", data)), reverse=True) 7 | 8 | 9 | def solve(num_groups=3): 10 | groups = set() 11 | target = sum(PACKAGES) // num_groups 12 | bound = len(PACKAGES) // num_groups 13 | 14 | def recur(weights, cur, qe, count): 15 | if cur == target: 16 | groups.add((count, qe)) 17 | return 18 | if cur < target and weights and count < bound: 19 | recur(weights[1:], cur, qe, count) 20 | recur(weights[1:], cur + weights[0], qe * weights[0], count + 1) 21 | 22 | recur(PACKAGES, 0, 1, 0) 23 | return min(groups)[1] 24 | 25 | 26 | print(f"Part 1: {solve(3)}") # 11846773891 27 | print(f"Part 2: {solve(4)}") # 80393059 28 | -------------------------------------------------------------------------------- /2015/25.py: -------------------------------------------------------------------------------- 1 | START = 20151125 2 | BASE = 252533 3 | MOD = 33554393 4 | ROW = 2947 5 | COL = 3029 6 | 7 | 8 | def get_exp(r, c): 9 | return ((r + c - 1) * (r + c - 2) >> 1) + c 10 | 11 | 12 | def solve(base, exp, mod): 13 | res = 1 14 | while exp: 15 | if exp & 1: 16 | _, res = divmod(res * base, mod) 17 | exp >>= 1 18 | base = base * base % mod 19 | return res * START % mod 20 | 21 | 22 | print(f"Part 1: {solve(BASE, get_exp(ROW, COL) - 1, MOD)}") # 19980801 23 | -------------------------------------------------------------------------------- /2016/01.py: -------------------------------------------------------------------------------- 1 | with open("01.txt", "r") as file: 2 | data = file.read().strip() 3 | 4 | MOVES = [(str(x[0]), int(x[1:])) for x in data.split(", ")] 5 | DIRS = { 6 | "R": 0 - 1j, 7 | "L": 0 + 1j, 8 | } 9 | 10 | 11 | def part_one(): 12 | face, pos = 0 + 1j, 0 + 0j 13 | for d, blocks in MOVES: 14 | face *= DIRS[d] 15 | pos += face * blocks 16 | return int(abs(pos.real) + abs(pos.imag)) 17 | 18 | 19 | def part_two(): 20 | face, pos = 0 + 1j, 0 + 0j 21 | seen = set() 22 | for d, blocks in MOVES: 23 | face *= DIRS[d] 24 | for _ in range(blocks): 25 | if pos in seen: 26 | return int(abs(pos.real) + abs(pos.imag)) 27 | seen.add(pos) 28 | pos += face 29 | return -1 30 | 31 | 32 | print(f"Part 1: {part_one()}") # 209 33 | print(f"Part 2: {part_two()}") # 136 34 | -------------------------------------------------------------------------------- /2016/02.py: -------------------------------------------------------------------------------- 1 | with open("02.txt", "r") as file: 2 | data = file.read().strip() 3 | 4 | INSTRUCTIONS = data.split("\n") 5 | KEYPAD = [ 6 | [1, 2, 3], 7 | [4, 5, 6], 8 | [7, 8, 9], 9 | ] 10 | KEYPAD2 = [ 11 | [".", ".", "1", ".", "."], 12 | [".", "2", "3", "4", "."], 13 | ["5", "6", "7", "8", "9"], 14 | [".", "A", "B", "C", "."], 15 | [".", ".", "D", ".", "."], 16 | ] 17 | DIRS = { 18 | "U": 0 - 1j, 19 | "D": 0 + 1j, 20 | "L": -1 + 0j, 21 | "R": 1 + 0j, 22 | } 23 | 24 | 25 | def find_key(moves, keypad, pos): 26 | N, M = len(keypad), len(keypad[0]) 27 | for c in moves: 28 | npos = pos + DIRS[c] 29 | ni, nj = int(npos.imag), int(npos.real) 30 | if 0 <= ni < N and 0 <= nj < M and keypad[ni][nj] != ".": 31 | pos = npos 32 | i, j = int(pos.imag), int(pos.real) 33 | return keypad[i][j], pos 34 | 35 | 36 | def part_one(): 37 | res, pos = 0, 1 + 1j 38 | for moves in INSTRUCTIONS: 39 | key, npos = find_key(moves, KEYPAD, pos) 40 | res = res * 10 + key 41 | pos = npos 42 | return res 43 | 44 | 45 | def part_two(): 46 | res, pos = [], 2j 47 | for moves in INSTRUCTIONS: 48 | key, npos = find_key(moves, KEYPAD2, pos) 49 | res.append(key) 50 | pos = npos 51 | return "".join(res) 52 | 53 | 54 | print(f"Part 1: {part_one()}") # 56983 55 | print(f"Part 2: {part_two()}") # 8B8B1 56 | -------------------------------------------------------------------------------- /2016/03.py: -------------------------------------------------------------------------------- 1 | with open("03.txt", "r") as file: 2 | data = file.read().strip() 3 | 4 | TRIANGLES = [tuple(map(int, line.split())) for line in data.split("\n")] 5 | 6 | 7 | def possible(*args): 8 | return sum(args) > 2 * max(args) 9 | 10 | 11 | def part_one(): 12 | return sum(possible(*triangle) for triangle in TRIANGLES) 13 | 14 | 15 | def part_two(): 16 | flatten = [x[i] for i in range(3) for x in TRIANGLES] 17 | return sum(possible(*flatten[i : i + 3]) for i in range(0, len(flatten), 3)) 18 | 19 | 20 | print(f"Part 1: {part_one()}") # 1032 21 | print(f"Part 2: {part_two()}") # 1838 22 | -------------------------------------------------------------------------------- /2016/04.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import Counter 3 | 4 | with open("04.txt", "r") as file: 5 | data = file.read().strip() 6 | 7 | ROOMS = re.findall(r"([a-z-]+)-(\d+)\[([a-z]+)\]", data) 8 | 9 | 10 | def part_one(): 11 | return sum( 12 | int(sector) 13 | for name, sector, checksum in ROOMS 14 | if checksum 15 | == "".join( 16 | x[0] 17 | for x in sorted( 18 | Counter(name.replace("-", "")).most_common(), 19 | key=lambda x: (-x[1], x[0]), 20 | )[:5] 21 | ) 22 | ) 23 | 24 | 25 | def part_two(): 26 | return next( 27 | int(sector) 28 | for name, sector, checksum in ROOMS 29 | if checksum 30 | == "".join( 31 | x[0] 32 | for x in sorted( 33 | Counter(name.replace("-", "")).most_common(), 34 | key=lambda x: (-x[1], x[0]), 35 | )[:5] 36 | ) 37 | and "north" 38 | in "".join( 39 | chr((ord(c) - ord("a") + int(sector)) % 26 + ord("a")) 40 | for c in name.replace("-", " ") 41 | ) 42 | ) 43 | 44 | 45 | print(f"Part 1: {part_one()}") # 361724 46 | print(f"Part 2: {part_two()}") # 482 47 | -------------------------------------------------------------------------------- /2016/05.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | DOOR_ID = "abbhdwsy" 4 | 5 | 6 | def md5(s): 7 | return hashlib.md5(s.encode()).hexdigest() 8 | 9 | 10 | def part_one(): 11 | password = [] 12 | i = 0 13 | while len(password) < 8: 14 | h = md5(DOOR_ID + str(i)) 15 | if h.startswith("00000"): 16 | password.append(h[5]) 17 | i += 1 18 | return "".join(password) 19 | 20 | 21 | def part_two(): 22 | password = [None] * 8 23 | i = 0 24 | while None in password: 25 | h = md5(DOOR_ID + str(i)) 26 | if ( 27 | h.startswith("00000") 28 | and h[5].isdigit() 29 | and int(h[5]) < 8 30 | and password[int(h[5])] is None 31 | ): 32 | password[int(h[5])] = h[6] 33 | i += 1 34 | return "".join(password) 35 | 36 | 37 | print(f"Part 1: {part_one()}") # 801b56a7 38 | print(f"Part 2: {part_two()}") # 424a0197 39 | -------------------------------------------------------------------------------- /2016/06.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | 3 | with open("06.txt", "r") as file: 4 | data = file.read().strip() 5 | 6 | MESSAGES = data.split("\n") 7 | 8 | 9 | def solve(least_common=False): 10 | return "".join( 11 | count.most_common()[0 - least_common][0] 12 | for count in [Counter(message) for message in zip(*MESSAGES)] 13 | ) 14 | 15 | 16 | print(f"Part 1: {solve()}") # dzqckwsd 17 | print(f"Part 2: {solve(least_common=True)}") # lragovly 18 | -------------------------------------------------------------------------------- /2016/07.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | with open("07.txt", "r") as file: 4 | data = file.read().strip() 5 | 6 | IPS = [re.split(r"\[|\]", line) for line in data.split("\n")] 7 | SUPER = [ip[::2] for ip in IPS] 8 | HYPER = [ip[1::2] for ip in IPS] 9 | 10 | 11 | def is_abba(s): 12 | return any( 13 | s[i] == s[i + 3] and s[i + 1] == s[i + 2] and s[i] != s[i + 1] 14 | for i in range(len(s) - 3) 15 | ) 16 | 17 | 18 | def is_ababab(sup, hyp): 19 | return any( 20 | a == c and a != b and b + a + b in hyp 21 | for a, b, c in zip(sup, sup[1:], sup[2:]) 22 | ) 23 | 24 | 25 | def part_one(): 26 | return sum( 27 | any(is_abba(s) for s in sup) and not any(is_abba(s) for s in hyp) 28 | for sup, hyp in zip(SUPER, HYPER) 29 | ) 30 | 31 | 32 | def part_two(): 33 | return sum( 34 | any(is_ababab(sup, hyp) for sup in sups) 35 | for sups, hyps in zip(SUPER, HYPER) 36 | for hyp in hyps 37 | ) 38 | 39 | 40 | print(f"Part 1: {part_one()}") # 118 41 | print(f"Part 2: {part_two()}") # 260 42 | -------------------------------------------------------------------------------- /2016/08.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | with open("08.txt", "r") as file: 4 | data = file.read().strip() 5 | 6 | RECT = re.compile(r"rect (\d+)x(\d+)") 7 | ROT_ROW = re.compile(r"rotate row y=(\d+) by (\d+)") 8 | ROT_COL = re.compile(r"rotate column x=(\d+) by (\d+)") 9 | 10 | 11 | def rect(screen, x, y): 12 | for i in range(y): 13 | screen[i][:x] = ["#"] * x 14 | return screen 15 | 16 | 17 | def rotate_row(screen, y, by): 18 | screen[y] = screen[y][-by:] + screen[y][:-by] 19 | return screen 20 | 21 | 22 | def rotate_col(screen, x, by): 23 | col = [screen[i][x] for i in range(len(screen))] 24 | col = col[-by:] + col[:-by] 25 | for i in range(len(screen)): 26 | screen[i][x] = col[i] 27 | return screen 28 | 29 | 30 | def make_screen(data): 31 | screen = [[" "] * 50 for _ in range(6)] 32 | for command in data.split("\n"): 33 | if m := re.match(RECT, command): 34 | screen = rect(screen, int(m.group(1)), int(m.group(2))) 35 | elif m := re.match(ROT_ROW, command): 36 | screen = rotate_row(screen, int(m.group(1)), int(m.group(2))) 37 | elif m := re.match(ROT_COL, command): 38 | screen = rotate_col(screen, int(m.group(1)), int(m.group(2))) 39 | return screen 40 | 41 | 42 | SCREEN = make_screen(data) 43 | 44 | 45 | def part_one(): 46 | return sum(row.count("#") for row in SCREEN) 47 | 48 | 49 | def part_two(): 50 | return "\n" + "\n".join("".join(row) for row in SCREEN) 51 | 52 | 53 | print(f"Part 1: {part_one()}") # 106 54 | print(f"Part 2: {part_two()}") # CFLELOYFCS 55 | -------------------------------------------------------------------------------- /2016/09.py: -------------------------------------------------------------------------------- 1 | with open("09.txt", "r") as file: 2 | data = file.read().strip() 3 | 4 | 5 | def decompress(data, version=1): 6 | i = 0 7 | res = 0 8 | while i < len(data): 9 | if data[i] == "(": 10 | j = data.index(")", i) 11 | length, repeat = map(int, data[i + 1 : j].split("x")) 12 | if version == 1: 13 | res += length * repeat 14 | else: 15 | res += decompress(data[j + 1 : j + 1 + length], version) * repeat 16 | i = j + 1 + length 17 | else: 18 | res += 1 19 | i += 1 20 | return res 21 | 22 | 23 | print(f"Part 1: {decompress(data)}") # 102239 24 | print(f"Part 2: {decompress(data, version=2)}") # 10780403063 25 | -------------------------------------------------------------------------------- /2016/11.py: -------------------------------------------------------------------------------- 1 | from itertools import accumulate 2 | 3 | 4 | def solve(*args): 5 | return sum(2 * (x - 1) - 1 for x in accumulate(args)) 6 | 7 | 8 | print(f"Part 1: {solve(4, 2, 4)}") # 31 9 | print(f"Part 2: {solve(8, 2, 4)}") # 55 10 | -------------------------------------------------------------------------------- /2016/12.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | with open("12.txt", "r") as file: 4 | data = file.read().strip() 5 | 6 | INSTRUCTIONS = [re.split(r"[ ,]+", line.strip()) for line in data.split("\n")] 7 | 8 | 9 | class CPU: 10 | def __init__(self, instructions, reg_c=0): 11 | self.registers = {"a": 0, "b": 0, "c": reg_c, "d": 0} 12 | self.pos = 0 13 | self.instructions = instructions 14 | 15 | def cpy(self, x, y): 16 | if x in self.registers: 17 | self.registers[y] = self.registers[x] 18 | else: 19 | self.registers[y] = int(x) 20 | self.pos += 1 21 | 22 | def inc(self, x): 23 | self.registers[x] += 1 24 | self.pos += 1 25 | 26 | def dec(self, x): 27 | self.registers[x] -= 1 28 | self.pos += 1 29 | 30 | def jnz(self, x, y): 31 | if self.registers.get(x, 1) != 0: 32 | self.pos += int(y) 33 | else: 34 | self.pos += 1 35 | 36 | def step(self): 37 | op, *args = self.instructions[self.pos] 38 | getattr(self, op)(*args) 39 | 40 | def run(self): 41 | while 0 <= self.pos < len(self.instructions): 42 | self.step() 43 | return self.registers["a"] 44 | 45 | 46 | def part_one(): 47 | cpu = CPU(INSTRUCTIONS) 48 | return cpu.run() 49 | 50 | 51 | def part_two(): 52 | cpu = CPU(INSTRUCTIONS, 1) 53 | return cpu.run() 54 | 55 | 56 | print(f"Part 1: {part_one()}") # 317993 57 | print(f"Part 2: {part_two()}") # 9227647 58 | -------------------------------------------------------------------------------- /2016/13.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | 3 | INPUT = 1362 4 | 5 | 6 | def is_open(x, y): 7 | n = x * x + 3 * x + 2 * x * y + y + y * y + INPUT 8 | return not n.bit_count() & 1 9 | 10 | 11 | def part_one(): 12 | q = deque([(1, 1)]) 13 | seen = set(((1, 1))) 14 | res = 0 15 | while q: 16 | for _ in range(len(q)): 17 | i, j = q.popleft() 18 | if (i, j) == (31, 39): 19 | return res 20 | for ni, nj in ((i + 1, j), (i - 1, j), (i, j + 1), (i, j - 1)): 21 | if ( 22 | ni >= 0 23 | and nj >= 0 24 | and is_open(ni, nj) 25 | and (ni, nj) not in seen 26 | ): 27 | q.append((ni, nj)) 28 | seen.add((ni, nj)) 29 | res += 1 30 | 31 | 32 | def part_two(): 33 | q = deque([(1, 1)]) 34 | seen = set() 35 | for _ in range(51): 36 | for _ in range(len(q)): 37 | i, j = q.popleft() 38 | seen.add((i, j)) 39 | for ni, nj in ((i + 1, j), (i - 1, j), (i, j + 1), (i, j - 1)): 40 | if ( 41 | ni >= 0 42 | and nj >= 0 43 | and is_open(ni, nj) 44 | and (ni, nj) not in seen 45 | ): 46 | q.append((ni, nj)) 47 | return len(seen) 48 | 49 | 50 | print(f"Part 1: {part_one()}") # 82 51 | print(f"Part 2: {part_two()}") # 138 52 | -------------------------------------------------------------------------------- /2016/14.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from functools import cache 3 | 4 | SALT = "zpqevtbw" 5 | 6 | 7 | def md5(s): 8 | return hashlib.md5(s.encode()).hexdigest() 9 | 10 | 11 | @cache 12 | def get_hash(salt, i): 13 | return md5(salt + str(i)) 14 | 15 | 16 | @cache 17 | def get_stretch_hash(salt, i): 18 | h = get_hash(salt, i) 19 | for _ in range(2016): 20 | h = md5(h) 21 | return h 22 | 23 | 24 | @cache 25 | def get_triple(h): 26 | for i in range(len(h) - 2): 27 | if h[i] == h[i + 1] == h[i + 2]: 28 | return h[i] 29 | return None 30 | 31 | 32 | @cache 33 | def get_quintuple(h, c): 34 | return c * 5 in h 35 | 36 | 37 | def get_keys(salt, get_hash): 38 | keys = [] 39 | i = 0 40 | while len(keys) < 64: 41 | h = get_hash(salt, i) 42 | c = get_triple(h) 43 | if c: 44 | for j in range(i + 1, i + 1001): 45 | if get_quintuple(get_hash(salt, j), c): 46 | keys.append(i) 47 | break 48 | i += 1 49 | return keys 50 | 51 | 52 | print(f"Part 1: {get_keys(SALT, get_hash)[-1]}") # 16106 53 | print(f"Part 2: {get_keys(SALT, get_stretch_hash)[-1]}") # 22423 54 | -------------------------------------------------------------------------------- /2016/15.py: -------------------------------------------------------------------------------- 1 | import re 2 | from itertools import count 3 | 4 | with open("15.txt", "r") as file: 5 | data = file.read().strip() 6 | 7 | DISCS = [ 8 | (int(num_pos), int(pos) + i) 9 | for i, (num_pos, pos) in enumerate( 10 | re.findall(r"(\d+) positions; .+ (\d+)", data), start=1 11 | ) 12 | ] 13 | 14 | 15 | def solve(discs): 16 | for t in count(): 17 | if all((pos + t) % num_pos == 0 for num_pos, pos in discs): 18 | return t 19 | 20 | 21 | print(f"Part 1: {solve(DISCS)}") # 203660 22 | print(f"Part 2: {solve(DISCS + [(11, 7)])}") # 2408135 23 | -------------------------------------------------------------------------------- /2016/16.py: -------------------------------------------------------------------------------- 1 | STATE = list(map(int, "11110010111001001")) 2 | 3 | 4 | def dragon_curve(a, disk): 5 | while len(a) < disk: 6 | b = [1 - x for x in reversed(a)] 7 | a = a + [0] + b 8 | return a[:disk] 9 | 10 | 11 | def checksum(a): 12 | while not len(a) & 1: 13 | a = [int(a[i] == a[i + 1]) for i in range(0, len(a), 2)] 14 | return "".join(map(str, a)) 15 | 16 | 17 | print(f"Part 1: {checksum(dragon_curve(STATE, 272))}") # 01110011101111011 18 | print(f"Part 2: {checksum(dragon_curve(STATE, 35651584))}") # 11001111011000111 19 | -------------------------------------------------------------------------------- /2016/17.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from collections import deque 3 | 4 | PASSCODE = "ioramepc" 5 | 6 | 7 | def md5(s): 8 | return hashlib.md5(s.encode()).hexdigest()[:4] 9 | 10 | 11 | def solve(pt2=False): 12 | q = deque([(0, 0, "")]) 13 | res = None 14 | while q: 15 | i, j, path = q.popleft() 16 | if i == j == 3: 17 | if not pt2: 18 | return path 19 | res = path 20 | continue 21 | u, d, l, r = md5(PASSCODE + path) 22 | if i > 0 and u in "bcdef": 23 | q.append((i - 1, j, path + "U")) 24 | if i < 3 and d in "bcdef": 25 | q.append((i + 1, j, path + "D")) 26 | if j > 0 and l in "bcdef": 27 | q.append((i, j - 1, path + "L")) 28 | if j < 3 and r in "bcdef": 29 | q.append((i, j + 1, path + "R")) 30 | return len(res) 31 | 32 | 33 | print(f"Part 1: {solve()}") # RDDRULDDRR 34 | print(f"Part 2: {solve(True)}") # 766 35 | -------------------------------------------------------------------------------- /2016/18.py: -------------------------------------------------------------------------------- 1 | with open("18.txt", "r") as file: 2 | data = file.read().strip() 3 | 4 | 5 | def calc_row(row): 6 | res = [] 7 | for i in range(len(row)): 8 | l = 1 if i - 1 == -1 else row[i - 1] 9 | c = row[i] 10 | r = 1 if i + 1 == len(row) else row[i + 1] 11 | res.append( 12 | int( 13 | (l, c, r) 14 | not in {(0, 0, 1), (1, 0, 0), (0, 1, 1), (1, 1, 0)} 15 | ) 16 | ) 17 | return res 18 | 19 | 20 | def solve(data, num_rows): 21 | row = list(map(lambda x: int(x == "."), data)) 22 | res = sum(row) 23 | for _ in range(num_rows - 1): 24 | row = calc_row(row) 25 | res += sum(row) 26 | return res 27 | 28 | 29 | print(f"Part 1: {solve(data, 40)}") # 2005 30 | print(f"Part 2: {solve(data, 400000)}") # 20008491 31 | -------------------------------------------------------------------------------- /2016/19.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | 3 | ELVES = 3012210 4 | 5 | 6 | def part_one(n): 7 | # https://en.wikipedia.org/wiki/Josephus_problem 8 | return ((1 << (n.bit_length() - 1)) - 1 & n << 1) + 1 9 | 10 | 11 | def part_two(n): 12 | left = deque(range(1, n >> 1)) 13 | right = deque(range(n >> 1, n + 1)) 14 | while left and right: 15 | if len(left) > len(right): 16 | left.pop() 17 | else: 18 | right.popleft() 19 | right.append(left.popleft()) 20 | left.append(right.popleft()) 21 | return left[0] or right[0] 22 | 23 | 24 | print(f"Part 1: {part_one(ELVES)}") # 1830117 25 | print(f"Part 2: {part_two(ELVES)}") # 1417887 26 | -------------------------------------------------------------------------------- /2016/20.py: -------------------------------------------------------------------------------- 1 | with open("20.txt", "r") as file: 2 | data = file.read().strip() 3 | 4 | IP_RANGES = sorted( 5 | tuple(map(int, line.split("-"))) 6 | for line in data.split("\n") 7 | ) 8 | 9 | 10 | def part_one(ips): 11 | res = 0 12 | for lo, hi in ips: 13 | if lo > res: 14 | return res 15 | res = max(res, hi + 1) 16 | return -1 17 | 18 | 19 | def part_two(ips): 20 | res = cur = 0 21 | for lo, hi in ips: 22 | if lo > cur: 23 | res += lo - cur 24 | cur = max(cur, hi + 1) 25 | return res + (1 << 32) - cur 26 | 27 | 28 | print(f"Part 1: {part_one(IP_RANGES)}") # 32259706 29 | print(f"Part 2: {part_two(IP_RANGES)}") # 113 30 | -------------------------------------------------------------------------------- /2016/24.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict, deque 2 | from itertools import permutations 3 | 4 | with open("24.txt", "r") as file: 5 | data = file.read().strip() 6 | 7 | 8 | def get_points(data): 9 | return [ 10 | (int(c), i, j) 11 | for i, row in enumerate(data.split("\n")) 12 | for j, c in enumerate(row) 13 | if c.isdigit() 14 | ] 15 | 16 | 17 | def bfs(grid, start, end): 18 | n, m = len(grid), len(grid[0]) 19 | q = deque([(start, 0)]) 20 | seen = set() 21 | while q: 22 | (i, j), dist = q.popleft() 23 | if (i, j) == end: 24 | return dist 25 | for di, dj in ((1, 0), (-1, 0), (0, 1), (0, -1)): 26 | ni, nj = i + di, j + dj 27 | if ( 28 | 0 > ni 29 | or ni >= n 30 | or 0 > nj 31 | or nj >= m 32 | or (ni, nj) in seen 33 | or grid[ni][nj] == "#" 34 | ): 35 | continue 36 | seen.add((ni, nj)) 37 | q.append(((ni, nj), dist + 1)) 38 | return -1 39 | 40 | 41 | def build_graph(grid, points): 42 | graph = defaultdict(dict) 43 | for n, i, j in points: 44 | for m, k, l in points: 45 | if n == m: 46 | continue 47 | dist = bfs(grid, (i, j), (k, l)) 48 | graph[n][m] = dist 49 | graph[m][n] = dist 50 | return graph 51 | 52 | 53 | def solve(data, pt2=False): 54 | points = get_points(data) 55 | grid = [list(line) for line in data.split("\n")] 56 | graph = build_graph(grid, points) 57 | res = float("inf") 58 | for p in permutations(range(1, 8)): 59 | node = cur = 0 60 | for nei in p: 61 | cur += graph[node][nei] 62 | node = nei 63 | cur += graph[node][0] * pt2 64 | res = min(res, cur) 65 | return res 66 | 67 | 68 | print(f"Part 1: {solve(data)}") # 498 69 | print(f"Part 2: {solve(data, True)}") # 804 70 | -------------------------------------------------------------------------------- /2016/25.py: -------------------------------------------------------------------------------- 1 | import re 2 | from itertools import count 3 | 4 | with open("25.txt", "r") as file: 5 | data = file.read().strip() 6 | 7 | 8 | class CPU: 9 | def __init__(self, instructions, reg_a=0): 10 | self.registers = {"a": reg_a, "b": 0, "c": 0, "d": 0} 11 | self.pos = 0 12 | self.instructions = instructions 13 | self.transmission = [] 14 | 15 | def cpy(self, x, y): 16 | if x in self.registers: 17 | self.registers[y] = self.registers[x] 18 | else: 19 | self.registers[y] = int(x) 20 | self.pos += 1 21 | 22 | def inc(self, x): 23 | self.registers[x] += 1 24 | self.pos += 1 25 | 26 | def dec(self, x): 27 | self.registers[x] -= 1 28 | self.pos += 1 29 | 30 | def jnz(self, x, y): 31 | if x in self.registers: 32 | if self.registers[x] != 0: 33 | self.pos += ( 34 | int(y) 35 | if y.lstrip("-").isdigit() 36 | else self.registers.get(y, 1) 37 | ) 38 | else: 39 | self.pos += 1 40 | elif int(x) != 0: 41 | self.pos += ( 42 | int(y) if y.lstrip("-").isdigit() else self.registers.get(y, 1) 43 | ) 44 | else: 45 | self.pos += 1 46 | 47 | def out(self, x): 48 | self.transmission.append(self.registers[x]) 49 | self.pos += 1 50 | 51 | def step(self): 52 | op, *args = self.instructions[self.pos] 53 | getattr(self, op)(*args) 54 | 55 | def run(self): 56 | while len(self.transmission) < 20: 57 | self.step() 58 | return self.transmission == [0, 1] * 10 59 | 60 | 61 | def part_one(): 62 | for i in count(): 63 | insts = [re.split(r"[ ,]+", line.strip()) for line in data.split("\n")] 64 | cpu = CPU(insts, reg_a=i) 65 | if cpu.run(): 66 | return i 67 | 68 | 69 | print(f"Part 1: {part_one()}") # 180 70 | -------------------------------------------------------------------------------- /2017/01.py: -------------------------------------------------------------------------------- 1 | with open("01.txt", "r") as file: 2 | data = file.read().strip() 3 | 4 | 5 | def part_one(): 6 | return sum( 7 | int(x) 8 | for x, y in zip(data, data[1:] + data[:1]) 9 | if x == y 10 | ) 11 | 12 | 13 | def part_two(): 14 | return sum( 15 | int(x) 16 | for x, y in zip(data, data[len(data) >> 1 :] + data[: len(data) >> 1]) 17 | if x == y 18 | ) 19 | 20 | 21 | print(f"Part 1: {part_one()}") # 1393 22 | print(f"Part 2: {part_two()}") # 1292 23 | -------------------------------------------------------------------------------- /2017/02.py: -------------------------------------------------------------------------------- 1 | from itertools import permutations 2 | 3 | with open("02.txt", "r") as file: 4 | data = file.read().strip() 5 | 6 | SPREADSHEET = [ 7 | list(map(int, line.split())) for line in data.splitlines() 8 | ] 9 | 10 | 11 | def part_one(): 12 | return sum(max(line) - min(line) for line in SPREADSHEET) 13 | 14 | 15 | def part_two(): 16 | return sum( 17 | a // b 18 | for row in SPREADSHEET 19 | for a, b in permutations(row, 2) 20 | if a % b == 0 21 | ) 22 | 23 | 24 | print(f"Part 1: {part_one()}") # 47623 25 | print(f"Part 2: {part_two()}") # 312 26 | -------------------------------------------------------------------------------- /2017/03.py: -------------------------------------------------------------------------------- 1 | from itertools import product 2 | 3 | SQUARE = 325489 4 | 5 | 6 | def part_one(): 7 | layer = 1 8 | while layer * layer < SQUARE: 9 | layer += 2 10 | distance = abs(SQUARE - (layer * layer - ((layer - 1) >> 1))) 11 | return distance + ((layer - 1) >> 1) 12 | 13 | 14 | def spiral_sum(): 15 | grid = {} 16 | grid[0, 0] = 1 17 | x, y = 0, 0 18 | dx, dy = 0, -1 19 | while True: 20 | if (x == y) or (x < 0 and x == -y) or (x > 0 and x == 1 - y): 21 | dx, dy = -dy, dx 22 | x, y = x + dx, y + dy 23 | grid[x, y] = sum( 24 | grid.get((x + dx, y + dy), 0) 25 | for dx, dy in filter(any, product([-1, 0, 1], repeat=2)) 26 | ) 27 | yield grid[x, y] 28 | 29 | 30 | def part_two(): 31 | return next(x for x in spiral_sum() if x > SQUARE) 32 | 33 | 34 | print(f"Part 1: {part_one()}") # 552 35 | print(f"Part 2: {part_two()}") # 330785 36 | -------------------------------------------------------------------------------- /2017/04.py: -------------------------------------------------------------------------------- 1 | with open("04.txt", "r") as file: 2 | data = file.read().strip() 3 | 4 | 5 | def is_valid(passphrase): 6 | words = passphrase.split() 7 | return len(words) == len(set(words)) 8 | 9 | 10 | def is_valid_anagram(passphrase): 11 | words = passphrase.split() 12 | return len(words) == len(set(map(lambda w: "".join(sorted(w)), words))) 13 | 14 | 15 | def part_one(data): 16 | return sum(is_valid(passphrase) for passphrase in data.split("\n")) 17 | 18 | 19 | def part_two(data): 20 | return sum(is_valid_anagram(passphrase) for passphrase in data.split("\n")) 21 | 22 | 23 | print(f"Part 1: {part_one(data)}") # 325 24 | print(f"Part 2: {part_two(data)}") # 119 25 | -------------------------------------------------------------------------------- /2017/05.py: -------------------------------------------------------------------------------- 1 | with open("05.txt", "r") as file: 2 | data = file.read().strip() 3 | 4 | 5 | def calc_jumps(instructions, part_two=False): 6 | i = res = 0 7 | while 0 <= i < len(instructions): 8 | ni = i + instructions[i] 9 | instructions[i] += 1 - 2 * (part_two and instructions[i] >= 3) 10 | i = ni 11 | res += 1 12 | return res 13 | 14 | 15 | def part_one(): 16 | return calc_jumps(list(map(int, data.split("\n")))) 17 | 18 | 19 | def part_two(): 20 | return calc_jumps(list(map(int, data.split("\n"))), part_two=True) 21 | 22 | 23 | print(f"Part 1: {part_one()}") # 315613 24 | print(f"Part 2: {part_two()}") # 22570529 25 | -------------------------------------------------------------------------------- /2017/06.py: -------------------------------------------------------------------------------- 1 | from itertools import count 2 | 3 | with open("06.txt", "r") as file: 4 | data = file.read().strip() 5 | 6 | BANKS = [int(x) for x in data.split()] 7 | 8 | 9 | def reallocate(banks): 10 | banks = banks[:] 11 | i = banks.index(max(banks)) 12 | blocks = banks[i] 13 | banks[i] = 0 14 | while blocks: 15 | i = (i + 1) % len(banks) 16 | banks[i] += 1 17 | blocks -= 1 18 | return banks 19 | 20 | 21 | def run_cycle(banks): 22 | seen = {} 23 | for i in count(): 24 | if tuple(banks) not in seen: 25 | seen[tuple(banks)] = i 26 | banks = reallocate(banks) 27 | else: 28 | return len(seen), i - seen[tuple(banks)] 29 | 30 | 31 | part_one, part_two = run_cycle(BANKS) 32 | 33 | print(f"Part 1: {part_one}") # 11137 34 | print(f"Part 2: {part_two}") # 1037 35 | -------------------------------------------------------------------------------- /2017/07.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import Counter 3 | from copy import deepcopy 4 | 5 | with open("07.txt", "r") as file: 6 | data = file.read().strip() 7 | 8 | 9 | def parse_input(data): 10 | res = {} 11 | for k, w, v in re.findall(r"(\w+) \((\d+)\)(?: -> (.+))?", data): 12 | if v: 13 | v = v.split(", ") 14 | res[k] = [int(w), v] 15 | else: 16 | res[k] = (int(w), []) 17 | return res 18 | 19 | 20 | GRAPH = parse_input(data) 21 | 22 | 23 | def calc_weight(g, node): 24 | w, neis = g[node] 25 | if not neis: 26 | return w 27 | new_w = w + sum(calc_weight(g, nei) for nei in neis) 28 | g[node][0] = new_w 29 | return new_w 30 | 31 | 32 | def part_one(g): 33 | indegrees = Counter() 34 | for k, (w, neis) in g.items(): 35 | for nei in neis: 36 | indegrees[nei] += 1 37 | return list(set(g.keys()) - set(indegrees.keys()))[0] 38 | 39 | 40 | def part_two(g): 41 | cur = part_one(g) 42 | g2 = deepcopy(g) 43 | calc_weight(g2, cur) 44 | diff = 0 45 | neis = g2[cur][1] 46 | while neis: 47 | nneis = None 48 | c = Counter(g2[nei][0] for nei in neis) 49 | if len(c) == 1: 50 | return g[cur][0] - diff 51 | common = c.most_common()[0][0] 52 | for nei in neis: 53 | if g2[nei][0] != common: 54 | diff = g2[nei][0] - common 55 | cur = nei 56 | nneis = g2[nei][1] 57 | neis = nneis 58 | 59 | 60 | print(f"Part 1: {part_one(GRAPH)}") # dgoocsw 61 | print(f"Part 2: {part_two(GRAPH)}") # 1275 62 | -------------------------------------------------------------------------------- /2017/08.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import defaultdict 3 | from operator import eq, ge, gt, le, lt, ne 4 | 5 | 6 | class CPU: 7 | def __init__(self, instructions): 8 | self.registers = defaultdict(int) 9 | self.instructions = instructions 10 | self.ops = { 11 | "<": lt, 12 | "<=": le, 13 | "==": eq, 14 | "!=": ne, 15 | ">=": ge, 16 | ">": gt, 17 | } 18 | self.max_mem = 0 19 | 20 | def inc(self, x, y): 21 | self.registers[x] += y 22 | 23 | def dec(self, x, y): 24 | self.registers[x] -= y 25 | 26 | def eval_condition(self, x, op, y): 27 | return self.ops[op](self.registers[x], y) 28 | 29 | def step(self, *args): 30 | op, reg_a, x, reg_b, cond, y = args 31 | if self.eval_condition(reg_b, cond, y): 32 | getattr(self, op)(reg_a, x) 33 | self.max_mem = max(self.max_mem, self.registers[reg_a]) 34 | 35 | def run(self, part_two=False): 36 | for line in self.instructions: 37 | self.step(*line) 38 | return self.max_mem if part_two else max(self.registers.values()) 39 | 40 | 41 | with open("08.txt", "r") as file: 42 | data = file.read().strip() 43 | 44 | INSTRUCTIONS = [ 45 | (op, reg_a, int(x), reg_b, cond, int(y)) 46 | for reg_a, op, x, reg_b, cond, y in re.findall( 47 | r"(\w+) (inc|dec) (-?\d+) if (\w+) ([<>=!]+) (-?\d+)", data 48 | ) 49 | ] 50 | 51 | print(f"Part 1: {CPU(INSTRUCTIONS).run()}") # 5752 52 | print(f"Part 2: {CPU(INSTRUCTIONS).run(part_two=True)}") # 6366 53 | -------------------------------------------------------------------------------- /2017/09.py: -------------------------------------------------------------------------------- 1 | with open("09.txt", "r") as file: 2 | data = file.read().strip() 3 | 4 | 5 | def clean_garbage(s): 6 | garbage = False 7 | cleaned = "" 8 | removed = 0 9 | i = 0 10 | while i < len(s): 11 | if s[i] == "!": 12 | i += 1 13 | elif s[i] == "<" and not garbage: 14 | garbage = True 15 | elif s[i] == ">" and garbage: 16 | garbage = False 17 | elif garbage: 18 | removed += 1 19 | else: 20 | cleaned += s[i] 21 | i += 1 22 | return cleaned, removed 23 | 24 | 25 | def score(s): 26 | score = depth = 0 27 | for c in s: 28 | score += depth * (c == "}") 29 | depth += 1 if c == "{" else -1 if c == "}" else 0 30 | return score 31 | 32 | 33 | CLEANED, REMOVED = clean_garbage(data) 34 | 35 | print(f"Part 1: {score(CLEANED)}") # 12396 36 | print(f"Part 2: {REMOVED}") # 6346 37 | -------------------------------------------------------------------------------- /2017/10.py: -------------------------------------------------------------------------------- 1 | from functools import reduce 2 | 3 | 4 | class Knot: 5 | def __init__(self, size): 6 | self.size = size 7 | self.state = list(range(size)) 8 | self.pos = 0 9 | self.skip = 0 10 | 11 | def twist(self, length): 12 | for i in range(length >> 1): 13 | a = (self.pos + i) % self.size 14 | b = (self.pos + length - 1 - i) % self.size 15 | self.state[a], self.state[b] = self.state[b], self.state[a] 16 | self.pos = (self.pos + length + self.skip) % self.size 17 | self.skip += 1 18 | 19 | def twist_sequence(self, lengths): 20 | for length in lengths: 21 | self.twist(length) 22 | 23 | def dense_hash(self): 24 | return [ 25 | reduce(lambda a, b: a ^ b, self.state[i : i + 16]) 26 | for i in range(0, self.size, 16) 27 | ] 28 | 29 | def knot_hash(self): 30 | return "".join(f"{x:02x}" for x in self.dense_hash()) 31 | 32 | 33 | with open("10.txt", "r") as file: 34 | data = file.read().strip() 35 | 36 | 37 | def part_one(data): 38 | lengths = list(map(int, data.split(","))) 39 | knot = Knot(256) 40 | knot.twist_sequence(lengths) 41 | return knot.state[0] * knot.state[1] 42 | 43 | 44 | def part_two(data): 45 | lengths = [ord(c) for c in data] + [17, 31, 73, 47, 23] 46 | knot = Knot(256) 47 | for _ in range(64): 48 | knot.twist_sequence(lengths) 49 | return knot.knot_hash() 50 | 51 | 52 | print(f"Part 1: {part_one(data)}") # 6909 53 | print(f"Part 2: {part_two(data)}") # 9d5f4561367d379cfbf04f8c471c0095 54 | -------------------------------------------------------------------------------- /2017/11.py: -------------------------------------------------------------------------------- 1 | with open("11.txt", "r") as file: 2 | data = file.read().strip() 3 | 4 | DIRS = { 5 | "nw": -1, 6 | "ne": 1 - 1j, 7 | "sw": -1 + 1j, 8 | "se": 1, 9 | "n": -1j, 10 | "s": 1j, 11 | } 12 | 13 | 14 | def calc_distance(pos): 15 | q, r = int(pos.real), int(pos.imag) 16 | return (abs(q) + abs(r) + abs(q + r)) // 2 17 | 18 | 19 | def take_steps(data): 20 | pos = 0j 21 | res = 0 22 | for step in data.split(","): 23 | pos += DIRS[step] 24 | res = max(res, calc_distance(pos)) 25 | return calc_distance(pos), res 26 | 27 | 28 | part_one, part_two = take_steps(data) 29 | 30 | print(f"Part 1: {part_one}") # 685 31 | print(f"Part 2: {part_two}") # 1457 32 | -------------------------------------------------------------------------------- /2017/12.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict, deque 2 | 3 | with open("12.txt", "r") as file: 4 | data = file.read().strip() 5 | 6 | GRAPH = defaultdict(list) 7 | for line in data.split("\n"): 8 | u, neis = line.split(" <-> ") 9 | GRAPH[u].extend(v for v in neis.split(", ")) 10 | 11 | 12 | def bfs(g, start): 13 | seen = set() 14 | q = deque([start]) 15 | while q: 16 | u = q.popleft() 17 | if u in seen: 18 | continue 19 | seen.add(u) 20 | q.extend(g[u]) 21 | return seen 22 | 23 | 24 | def part_two(g): 25 | seen = set() 26 | res = 0 27 | for u in g: 28 | if u in seen: 29 | continue 30 | seen |= bfs(g, u) 31 | res += 1 32 | return res 33 | 34 | 35 | print(f"Part 1: {len(bfs(GRAPH, '0'))}") # 152 36 | print(f"Part 2: {part_two(GRAPH)}") # 186 37 | -------------------------------------------------------------------------------- /2017/13.py: -------------------------------------------------------------------------------- 1 | import re 2 | from itertools import count 3 | 4 | with open("13.txt", "r") as file: 5 | data = file.read().strip() 6 | 7 | LAYERS = { 8 | int(k): int(v) 9 | for line in data.splitlines() 10 | for k, v in [line.split(": ")] 11 | } 12 | 13 | 14 | def part_one(): 15 | res = 0 16 | for i in range(max(LAYERS) + 1): 17 | if i in LAYERS: 18 | if i % (LAYERS[i] * 2 - 2) == 0: 19 | res += i * LAYERS[i] 20 | return res 21 | 22 | 23 | def part_two(): 24 | for t in count(): 25 | if all((i + t) % (LAYERS[i] * 2 - 2) for i in LAYERS): 26 | return t 27 | 28 | 29 | print(f"Part 1: {part_one()}") # 2604 30 | print(f"Part 2: {part_two()}") # 3941460 31 | -------------------------------------------------------------------------------- /2017/15.py: -------------------------------------------------------------------------------- 1 | A, B = 679, 771 2 | MOD = (1 << 31) - 1 3 | 4 | 5 | def part_one(a, b): 6 | res = 0 7 | for _ in range(40000000): 8 | a = (a * 16807) % MOD 9 | b = (b * 48271) % MOD 10 | if a & 0xFFFF == b & 0xFFFF: 11 | res += 1 12 | return res 13 | 14 | 15 | def part_two(a, b): 16 | def a_gen(x): 17 | while True: 18 | x = (x * 16807) % MOD 19 | if x % 4 == 0: 20 | yield x 21 | 22 | def b_gen(x): 23 | while True: 24 | x = (x * 48271) % MOD 25 | if x % 8 == 0: 26 | yield x 27 | 28 | na, nb = a_gen(a), b_gen(b) 29 | return sum( 30 | next(na) & 0xFFFF == next(nb) & 0xFFFF 31 | for _ in range(5000000) 32 | ) 33 | 34 | 35 | print(f"Part 1: {part_one(A, B)}") # 626 36 | print(f"Part 2: {part_two(A, B)}") # 306 37 | -------------------------------------------------------------------------------- /2017/16.py: -------------------------------------------------------------------------------- 1 | class Dance: 2 | def __init__(self, programs): 3 | self.programs = programs 4 | 5 | def spin(self, n): 6 | self.programs = self.programs[-n:] + self.programs[:-n] 7 | 8 | def exchange(self, i, j): 9 | self.programs[i], self.programs[j] = self.programs[j], self.programs[i] 10 | 11 | def partner(self, a, b): 12 | i, j = self.programs.index(a), self.programs.index(b) 13 | self.exchange(i, j) 14 | 15 | def dance(self, moves): 16 | for move in moves: 17 | if move[0] == "s": 18 | self.spin(int(move[1:])) 19 | elif move[0] == "x": 20 | i, j = map(int, move[1:].split("/")) 21 | self.exchange(i, j) 22 | elif move[0] == "p": 23 | a, b = move[1:].split("/") 24 | self.partner(a, b) 25 | 26 | 27 | with open("16.txt", "r") as file: 28 | data = file.read().strip() 29 | 30 | 31 | def part_one(data): 32 | dance = Dance(list("abcdefghijklmnop")) 33 | dance.dance(data.split(",")) 34 | return "".join(dance.programs) 35 | 36 | 37 | def part_two(data): 38 | cur = "abcdefghijklmnop" 39 | dance = Dance(list(cur)) 40 | seen = set() 41 | while cur not in seen: 42 | seen.add(cur) 43 | dance.dance(data.split(",")) 44 | cur = "".join(dance.programs) 45 | dance = Dance(list("abcdefghijklmnop")) 46 | for _ in range(1000000000 % len(seen)): 47 | dance.dance(data.split(",")) 48 | return "".join(dance.programs) 49 | 50 | 51 | print(f"Part 1: {part_one(data)}") # olgejankfhbmpidc 52 | print(f"Part 2: {part_two(data)}") # gfabehpdojkcimnl 53 | -------------------------------------------------------------------------------- /2017/17.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | 3 | INPUT = 349 4 | 5 | 6 | def spinlock(n, steps): 7 | buffer = deque([0]) 8 | for i in range(1, steps + 1): 9 | buffer.rotate(-n) 10 | buffer.append(i) 11 | return buffer 12 | 13 | 14 | def part_one(): 15 | buffer = spinlock(INPUT, 2017) 16 | return buffer[0] 17 | 18 | 19 | def part_two(): 20 | buffer = spinlock(INPUT, 50000000) 21 | return buffer[buffer.index(0) + 1] 22 | 23 | 24 | print(f"Part 1: {part_one()}") # 640 25 | print(f"Part 2: {part_two()}") # 47949463 26 | -------------------------------------------------------------------------------- /2017/19.py: -------------------------------------------------------------------------------- 1 | with open("19.txt", "r") as file: 2 | data = file.read() 3 | 4 | MAZE = [list(line) for line in data.split("\n")] 5 | 6 | 7 | def get_char(pos): 8 | return MAZE[int(pos.imag)][int(pos.real)] 9 | 10 | 11 | def change_direction(pos, dir): 12 | cand = dir * 1j 13 | if get_char(pos + cand) != " ": 14 | return cand 15 | return dir * -1j 16 | 17 | 18 | def solve(): 19 | pos = MAZE[0].index("|") + 0j 20 | dir = 1j 21 | res = [] 22 | steps = 0 23 | c = get_char(pos) 24 | while c != " ": 25 | steps += 1 26 | if c.isalpha(): 27 | res.append(c) 28 | elif c == "+": 29 | dir = change_direction(pos, dir) 30 | pos += dir 31 | c = get_char(pos) 32 | return "".join(res), steps 33 | 34 | 35 | part_one, part_two = solve() 36 | 37 | print(f"Part 1: {part_one}") # RUEDAHWKSM 38 | print(f"Part 2: {part_two}") # 17264 39 | -------------------------------------------------------------------------------- /2017/20.py: -------------------------------------------------------------------------------- 1 | import math 2 | import re 3 | from collections import defaultdict 4 | from itertools import batched 5 | 6 | 7 | def parse_input(): 8 | with open("20.txt", "r") as file: 9 | data = file.read() 10 | return batched(batched(map(int, re.findall(r"(-?\d+)", data)), 3), 3) 11 | 12 | 13 | def calc_pos(p, v, a, t): 14 | return tuple(np + nv * t + (na * t * (t + 1) >> 1) for np, nv, na in zip(p, v, a)) 15 | 16 | 17 | def part_one(): 18 | particles = parse_input() 19 | res = 0 20 | low = math.inf 21 | for i, particle in enumerate(particles): 22 | cur = calc_pos(*particle, 1000) 23 | if (nlow := sum(abs(x) for x in cur)) < low: 24 | res = i 25 | low = nlow 26 | return res 27 | 28 | 29 | def part_two(): 30 | particles = parse_input() 31 | for t in range(50): 32 | cur = defaultdict(list) 33 | for particle in particles: 34 | npos = tuple(calc_pos(*particle, t)) 35 | cur[npos].append(particle) 36 | particles = [] 37 | for a in cur.values(): 38 | if len(a) == 1: 39 | particles.extend(a) 40 | return len(particles) 41 | 42 | 43 | print(f"Part 1: {part_one()}") 44 | print(f"Part 2: {part_two()}") 45 | -------------------------------------------------------------------------------- /2017/21.py: -------------------------------------------------------------------------------- 1 | from functools import cache 2 | 3 | 4 | def parse_input(): 5 | with open("21.txt", "r") as file: 6 | data = file.read() 7 | return { 8 | tuple(x.split("/")): tuple(y.split("/")) 9 | for line in data.splitlines() 10 | for x, y in [line.split(" => ")] 11 | } 12 | 13 | 14 | RULES = parse_input() 15 | 16 | 17 | def rotate(pattern): 18 | return tuple("".join(row) for row in zip(*pattern[::-1])) 19 | 20 | 21 | @cache 22 | def get_rule(pattern): 23 | for _ in range(4): 24 | if pattern in RULES: 25 | return RULES[pattern] 26 | pattern = rotate(pattern) 27 | pattern = tuple(reversed(pattern)) 28 | for _ in range(4): 29 | if pattern in RULES: 30 | return RULES[pattern] 31 | pattern = rotate(pattern) 32 | 33 | 34 | def step(pattern): 35 | n = len(pattern) 36 | k = 2 + (n & 1) 37 | row_sz = n // k 38 | squares = [] 39 | for i in range(row_sz): 40 | cur = [] 41 | for j in range(row_sz): 42 | square = tuple( 43 | "".join(pattern[i * k + di][j * k + dj] for dj in range(k)) 44 | for di in range(k) 45 | ) 46 | cur.append(get_rule(square)) 47 | squares.append(cur) 48 | return tuple("".join(line) for square in squares for line in zip(*square)) 49 | 50 | 51 | def part_one(): 52 | pattern = (".#.", "..#", "###") 53 | for _ in range(5): 54 | pattern = step(pattern) 55 | return sum(row.count("#") for row in pattern) 56 | 57 | 58 | def part_two(): 59 | pattern = (".#.", "..#", "###") 60 | for _ in range(18): 61 | pattern = step(pattern) 62 | return sum(row.count("#") for row in pattern) 63 | 64 | 65 | print(f"Part 1: {part_one()}") 66 | print(f"Part 2: {part_two()}") 67 | -------------------------------------------------------------------------------- /2017/22.py: -------------------------------------------------------------------------------- 1 | def parse_input(): 2 | with open("22.txt", "r") as file: 3 | data = file.read() 4 | grid = data.splitlines() 5 | return ( 6 | {(i, j) for i, row in enumerate(grid) for j, v in enumerate(row) if v == "#"}, 7 | len(grid) >> 1, 8 | len(grid[0]) >> 1, 9 | ) 10 | 11 | 12 | def part_one(): 13 | infected, i, j = parse_input() 14 | di, dj = -1, 0 15 | res = 0 16 | for _ in range(10000): 17 | if (i, j) in infected: 18 | infected.remove((i, j)) 19 | di, dj = dj, -di 20 | else: 21 | res += 1 22 | infected.add((i, j)) 23 | di, dj = -dj, di 24 | i += di 25 | j += dj 26 | return res 27 | 28 | 29 | def part_two(): 30 | infected, i, j = parse_input() 31 | weakened = set() 32 | flagged = set() 33 | di, dj = -1, 0 34 | res = 0 35 | for _ in range(10000000): 36 | if (i, j) in infected: 37 | di, dj = dj, -di 38 | flagged.add((i, j)) 39 | infected.remove((i, j)) 40 | elif (i, j) in flagged: 41 | di, dj = -di, -dj 42 | flagged.remove((i, j)) 43 | elif (i, j) in weakened: 44 | res += 1 45 | weakened.remove((i, j)) 46 | infected.add((i, j)) 47 | else: 48 | di, dj = -dj, di 49 | weakened.add((i, j)) 50 | i += di 51 | j += dj 52 | return res 53 | 54 | 55 | print(f"Part 1: {part_one()}") 56 | print(f"Part 2: {part_two()}") 57 | -------------------------------------------------------------------------------- /2017/24.py: -------------------------------------------------------------------------------- 1 | def parse_input(): 2 | with open("24.txt", "r") as file: 3 | data = file.read() 4 | return sorted(sorted(map(int, line.split("/"))) for line in data.splitlines()) 5 | 6 | 7 | PORTS = parse_input() 8 | 9 | 10 | def bt(cur, seen, pt2=False): 11 | res = 0 12 | max_len = 0 13 | for i, (u, v) in enumerate(PORTS): 14 | if i in seen: 15 | continue 16 | if v == cur: 17 | u, v = v, u 18 | if u != cur: 19 | continue 20 | strength, length = bt(v, seen | {i}, pt2) 21 | strength += u + v 22 | length += 1 23 | if pt2: 24 | if length == max_len: 25 | res = max(res, strength) 26 | if length > max_len: 27 | max_len = length 28 | res = strength 29 | else: 30 | res = max(res, strength) 31 | return res, max_len 32 | 33 | 34 | def part_one(): 35 | return bt(0, set())[0] 36 | 37 | 38 | def part_two(): 39 | return bt(0, set(), True)[0] 40 | 41 | 42 | print(f"Part 1: {part_one()}") 43 | print(f"Part 2: {part_two()}") 44 | -------------------------------------------------------------------------------- /2017/25.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def parse_input(): 5 | with open("25.txt", "r") as file: 6 | data = file.read() 7 | prelude, *bp = data.split("\n\n") 8 | state = prelude.splitlines()[0][-2] 9 | steps = int(re.search(r"(\d+)", prelude).group(1)) 10 | regex = ( 11 | r"In state (\w):\s+If the current value is 0:\s+- Write the value (\d).\s+" 12 | r"- Move one slot to the (\w+).\s+- Continue with state (\w).\s+" 13 | r"If the current value is 1:\s+- Write the value (\d).\s+" 14 | r"- Move one slot to the (\w+).\s+- Continue with state (\w)." 15 | ) 16 | bp = [re.search(regex, section).groups() for section in bp] 17 | bp = { 18 | a: [ 19 | (int(b), 1 if c == "right" else -1, d), 20 | (int(e), 1 if f == "right" else -1, g), 21 | ] 22 | for a, b, c, d, e, f, g in bp 23 | } 24 | return state, steps, bp 25 | 26 | 27 | def part_one(): 28 | state, steps, bp = parse_input() 29 | cur = 0 30 | seen = set() 31 | for _ in range(steps): 32 | write, direction, nstate = bp[state][int(cur in seen)] 33 | if write: 34 | seen.add(cur) 35 | else: 36 | seen.discard(cur) 37 | cur += direction 38 | state = nstate 39 | return len(seen) 40 | 41 | 42 | print(f"Part 1: {part_one()}") 43 | -------------------------------------------------------------------------------- /2018/01.py: -------------------------------------------------------------------------------- 1 | from itertools import accumulate, cycle 2 | 3 | 4 | def parse_input(): 5 | with open("01.txt", "r") as file: 6 | data = file.read() 7 | return [int(x) for x in data.splitlines()] 8 | 9 | 10 | SEQ = parse_input() 11 | 12 | 13 | def part_one(): 14 | return sum(SEQ) 15 | 16 | 17 | def part_two(): 18 | seen = set() 19 | for x in accumulate(cycle(SEQ)): 20 | if x in seen: 21 | return x 22 | seen.add(x) 23 | 24 | 25 | print(f"Part 1: {part_one()}") 26 | print(f"Part 2: {part_two()}") 27 | -------------------------------------------------------------------------------- /2018/02.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | from itertools import chain 3 | 4 | 5 | def parse_input(): 6 | with open("02.txt", "r") as file: 7 | data = file.read() 8 | return data.splitlines() 9 | 10 | 11 | BOXES = parse_input() 12 | 13 | 14 | def part_one(): 15 | freq = Counter(chain.from_iterable(set(Counter(box).values()) for box in BOXES)) 16 | return freq[2] * freq[3] 17 | 18 | 19 | def part_two(): 20 | for a in BOXES: 21 | for b in BOXES: 22 | cur = "".join(c for c, d in zip(a, b) if c == d) 23 | if len(cur) == 25: 24 | return cur 25 | 26 | 27 | print(f"Part 1: {part_one()}") 28 | print(f"Part 2: {part_two()}") 29 | -------------------------------------------------------------------------------- /2018/03.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def parse_input(): 5 | with open("03.txt", "r") as file: 6 | data = file.read() 7 | return [ 8 | tuple(map(int, claim)) 9 | for claim in re.findall(r"\d+ @ (\d+),(\d+): (\d+)x(\d+)", data) 10 | ] 11 | 12 | 13 | CLAIMS = parse_input() 14 | FABRIC = [[0] * 1000 for _ in range(1000)] 15 | 16 | 17 | def part_one(): 18 | for y, x, h, w in CLAIMS: 19 | for i in range(y, y + h): 20 | for j in range(x, x + w): 21 | FABRIC[i][j] += 1 22 | return sum(x > 1 for row in FABRIC for x in row) 23 | 24 | 25 | def part_two(): 26 | for id, (y, x, h, w) in enumerate(CLAIMS, start=1): 27 | if all(FABRIC[i][j] == 1 for i in range(y, y + h) for j in range(x, x + w)): 28 | return id 29 | 30 | 31 | print(f"Part 1: {part_one()}") 32 | print(f"Part 2: {part_two()}") 33 | -------------------------------------------------------------------------------- /2018/04.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import Counter, defaultdict 3 | 4 | 5 | def parse_input(): 6 | with open("04.txt", "r") as file: 7 | data = file.read() 8 | data = re.findall(r"\[(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d)\] (.+)", data) 9 | data = sorted(tuple(map(int, x[:5])) + (x[5],) for x in data) 10 | guards = defaultdict(Counter) 11 | for _, _, _, _, minute, record in data: 12 | if record.startswith("Guard"): 13 | cur = int(re.search(r"\d+", record).group()) 14 | elif record.startswith("falls"): 15 | s = minute 16 | else: 17 | for t in range(s, minute): 18 | guards[cur][t] += 1 19 | return guards 20 | 21 | 22 | GUARDS = parse_input() 23 | 24 | 25 | def part_one(): 26 | guard, minutes = max(GUARDS.items(), key=lambda x: sum(x[1].values())) 27 | return guard * minutes.most_common(1)[0][0] 28 | 29 | 30 | def part_two(): 31 | guard, minutes = max(GUARDS.items(), key=lambda x: x[1].most_common(1)[0][1]) 32 | return guard * minutes.most_common(1)[0][0] 33 | 34 | 35 | print(f"Part 1: {part_one()}") 36 | print(f"Part 2: {part_two()}") 37 | -------------------------------------------------------------------------------- /2018/05.py: -------------------------------------------------------------------------------- 1 | import math 2 | from string import ascii_letters 3 | 4 | 5 | def parse_input(): 6 | with open("05.txt", "r") as file: 7 | data = file.read() 8 | return data.strip() 9 | 10 | 11 | POLYMER = parse_input() 12 | 13 | 14 | def solve(polymer): 15 | stack = [] 16 | for c in polymer: 17 | if not stack or abs(ord(c) - ord(stack[-1])) != 32: 18 | stack.append(c) 19 | elif stack and abs(ord(c) - ord(stack[-1])) == 32: 20 | stack.pop() 21 | return len(stack) 22 | 23 | 24 | def part_one(): 25 | return solve(POLYMER) 26 | 27 | 28 | def part_two(): 29 | res = math.inf 30 | for lo, up in zip(ascii_letters[:26], ascii_letters[26:]): 31 | polymer = POLYMER.replace(lo, "").replace(up, "") 32 | res = min(res, solve(polymer)) 33 | return res 34 | 35 | 36 | print(f"Part 1: {part_one()}") 37 | print(f"Part 2: {part_two()}") 38 | -------------------------------------------------------------------------------- /2018/06.py: -------------------------------------------------------------------------------- 1 | import math 2 | import re 3 | from collections import Counter 4 | from itertools import batched 5 | 6 | 7 | def parse_input(): 8 | with open("06.txt", "r") as file: 9 | data = file.read() 10 | return list(batched(map(int, re.findall(r"\d+", data)), 2)) 11 | 12 | 13 | COORDS = parse_input() 14 | N, M = max((i for i, _ in COORDS)), max((j for _, j in COORDS)) 15 | 16 | 17 | def nearest(i, j): 18 | res = math.inf 19 | coords = [] 20 | for x, y in COORDS: 21 | distance = abs(i - x) + abs(j - y) 22 | if distance < res: 23 | res = distance 24 | coords = [(x, y)] 25 | elif distance == res: 26 | coords.append((x, y)) 27 | return coords[0] if len(coords) == 1 else (-1, -1) 28 | 29 | 30 | def part_one(): 31 | areas = Counter() 32 | edges = set() 33 | for i in range(N + 1): 34 | for j in (0, M): 35 | x, y = nearest(i, j) 36 | if (x, y) != (-1, -1): 37 | edges.add((x, y)) 38 | for j in range(M + 1): 39 | for i in (0, N): 40 | x, y = nearest(i, j) 41 | if (x, y) != (-1, -1): 42 | edges.add((x, y)) 43 | for i in range(N + 1): 44 | for j in range(M + 1): 45 | x, y = nearest(i, j) 46 | if (x, y) == (-1, -1) or (x, y) in edges: 47 | continue 48 | areas[(x, y)] += 1 49 | return max(areas.values()) 50 | 51 | 52 | def part_two(): 53 | return sum( 54 | sum(abs(i - x) + abs(j - y) for x, y in COORDS) < 10000 55 | for i in range(N + 1) 56 | for j in range(M + 1) 57 | ) 58 | 59 | 60 | print(f"Part 1: {part_one()}") 61 | print(f"Part 2: {part_two()}") 62 | -------------------------------------------------------------------------------- /2018/07.py: -------------------------------------------------------------------------------- 1 | import heapq 2 | import re 3 | from collections import defaultdict 4 | from itertools import batched 5 | 6 | 7 | def parse_input(): 8 | with open("07.txt", "r") as file: 9 | data = file.read() 10 | edges = list(batched(re.findall(r" (\w) ", data), 2)) 11 | indegs = defaultdict(int) 12 | g = defaultdict(list) 13 | for u, v in edges: 14 | g[u].append(v) 15 | indegs[u] 16 | indegs[v] += 1 17 | return g, indegs 18 | 19 | 20 | def solve(pt2=False): 21 | g, indegs = parse_input() 22 | heap = [u for u, indeg in indegs.items() if indeg == 0] 23 | heapq.heapify(heap) 24 | seen = set(heap) 25 | res = [] 26 | workers = [] 27 | t = 0 28 | while heap or workers: 29 | if pt2: 30 | while heap and len(workers) < 5: 31 | cur = heapq.heappop(heap) 32 | heapq.heappush(workers, (t + ord(cur) - ord("A") + 61, cur)) 33 | t, cur = heapq.heappop(workers) 34 | else: 35 | cur = heapq.heappop(heap) 36 | res.append(cur) 37 | for nei in g[cur]: 38 | if nei in seen: 39 | continue 40 | indegs[nei] -= 1 41 | if indegs[nei] == 0: 42 | seen.add(nei) 43 | heapq.heappush(heap, nei) 44 | return t if pt2 else "".join(res) 45 | 46 | 47 | def part_one(): 48 | return solve() 49 | 50 | 51 | def part_two(): 52 | return solve(True) 53 | 54 | 55 | print(f"Part 1: {part_one()}") 56 | print(f"Part 2: {part_two()}") 57 | -------------------------------------------------------------------------------- /2018/08.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | 3 | 4 | def parse_input(): 5 | with open("08.txt", "r") as file: 6 | data = file.read() 7 | return deque(map(int, data.split(" "))) 8 | 9 | 10 | def part_one(): 11 | license = parse_input() 12 | 13 | def dfs(): 14 | res = 0 15 | children, entries = license.popleft(), license.popleft() 16 | for _ in range(children): 17 | res += dfs() 18 | for _ in range(entries): 19 | res += license.popleft() 20 | return res 21 | 22 | return dfs() 23 | 24 | 25 | def part_two(): 26 | license = parse_input() 27 | 28 | def dfs(): 29 | res = 0 30 | values = [] 31 | children, entries = license.popleft(), license.popleft() 32 | for _ in range(children): 33 | values.append(dfs()) 34 | for _ in range(entries): 35 | cur = license.popleft() 36 | if children == 0: 37 | res += cur 38 | elif 0 < cur <= children: 39 | res += values[cur - 1] 40 | return res 41 | 42 | return dfs() 43 | 44 | 45 | print(f"Part 1: {part_one()}") 46 | print(f"Part 2: {part_two()}") 47 | -------------------------------------------------------------------------------- /2018/09.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import deque 3 | 4 | 5 | def parse_input(): 6 | with open("09.txt", "r") as file: 7 | data = file.read() 8 | return map(int, re.findall(r"\d+", data)) 9 | 10 | 11 | PLAYERS, LAST = parse_input() 12 | 13 | 14 | def solve(last): 15 | scores = [0] * PLAYERS 16 | q = deque([0]) 17 | for i in range(1, last + 1): 18 | if i % 23 == 0: 19 | player = (i - 1) % PLAYERS 20 | q.rotate(7) 21 | scores[player] += i + q.pop() 22 | q.rotate(-1) 23 | else: 24 | q.rotate(-1) 25 | q.append(i) 26 | return max(scores) 27 | 28 | 29 | def part_one(): 30 | return solve(LAST) 31 | 32 | 33 | def part_two(): 34 | return solve(LAST * 100) 35 | 36 | 37 | print(f"Part 1: {part_one()}") 38 | print(f"Part 2: {part_two()}") 39 | -------------------------------------------------------------------------------- /2018/10.py: -------------------------------------------------------------------------------- 1 | import math 2 | import re 3 | from functools import cache 4 | from itertools import batched, count 5 | 6 | 7 | def parse_input(): 8 | with open("10.txt", "r") as file: 9 | data = file.read() 10 | return list(batched(map(int, re.findall(r"-?\d+", data)), 4)) 11 | 12 | 13 | POINTS = parse_input() 14 | 15 | 16 | @cache 17 | def solve(): 18 | space = math.inf 19 | prev = set() 20 | max_i = max_j = -math.inf 21 | min_i = min_j = math.inf 22 | for t in count(): 23 | nmax_i = nmax_j = -math.inf 24 | nmin_i = nmin_j = math.inf 25 | cur = set() 26 | for j, i, dj, di in POINTS: 27 | i += di * t 28 | j += dj * t 29 | nmax_i = max(nmax_i, i) 30 | nmax_j = max(nmax_j, j) 31 | nmin_i = min(nmin_i, i) 32 | nmin_j = min(nmin_j, j) 33 | cur.add((i, j)) 34 | nspace = nmax_i - nmin_i + nmax_j - nmin_j 35 | if nspace > space: 36 | return ( 37 | "\n".join( 38 | "".join( 39 | "\u2588" if (x, y) in prev else " " 40 | for y in range(min_j, max_j + 1) 41 | ) 42 | for x in range(min_i, max_i + 1) 43 | ), 44 | t - 1, 45 | ) 46 | else: 47 | space = nspace 48 | prev = cur 49 | max_i = nmax_i 50 | max_j = nmax_j 51 | min_i = nmin_i 52 | min_j = nmin_j 53 | 54 | 55 | def part_one(): 56 | return "\n" + solve()[0] 57 | 58 | 59 | def part_two(): 60 | return solve()[1] 61 | 62 | 63 | print(f"Part 1: {part_one()}") 64 | print(f"Part 2: {part_two()}") 65 | -------------------------------------------------------------------------------- /2018/11.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | 4 | def parse_input(): 5 | with open("11.txt", "r") as file: 6 | data = file.read() 7 | serial_number = int(data) 8 | grid = [ 9 | [ 10 | ((((y * (x + 10)) + serial_number) * (x + 10)) // 100 % 10) - 5 11 | for y in range(1, 301) 12 | ] 13 | for x in range(1, 301) 14 | ] 15 | acc = [[0] * 300 for _ in range(300)] 16 | for x in range(300): 17 | for y in range(300): 18 | acc[x][y] = grid[x][y] 19 | if x > 0: 20 | acc[x][y] += acc[x - 1][y] 21 | if y > 0: 22 | acc[x][y] += acc[x][y - 1] 23 | if x > 0 and y > 0: 24 | acc[x][y] -= acc[x - 1][y - 1] 25 | return acc 26 | 27 | 28 | ACC = parse_input() 29 | 30 | 31 | def calc_power(sz): 32 | power = -math.inf 33 | res = (0, 0, sz, power) 34 | for x in range(300 - sz + 1): 35 | for y in range(300 - sz + 1): 36 | total = ACC[x + sz - 1][y + sz - 1] 37 | if x > 0: 38 | total -= ACC[x - 1][y + sz - 1] 39 | if y > 0: 40 | total -= ACC[x + sz - 1][y - 1] 41 | if x > 0 and y > 0: 42 | total += ACC[x - 1][y - 1] 43 | if total > power: 44 | power = total 45 | res = (x + 1, y + 1, sz, power) 46 | return res 47 | 48 | 49 | def part_one(): 50 | return ",".join(map(str, calc_power(3)[:2])) 51 | 52 | 53 | def part_two(): 54 | return ",".join( 55 | map(str, max((calc_power(sz) for sz in range(1, 301)), key=lambda x: x[3])[:3]) 56 | ) 57 | 58 | 59 | print(f"Part 1: {part_one()}") 60 | print(f"Part 2: {part_two()}") 61 | -------------------------------------------------------------------------------- /2018/12.py: -------------------------------------------------------------------------------- 1 | from itertools import count 2 | 3 | 4 | def parse_input(): 5 | with open("12.txt", "r") as file: 6 | data = file.read() 7 | state, notes = data.split("\n\n") 8 | state = state.split()[-1] 9 | notes = {k: v for line in notes.splitlines() for k, v in [line.split(" => ")]} 10 | return state, notes 11 | 12 | 13 | def step(state, notes, left): 14 | state = "....." + state + ".." 15 | n = len(state) 16 | nstate = [] 17 | for i in range(n): 18 | cur = state[i - 2 : i + 3] 19 | nstate.append(notes.get(cur, ".")) 20 | state = "".join(nstate) 21 | left += 5 22 | res = sum(i for i, v in enumerate(state, start=-left) if v == "#") 23 | return state, left, res 24 | 25 | 26 | def part_one(): 27 | state, notes = parse_input() 28 | left = 0 29 | for _ in range(20): 30 | state, left, res = step(state, notes, left) 31 | return res 32 | 33 | 34 | def part_two(): 35 | state, notes = parse_input() 36 | left = 0 37 | prev = 0 38 | diff = 0 39 | for t in count(): 40 | state, left, cur = step(state, notes, left) 41 | if diff == cur - prev: 42 | return diff * (50_000_000_000 - t) + prev 43 | diff = cur - prev 44 | prev = cur 45 | 46 | 47 | print(f"Part 1: {part_one()}") 48 | print(f"Part 2: {part_two()}") 49 | -------------------------------------------------------------------------------- /2018/14.py: -------------------------------------------------------------------------------- 1 | def parse_input(): 2 | with open("14.txt", "r") as file: 3 | data = file.read() 4 | return data.strip() 5 | 6 | 7 | def part_one(): 8 | a = [3, 7] 9 | i, j = 0, 1 10 | x = int(parse_input()) 11 | while len(a) < x + 10: 12 | cur = a[i] + a[j] 13 | if cur > 9: 14 | a.append(1) 15 | a.append(cur % 10) 16 | i = (i + a[i] + 1) % len(a) 17 | j = (j + a[j] + 1) % len(a) 18 | return int("".join(map(str, a[x : x + 10]))) 19 | 20 | 21 | def part_two(): 22 | a = [3, 7] 23 | i, j = 0, 1 24 | target = list(map(int, parse_input())) 25 | n = len(target) 26 | while True: 27 | cur = a[i] + a[j] 28 | if cur > 9: 29 | a.append(1) 30 | if len(a) >= n and a[-n:] == target: 31 | return len(a) - n 32 | a.append(cur % 10) 33 | if len(a) >= n and a[-n:] == target: 34 | return len(a) - n 35 | i = (i + a[i] + 1) % len(a) 36 | j = (j + a[j] + 1) % len(a) 37 | 38 | 39 | print(f"Part 1: {part_one()}") 40 | print(f"Part 2: {part_two()}") 41 | -------------------------------------------------------------------------------- /2019/01.py: -------------------------------------------------------------------------------- 1 | with open('01.txt', 'r') as file: 2 | data = file.readlines() 3 | data = [int(x) for x in data] 4 | 5 | 6 | def fuel_required(mass): 7 | return int(mass/3) - 2 8 | 9 | 10 | total = 0 11 | for module in data: 12 | total += fuel_required(module) 13 | print(total) # 3358992 14 | 15 | 16 | def fuel2(mass): 17 | res = 0 18 | fuel = fuel_required(mass) 19 | res += fuel 20 | while fuel > 6: 21 | fuel = fuel_required(fuel) 22 | res += fuel 23 | return res 24 | 25 | 26 | total = 0 27 | for module in data: 28 | total += fuel2(module) 29 | print(total) # 5035632 30 | -------------------------------------------------------------------------------- /2019/02.py: -------------------------------------------------------------------------------- 1 | with open('02.txt', 'r') as file: 2 | data = file.read() 3 | data = data.split(',') 4 | data = [int(x) for x in data] 5 | data_copy = data[:] 6 | 7 | 8 | def opcode(lst): 9 | i = 0 10 | while lst[i] != 99: 11 | if lst[i] == 1: 12 | lst[lst[i+3]] = lst[lst[i+1]] + lst[lst[i+2]] 13 | elif lst[i] == 2: 14 | lst[lst[i+3]] = lst[lst[i+1]] * lst[lst[i+2]] 15 | i += 4 16 | return lst 17 | 18 | 19 | # replace position 1 with the value 12 and replace position 2 with the value 2 20 | data[1] = 12 21 | data[2] = 2 22 | 23 | # What value is left at position 0 after the program halts? 24 | opcode(data) 25 | print(data[0]) # 3654868 26 | 27 | data = data_copy[:] 28 | 29 | 30 | def part2(): 31 | for i in range(100): 32 | for j in range(100): 33 | data = data_copy[:] 34 | data[1] = i 35 | data[2] = j 36 | opcode(data) 37 | if data[0] == 19690720: 38 | return i, j 39 | 40 | 41 | print(part2()) # 70, 14 (answer is 7014) 42 | -------------------------------------------------------------------------------- /2019/03.py: -------------------------------------------------------------------------------- 1 | with open('03.txt', 'r') as file: 2 | data = file.readlines() 3 | data1 = data[0].split(',') 4 | data2 = data[1].split(',') 5 | 6 | 7 | def make_wire(moves): 8 | point = [0, 0] 9 | wire = set() 10 | steps = 0 11 | step_count = {} 12 | for move in moves: 13 | direction = move[0] 14 | units = int(move[1:]) 15 | if direction in ['R', 'U']: 16 | sign = 1 17 | else: 18 | sign = -1 19 | if direction in ['L', 'R']: 20 | axis = 0 21 | else: 22 | axis = 1 23 | for _ in range(units): 24 | point[axis] += sign 25 | new_point = tuple(point) 26 | wire.add(new_point) 27 | steps += 1 28 | if new_point not in step_count: 29 | step_count[new_point] = steps 30 | return wire, step_count 31 | 32 | 33 | wire1 = make_wire(data1) 34 | wire2 = make_wire(data2) 35 | crosses = wire1[0].intersection(wire2[0]) 36 | 37 | 38 | mini = float('Inf') 39 | for cross in crosses: 40 | mini = min(mini, abs(cross[0]) + abs(cross[1])) 41 | 42 | print(mini) # 273 43 | 44 | 45 | cross_step_counts = [wire1[1][x] + wire2[1][x] for x in crosses] 46 | print(min(cross_step_counts)) # 15622 47 | -------------------------------------------------------------------------------- /2019/04.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def password_count(l, r): 5 | count = 0 6 | for i in range(l, r): 7 | i = str(i) 8 | x = re.findall(r'(.)\1+', i) 9 | if list(i) != sorted(list(i)): 10 | continue 11 | elif not x: 12 | continue 13 | else: 14 | count += 1 15 | return count 16 | 17 | 18 | print(password_count(245182, 790573)) # 1099 19 | 20 | 21 | def password_count2(l, r): 22 | count = 0 23 | for i in range(l, r): 24 | i = str(i) 25 | x = re.findall(r'(.)\1+', i) 26 | y = re.findall(r'(.)\1{2,5}', i) 27 | for m in y: 28 | if m in x: 29 | x.remove(m) 30 | if list(i) != sorted(list(i)): 31 | continue 32 | elif not x: 33 | continue 34 | else: 35 | count += 1 36 | return count 37 | 38 | 39 | print(password_count2(245182, 790573)) # 710 40 | 41 | 42 | def has_double(n): 43 | num = str(n) 44 | for i in range(10): 45 | if str(i)*2 in num: 46 | return True 47 | return False 48 | 49 | 50 | def has_only_double(n): 51 | num = str(n) 52 | for i in range(10): 53 | if str(i)*2 in num and not str(i)*3 in num: 54 | return True 55 | return False 56 | 57 | 58 | total = 0 59 | for x in range(245182, 790573): 60 | if has_double(x) and list(str(x)) == sorted(list(str(x))): 61 | total += 1 62 | print(total) 63 | 64 | 65 | total = 0 66 | for x in range(245182, 790573): 67 | if has_only_double(x) and list(str(x)) == sorted(list(str(x))): 68 | total += 1 69 | print(total) 70 | -------------------------------------------------------------------------------- /2019/06.py: -------------------------------------------------------------------------------- 1 | with open('06.txt', 'r') as data: 2 | data = [x.split(')') for x in data.read().split('\n')] 3 | data = data[:-1] 4 | orbits = {x[1]: x[0] for x in data} 5 | 6 | 7 | # PART 1 8 | 9 | def count_paths(orbits): 10 | total = 0 11 | for planet in orbits.keys(): 12 | curr = orbits[planet] 13 | while True: 14 | total += 1 15 | if curr == 'COM': 16 | break 17 | curr = orbits[curr] 18 | return total 19 | 20 | 21 | print(count_paths(orbits)) # 142497 22 | 23 | 24 | # PART 2 25 | 26 | def find_path(origin, orbits): 27 | curr = orbits[origin] 28 | planets = [] 29 | while True: 30 | if curr == 'COM': 31 | break 32 | planets.append(curr) 33 | curr = orbits[curr] 34 | return planets 35 | 36 | 37 | def find_intersection(path1, path2): 38 | for x in path2: 39 | if x in path1: 40 | i = path1.index(x) 41 | k = path2.index(x) 42 | intersect = path1[:i] + path2[k:0:-1] 43 | break 44 | return intersect 45 | 46 | 47 | my_path = find_path('YOU', orbits) 48 | santa_path = find_path('SAN', orbits) 49 | print(len(find_intersection(my_path, santa_path))) # 301 50 | -------------------------------------------------------------------------------- /2019/08.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | 3 | with open('08.txt', 'r') as data: 4 | data = data.read().strip() 5 | 6 | 7 | def find_layers(s, width, height): 8 | pointer = 0 9 | w, h = width, height 10 | layers = [] 11 | while pointer < len(s): 12 | layer = [] 13 | for _ in range(h): 14 | layer.append(list(s[pointer:pointer+w])) 15 | pointer += w 16 | layers.append(layer) 17 | return layers 18 | 19 | 20 | def find_min_zeros(image): 21 | min_zeros = (float('Inf'), 0) # number of zeros, layer index 22 | for i in range(len(image)): 23 | c = Counter() 24 | for line in image[i]: 25 | c += Counter(line) 26 | if c['0'] < min_zeros[0]: 27 | min_zeros = (c['0'], i) 28 | return min_zeros 29 | 30 | 31 | def calc_part1(image, layer): 32 | c = Counter() 33 | for line in image[layer]: 34 | c += Counter(line) 35 | return c['1'] * c['2'] 36 | 37 | 38 | image = find_layers(data, 25, 6) 39 | layer = find_min_zeros(image)[1] 40 | print(calc_part1(image, layer)) # 1224 41 | 42 | 43 | def decode_image(image, width, height): 44 | final_image = [['.' for _ in range(width)] for _ in range(height)] 45 | for layer in image: 46 | for i in range(height): 47 | for j in range(width): 48 | if layer[i][j] == '1' and final_image[i][j] == '.': 49 | final_image[i][j] = '|' 50 | if layer[i][j] == '0' and final_image[i][j] == '.': 51 | final_image[i][j] = ' ' 52 | for row in final_image: 53 | print(''.join(row)) 54 | 55 | 56 | decode_image(image, 25, 6) # EBZUR 57 | -------------------------------------------------------------------------------- /2019/16.py: -------------------------------------------------------------------------------- 1 | with open('16.txt') as data: 2 | data = list(map(int, data.read().strip())) 3 | 4 | 5 | def fft2(number): 6 | length = len(number) 7 | copy = number[:] 8 | for i in range(length): 9 | j = i 10 | step = i + 1 11 | total = 0 12 | while j < length: 13 | total += sum(copy[j:j + step]) 14 | j += 2 * step 15 | total -= sum(copy[j:j + step]) 16 | j += 2 * step 17 | number[i] = abs(total) % 10 18 | return number 19 | 20 | 21 | # Part 1 22 | number = data[:] 23 | for _ in range(100): 24 | number = fft2(number) 25 | res = ''.join(map(str, number[:8])) 26 | print(f'Part 1: {res}') # 89576828 27 | 28 | # Part 2 29 | offset = int(''.join(map(str, data[:7]))) 30 | number = (data * 10000)[offset:] 31 | length = len(number) 32 | for _ in range(100): 33 | for i in range(length-2, -1, -1): 34 | number[i] += number[i+1] 35 | number[i] %= 10 36 | res = ''.join(map(str, number[:8])) 37 | print(f'Part 2: {res}') # 23752579 38 | -------------------------------------------------------------------------------- /2020/01.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const buffer = fs.readFileSync('01.txt'); 3 | const s = buffer.toString().split('\n'); 4 | let data = s.map((elem) => parseInt(elem)); 5 | 6 | 7 | function part1(data) { 8 | let seen = new Set(); 9 | for (let i=0; i 0) { 31 | r -= 1; 32 | } else { 33 | l += 1; 34 | } 35 | } 36 | } 37 | } 38 | 39 | 40 | console.log(part1(data)); 41 | console.log(part2(data)); -------------------------------------------------------------------------------- /2020/01.py: -------------------------------------------------------------------------------- 1 | from tools import timer 2 | 3 | 4 | def load_data(): 5 | with open('01.txt', 'r') as file: 6 | data = list(map(int, file.read().split('\n'))) 7 | return data 8 | 9 | data = load_data() 10 | 11 | 12 | @timer 13 | def part1(data): 14 | seen = set() 15 | for v in data: 16 | if 2020-v in seen: 17 | return v * (2020-v) 18 | else: 19 | seen.add(v) 20 | 21 | @timer 22 | def part2(data): 23 | data.sort() 24 | n = len(data) 25 | for i in range(n - 2): 26 | l, r = i+1, n-1 27 | while l < r: 28 | a, b, c = data[i], data[l], data[r] 29 | cur = a+b+c 30 | if cur == 2020: 31 | return a * b * c 32 | if cur-2020 > 0: 33 | r -= 1 34 | else: 35 | l += 1 36 | 37 | 38 | print(f'Answer for part 1: {part1(data)}') # 1018944 39 | print(f'Answer for part 2: {part2(data)}') # 8446464 40 | 41 | 42 | ''' 43 | You could also just use itertools.combinations, 44 | but where's the fun in that? ¯\_(ツ)_/¯ 45 | ''' 46 | 47 | 48 | from itertools import combinations 49 | 50 | data = load_data() #reload data for accurate timing 51 | 52 | @timer 53 | def part1_alternative(data): 54 | for a, b in combinations(data, 2): 55 | if a + b == 2020: 56 | return a * b 57 | 58 | @timer 59 | def part2_alternative(data): 60 | for a, b, c in combinations(data, 3): 61 | if a + b + c == 2020: 62 | return a * b * c 63 | 64 | 65 | print(part1_alternative(data)) 66 | print(part2_alternative(data)) -------------------------------------------------------------------------------- /2020/02.py: -------------------------------------------------------------------------------- 1 | with open('02.txt', 'r') as file: 2 | data = file.read().split('\n') 3 | 4 | 5 | def part1(data): 6 | res = 0 7 | for row in data: 8 | bounds, c, s = row.split() 9 | a, b = map(int, bounds.split('-')) 10 | if a <= s.count(c[0]) <= b: 11 | res += 1 12 | return res 13 | 14 | 15 | def part2(data): 16 | res = 0 17 | for row in data: 18 | pos, c, s = row.split() 19 | i, j = map(int, pos.split('-')) 20 | if (s[i-1] == c[0]) ^ (s[j-1] == c[0]): 21 | res += 1 22 | return res 23 | 24 | 25 | print(f'Answer for part 1: {part1(data)}') # 477 26 | print(f'Answer for part 2: {part2(data)}') # 686 27 | -------------------------------------------------------------------------------- /2020/03.py: -------------------------------------------------------------------------------- 1 | with open('03.txt', 'r') as file: 2 | data = file.read().split('\n') 3 | 4 | 5 | def part1(data, r, d): 6 | n, m = len(data), len(data[0]) 7 | j = 0 8 | res = 0 9 | for i in range(0, n, d): 10 | if data[i][j] == '#': 11 | res += 1 12 | j = (j+r) % m 13 | return res 14 | 15 | 16 | def part2(data): 17 | res = 1 18 | for r, d in [(1, 1), (3, 1), (5, 1), (7, 1), (1, 2)]: 19 | res *= part1(data, r, d) 20 | return res 21 | 22 | 23 | print(f'Answer for part 1: {part1(data, 3, 1)}') # 270 24 | print(f'Answer for part 2: {part2(data)}') # 2122848000 25 | -------------------------------------------------------------------------------- /2020/04.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | def parse_input(data): 4 | return [{k:v for k, v in [x.split(':') for x in row.split()]} for row in data] 5 | 6 | 7 | def has_valid_fields(passport): 8 | valid_fields = {'byr', 'iyr', 'eyr', 'hgt', 'hcl', 'ecl', 'pid'} 9 | return valid_fields.issubset(set(passport.keys())) 10 | 11 | 12 | def has_valid_byr(passport): 13 | return 1920 <= int(passport.get('byr', -1)) <= 2002 14 | 15 | 16 | def has_valid_iyr(passport): 17 | return 2010 <= int(passport.get('iyr', -1)) <= 2020 18 | 19 | 20 | def has_valid_eyr(passport): 21 | return 2020 <= int(passport.get('eyr', -1)) <= 2030 22 | 23 | 24 | def has_valid_hgt(passport): 25 | hgt = passport.get('hgt', ' ') 26 | return bool(re.match(r'1([5-8][\d]|9[0-3])cm|(59|6[\d]|7[0-6])in', hgt)) 27 | 28 | 29 | def has_valid_hcl(passport): 30 | hcl = passport.get('hcl', ' ') 31 | return bool(re.match(r'#[0-9a-f]{6}', hcl)) 32 | 33 | 34 | def has_valid_ecl(passport): 35 | return passport['ecl'] in {'amb', 'blu', 'brn', 'gry', 'grn', 'hzl', 'oth'} 36 | 37 | 38 | def has_valid_pid(passport): 39 | return len(passport['pid']) == 9 and passport['pid'].isdecimal() 40 | 41 | 42 | def check_all(passport): 43 | checks = [has_valid_fields, has_valid_byr, has_valid_iyr, has_valid_eyr, 44 | has_valid_hgt, has_valid_hcl, has_valid_ecl, has_valid_pid] 45 | return all(func(passport) for func in checks) 46 | 47 | 48 | def part1(passports): 49 | return sum(has_valid_fields(passport) for passport in passports) 50 | 51 | 52 | def part2(passports): 53 | return sum(check_all(passport) for passport in passports) 54 | 55 | 56 | if __name__ == '__main__': 57 | with open('04.txt', 'r') as file: 58 | data = file.read().split('\n\n') 59 | 60 | passports = parse_input(data) 61 | 62 | print(f'Part 1: {part1(passports)}') # 219 63 | print(f'Part 2: {part2(passports)}') # 127 64 | -------------------------------------------------------------------------------- /2020/05.py: -------------------------------------------------------------------------------- 1 | def convert(boarding_pass): 2 | return int(boarding_pass.translate(str.maketrans('FBLR', '0101')), 2) 3 | 4 | 5 | def part_one(seats): 6 | return max(seats) 7 | 8 | 9 | def part_two(seats): 10 | return list((set(range(min(seats), max(seats))) - seats))[0] 11 | 12 | 13 | if __name__ == '__main__': 14 | with open('05.txt', 'r') as file: 15 | seats = set(convert(boarding_pass) for boarding_pass in file.read().split('\n')) 16 | 17 | print(f'Part 1: {part_one(seats)}') # 959 18 | print(f'Part 2: {part_two(seats)}') # 527 19 | -------------------------------------------------------------------------------- /2020/06.py: -------------------------------------------------------------------------------- 1 | with open('06.txt', 'r') as file: 2 | data = file.read().split('\n\n') 3 | 4 | def part_one(data): 5 | return sum(len(set.union(*[set(s) for s in group.split()])) for group in data) 6 | 7 | 8 | def part_two(data): 9 | return sum(len(set.intersection(*[set(s) for s in group.split()])) for group in data) 10 | 11 | 12 | print(f'Part 1: {part_one(data)}') # 6775 13 | print(f'Part 2: {part_two(data)}') # 3356 14 | -------------------------------------------------------------------------------- /2020/07.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict, deque 2 | import re 3 | 4 | with open('07.txt', 'r') as file: 5 | data = file.read() 6 | 7 | 8 | def parse_input(data): 9 | bags = re.findall(r'([a-z]+ [a-z]+) bags contain (.+)', data) 10 | pattern = re.compile(r'(\d+) ([a-z]+ [a-z]+) bag') 11 | graph = {bag: {nested_bag: int(v) for v, nested_bag in pattern.findall(nested_bags)} for bag, nested_bags in bags} 12 | return graph 13 | 14 | 15 | def create_reverse_graph(graph): 16 | rev = defaultdict(list) 17 | for k, v in graph.items(): 18 | for new_key in v.keys(): 19 | rev[new_key].append(k) 20 | return rev 21 | 22 | 23 | def part_one(rev): 24 | q = deque(['shiny gold']) 25 | seen = set() 26 | while q: 27 | cur = q.popleft() 28 | if cur not in seen: 29 | q += deque(rev[cur]) 30 | seen.add(cur) 31 | return len(seen) - 1 # remove one for 'shiny gold' 32 | 33 | 34 | def part_two(graph): 35 | def dfs(bag): 36 | return 1 + sum((v * dfs(k)) for k, v in graph[bag].items()) 37 | return dfs('shiny gold') - 1 # remove one for 'shiny gold' 38 | 39 | 40 | graph = parse_input(data) 41 | rev_graph = create_reverse_graph(graph) 42 | 43 | print(f'Part 1: {part_one(rev_graph)}') # 372 44 | print(f'Part 2: {part_two(graph)}') # 8015 45 | -------------------------------------------------------------------------------- /2020/08.py: -------------------------------------------------------------------------------- 1 | from bootcode import BootCode 2 | 3 | with open('08.txt', 'r') as file: 4 | data = file.read() 5 | 6 | def parse_input(data): 7 | return [[line.split(' ')[0], int(line.split(' ')[1])] for line in data.split('\n')] 8 | 9 | 10 | def part_one(): 11 | return bc.run()[0] 12 | 13 | 14 | def part_two(): 15 | return bc.repair() 16 | 17 | 18 | data = parse_input(data) 19 | bc = BootCode(data) 20 | 21 | print(f'Part 1: {part_one()}') # 1859 22 | print(f'Part 2: {part_two()}') # 1235 23 | -------------------------------------------------------------------------------- /2020/09.py: -------------------------------------------------------------------------------- 1 | with open('09.txt', 'r') as file: 2 | data = file.read() 3 | 4 | def parse_input(data): 5 | return list(map(int, data.split('\n'))) 6 | 7 | 8 | def part_one(data): 9 | preamble = set(data[:25]) 10 | i = 0 11 | while True: 12 | cur = data[i+25] 13 | if not any((cur - x) in preamble for x in preamble): 14 | return cur 15 | preamble.remove(data[i]) 16 | preamble.add(data[i+25]) 17 | i += 1 18 | 19 | 20 | def part_two(data): 21 | cur, target = 0, 552655238 22 | i, j = 0, 0 23 | while j < len(data): 24 | while cur > target and i < j: 25 | cur -= data[i] 26 | i += 1 27 | if cur == target: 28 | return min(data[i:j]) + max(data[i:j]) 29 | if j < len(data): 30 | cur += data[j] 31 | j += 1 32 | 33 | 34 | data = parse_input(data) 35 | print(f'Part 1: {part_one(data)}') # 552655238 36 | print(f'Part 2: {part_two(data)}') # 70672245 37 | -------------------------------------------------------------------------------- /2020/10.py: -------------------------------------------------------------------------------- 1 | from functools import lru_cache 2 | 3 | with open('10.txt', 'r') as file: 4 | data = file.read() 5 | 6 | 7 | def parse_input(data): 8 | jolts = sorted(list(map(int, data.split('\n')))) 9 | return [0] + jolts + [jolts[-1] + 3] 10 | 11 | 12 | def part_one(data): 13 | diff = [data[i+1] - data[i] for i in range(len(data)-1)] 14 | a = diff.count(1) 15 | b = diff.count(3) 16 | return a * b 17 | 18 | 19 | def make_graph(data): 20 | graph = {} 21 | for i, x in enumerate(data): 22 | graph[x] = [y for y in data[i+1:i+4] if 0 < y-x <= 3] 23 | return graph 24 | 25 | 26 | def part_two(data): 27 | graph = make_graph(data) 28 | @lru_cache(None) 29 | def dfs(node): 30 | if not graph[node]: 31 | return 1 32 | return sum(dfs(nnode) for nnode in graph[node]) 33 | return dfs(0) 34 | 35 | 36 | def part_two_alt(data): 37 | dp = {0: 1} 38 | for x in data[1:]: 39 | dp[x] = dp.get(x-1, 0) + dp.get(x-2, 0) + dp.get(x-3, 0) 40 | return dp[data[-1]] 41 | 42 | 43 | data = parse_input(data) 44 | print(f'Part 1: {part_one(data)}') # 2376 45 | print(f'Part 2: {part_two(data)}') # 129586085429248 46 | print(f'Part 2 alt: {part_two_alt(data)}') # 0.000048 seconds, improved from 0.000497 seconds 47 | -------------------------------------------------------------------------------- /2020/12.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | with open('12.txt', 'r') as file: 4 | data = file.read() 5 | 6 | 7 | def parse_input(data): 8 | res = re.findall(r'([A-Z]+)(\d+)', data) 9 | return [(s, int(x)) for s, x in res] 10 | 11 | DIRS = { 12 | 'N': 0 + 1j, 13 | 'E': 1 + 0j, 14 | 'S': 0 + -1j, 15 | 'W': -1 + 0j, 16 | 'R': 0 - 1j, 17 | 'L': 0 + 1j, 18 | } 19 | 20 | 21 | def part_one(instr): 22 | pos = 0 + 0j 23 | direction = 1 + 0j # EAST 24 | for d, n in instr: 25 | if d in 'RL': 26 | direction *= DIRS[d] ** (n // 90) 27 | elif d in 'ENWS': 28 | pos += DIRS[d] * n 29 | else: # d == 'F' 30 | pos += direction * n 31 | return abs(int(pos.real)) + abs(int(pos.imag)) 32 | 33 | 34 | def part_two(instr): 35 | way = 10 + 1j 36 | pos = 0 + 0j 37 | for d, n in instr: 38 | if d in 'RL': 39 | way *= DIRS[d] ** (n // 90) 40 | elif d in 'ENWS': 41 | way += DIRS[d] * n 42 | else: # d == 'F' 43 | pos += way * n 44 | return abs(int(pos.real)) + abs(int(pos.imag)) 45 | 46 | 47 | instr = parse_input(data) 48 | print(f'Part 1: {part_one(instr)}') # 882 49 | print(f'Part 2: {part_two(instr)}') # 28885 50 | -------------------------------------------------------------------------------- /2020/13.py: -------------------------------------------------------------------------------- 1 | from functools import reduce 2 | 3 | with open('13.txt', 'r') as file: 4 | data = file.read() 5 | 6 | 7 | def parse_input(data): 8 | return [int(x) if x.isdigit() else x for x in data.split(',')] 9 | 10 | 11 | def part_one(data): 12 | time = 1002460 13 | buses = [x for x in data if type(x) is int] 14 | res = [(x - time % x, x) for x in buses] 15 | return reduce(lambda x, y: x*y, min(res)) 16 | 17 | 18 | def ext_gcd(a, b): 19 | if b == 1: 20 | return b 21 | initial_b = b 22 | x, y = 0, 1 23 | while a > 1: 24 | q = a//b 25 | a, b = b, a%b 26 | x, y = y - (q*x), x 27 | return y + initial_b if y<0 else y 28 | 29 | 30 | def chinese_remainder(nums, mods): 31 | product = reduce(lambda x, y: x*y, nums) 32 | total = 0 33 | for n, m in zip(nums, mods): 34 | p = product // n 35 | total += ext_gcd(p, n) * p * m 36 | return total % product 37 | 38 | 39 | def part_two(data): 40 | buses = [x for x in data if type(x) is int] 41 | mods = [-i%v for i, v in enumerate(data) if v!='x'] 42 | return chinese_remainder(buses, mods) 43 | 44 | 45 | def part_two_sieve(data): 46 | ''' 47 | Alternative solution that still utilizes the Chinese remainder theorem, 48 | but finds the answer by sieving 49 | ''' 50 | buses = [x for x in data if type(x) is int] 51 | mods = [-i%v for i, v in enumerate(data) if v!='x'] 52 | x, step = 0, 1 53 | for d, r in zip(buses, mods): 54 | while x % d != r: 55 | x += step 56 | step *= d 57 | return x 58 | 59 | 60 | data = parse_input(data) 61 | print(f'Part 1: {part_one(data)}') # 4808 62 | print(f'Part 2: {part_two(data)}') # 741745043105674 63 | print(f'Part 2 (sieve): {part_two_sieve(data)}') # 741745043105674 64 | -------------------------------------------------------------------------------- /2020/14.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import defaultdict 3 | 4 | with open('14.txt', 'r') as file: 5 | data = file.read() 6 | 7 | 8 | def parse_input(data): 9 | res = defaultdict(list) 10 | mask = None 11 | for line in data.split('\n'): 12 | if line[:4] == 'mask': 13 | mask = line[-36:] 14 | if line[:3] =='mem': 15 | match = re.match(r'mem\[(\d+)\] = (\d+)', line) 16 | res[mask].append((int(match.group(1)), int(match.group(2)))) 17 | return res 18 | 19 | 20 | def bitmask(mask, num): 21 | num = bin(num)[2:].zfill(36) 22 | res = [num[i] if v=='X' else v for i, v in enumerate(mask)] 23 | return int(''.join(res), 2) 24 | 25 | 26 | def bitmask_two(mask, num): 27 | num = bin(num)[2:].zfill(36) 28 | res = [] 29 | count = mask.count('X') 30 | new = ''.join('{}' if v=='X' else '1' if v=='1' else num[i] for i, v in enumerate(mask)) 31 | for n in range(2**count): 32 | n = bin(n)[2:].zfill(count) 33 | res.append(new.format(*n)) 34 | return [int(x, 2) for x in res] 35 | 36 | 37 | def part_one(data): 38 | res = {} 39 | for mask, ints in data.items(): 40 | for k, v in ints: 41 | res[k] = bitmask(mask, v) 42 | return sum(res.values()) 43 | 44 | 45 | def part_two(data): 46 | res = {} 47 | for mask, ints in data.items(): 48 | for k, v in ints: 49 | for addr in bitmask_two(mask, k): 50 | res[addr] = v 51 | return sum(res.values()) 52 | 53 | 54 | data = parse_input(data) 55 | print(f'Part 1: {part_one(data)}') # 6513443633260 56 | print(f'Part 2: {part_two(data)}') # 3442819875191 57 | -------------------------------------------------------------------------------- /2020/15.py: -------------------------------------------------------------------------------- 1 | data = [12,1,16,3,11,0] 2 | 3 | def van_eck(data, end=2020): 4 | i = len(data) - 1 5 | seen = {v: k for k, v in enumerate(data)} 6 | cur = data[-1] 7 | while i < end - 1: 8 | prev = cur 9 | cur = i - seen.get(cur, i) 10 | seen[prev] = i 11 | i += 1 12 | return cur 13 | 14 | 15 | def part_one(data): 16 | return van_eck(data, 2020) 17 | 18 | 19 | def part_two(data): 20 | return van_eck(data, 30000000) 21 | 22 | 23 | print(f'Part 1: {part_one(data)}') # 1696 24 | print(f'Part 2: {part_two(data)}') # 37385 25 | -------------------------------------------------------------------------------- /2020/17.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from itertools import product 3 | 4 | with open('17.txt', 'r') as file: 5 | data = file.read() 6 | 7 | 8 | def parse_input(data): 9 | return [list(line) for line in data.split('\n')] 10 | 11 | 12 | data = parse_input(data) 13 | 14 | 15 | def find_active(data, dim=3): 16 | active = set() 17 | for i in range(len(data)): 18 | for j in range(len(data[0])): 19 | if data[i][j] == '#': 20 | active.add((i, j, *([0]*(dim-2)))) 21 | return active 22 | 23 | 24 | def find_neighbors(active, dim=3): 25 | neighbors = defaultdict(int) 26 | for cube in active: 27 | for delta in filter(any, product([-1, 0, 1], repeat=dim)): 28 | cur = tuple(x+dx for x, dx in zip(cube, delta)) 29 | neighbors[cur] += 1 30 | return neighbors 31 | 32 | 33 | def step(active, dim=3): 34 | new_active = set() 35 | neighbors = find_neighbors(active, dim) 36 | for k, v in neighbors.items(): 37 | if v == 3 or (v == 2 and k in active): 38 | new_active.add(k) 39 | return new_active 40 | 41 | 42 | def part_one(data): 43 | active = find_active(data, 3) 44 | for _ in range(6): 45 | active = step(active, 3) 46 | return len(active) 47 | 48 | 49 | def part_two(data): 50 | active = find_active(data, 4) 51 | for _ in range(6): 52 | active = step(active, 4) 53 | return len(active) 54 | 55 | 56 | print(f'Part 1: {part_one(data)}') # 252 57 | print(f'Part 2: {part_two(data)}') # 2160 58 | -------------------------------------------------------------------------------- /2020/18.py: -------------------------------------------------------------------------------- 1 | with open('18.txt', 'r') as file: 2 | data = file.read() 3 | 4 | 5 | def parse_input(data): 6 | data = [line for line in data.split('\n')] 7 | return data 8 | 9 | 10 | data = parse_input(data) 11 | 12 | 13 | class N: 14 | def __init__(self, n): 15 | self.n = n 16 | 17 | def __add__(self, other): 18 | return N(self.n + other.n) 19 | 20 | def __sub__(self, other): 21 | return N(self.n * other.n) 22 | 23 | def __mul__(self, other): 24 | return N(self.n + other.n) 25 | 26 | 27 | def eval_line(line, swap=False): 28 | for i in range(10): 29 | line = line.replace(f'{i}', f'N({i})') 30 | line = line.replace('*', '-') 31 | if swap: line = line.replace('+', '*') 32 | return eval(line, {'N': N}).n 33 | 34 | 35 | def part_one(data): 36 | return sum(eval_line(line) for line in data) 37 | 38 | 39 | def part_two(data): 40 | return sum(eval_line(line, swap=True) for line in data) 41 | 42 | 43 | print(f'Part 1: {part_one(data)}') # 131076645626 44 | print(f'Part 2: {part_two(data)}') # 109418509151782 45 | -------------------------------------------------------------------------------- /2020/19.py: -------------------------------------------------------------------------------- 1 | with open('19.txt', 'r') as file: 2 | data = file.read() 3 | 4 | 5 | def parse_input(data): 6 | rules_raw = [line for line in data.split('\n')[:135]] 7 | rules = {} 8 | for line in rules_raw: 9 | n, options = line.split(': ') 10 | n = int(n) 11 | if options in ('"a"', '"b"'): 12 | rules[n] = options[1] 13 | else: 14 | rules[n] = [[int(x) for x in option.split()] for option in options.split("|")] 15 | messages = [line for line in data.split('\n')[136:]] 16 | return rules, messages 17 | 18 | 19 | rules, messages = parse_input(data) 20 | 21 | 22 | def recur(rules, n, cur): 23 | if type(rules[n]) is str: 24 | return set([1]) if (cur and cur[0] == rules[n]) else set() 25 | 26 | res = set() 27 | for options in rules[n]: 28 | matches = set([0]) 29 | for option in options: 30 | update = set() 31 | for x in matches: 32 | update |= {x+y for y in recur(rules, option, cur[x:])} 33 | matches = update 34 | res |= matches 35 | return res 36 | 37 | 38 | def part_one(rules, messages): 39 | return sum(1 if len(message) in recur(rules, 0, message) else 0 for message in messages) 40 | 41 | 42 | def part_two(rules, messages): 43 | rules[8] = [[42], [42, 8]] 44 | rules[11] = [[42, 31], [42, 11, 31]] 45 | return sum(1 if len(message) in recur(rules, 0, message) else 0 for message in messages) 46 | 47 | 48 | print(f'Part 1: {part_one(rules, messages)}') # 239 49 | print(f'Part 2: {part_two(rules, messages)}') # 405 50 | -------------------------------------------------------------------------------- /2020/21.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import defaultdict, Counter 3 | 4 | with open('21.txt', 'r') as file: 5 | data = file.read() 6 | 7 | 8 | def parse_input(data): 9 | ingredients, allergens = [], [] 10 | for line in data.split('\n'): 11 | m = re.findall(r'(.*) \(contains (.*)\)', line) 12 | ingredients.append(m[0][0].split(' ')) 13 | allergens.append(m[0][1].split(', ')) 14 | return ingredients, allergens 15 | 16 | 17 | ingredients, allergens = parse_input(data) 18 | 19 | 20 | def count_ings(ingredients): 21 | res = Counter() 22 | for row in ingredients: 23 | res += Counter(row) 24 | return res 25 | 26 | 27 | ing_count = count_ings(ingredients) 28 | 29 | 30 | def make_allergen_mapping(ingredients, allergens): 31 | possible = defaultdict(set) 32 | for i in range(len(ingredients)): 33 | for allergen in allergens[i]: 34 | if not possible[allergen]: 35 | possible[allergen] = set(ingredients[i]) 36 | else: 37 | possible[allergen] &= set(ingredients[i]) 38 | used = set() 39 | res = {} 40 | while len(used) < len(possible): 41 | for alg, ing in possible.items(): 42 | if len(ing - used) == 1: 43 | cur = list(ing-used)[0] 44 | res[alg] = cur 45 | used.add(cur) 46 | break 47 | return res 48 | 49 | 50 | alg_map = make_allergen_mapping(ingredients, allergens) 51 | 52 | 53 | def part_one(ing_count, alg_map): 54 | alg_set = set(alg_map.values()) 55 | return sum(ing_count[ing] for ing in ing_count if ing not in alg_set) 56 | 57 | 58 | def part_two(alg_map): 59 | return ','.join(alg_map[ing] for ing in sorted(list(alg_map))) 60 | 61 | 62 | print(f'Part 1: {part_one(ing_count, alg_map)}') # 1958 63 | print(f'Part 2: {part_two(alg_map)}') # xxscc,mjmqst,gzxnc,vvqj,trnnvn,gbcjqbm,dllbjr,nckqzsg 64 | -------------------------------------------------------------------------------- /2020/22.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | 3 | with open('22.txt', 'r') as file: 4 | data = file.read() 5 | 6 | 7 | def parse_input(data): 8 | player_one = deque([int(n) for n in data.split('\n')[1:26]]) 9 | player_two = deque([int(n) for n in data.split('\n')[28:]]) 10 | return player_one, player_two 11 | 12 | 13 | def part_one(p1, p2): 14 | while p1 and p2: 15 | c1, c2 = p1.popleft(), p2.popleft() 16 | if c1 > c2: 17 | p1 += deque([c1, c2]) 18 | else: 19 | p2 += deque([c2, c1]) 20 | winner = p1 or p2 21 | return sum((len(winner) - i) * v for i, v in enumerate(winner)) 22 | 23 | 24 | def part_two(p1, p2): 25 | def recurse(p1, p2, seen): 26 | while p1 and p2: 27 | if (tuple(p1), tuple(p2)) in seen: 28 | return 1, p1 29 | 30 | seen.add((tuple(p1), tuple(p2))) 31 | 32 | c1, c2 = p1.popleft(), p2.popleft() 33 | if c1 <= len(p1) and c2 <= len(p2): 34 | sp1, sp2 = deque(list(p1)[:c1]), deque(list(p2)[:c2]) 35 | winner, _ = recurse(sp1, sp2, set()) 36 | else: 37 | winner = 1 if c1 > c2 else 0 38 | 39 | if winner == 1: 40 | p1 += deque([c1, c2]) 41 | else: 42 | p2 += deque([c2, c1]) 43 | 44 | return (1, p1) if p1 else (0, p2) 45 | 46 | winner = recurse(p1, p2, set())[1] 47 | return sum((len(winner) - i) * v for i, v in enumerate(winner)) 48 | 49 | 50 | p1, p2 = parse_input(data) 51 | print(f'Part 1: {part_one(p1, p2)}') # 33473 52 | p1, p2 = parse_input(data) 53 | print(f'Part 2: {part_two(p1, p2)}') # 31793 54 | -------------------------------------------------------------------------------- /2020/23.py: -------------------------------------------------------------------------------- 1 | inp = 925176834 2 | 3 | class Node: 4 | def __init__(self, val): 5 | self.val = val 6 | self.nxt = None 7 | 8 | 9 | def build_ll(inp, pt2=False): 10 | inp = list(map(int, str(inp))) 11 | 12 | nodes = {} 13 | prev = None 14 | 15 | for x in inp: 16 | cur = Node(x) 17 | if prev: 18 | prev.nxt = cur 19 | prev = cur 20 | nodes[x] = cur 21 | 22 | if pt2: 23 | for x in range(10, 1000001): 24 | cur = Node(x) 25 | prev.nxt = cur 26 | prev = cur 27 | nodes[x] = cur 28 | 29 | head = nodes[inp[0]] 30 | prev.nxt = head 31 | 32 | return head, nodes 33 | 34 | 35 | def play_game(head, nodes, steps): 36 | maxx = len(nodes) 37 | cur = head 38 | 39 | for _ in range(steps): 40 | val = cur.val 41 | a = cur.nxt 42 | b = a.nxt 43 | c = b.nxt 44 | cur.nxt = c.nxt 45 | dest = val-1 or maxx 46 | while dest in (a.val, b.val, c.val): 47 | dest = dest-1 or maxx 48 | dest_node = nodes[dest] 49 | c.nxt = dest_node.nxt 50 | dest_node.nxt = a 51 | cur = cur.nxt 52 | 53 | return nodes[1] 54 | 55 | 56 | def part_one(inp): 57 | head, nodes = build_ll(inp) 58 | cur = play_game(head, nodes, 100) 59 | res = [] 60 | cur = cur.nxt 61 | while cur.val != 1: 62 | res.append(str(cur.val)) 63 | cur = cur.nxt 64 | return ''.join(res) 65 | 66 | 67 | def part_two(inp): 68 | head, nodes = build_ll(inp, pt2=True) 69 | cur = play_game(head, nodes, 10000000) 70 | return cur.nxt.val * cur.nxt.nxt.val 71 | 72 | 73 | print(f'Part 1: {part_one(inp)}') # 69852437 74 | print(f'Part 2: {part_two(inp)}') # 91408386135 75 | -------------------------------------------------------------------------------- /2020/24.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | with open('24.txt', 'r') as file: 4 | data = file.read().split('\n') 5 | 6 | DIRS = { 7 | 'nw': 0 - 1j, 8 | 'ne': 1 - 1j, 9 | 'sw': -1 + 1j, 10 | 'se': 0 + 1j, 11 | 'e': 1 + 0j, 12 | 'w': -1 + 0j, 13 | } 14 | 15 | 16 | def calc_tile(line): 17 | res = 0 + 0j 18 | i = 0 19 | while i < len(line): 20 | if line[i] in {'n', 's'}: 21 | cur = line[i:i+2] 22 | i += 2 23 | else: 24 | cur = line[i] 25 | i += 1 26 | res += DIRS[cur] 27 | return res 28 | 29 | 30 | def find_black(data): 31 | res = defaultdict(int) 32 | for line in data: 33 | pos = calc_tile(line) 34 | res[pos] ^= 1 35 | return {k for k, v in res.items() if v == 1} 36 | 37 | 38 | def find_neighbors(tiles): 39 | neighbors = defaultdict(int) 40 | for tile in tiles: 41 | for delta in DIRS.values(): 42 | cur = tile + delta 43 | neighbors[cur] += 1 44 | return neighbors 45 | 46 | 47 | def step(tiles): 48 | new_black = set() 49 | neighbors = find_neighbors(tiles) 50 | for k, v in neighbors.items(): 51 | if v == 2 or (v == 1 and k in tiles): 52 | new_black.add(k) 53 | return new_black 54 | 55 | 56 | def part_one(data): 57 | return len(find_black(data)) 58 | 59 | 60 | def part_two(data): 61 | tiles = find_black(data) 62 | for _ in range(100): 63 | tiles = step(tiles) 64 | return len(tiles) 65 | 66 | 67 | print(f'Part 1: {part_one(data)}') # 388 68 | print(f'Part 2: {part_two(data)}') # 4002 69 | -------------------------------------------------------------------------------- /2020/25.py: -------------------------------------------------------------------------------- 1 | with open('25.txt', 'r') as file: 2 | data = file.read() 3 | 4 | 5 | def parse_data(data): 6 | return [int(x) for x in data.split('\n')] 7 | 8 | 9 | key1, key2 = parse_data(data) 10 | 11 | 12 | def find_loop_size(key): 13 | res = 0 14 | cur = 1 15 | while cur != key: 16 | cur = (cur * 7) % 20201227 17 | res += 1 18 | return res 19 | 20 | 21 | def transform(key, loop_size): 22 | cur = 1 23 | for _ in range(loop_size): 24 | cur = (cur * key) % 20201227 25 | return cur 26 | 27 | 28 | def part_one(key1, key2): 29 | return transform(key2, find_loop_size(key1)) 30 | 31 | 32 | print(f'Part 1: {part_one(key1, key2)}') # 10548634 33 | -------------------------------------------------------------------------------- /2020/bootcode/__init__.py: -------------------------------------------------------------------------------- 1 | class BootCode: 2 | def __init__(self, instructions): 3 | self.instructions = instructions 4 | self.accum = 0 5 | self.idx = 0 6 | self.ops = { 7 | 'acc': self.acc, 8 | 'jmp': self.jmp, 9 | 'nop': self.nop, 10 | } 11 | 12 | def acc(self, val): 13 | self.accum += val 14 | self.idx += 1 15 | 16 | def jmp(self, val): 17 | self.idx += val 18 | 19 | def nop(self, val): 20 | self.idx += 1 21 | 22 | def reset(self): 23 | self.idx = 0 24 | self.accum = 0 25 | 26 | def step(self): 27 | op, val = self.instructions[self.idx] 28 | self.ops[op](val) 29 | 30 | def repair(self): 31 | swap = {'jmp': 'nop', 'nop': 'jmp'} 32 | cycle = list(self.run()[1]) 33 | while cycle: 34 | cur = cycle.pop() 35 | op, val = self.instructions[cur] 36 | if op in ('jmp', 'nop'): 37 | self.instructions[cur] = [swap[op], val] 38 | res, seen = self.run() 39 | if not seen: 40 | return res 41 | self.instructions[cur] = [op, val] 42 | 43 | def run(self): 44 | self.reset() 45 | cycle = set() 46 | while True: 47 | if self.idx == len(self.instructions): 48 | return self.accum, None 49 | if self.idx in cycle: 50 | return self.accum, cycle 51 | cycle.add(self.idx) 52 | self.step() 53 | -------------------------------------------------------------------------------- /2021/01.py: -------------------------------------------------------------------------------- 1 | with open('01.txt', 'r') as file: 2 | raw_data = file.read() 3 | 4 | 5 | def parse_input(raw_data): 6 | return list(map(int, raw_data.split('\n'))) 7 | 8 | 9 | data = parse_input(raw_data) 10 | 11 | 12 | def compare(n, arr): 13 | return sum(arr[i] > arr[i - n] for i in range(n, len(arr))) 14 | 15 | 16 | def part_one(data): 17 | return compare(1, data) 18 | 19 | 20 | def part_two(data): 21 | return compare(3, data) 22 | 23 | 24 | print(f'Part 1: {part_one(data)}') # 1446 25 | print(f'Part 2: {part_two(data)}') # 1486 26 | -------------------------------------------------------------------------------- /2021/02.py: -------------------------------------------------------------------------------- 1 | with open('02.txt', 'r') as file: 2 | raw_data = file.read() 3 | 4 | 5 | def parse_input(raw_data): 6 | res = [] 7 | for line in raw_data.split('\n'): 8 | command, num = line.split() 9 | res.append((command, int(num))) 10 | return res 11 | 12 | 13 | data = parse_input(raw_data) 14 | 15 | 16 | def part_one(data): 17 | x = y = 0 18 | for command, num in data: 19 | if command == 'forward': 20 | x += num 21 | elif command == 'down': 22 | y += num 23 | else: 24 | y -= num 25 | return x * y 26 | 27 | 28 | def part_two(data): 29 | x = y = aim = 0 30 | for command, num in data: 31 | if command == 'forward': 32 | x += num 33 | y += aim * num 34 | elif command == 'down': 35 | aim += num 36 | else: 37 | aim -= num 38 | return x * y 39 | 40 | 41 | print(f'Part 1: {part_one(data)}') # 1804520 42 | print(f'Part 2: {part_two(data)}') # 1971095320 43 | -------------------------------------------------------------------------------- /2021/03.py: -------------------------------------------------------------------------------- 1 | with open('03.txt', 'r') as file: 2 | raw_data = file.read() 3 | 4 | 5 | def parse_input(raw_data): 6 | return raw_data.split('\n') 7 | 8 | 9 | data = parse_input(raw_data) 10 | 11 | 12 | def part_one(data): 13 | transpose = [''.join(x) for x in zip(*data)] 14 | res = ['1' if x.count('1') > x.count('0') else '0' for x in transpose] 15 | gamma = int(''.join(res), 2) 16 | epsilon = gamma ^ (1 << len(res)) - 1 17 | return gamma * epsilon 18 | 19 | 20 | def part_two(data): 21 | def calc_rating(data, bit=1): 22 | i = 0 23 | cur = data[:] 24 | while len(cur) > 1: 25 | col = ''.join(list(zip(*cur))[i]) 26 | keep = bit if col.count('1') >= col.count('0') else 1 - bit 27 | cur = [num for num in cur if num[i] == str(keep)] 28 | i += 1 29 | return int(cur[0], 2) 30 | 31 | oxygen_rating = calc_rating(data, 1) 32 | co2_rating = calc_rating(data, 0) 33 | 34 | return oxygen_rating * co2_rating 35 | 36 | 37 | print(f'Part 1: {part_one(data)}') # 852500 38 | print(f'Part 2: {part_two(data)}') # 1007985 39 | -------------------------------------------------------------------------------- /2021/04.py: -------------------------------------------------------------------------------- 1 | with open('04.txt', 'r') as file: 2 | raw_data = file.read() 3 | 4 | 5 | def parse_input(raw_data): 6 | bingo_numbers, *cards = raw_data.split('\n\n') 7 | bingo_numbers = list(map(int, bingo_numbers.split(','))) 8 | cards = [ 9 | [ 10 | list(map(int, row.split())) 11 | for row in card.split('\n') 12 | ] 13 | for card in cards 14 | ] 15 | return bingo_numbers, cards 16 | 17 | 18 | bingo_numbers, cards = parse_input(raw_data) 19 | 20 | 21 | def check_rows(card): 22 | return any(all(x == 'X' for x in row) for row in card) 23 | 24 | 25 | def check_card_win(card): 26 | transpose = list(map(list, zip(*card))) 27 | return check_rows(card) or check_rows(transpose) 28 | 29 | 30 | def check_card_number(card, x): 31 | for i, row in enumerate(card): 32 | for j, v in enumerate(row): 33 | if v == x: 34 | card[i][j] = 'X' 35 | return card 36 | 37 | 38 | def sum_card(card): 39 | return sum(sum(x for x in row if type(x) == int) for row in card) 40 | 41 | 42 | def part_one(bingo_numbers, cards): 43 | for num in bingo_numbers: 44 | for i, card in enumerate(cards): 45 | cur = check_card_number(card, num) 46 | if check_card_win(cur): 47 | return sum_card(cur) * num 48 | cards[i] = cur 49 | 50 | 51 | def part_two(bingo_numbers, cards): 52 | winners = set() 53 | for num in bingo_numbers: 54 | for i, card in enumerate(cards): 55 | if i in winners: 56 | continue 57 | cur = check_card_number(card, num) 58 | if check_card_win(cur): 59 | if len(winners) == len(cards) - 1: 60 | return sum_card(cur) * num 61 | winners.add(i) 62 | cards[i] = cur 63 | 64 | 65 | print(f'Part 1: {part_one(bingo_numbers, cards)}') # 50008 66 | print(f'Part 2: {part_two(bingo_numbers, cards)}') # 17408 67 | -------------------------------------------------------------------------------- /2021/05.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import Counter 3 | 4 | with open('05.txt', 'r') as file: 5 | raw_data = file.read() 6 | 7 | 8 | def parse_input(raw_data): 9 | lines = re.findall(r'(\d+),(\d+) -> (\d+),(\d+)', raw_data) 10 | return [list(map(int, line)) for line in lines] 11 | 12 | 13 | data = parse_input(raw_data) 14 | 15 | 16 | def straight_lines(data): 17 | points = Counter() 18 | for x1, y1, x2, y2 in data: 19 | if x1 == x2: 20 | y1, y2 = min(y1, y2), max(y1, y2) 21 | for i in range(y1, y2+1): 22 | points[(x1, i)] += 1 23 | elif y1 == y2: 24 | x1, x2 = min(x1, x2), max(x1, x2) 25 | for i in range(x1, x2+1): 26 | points[(i, y1)] += 1 27 | return points 28 | 29 | 30 | def diagonal_lines(data): 31 | points = Counter() 32 | for x1, y1, x2, y2 in data: 33 | if x1 != x2 and y1 != y2: 34 | while x1 != x2: 35 | points[(x1, y1)] += 1 36 | x1 += 1 if x1 < x2 else -1 37 | y1 += 1 if y1 < y2 else -1 38 | points[(x1, y1)] += 1 39 | return points 40 | 41 | 42 | def part_one(data): 43 | points = straight_lines(data) 44 | return sum(v > 1 for v in points.values()) 45 | 46 | 47 | def part_two(data): 48 | points = straight_lines(data) + diagonal_lines(data) 49 | return sum(v > 1 for v in points.values()) 50 | 51 | 52 | print(f'Part 1: {part_one(data)}') # 7438 53 | print(f'Part 2: {part_two(data)}') # 21406 54 | -------------------------------------------------------------------------------- /2021/06.py: -------------------------------------------------------------------------------- 1 | with open('06.txt', 'r') as file: 2 | raw_data = file.read() 3 | 4 | 5 | def parse_input(raw_data): 6 | res = [0] * 9 7 | for i in map(int, raw_data.split(',')): 8 | res[i] += 1 9 | return res 10 | 11 | 12 | def solve(days): 13 | fish = parse_input(raw_data) 14 | for i in range(days): 15 | fish[(i + 7) % 9] += fish[i % 9] 16 | return sum(fish) 17 | 18 | 19 | def part_one(): 20 | return solve(80) 21 | 22 | 23 | def part_two(): 24 | return solve(256) 25 | 26 | 27 | print(f'Part 1: {part_one()}') # 391888 28 | print(f'Part 2: {part_two()}') # 1754597645339 29 | -------------------------------------------------------------------------------- /2021/07.py: -------------------------------------------------------------------------------- 1 | with open("07.txt", "r") as file: 2 | raw_data = file.read() 3 | 4 | 5 | def parse_input(raw_data): 6 | return list(map(int, raw_data.split(","))) 7 | 8 | 9 | def median(a): 10 | n = len(a) 11 | a.sort() 12 | if n & 1: 13 | return a[n // 2] 14 | else: 15 | return (a[n // 2 - 1] + a[n // 2]) / 2 16 | 17 | 18 | def mean(a): 19 | return sum(a) / len(a) 20 | 21 | 22 | def gauss(n): 23 | return (n * (n + 1)) // 2 24 | 25 | 26 | def part_one(data): 27 | m = int(median(data)) 28 | return sum(abs(x - m) for x in data) 29 | 30 | 31 | def part_two(data): 32 | res = float("inf") 33 | m = int(mean(data)) 34 | for i in range(m - 1, m + 2): 35 | res = min(res, sum(gauss(abs(x - i)) for x in data)) 36 | return res 37 | 38 | 39 | data = parse_input(raw_data) 40 | print(f"Part 1: {part_one(data)}") # 335330 41 | print(f"Part 2: {part_two(data)}") # 92439766 42 | -------------------------------------------------------------------------------- /2021/09.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | from functools import reduce 3 | 4 | with open("09.txt", "r") as file: 5 | raw_data = file.read() 6 | 7 | 8 | def parse_input(raw_data): 9 | res = [] 10 | for line in raw_data.split("\n"): 11 | res.append(list(map(int, line))) 12 | return res 13 | 14 | 15 | DATA = parse_input(raw_data) 16 | N, M = len(DATA), len(DATA[0]) 17 | 18 | 19 | def check_low(i, j): 20 | val = DATA[i][j] 21 | cur = float("inf") 22 | for ni, nj in ((i + 1, j), (i - 1, j), (i, j + 1), (i, j - 1)): 23 | if 0 <= ni < N and 0 <= nj < M: 24 | cur = min(cur, DATA[ni][nj]) 25 | if cur <= val: 26 | return 0 27 | return val + 1 28 | 29 | 30 | def part_one(data): 31 | return sum(check_low(i, j) for i in range(N) for j in range(M)) 32 | 33 | 34 | SEEN = set() 35 | 36 | 37 | def flood_fill(i, j): 38 | res = 0 39 | q = deque([(i, j)]) 40 | SEEN.add((i, j)) 41 | while q: 42 | ci, cj = q.popleft() 43 | res += 1 44 | for ni, nj in ((ci + 1, cj), (ci - 1, cj), (ci, cj + 1), (ci, cj - 1)): 45 | if ( 46 | 0 <= ni < N and 47 | 0 <= nj < M and 48 | DATA[ni][nj] != 9 and 49 | (ni, nj) not in SEEN 50 | ): 51 | q.append((ni, nj)) 52 | SEEN.add((ni, nj)) 53 | return res 54 | 55 | 56 | def part_two(data): 57 | res = [] 58 | for i in range(N): 59 | for j in range(M): 60 | if data[i][j] == 9 or (i, j) in SEEN: 61 | continue 62 | res.append(flood_fill(i, j)) 63 | return reduce(lambda x, y: x*y, sorted(res)[-3:]) 64 | 65 | 66 | print(f"Part 1: {part_one(DATA)}") # 591 67 | print(f"Part 2: {part_two(DATA)}") # 1113424 68 | -------------------------------------------------------------------------------- /2021/10.py: -------------------------------------------------------------------------------- 1 | with open("10.txt", "r") as file: 2 | raw_data = file.read() 3 | 4 | 5 | def parse_input(raw_data): 6 | return raw_data.split("\n") 7 | 8 | 9 | DATA = parse_input(raw_data) 10 | MAP = { 11 | ")": "(", 12 | "]": "[", 13 | "}": "{", 14 | ">": "<" 15 | } 16 | POINTS = { 17 | ")": 3, 18 | "]": 57, 19 | "}": 1197, 20 | ">": 25137, 21 | "(": 1, 22 | "[": 2, 23 | "{": 3, 24 | "<": 4 25 | } 26 | 27 | 28 | def corrupt_calc(line): 29 | stack = [] 30 | res = 0 31 | for c in line: 32 | if not stack or c in "([{<": 33 | stack.append(c) 34 | else: 35 | cur = stack.pop() 36 | if MAP[c] != cur: 37 | res += POINTS[c] 38 | return res 39 | 40 | 41 | def find_remaining_stack(line): 42 | stack = [] 43 | for c in line: 44 | if not stack or c in "([{<": 45 | stack.append(c) 46 | elif MAP[c] == stack[-1]: 47 | stack.pop() 48 | return stack 49 | 50 | 51 | def calc_score(stack): 52 | res = 0 53 | while stack: 54 | res = res * 5 + POINTS[stack.pop()] 55 | return res 56 | 57 | 58 | def part_one(data): 59 | return sum(corrupt_calc(line) for line in data) 60 | 61 | 62 | def part_two(data): 63 | res = [] 64 | for line in data: 65 | if not corrupt_calc(line): 66 | stack = find_remaining_stack(line) 67 | res.append(calc_score(stack)) 68 | return sorted(res)[len(res) // 2] 69 | 70 | 71 | print(f"Part 1: {part_one(DATA)}") # 358737 72 | print(f"Part 2: {part_two(DATA)}") # 4329504793 73 | -------------------------------------------------------------------------------- /2021/11.py: -------------------------------------------------------------------------------- 1 | from itertools import count, product 2 | 3 | with open("11.txt", "r") as file: 4 | raw_data = file.read() 5 | 6 | 7 | def parse_input(raw_data): 8 | res = [] 9 | for row in raw_data.split("\n"): 10 | res.append(list(map(int, row))) 11 | return res 12 | 13 | 14 | DATA = parse_input(raw_data) 15 | N, M = len(DATA), len(DATA[0]) 16 | 17 | 18 | def flash(i, j, grid, flashed): 19 | grid[i][j] = 0 20 | flashed.add((i, j)) 21 | for di, dj in filter(any, product([-1, 0, 1], repeat=2)): 22 | ni, nj = i + di, j + dj 23 | if ( 24 | 0 <= ni < N and 25 | 0 <= nj < M and 26 | (ni, nj) not in flashed 27 | ): 28 | grid[ni][nj] += 1 29 | if grid[ni][nj] == 10: 30 | flash(ni, nj, grid, flashed) 31 | 32 | 33 | def step(grid): 34 | flashed = set() 35 | for i in range(N): 36 | for j in range(M): 37 | grid[i][j] += int((i, j) not in flashed) 38 | if grid[i][j] == 10: 39 | flash(i, j, grid, flashed) 40 | return len(flashed) 41 | 42 | 43 | def part_one(): 44 | grid = [row[:] for row in DATA] 45 | res = 0 46 | for _ in range(100): 47 | res += step(grid) 48 | return res 49 | 50 | 51 | def part_two(): 52 | grid = [row[:] for row in DATA] 53 | for i in count(1): 54 | if step(grid) == N * M: 55 | return i 56 | 57 | 58 | print(f"Part 1: {part_one()}") # 1688 59 | print(f"Part 2: {part_two()}") # 403 60 | -------------------------------------------------------------------------------- /2021/12.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | with open("12.txt", "r") as file: 4 | raw_data = file.read() 5 | 6 | 7 | def parse_input(raw_data): 8 | res = defaultdict(list) 9 | for row in raw_data.split("\n"): 10 | u, v = row.split('-') 11 | res[u].append(v) 12 | res[v].append(u) 13 | return res 14 | 15 | 16 | GRAPH = parse_input(raw_data) 17 | 18 | 19 | def dfs(node, seen, two=False): 20 | if node == 'end': 21 | return 1 22 | if node == 'start' and seen: 23 | return 0 24 | if node.islower() and node in seen: 25 | if two: 26 | two = False 27 | else: 28 | return 0 29 | seen = seen | {node} 30 | res = 0 31 | for nei in GRAPH[node]: 32 | res += dfs(nei, seen, two) 33 | return res 34 | 35 | 36 | def part_one(): 37 | return dfs('start', set()) 38 | 39 | 40 | def part_two(): 41 | return dfs('start', set(), True) 42 | 43 | 44 | print(f"Part 1: {part_one()}") # 3369 45 | print(f"Part 2: {part_two()}") # 85883 46 | -------------------------------------------------------------------------------- /2021/13.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | with open('13.txt', 'r') as file: 4 | raw_data = file.read() 5 | 6 | 7 | def parse_input(raw_data): 8 | points = [ 9 | (int(x), int(y)) 10 | for x, y in re.findall(r'(\d+),(\d+)', raw_data) 11 | ] 12 | folds = [ 13 | (axis, int(n)) 14 | for axis, n in re.findall(r'fold along ([xy])=(\d+)', raw_data) 15 | ] 16 | return points, folds 17 | 18 | 19 | POINTS, FOLDS = parse_input(raw_data) 20 | 21 | 22 | def make_fold(axis, line, points): 23 | res = set() 24 | for x, y in points: 25 | if axis == 'x': 26 | x = line - abs(x - line) 27 | else: # axis == 'y' 28 | y = line - abs(y - line) 29 | res.add((x, y)) 30 | return res 31 | 32 | 33 | def part_one(): 34 | return len(make_fold(*FOLDS[0], set(POINTS))) 35 | 36 | 37 | def part_two(): 38 | points = set(POINTS) 39 | for fold in FOLDS: 40 | points = make_fold(*fold, points) 41 | 42 | max_x, max_y = max(x for x, _ in points), max(y for _, y in points) 43 | 44 | grid = [[' ' for _ in range(max_x + 1)] for _ in range(max_y + 1)] 45 | for j, i in points: 46 | grid[i][j] = '#' 47 | 48 | for row in grid: 49 | print(''.join(row)) 50 | 51 | 52 | print(f"Part 1: {part_one()}") # 701 53 | print(f"Part 2: {part_two()}") # FPEKBEJL 54 | -------------------------------------------------------------------------------- /2021/14.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import Counter 3 | 4 | with open('14.txt', 'r') as file: 5 | raw_data = file.read() 6 | 7 | 8 | def parse_input(raw_data): 9 | template = raw_data.split('\n')[0] 10 | pairs = re.findall(r'(.+) -> (.+)', raw_data) 11 | return template, dict(pairs) 12 | 13 | 14 | TEMPLATE, RULES = parse_input(raw_data) 15 | 16 | 17 | def step(count): 18 | res = Counter() 19 | for k in count: 20 | res[k[0] + RULES[k]] += count[k] 21 | res[RULES[k] + k[1]] += count[k] 22 | return res 23 | 24 | 25 | def run_n_steps(n): 26 | cur = Counter() 27 | for i in range(len(TEMPLATE) - 1): 28 | cur[TEMPLATE[i:i+2]] += 1 29 | 30 | for _ in range(n): 31 | nxt = step(cur) 32 | cur = nxt 33 | 34 | res = Counter() 35 | for k in cur: 36 | res[k[0]] += cur[k] 37 | res[TEMPLATE[-1]] += 1 38 | 39 | return max(res.values()) - min(res.values()) 40 | 41 | 42 | def part_one(): 43 | return run_n_steps(10) 44 | 45 | 46 | def part_two(): 47 | return run_n_steps(40) 48 | 49 | 50 | print(f'Part 1: {part_one()}') # 2745 51 | print(f'Part 2: {part_two()}') # 3420801168962 52 | -------------------------------------------------------------------------------- /2021/15.py: -------------------------------------------------------------------------------- 1 | import heapq 2 | 3 | with open('15.txt', 'r') as file: 4 | raw_data = file.read() 5 | 6 | 7 | def parse_input(raw_data): 8 | res = [] 9 | for line in raw_data.split('\n'): 10 | res.append(list(map(int, line))) 11 | return res 12 | 13 | 14 | CAVERN = parse_input(raw_data) 15 | N, M = len(CAVERN), len(CAVERN[0]) 16 | 17 | 18 | def dijkstra(graph): 19 | r, c = len(graph), len(graph[0]) 20 | costs = {} 21 | heap = [(0, 0, 0)] 22 | while heap: 23 | cost, i, j = heapq.heappop(heap) 24 | if (i, j) == (r - 1, c - 1): 25 | return cost 26 | for ni, nj in ((i + 1, j), (i - 1, j), (i, j + 1), (i, j - 1)): 27 | if 0 <= ni < r and 0 <= nj < c: 28 | ncost = cost + graph[ni][nj] 29 | if costs.get((ni, nj), float('inf')) <= ncost: 30 | continue 31 | costs[(ni, nj)] = ncost 32 | heapq.heappush(heap, (ncost, ni, nj)) 33 | 34 | 35 | def part_one(): 36 | return dijkstra(CAVERN) 37 | 38 | 39 | def part_two(): 40 | r, c = len(CAVERN) * 5, len(CAVERN[0]) * 5 41 | expanded = [[0 for _ in range(c)] for _ in range(r)] 42 | for i in range(r): 43 | for j in range(c): 44 | dist = i // N + j // M 45 | cur = CAVERN[i % N][j % M] + dist 46 | cur = cur % 9 or cur 47 | expanded[i][j] = cur 48 | return dijkstra(expanded) 49 | 50 | 51 | print(f'Part 1: {part_one()}') # 748 52 | print(f'Part 2: {part_two()}') # 3045 53 | -------------------------------------------------------------------------------- /2021/16.py: -------------------------------------------------------------------------------- 1 | from functools import reduce 2 | 3 | with open('16.txt', 'r') as file: 4 | raw_data = file.read() 5 | 6 | 7 | def parse_input(raw_data): 8 | return ''.join(format(int(x, 16), '04b') for x in raw_data) 9 | 10 | 11 | DATA = parse_input(raw_data) 12 | VERSIONS = [] 13 | OPS = { 14 | 0: sum, 15 | 1: lambda iter: reduce(lambda x, y: x * y, iter), 16 | 2: min, 17 | 3: max, 18 | 5: lambda x: int(x[0] > x[1]), 19 | 6: lambda x: int(x[0] < x[1]), 20 | 7: lambda x: int(x[0] == x[1]) 21 | } 22 | 23 | 24 | def parse_literal(packet, i): 25 | ni = i 26 | res = [] 27 | while True: 28 | res.append(packet[ni + 1 : ni + 5]) 29 | if packet[ni] == '0': 30 | return ni + 5, int(''.join(res), 2) 31 | ni += 5 32 | 33 | 34 | def parse_packet(packet, i): 35 | version, type_id = int(packet[i : i + 3], 2), int(packet[i + 3 : i + 6], 2) 36 | VERSIONS.append(version) 37 | if type_id == 4: 38 | ni, val = parse_literal(packet, i + 6) 39 | return ni, val 40 | 41 | values = [] 42 | len_type_id = packet[i + 6] 43 | if len_type_id == '0': 44 | total_len = int(packet[i + 7 : i + 22], 2) 45 | ni = i + 22 46 | while ni < i + 22 + total_len: 47 | ni, val = parse_packet(packet, ni) 48 | values.append(val) 49 | else: 50 | packets = int(packet[i + 7 : i + 18], 2) 51 | ni = i + 18 52 | for _ in range(packets): 53 | ni, val = parse_packet(packet, ni) 54 | values.append(val) 55 | 56 | return ni, OPS[type_id](values) 57 | 58 | 59 | def part_one(): 60 | _ = parse_packet(DATA, 0) 61 | return sum(VERSIONS) 62 | 63 | 64 | def part_two(): 65 | return parse_packet(DATA, 0)[1] 66 | 67 | 68 | print(f'Part 1: {part_one()}') # 955 69 | print(f'Part 2: {part_two()}') # 158135423448 70 | -------------------------------------------------------------------------------- /2021/17.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | with open('17.txt', 'r') as file: 4 | raw_data = file.read() 5 | 6 | 7 | def parse_input(raw_data): 8 | pairs = re.findall(r'(-?\d+)..(-?\d+)', raw_data) 9 | return [int(x) for pair in pairs for x in pair] 10 | 11 | 12 | MIN_X, MAX_X, MIN_Y, MAX_Y = parse_input(raw_data) 13 | 14 | 15 | def step(x, y, dx, dy): 16 | x += dx 17 | y += dy 18 | dx += -1 if dx > 0 else 1 if dx < 0 else 0 19 | dy += -1 20 | return x, y, dx, dy 21 | 22 | 23 | def run_steps(dx, dy): 24 | x, y = 0, 0 25 | while y >= MIN_Y: 26 | x, y, dx, dy = step(x, y, dx, dy) 27 | if MIN_X <= x <= MAX_X and MIN_Y <= y <= MAX_Y: 28 | return True 29 | return False 30 | 31 | 32 | def part_one(): 33 | return MIN_Y * (MIN_Y + 1) // 2 34 | 35 | 36 | def part_two(): 37 | return sum( 38 | run_steps(x, y) 39 | for x in range(MAX_X + 1) 40 | for y in range(MIN_Y, -MIN_Y) 41 | ) 42 | 43 | 44 | print(f'Part 1: {part_one()}') # 8256 45 | print(f'Part 2: {part_two()}') # 2326 46 | -------------------------------------------------------------------------------- /2021/20.py: -------------------------------------------------------------------------------- 1 | from itertools import product 2 | 3 | with open('20.txt', 'r') as file: 4 | raw_data = file.read() 5 | 6 | 7 | def parse_input(raw_data): 8 | key, image = raw_data.split('\n\n') 9 | key = [int(x == '#') for x in key] 10 | return key, [[int(x == '#') for x in line] for line in image.split('\n')] 11 | 12 | 13 | KEY, IMAGE = parse_input(raw_data) 14 | 15 | 16 | def get_surrounding(i, j, image, default=0): 17 | N, M = len(image), len(image[0]) 18 | res = [] 19 | for di, dj in product((-1, 0, 1), repeat=2): 20 | ni, nj = i+di, j+dj 21 | if 0 <= ni < N and 0 <= nj < M: 22 | res.append(image[ni][nj]) 23 | else: 24 | res.append(default) 25 | return res 26 | 27 | 28 | def get_output_pixel(b): 29 | idx = 0 30 | for n in b: 31 | idx <<= 1 32 | idx += n 33 | return KEY[idx] 34 | 35 | 36 | def calc_image(image, default=0): 37 | N, M = len(image), len(image[0]) 38 | res = [] 39 | for i in range(-3, N+3): 40 | cur = [] 41 | for j in range(-3, M+3): 42 | b = get_surrounding(i, j, image, default) 43 | cur.append(get_output_pixel(b)) 44 | res.append(cur) 45 | return res 46 | 47 | 48 | def part_one(): 49 | image = [[x for x in row] for row in IMAGE] 50 | for i in range(2): 51 | image = calc_image(image, i & 1) 52 | return sum(sum(row) for row in image) 53 | 54 | 55 | def part_two(): 56 | image = [[x for x in row] for row in IMAGE] 57 | for i in range(50): 58 | image = calc_image(image, i & 1) 59 | return sum(sum(row) for row in image) 60 | 61 | 62 | print(f'Part 1: {part_one()}') # 5354 63 | print(f'Part 2: {part_two()}') # 18269 64 | -------------------------------------------------------------------------------- /2021/21.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import deque 3 | from functools import lru_cache 4 | from itertools import product 5 | 6 | with open('21.txt', 'r') as file: 7 | raw_data = file.read() 8 | 9 | 10 | def parse_data(raw_data): 11 | return (int(x) for x in re.findall(r'position: (\d+)', raw_data)) 12 | 13 | 14 | PLAYER_ONE, PLAYER_TWO = parse_data(raw_data) 15 | 16 | 17 | def part_one(pos_one, pos_two): 18 | pos = [pos_one, pos_two] 19 | rolls = 0 20 | q = deque(range(1, 101)) 21 | score = [0, 0] 22 | turn = 0 23 | while max(score) < 1000: 24 | move = sum(list(q)[:3]) 25 | q.rotate(-3) 26 | rolls += 3 27 | pos[turn] = (pos[turn] + move - 1) % 10 + 1 28 | score[turn] += pos[turn] 29 | turn = 1 - turn 30 | return min(score) * rolls 31 | 32 | 33 | def part_two(pos_one, pos_two): 34 | 35 | @lru_cache(None) 36 | def recur(pos, scores=(0, 0), turn=0): 37 | cur_pos, cur_scores = list(pos), list(scores) 38 | wins = [0, 0] 39 | for rolls in product((1, 2, 3), repeat=3): 40 | move = sum(rolls) 41 | cur_pos[turn] = (pos[turn] + move - 1) % 10 + 1 42 | cur_scores[turn] = scores[turn] + cur_pos[turn] 43 | if cur_scores[turn] >= 21: 44 | wins[turn] += 1 45 | else: 46 | one, two = recur(tuple(cur_pos), tuple(cur_scores), 1-turn) 47 | wins[0] += one 48 | wins[1] += two 49 | return wins 50 | 51 | return max(recur((pos_one, pos_two))) 52 | 53 | 54 | print(f'Part 1: {part_one(PLAYER_ONE, PLAYER_TWO)}') # 864900 55 | print(f'Part 2: {part_two(PLAYER_ONE, PLAYER_TWO)}') # 575111835924670 56 | -------------------------------------------------------------------------------- /2021/25.py: -------------------------------------------------------------------------------- 1 | with open('25.txt', 'r') as file: 2 | raw_data = file.read() 3 | 4 | 5 | def parse_input(raw_data): 6 | return [list(line) for line in raw_data.split('\n')] 7 | 8 | 9 | DATA = parse_input(raw_data) 10 | 11 | 12 | def move_east(arr): 13 | N, M = len(arr), len(arr[0]) 14 | moves = [] 15 | for i in range(N): 16 | for j in range(M): 17 | if arr[i][j] == '>' and arr[i][(j+1) % M] == '.': 18 | moves.append((i, j, (j+1) % M)) 19 | for i, j, nj in moves: 20 | arr[i][j] = '.' 21 | arr[i][nj] = '>' 22 | return arr, bool(moves) 23 | 24 | 25 | def move_south(arr): 26 | N, M = len(arr), len(arr[0]) 27 | moves = [] 28 | for i in range(N): 29 | for j in range(M): 30 | if arr[i][j] == 'v' and arr[(i+1) % N][j] == '.': 31 | moves.append((i, (i+1) % N, j)) 32 | for i, ni, j in moves: 33 | arr[i][j] = '.' 34 | arr[ni][j] = 'v' 35 | return arr, bool(moves) 36 | 37 | 38 | def part_one(arr): 39 | res = 0 40 | moved = True 41 | while moved: 42 | arr, east = move_east(arr) 43 | arr, south = move_south(arr) 44 | moved = east or south 45 | res += 1 46 | return res 47 | 48 | 49 | print(f'Part 1: {part_one(DATA)}') # 458 50 | -------------------------------------------------------------------------------- /2022/01.py: -------------------------------------------------------------------------------- 1 | with open("01.txt", "r") as file: 2 | data = file.read().strip() 3 | 4 | ELVES = sorted( 5 | sum(int(x) for x in elf.split("\n")) 6 | for elf in data.split("\n\n") 7 | ) 8 | 9 | print(f"Part 1: {ELVES[-1]}") # 71502 10 | print(f"Part 2: {sum(ELVES[-3:])}") # 208191 11 | -------------------------------------------------------------------------------- /2022/02.py: -------------------------------------------------------------------------------- 1 | with open("02.txt", "r") as file: 2 | data = file.read().strip() 3 | 4 | 5 | def part_one(data): 6 | res = 0 7 | for line in data.split("\n"): 8 | a, b = line.split(" ") 9 | cur = ord(b) - ord("A") - 22 10 | if a == "A": 11 | cur += 3 if b == "X" else 6 if b == "Y" else 0 12 | elif a == "B": 13 | cur += 0 if b == "X" else 3 if b == "Y" else 6 14 | else: 15 | cur += 6 if b == "X" else 0 if b == "Y" else 3 16 | res += cur 17 | return res 18 | 19 | 20 | def part_two(data): 21 | res = 0 22 | for line in data.split("\n"): 23 | a, b = line.split(" ") 24 | cur = (ord(b) - ord("A") - 23) * 3 25 | if a == "A": 26 | cur += 3 if b == "X" else 1 if b == "Y" else 2 27 | elif a == "B": 28 | cur += 1 if b == "X" else 2 if b == "Y" else 3 29 | else: 30 | cur += 2 if b == "X" else 3 if b == "Y" else 1 31 | res += cur 32 | return res 33 | 34 | 35 | print(f"Part 1: {part_one(data)}") # 13005 36 | print(f"Part 2: {part_two(data)}") # 11373 37 | -------------------------------------------------------------------------------- /2022/03.py: -------------------------------------------------------------------------------- 1 | with open("03.txt", "r") as file: 2 | data = file.read().strip() 3 | 4 | RUCKSACKS = data.split("\n") 5 | 6 | 7 | def calc_priority(group): 8 | return sum( 9 | ord(c) - ord("a") + 1 10 | if c.islower() 11 | else ord(c) - ord("A") + 27 12 | for c in group 13 | ) 14 | 15 | 16 | def part_one(): 17 | res = 0 18 | for rucksack in RUCKSACKS: 19 | m = len(rucksack) >> 1 20 | a, b = rucksack[:m], rucksack[m:] 21 | res += calc_priority(set(a) & set(b)) 22 | return res 23 | 24 | 25 | def part_two(): 26 | res = 0 27 | for i in range(0, len(RUCKSACKS), 3): 28 | a, b, c = RUCKSACKS[i : i + 3] 29 | res += calc_priority(set(a) & set(b) & set(c)) 30 | return res 31 | 32 | 33 | print(f"Part 1: {part_one()}") # 7826 34 | print(f"Part 2: {part_two()}") # 2577 35 | -------------------------------------------------------------------------------- /2022/04.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | with open("04.txt", "r") as file: 4 | data = file.read().strip() 5 | 6 | PAIRS = [ 7 | tuple(map(int, g)) 8 | for g in re.findall( 9 | r"(\d+)-(\d+),(\d+)-(\d+)", data 10 | ) 11 | ] 12 | 13 | 14 | def part_one(): 15 | return sum( 16 | (a <= c and b >= d) 17 | or (a >= c and b <= d) 18 | for a, b, c, d in PAIRS 19 | ) 20 | 21 | 22 | def part_two(): 23 | return sum( 24 | (a <= c and b >= c) 25 | or (a <= d and b >= d) 26 | or (c <= a and d >= a) 27 | or (c <= b and d >= b) 28 | for a, b, c, d in PAIRS 29 | ) 30 | 31 | 32 | print(f"Part 1: {part_one()}") # 567 33 | print(f"Part 2: {part_two()}") # 907 34 | -------------------------------------------------------------------------------- /2022/05.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | with open("05.txt", "r") as file: 4 | data = file.read().strip() 5 | 6 | MOVES = [ 7 | (int(x), int(y) - 1, int(z) - 1) 8 | for x, y, z in re.findall( 9 | r"move (\d+) from (\d+) to (\d+)", data 10 | ) 11 | ] 12 | 13 | 14 | def parse_crates(arrangement): 15 | stacks = [[] for _ in range(9)] 16 | for line in arrangement.split("\n"): 17 | for i, c in enumerate(line): 18 | if c.isalpha(): 19 | stacks[i // 4].append(c) 20 | return [stack[::-1] for stack in stacks] 21 | 22 | 23 | def solve(part_two=False): 24 | stacks = parse_crates(data.split("\n\n")[0]) 25 | for n, i, j in MOVES: 26 | cur = [stacks[i].pop() for _ in range(n)] 27 | stacks[j].extend(cur[::-1] if part_two else cur) 28 | return "".join(stack[-1] for stack in stacks) 29 | 30 | 31 | print(f"Part 1: {solve()}") # QNHWJVJZW 32 | print(f"Part 2: {solve(True)}") # BPCZJLFJW 33 | -------------------------------------------------------------------------------- /2022/06.py: -------------------------------------------------------------------------------- 1 | with open("06.txt", "r") as file: 2 | data = file.read().strip() 3 | 4 | 5 | def find_substring(data, n): 6 | cur = data[:n] 7 | i = n 8 | while i < len(data): 9 | if len(set(cur)) == n: 10 | return i 11 | cur = cur[1:] + data[i] 12 | i += 1 13 | 14 | 15 | print(f"Part 1: {find_substring(data, 4)}") # 1779 16 | print(f"Part 2: {find_substring(data, 14)}") # 2635 17 | -------------------------------------------------------------------------------- /2022/07.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | with open("07.txt", "r") as file: 4 | data = file.read().strip() 5 | 6 | 7 | def get_sizes(data): 8 | sizes = defaultdict(int) 9 | stack = [] 10 | for line in data.split("\n"): 11 | cmd = line.split() 12 | if cmd[0] == "dir" or cmd[1] == "ls": 13 | continue 14 | if cmd[0].isdigit(): 15 | cur = int(cmd[0]) 16 | for i in range(1, len(stack) + 1): 17 | sizes[tuple(stack[:i])] += cur 18 | elif cmd[1:3] == ["cd", ".."]: 19 | stack.pop() 20 | else: 21 | stack.append(cmd[2]) 22 | return sizes 23 | 24 | 25 | SIZES = get_sizes(data) 26 | 27 | 28 | def part_one(): 29 | return sum(x for x in SIZES.values() if x <= 100000) 30 | 31 | 32 | def part_two(): 33 | need = 30000000 - (70000000 - SIZES[("/",)]) 34 | res = SIZES[("/",)] 35 | for v in SIZES.values(): 36 | if v >= need: 37 | res = min(res, v) 38 | return res 39 | 40 | 41 | print(f"Part 1: {part_one()}") # 1447046 42 | print(f"Part 2: {part_two()}") # 578710 43 | -------------------------------------------------------------------------------- /2022/08.py: -------------------------------------------------------------------------------- 1 | with open("08.txt", "r") as file: 2 | data = file.read().strip() 3 | 4 | TREES = [list(map(int, line)) for line in data.split("\n")] 5 | N, M = len(TREES), len(TREES[0]) 6 | 7 | 8 | def is_visible(i, j): 9 | for di, dj in ((-1, 0), (1, 0), (0, -1), (0, 1)): 10 | ni, nj = i + di, j + dj 11 | while ( 12 | 0 <= ni < N 13 | and 0 <= nj < M 14 | and TREES[ni][nj] < TREES[i][j] 15 | ): 16 | ni += di 17 | nj += dj 18 | if not (0 <= ni < N and 0 <= nj < M): 19 | return True 20 | return False 21 | 22 | 23 | def scenic_score(i, j): 24 | res = 1 25 | for di, dj in ((-1, 0), (1, 0), (0, -1), (0, 1)): 26 | ni, nj = i + di, j + dj 27 | cur = 0 28 | while 0 <= ni < N and 0 <= nj < M: 29 | cur += 1 30 | if TREES[ni][nj] >= TREES[i][j]: 31 | break 32 | ni += di 33 | nj += dj 34 | res *= cur 35 | return res 36 | 37 | 38 | def part_one(): 39 | return sum(is_visible(i, j) for i in range(N) for j in range(M)) 40 | 41 | 42 | def part_two(): 43 | return max(scenic_score(i, j) for i in range(N) for j in range(M)) 44 | 45 | 46 | print(f"Part 1: {part_one()}") # 1832 47 | print(f"Part 2: {part_two()}") # 157320 48 | -------------------------------------------------------------------------------- /2022/09.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | with open("09.txt", "r") as file: 4 | data = file.read().strip() 5 | 6 | STEPS = [(x, int(y)) for x, y in re.findall(r"(\w) (\d+)", data)] 7 | 8 | DIRS = { 9 | "U": -1j, 10 | "D": 1j, 11 | "L": -1, 12 | "R": 1, 13 | } 14 | 15 | 16 | def update(rope, i, dir): 17 | sgn = lambda x: complex( 18 | (x.real > 0) - (x.real < 0), (x.imag > 0) - (x.imag < 0) 19 | ) 20 | head, tail = rope[i - 1], rope[i] 21 | dpos = head - tail 22 | if abs(dpos) > abs(1 + 1j): 23 | rope[i] += sgn(dpos) 24 | return rope 25 | 26 | 27 | def solve(knots): 28 | rope = [0] * knots 29 | seen = set() 30 | for dir, dist in STEPS: 31 | seen.add(rope[-1]) 32 | for _ in range(dist): 33 | rope[0] += DIRS[dir] 34 | for i in range(1, knots): 35 | rope = update(rope, i, dir) 36 | seen.add(rope[-1]) 37 | return len(seen) 38 | 39 | 40 | print(f"Part 1: {solve(2)}") # 6339 41 | print(f"Part 2: {solve(10)}") # 2541 42 | -------------------------------------------------------------------------------- /2022/10.py: -------------------------------------------------------------------------------- 1 | with open("10.txt", "r") as file: 2 | data = file.read().strip() 3 | 4 | 5 | def parse_data(data): 6 | reg = [1] 7 | for line in data.split("\n"): 8 | reg.append(reg[-1]) 9 | if x := line[5:]: 10 | reg.append(reg[-1] + int(x)) 11 | return reg 12 | 13 | 14 | REG = parse_data(data) 15 | 16 | 17 | def part_one(): 18 | return sum( 19 | cycle * x 20 | for cycle, x in enumerate(REG, 1) 21 | if cycle % 40 == 20 22 | ) 23 | 24 | 25 | def part_two(): 26 | for cycle, x in enumerate(REG): 27 | print( 28 | "#" if abs(cycle % 40 - x) <= 1 else " ", 29 | end="" if (cycle + 1) % 40 else "\n", 30 | ) 31 | 32 | 33 | print(f"Part 1: {part_one()}") # 11720 34 | print(f"Part 2: {part_two()}") # ERCREPCJ 35 | -------------------------------------------------------------------------------- /2022/12.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | 3 | 4 | def parse_grid(data): 5 | grid = [list(line) for line in data.split("\n")] 6 | a_list = [] 7 | start = end = None 8 | for i, row in enumerate(grid): 9 | for j, v in enumerate(row): 10 | if v == "S": 11 | start = (i, j) 12 | a_list.append((i, j)) 13 | grid[i][j] = "a" 14 | elif v == "E": 15 | end = (i, j) 16 | grid[i][j] = "z" 17 | elif v == "a": 18 | a_list.append((i, j)) 19 | return grid, start, end, a_list 20 | 21 | 22 | def bfs(grid, starts, end): 23 | seen = set() 24 | q = deque([(start, 0) for start in starts]) 25 | while q: 26 | pos, dist = q.popleft() 27 | if pos == end: 28 | return dist 29 | if pos in seen: 30 | continue 31 | seen.add(pos) 32 | x, y = pos 33 | for dx, dy in ((0, 1), (1, 0), (0, -1), (-1, 0)): 34 | if ( 35 | 0 <= x + dx < len(grid) 36 | and 0 <= y + dy < len(grid[0]) 37 | and ord(grid[x + dx][y + dy]) - ord(grid[x][y]) <= 1 38 | ): 39 | q.append(((x + dx, y + dy), dist + 1)) 40 | return float("inf") 41 | 42 | 43 | with open("12.txt", "r") as file: 44 | data = file.read().strip() 45 | 46 | GRID, START, END, A_LIST = parse_grid(data) 47 | 48 | 49 | def part_one(): 50 | return bfs(GRID, [START], END) 51 | 52 | 53 | def part_two(): 54 | return bfs(GRID, A_LIST, END) 55 | 56 | 57 | print(f"Part 1: {part_one()}") # 339 58 | print(f"Part 2: {part_two()}") # 332 59 | -------------------------------------------------------------------------------- /2022/13.py: -------------------------------------------------------------------------------- 1 | from ast import literal_eval 2 | from math import prod 3 | 4 | with open("13.txt", "r") as file: 5 | data = file.read().strip() 6 | 7 | 8 | def parse_input(data): 9 | res = [] 10 | for pair in data.split("\n\n"): 11 | a, b = pair.split("\n") 12 | res.append([literal_eval(a), literal_eval(b)]) 13 | return res 14 | 15 | PACKETS = parse_input(data) 16 | 17 | 18 | class Packet: 19 | def __init__(self, packet): 20 | self.packet = packet 21 | 22 | def __lt__(self, other): 23 | return is_sorted(self.packet, other.packet) 24 | 25 | def __eq__(self, other): 26 | return self.packet == other.packet 27 | 28 | 29 | def is_sorted(a, b): 30 | if isinstance(a, int) and isinstance(b, int): 31 | return None if a == b else a < b 32 | if isinstance(a, int): 33 | a = [a] 34 | if isinstance(b, int): 35 | b = [b] 36 | for x, y in zip(a, b): 37 | if (cur := is_sorted(x, y)) is not None: 38 | return cur 39 | return None if len(a) == len(b) else len(a) < len(b) 40 | 41 | 42 | def part_one(): 43 | return sum( 44 | i 45 | for i, (a, b) in enumerate(PACKETS, start=1) 46 | if is_sorted(a, b) 47 | ) 48 | 49 | 50 | def part_two(): 51 | dividers = [Packet([[2]]), Packet([[6]])] 52 | packets = [ 53 | Packet(packet) for pair in PACKETS for packet in pair 54 | ] + dividers 55 | packets.sort() 56 | return prod(map(lambda x: packets.index(x) + 1, dividers)) 57 | 58 | 59 | print(f"Part 1: {part_one()}") # 5390 60 | print(f"Part 2: {part_two()}") # 19261 61 | -------------------------------------------------------------------------------- /2022/15.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | with open("15.txt", "r") as file: 4 | data = file.read().strip() 5 | 6 | 7 | def manhattan(a, b): 8 | return sum(abs(x - y) for x, y in zip(a, b)) 9 | 10 | 11 | COORDS = [ 12 | [int(d) for d in re.findall(r"-?\d+", line)] 13 | for line in data.splitlines() 14 | ] 15 | DISTS = {(a, b): manhattan((a, b), (c, d)) for a, b, c, d in COORDS} 16 | 17 | 18 | def part_one(): 19 | res = set() 20 | for (sx, sy), dist in DISTS.items(): 21 | ndist = abs(sy - 2000000) 22 | res.update(range(sx - dist + ndist, sx + dist - ndist)) 23 | return len(res) 24 | 25 | 26 | def part_two(): 27 | A, B = set(), set() 28 | for (sx, sy), dist in DISTS.items(): 29 | A.add(sy - sx + dist + 1) 30 | A.add(sy - sx - dist - 1) 31 | B.add(sx + sy + dist + 1) 32 | B.add(sx + sy - dist - 1) 33 | for a in A: 34 | for b in B: 35 | p, q = (b - a) // 2, (a + b) // 2 36 | if ( 37 | 0 < p < 4000000 38 | and 0 < q < 4000000 39 | and all( 40 | DISTS[k] < manhattan((p, q), k) 41 | for k in DISTS.keys() 42 | ) 43 | ): 44 | return 4000000 * p + q 45 | 46 | 47 | print(f"Part 1: {part_one()}") # 5040643 48 | print(f"Part 2: {part_two()}") # 11016575214126 49 | -------------------------------------------------------------------------------- /2022/16.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import defaultdict 3 | from functools import cache 4 | from itertools import product 5 | 6 | with open("16.txt", "r") as file: 7 | data = file.read().strip() 8 | 9 | 10 | def parse_input(data): 11 | valves, flows, g = set(), dict(), defaultdict(lambda: 1000) 12 | for v, f, us in re.findall(r"Valve (\w+) .*=(\d*); .* valves? (.*)", data): 13 | valves.add(v) 14 | if f != "0": 15 | flows[v] = int(f) 16 | for u in us.split(", "): 17 | g[u, v] = 1 18 | for k, i, j in product(valves, valves, valves): 19 | g[i, j] = min(g[i, j], g[i, k] + g[k, j]) 20 | return flows, g 21 | 22 | 23 | FLOWS, G = parse_input(data) 24 | 25 | 26 | @cache 27 | def search(t=30, u="AA", vs=frozenset(FLOWS), e=False): 28 | return max( 29 | [ 30 | FLOWS[v] * (t - G[u, v] - 1) + search(t - G[u, v] - 1, v, vs - {v}, e) 31 | for v in vs 32 | if G[u, v] < t 33 | ] 34 | + [search(26, vs=vs) if e else 0] 35 | ) 36 | 37 | 38 | print(f"Part 1: {search()}") # 1871 39 | print(f"Part 2: {search(26, e=True)}") # 2416 40 | -------------------------------------------------------------------------------- /2023/01.py: -------------------------------------------------------------------------------- 1 | NUMBERS = ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine"] 2 | 3 | with open("01.txt", "r") as file: 4 | values = file.read().strip().split("\n") 5 | 6 | 7 | def solve(value, pt2=False): 8 | res = [] 9 | for i, v in enumerate(value): 10 | if v.isdigit(): 11 | res.append(v) 12 | if pt2: 13 | for j, num in enumerate(NUMBERS, start=1): 14 | if value[i : i + len(num)] == num: 15 | res.append(str(j)) 16 | return int(res[0] + res[-1]) 17 | 18 | 19 | print(f"Part 1: {sum(solve(val) for val in values)}") # 55108 20 | print(f"Part 2: {sum(solve(val, pt2=True) for val in values)}") # 56324 21 | -------------------------------------------------------------------------------- /2023/02.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import Counter, defaultdict 3 | from math import prod 4 | 5 | with open("02.txt", "r") as file: 6 | data = file.read().strip().split("\n") 7 | 8 | 9 | def parse_game(game): 10 | cubes = defaultdict(int) 11 | for n, color in re.findall(r"(\d+) (\w+)", game): 12 | cubes[color] = max(cubes[color], int(n)) 13 | return cubes 14 | 15 | 16 | def is_possible(game): 17 | bag = Counter({"red": 12, "green": 13, "blue": 14}) 18 | cubes = Counter(parse_game(game)) 19 | return not any(v > 0 for v in (cubes - bag).values()) 20 | 21 | 22 | def find_power(game): 23 | return prod(parse_game(game).values()) 24 | 25 | 26 | def part_one(data): 27 | return sum(i for i, game in enumerate(data, start=1) if is_possible(game)) 28 | 29 | 30 | def part_two(data): 31 | return sum(find_power(game) for game in data) 32 | 33 | 34 | print(f"Part 1: {part_one(data)}") # 2085 35 | print(f"Part 2: {part_two(data)}") # 79315 36 | -------------------------------------------------------------------------------- /2023/03.py: -------------------------------------------------------------------------------- 1 | from itertools import product 2 | from math import prod 3 | 4 | with open("03.txt", "r") as file: 5 | data = file.read().strip() 6 | 7 | SCHEMATIC = data.split("\n") 8 | N, M = len(SCHEMATIC), len(SCHEMATIC[0]) 9 | 10 | 11 | def locate_symbols(symbols): 12 | return [ 13 | (i, j) 14 | for i, row in enumerate(SCHEMATIC) 15 | for j, c in enumerate(row) 16 | if c in symbols 17 | ] 18 | 19 | 20 | def find_num(i, j): 21 | l = r = j 22 | while 0 <= l and SCHEMATIC[i][l].isdigit(): 23 | l -= 1 24 | while r < M and SCHEMATIC[i][r].isdigit(): 25 | r += 1 26 | return l + 1, r 27 | 28 | 29 | def solve(symbols, pt2=False): 30 | positions = locate_symbols(symbols) 31 | res = 0 32 | seen = set() 33 | for i, j in positions: 34 | cur = [] 35 | for di, dj in filter(any, product([-1, 0, 1], repeat=2)): 36 | ni, nj = i + di, j + dj 37 | if ( 38 | (ni, nj) not in seen 39 | and 0 <= ni < N 40 | and 0 <= nj < M 41 | and SCHEMATIC[ni][nj].isdigit() 42 | ): 43 | l, r = find_num(ni, nj) 44 | seen |= {(ni, q) for q in range(l, r + 1)} 45 | cur.append(int(SCHEMATIC[ni][l:r])) 46 | if pt2 and len(cur) == 2: 47 | res += prod(cur) 48 | elif not pt2: 49 | res += sum(cur) 50 | return res 51 | 52 | 53 | print(f"Part 1: {solve('#$%&*+-/=@')}") # 529618 54 | print(f"Part 2: {solve('*', pt2=True)}") # 77509019 55 | -------------------------------------------------------------------------------- /2023/04.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import defaultdict 3 | 4 | with open("04.txt", "r") as file: 5 | data = file.read().strip().split("\n") 6 | 7 | 8 | def num_winners(game): 9 | a, b = game.split(":")[1].strip().split(" | ") 10 | a, b = re.findall(r"(\d+)", a), re.findall(r"(\d+)", b) 11 | return len(set(a) & set(b)) 12 | 13 | 14 | def part_one(data): 15 | return sum(1 << (x - 1) for game in data if (x := num_winners(game))) 16 | 17 | 18 | def part_two(data): 19 | copies = defaultdict(lambda: 1) 20 | for i, game in enumerate(data): 21 | for di in range(1, num_winners(game) + 1): 22 | copies[i + di] += copies[i] 23 | return sum(copies.values()) 24 | 25 | 26 | print(f"Part 1: {part_one(data)}") # 22897 27 | print(f"Part 2: {part_two(data)}") # 5095824 28 | -------------------------------------------------------------------------------- /2023/05.py: -------------------------------------------------------------------------------- 1 | from itertools import batched 2 | 3 | with open("05.txt", "r") as file: 4 | data = file.read().strip() 5 | 6 | SEEDS = list(map(int, data.split("\n\n")[0].split(": ")[1].split())) 7 | MAPS = [ 8 | [list(map(int, y.split(" "))) for y in x.split("\n")[1:]] 9 | for x in data.split("\n\n")[1:] 10 | ] 11 | 12 | 13 | def solve(cur, i=0): 14 | if i == len(MAPS): 15 | return cur 16 | res = [] 17 | ranges = cur 18 | for dst, src, offset in MAPS[i]: 19 | nranges = [] 20 | for s, e in ranges: 21 | l = (s, min(e, src)) 22 | m = (max(s, src), min(src + offset, e)) 23 | r = (max(src + offset, s), e) 24 | if l[0] < l[1]: 25 | nranges.append(l) 26 | if m[0] < m[1]: 27 | res.append((dst + m[0] - src, dst + m[1] - src)) 28 | if r[0] < r[1]: 29 | nranges.append(r) 30 | ranges = nranges 31 | return solve(res + ranges, i + 1) 32 | 33 | 34 | def part_one(): 35 | return min(min(solve([(seed, seed + 1)]))[0] for seed in SEEDS) 36 | 37 | 38 | def part_two(): 39 | return min( 40 | min(solve([(start, start + offset)]))[0] 41 | for start, offset in batched(SEEDS, n=2) 42 | ) 43 | 44 | 45 | print(f"Part 1: {part_one()}") # 379811651 46 | print(f"Part 2: {part_two()}") # 27992443 47 | -------------------------------------------------------------------------------- /2023/06.py: -------------------------------------------------------------------------------- 1 | import re 2 | from math import prod 3 | 4 | with open("06.txt", "r") as file: 5 | data = file.read().strip() 6 | 7 | RACES = list(zip(*(map(int, re.findall(r"(\d+)", x)) for x in data.split("\n")))) 8 | 9 | 10 | def binary_search(time, dist, left=True): 11 | lo, hi = 0, time 12 | while lo <= hi: 13 | mid = lo + (hi - lo >> 1) 14 | if mid * (time - mid) > dist: 15 | if left: 16 | hi = mid - 1 17 | else: 18 | lo = mid + 1 19 | else: 20 | if left: 21 | lo = mid + 1 22 | else: 23 | hi = mid - 1 24 | return lo if left else hi 25 | 26 | 27 | def num_winners(time, dist): 28 | return binary_search(time, dist, False) - binary_search(time, dist) + 1 29 | 30 | 31 | def part_one(): 32 | return prod(num_winners(t, d) for t, d in RACES) 33 | 34 | 35 | def part_two(): 36 | return num_winners(*(int("".join(str(x) for x in race)) for race in zip(*RACES))) 37 | 38 | 39 | print(f"Part 1: {part_one()}") # 140220 40 | print(f"Part 2: {part_two()}") # 39570185 41 | -------------------------------------------------------------------------------- /2023/07.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | from itertools import chain 3 | 4 | with open("07.txt", "r") as file: 5 | data = file.read().strip() 6 | 7 | HANDS = [(a, int(b)) for x in data.split("\n") for a, b in [x.split(" ")]] 8 | HAND_TYPES = [ 9 | lambda x, j: len(x) < 2, 10 | lambda x, j: max(x.values()) + j == 4, 11 | lambda x, j: (len(x) == 2 and 2 in x.values() and 3 in x.values()) 12 | or (Counter(x.values())[2] == 2 and j == 1), 13 | lambda x, j: (3 - j) in x.values(), 14 | lambda x, j: (2 in Counter(x.values()) and Counter(x.values())[2] == 2) 15 | or (j > 0 and 2 in Counter(x.values())), 16 | lambda x, j: (2 - j) in x.values(), 17 | ] 18 | 19 | 20 | def solve(part_two=False): 21 | sorted_hands = [[] for _ in range(7)] 22 | order = { 23 | v: i for i, v in enumerate("J23456789TQKA" if part_two else "23456789TJQKA") 24 | } 25 | 26 | for hand, bid in HANDS: 27 | cur = Counter(hand) 28 | if part_two: 29 | jokers = cur["J"] 30 | del cur["J"] 31 | else: 32 | jokers = 0 33 | i = next( 34 | (i for i, hand_type in enumerate(HAND_TYPES) if hand_type(cur, jokers)), 6 35 | ) 36 | sorted_hands[i].append((hand, bid)) 37 | 38 | for i, v in enumerate(sorted_hands): 39 | sorted_hands[i] = sorted(v, key=lambda x: [order[c] for c in x[0]]) 40 | 41 | return sum( 42 | i * bid 43 | for i, (hand, bid) in enumerate( 44 | chain.from_iterable(sorted_hands[::-1]), start=1 45 | ) 46 | ) 47 | 48 | 49 | print(f"Part 1: {solve()}") # 253866470 50 | print(f"Part 2: {solve(True)}") # 254494947 51 | -------------------------------------------------------------------------------- /2023/08.py: -------------------------------------------------------------------------------- 1 | import re 2 | from itertools import count 3 | from math import lcm 4 | 5 | with open("08.txt", "r") as file: 6 | data = file.read().strip() 7 | 8 | INSTR, network = data.split("\n\n") 9 | N = len(INSTR) 10 | G = { 11 | a: (b, c) 12 | for line in network.split("\n") 13 | for a, b, c in [re.findall(r"(\w+)", line)] 14 | } 15 | 16 | 17 | def solve(u, *vs): 18 | for step in count(): 19 | if u in vs: 20 | return step 21 | i = INSTR[step % N] == "R" 22 | u = G[u][i] 23 | 24 | 25 | def part_one(): 26 | return solve("AAA", "ZZZ") 27 | 28 | 29 | def part_two(): 30 | us = [u for u in G if u[-1] == "A"] 31 | vs = [u for u in G if u[-1] == "Z"] 32 | return lcm(*(solve(u, *vs) for u in us)) 33 | 34 | 35 | print(f"Part 1: {part_one()}") # 18827 36 | print(f"Part 2: {part_two()}") # 20220305520997 37 | -------------------------------------------------------------------------------- /2023/09.py: -------------------------------------------------------------------------------- 1 | from itertools import pairwise 2 | 3 | with open("09.txt", "r") as file: 4 | data = file.read().strip() 5 | 6 | ROWS = [[int(x) for x in row.split(" ")] for row in data.split("\n")] 7 | 8 | 9 | def solve(row): 10 | return row[-1] + ( 11 | 0 if len(set(row)) == 1 else solve([b - a for a, b in pairwise(row)]) 12 | ) 13 | 14 | 15 | print(f"Part 1: {sum(solve(row) for row in ROWS)}") # 1743490457 16 | print(f"Part 2: {sum(solve(row[::-1]) for row in ROWS)}") # 1053 17 | -------------------------------------------------------------------------------- /2023/10.py: -------------------------------------------------------------------------------- 1 | with open("10.txt", "r") as file: 2 | data = file.read().strip() 3 | 4 | GRID = data.split("\n") 5 | MOVES = { 6 | ("|", (1, 0)): (1, 0), 7 | ("|", (-1, 0)): (-1, 0), 8 | ("-", (0, 1)): (0, 1), 9 | ("-", (0, -1)): (0, -1), 10 | ("L", (0, -1)): (-1, 0), 11 | ("L", (1, 0)): (0, 1), 12 | ("J", (0, 1)): (-1, 0), 13 | ("J", (1, 0)): (0, -1), 14 | ("7", (0, 1)): (1, 0), 15 | ("7", (-1, 0)): (0, -1), 16 | ("F", (0, -1)): (1, 0), 17 | ("F", (-1, 0)): (0, 1), 18 | } 19 | START = next( 20 | (i, j) for i, row in enumerate(GRID) for j, v in enumerate(row) if v == "S" 21 | ) 22 | 23 | 24 | def find_start_orientation(start): 25 | n, m = len(GRID), len(GRID[0]) 26 | i, j = start 27 | dirs = [(-1, 0, "|7F"), (1, 0, "|LJ"), (0, -1, "-FL"), (0, 1, "-J7")] 28 | for di, dj, valid in dirs: 29 | ni, nj = i + di, j + dj 30 | if 0 <= ni < n and 0 <= nj < m and GRID[ni][nj] in valid: 31 | return "|" if di else "-", (di, dj) 32 | 33 | 34 | def find_path(): 35 | cur, dir = find_start_orientation(START) 36 | i, j = START 37 | res = [(i, j)] 38 | while True: 39 | dir = MOVES[cur, dir] 40 | di, dj = dir 41 | i, j = i + di, j + dj 42 | if (i, j) == START: 43 | break 44 | cur = GRID[i][j] 45 | res.append((i, j)) 46 | return res 47 | 48 | 49 | def shoelace_area(points): 50 | n = len(points) 51 | res = 0 52 | for i in range(n): 53 | x1, y1 = points[i] 54 | x2, y2 = points[(i + 1) % n] 55 | res += x1 * y2 - x2 * y1 56 | return abs(res) >> 1 57 | 58 | 59 | def part_one(): 60 | return len(find_path()) >> 1 61 | 62 | 63 | def part_two(): 64 | # https://en.wikipedia.org/wiki/Pick%27s_theorem 65 | # https://en.wikipedia.org/wiki/Shoelace_formula 66 | return shoelace_area(find_path()) - part_one() + 1 67 | 68 | 69 | print(f"Part 1: {part_one()}") # 6757 70 | print(f"Part 2: {part_two()}") # 523 71 | -------------------------------------------------------------------------------- /2023/11.py: -------------------------------------------------------------------------------- 1 | from itertools import combinations 2 | 3 | with open("11.txt", "r") as file: 4 | data = file.read().strip() 5 | 6 | GRID = data.split("\n") 7 | 8 | 9 | def dist(u, v): 10 | return sum(abs(x - y) for x, y in zip(u, v)) 11 | 12 | 13 | def locate_galaxies(grid, expand=2): 14 | empty_rows = {i for i, row in enumerate(grid) if all(c == "." for c in row)} 15 | empty_cols = {j for j, col in enumerate(zip(*grid)) if all(c == "." for c in col)} 16 | galaxies = [] 17 | di = 0 18 | for i, row in enumerate(grid): 19 | di += (expand - 1) * (i in empty_rows) 20 | dj = 0 21 | for j, c in enumerate(row): 22 | dj += (expand - 1) * (j in empty_cols) 23 | if c == "#": 24 | galaxies.append((i + di, j + dj)) 25 | return galaxies 26 | 27 | 28 | def solve(pt2=False): 29 | return sum( 30 | dist(u, v) 31 | for u, v in combinations( 32 | locate_galaxies(GRID, expand=1000000 if pt2 else 2), r=2 33 | ) 34 | ) 35 | 36 | 37 | print(f"Part 1: {solve()}") # 9799681 38 | print(f"Part 2: {solve(True)}") # 513171773355 39 | -------------------------------------------------------------------------------- /2023/12.py: -------------------------------------------------------------------------------- 1 | from functools import cache 2 | 3 | with open("12.txt", "r") as file: 4 | data = file.read().strip() 5 | 6 | RECORDS = [ 7 | (x, tuple(map(int, y.split(",")))) 8 | for row in data.split("\n") 9 | for x, y in [row.split(" ")] 10 | ] 11 | 12 | 13 | @cache 14 | def dp(seq, nums): 15 | if not seq: 16 | return nums == () 17 | if not nums: 18 | return "#" not in seq 19 | res = 0 20 | if seq[0] in ".?": 21 | res += dp(seq[1:], nums) 22 | if seq[0] in "#?": 23 | n, r = len(seq), nums[0] 24 | if r <= n and "." not in seq[:r] and (r == n or seq[r] != "#"): 25 | res += dp(seq[r + 1 :], nums[1:]) 26 | return res 27 | 28 | 29 | def part_one(): 30 | return sum(dp(seq, nums) for seq, nums in RECORDS) 31 | 32 | 33 | def part_two(): 34 | return sum( 35 | dp(seq, nums) 36 | for seq, nums in ( 37 | ("?".join(seq for _ in range(5)), tuple(nums * 5)) for seq, nums in RECORDS 38 | ) 39 | ) 40 | 41 | 42 | print(f"Part 1: {part_one()}") # 7670 43 | print(f"Part 2: {part_two()}") # 157383940585037 44 | -------------------------------------------------------------------------------- /2023/13.py: -------------------------------------------------------------------------------- 1 | with open("13.txt", "r") as file: 2 | data = file.read().strip() 3 | 4 | GRIDS = [x.split("\n") for x in data.split("\n\n")] 5 | 6 | 7 | def find_line(grid, target): 8 | for i in range(1, len(grid)): 9 | cur = 0 10 | for a, b in zip(reversed(grid[:i]), grid[i:]): 11 | cur += sum(x != y for x, y in zip(a, b)) 12 | if cur == target: 13 | return i 14 | return 0 15 | 16 | 17 | def solve(target): 18 | return sum( 19 | find_line(tuple(zip(*grid)), target) + 100 * find_line(grid, target) 20 | for grid in GRIDS 21 | ) 22 | 23 | 24 | print(f"Part 1: {solve(0)}") # 35210 25 | print(f"Part 2: {solve(1)}") # 31974 26 | -------------------------------------------------------------------------------- /2023/15.py: -------------------------------------------------------------------------------- 1 | from functools import reduce 2 | 3 | with open("15.txt", "r") as file: 4 | data = file.read().strip() 5 | 6 | 7 | def hash(s): 8 | return reduce(lambda x, y: (x + ord(y)) * 17 % 256, s, 0) 9 | 10 | 11 | def hashmap(s, boxes): 12 | match s.replace("-", "").split("="): 13 | case [label]: 14 | boxes[hash(label)].pop(label, None) 15 | case [label, focal_len]: 16 | boxes[hash(label)][label] = int(focal_len) 17 | 18 | 19 | def part_one(): 20 | return sum(hash(s) for s in data.split(",")) 21 | 22 | 23 | def part_two(): 24 | boxes = [{} for _ in range(256)] 25 | for s in data.split(","): 26 | hashmap(s, boxes) 27 | return sum( 28 | i * j * focal_len 29 | for i, box in enumerate(boxes, start=1) 30 | for j, (label, focal_len) in enumerate(box.items(), start=1) 31 | ) 32 | 33 | 34 | print(f"Part 1: {part_one()}") # 507666 35 | print(f"Part 2: {part_two()}") # 233537 36 | -------------------------------------------------------------------------------- /2023/16.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | from functools import reduce 3 | 4 | with open("16.txt", "r") as file: 5 | data = file.read().strip() 6 | 7 | GRID = data.split("\n") 8 | 9 | 10 | def solve(grid, i, j, di, dj): 11 | n, m = len(grid), len(grid[0]) 12 | q = deque([(i, j, di, dj)]) 13 | seen = set() 14 | while q: 15 | i, j, di, dj = q.popleft() 16 | if 0 > i or i >= n or 0 > j or j >= m or (i, j, di, dj) in seen: 17 | continue 18 | seen.add((i, j, di, dj)) 19 | match grid[i][j]: 20 | case "/": 21 | q.append((i - dj, j - di, -dj, -di)) 22 | case "\\": 23 | q.append((i + dj, j + di, dj, di)) 24 | case "|" if dj: 25 | q.append((i + 1, j, 1, 0)) 26 | q.append((i - 1, j, -1, 0)) 27 | case "-" if di: 28 | q.append((i, j + 1, 0, 1)) 29 | q.append((i, j - 1, 0, -1)) 30 | case _: 31 | q.append((i + di, j + dj, di, dj)) 32 | return len(set((i, j) for i, j, _, _ in seen)) 33 | 34 | 35 | def part_one(): 36 | return solve(GRID, 0, 0, 0, 1) 37 | 38 | 39 | def part_two(): 40 | n, m = len(GRID), len(GRID[0]) 41 | starts = ( 42 | [(x, 0, 0, 1) for x in range(n)] 43 | + [(x, m - 1, 0, -1) for x in range(n)] 44 | + [(0, x, 1, 0) for x in range(m)] 45 | + [(n - 1, x, -1, 0) for x in range(m)] 46 | ) 47 | return reduce(lambda res, start: max(res, solve(GRID, *start)), starts, 0) 48 | 49 | 50 | print(f"Part 1: {part_one()}") # 7074 51 | print(f"Part 2: {part_two()}") # 7530 52 | -------------------------------------------------------------------------------- /2023/17.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from heapq import heappop, heappush 3 | from math import inf 4 | 5 | with open("17.txt", "r") as file: 6 | data = file.read().strip() 7 | 8 | GRID = data.split("\n") 9 | 10 | 11 | def dijkstra(grid, lo, hi): 12 | n, m = len(grid), len(grid[0]) 13 | dists = defaultdict(lambda: inf) 14 | heap = [(0, (0, 0, (0, 1))), (0, (0, 0, (1, 0)))] 15 | while heap: 16 | cost, (i, j, d) = heappop(heap) 17 | if (i, j) == (n - 1, m - 1): 18 | return cost 19 | if cost > dists[i, j, d]: 20 | continue 21 | di, dj = d 22 | for ndi, ndj in ((-dj, di), (dj, -di)): 23 | ncost = cost 24 | for dist in range(1, hi + 1): 25 | ni, nj = i + ndi * dist, j + ndj * dist 26 | if 0 <= ni < n and 0 <= nj < m: 27 | ncost += int(grid[ni][nj]) 28 | if dist < lo: 29 | continue 30 | k = (ni, nj, (ndi, ndj)) 31 | if ncost < dists[k]: 32 | dists[k] = ncost 33 | heappush(heap, (ncost, k)) 34 | return -1 35 | 36 | 37 | print(f"Part 1: {dijkstra(GRID, 1, 3)}") # 1155 38 | print(f"Part 2: {dijkstra(GRID, 4, 10)}") # 1283 39 | -------------------------------------------------------------------------------- /2023/18.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | with open("18.txt", "r") as file: 4 | data = file.read().strip() 5 | 6 | PLAN = [(d, int(n), color) for d, n, color in re.findall(r"(\w) (\d+) \(#(.+)\)", data)] 7 | 8 | 9 | def shoelace_area(points): 10 | n = len(points) 11 | res = 0 12 | for i in range(n): 13 | x1, y1 = points[i] 14 | x2, y2 = points[(i + 1) % n] 15 | res += x1 * y2 - x2 * y1 16 | return abs(res) >> 1 17 | 18 | 19 | def solve(plan, pt2=False): 20 | dirs = ( 21 | [(0, 1), (1, 0), (0, -1), (-1, 0)] 22 | if pt2 23 | else {"U": (-1, 0), "D": (1, 0), "R": (0, 1), "L": (0, -1)} 24 | ) 25 | points, perim = [], 0 26 | i = j = 0 27 | for d, n, color in plan: 28 | if pt2: 29 | n, d = int(color[:5], 16), int(color[-1]) 30 | perim += n 31 | di, dj = dirs[d] 32 | i, j = i + n * di, j + n * dj 33 | points.append((i, j)) 34 | return shoelace_area(points) + (perim >> 1) + 1 35 | 36 | 37 | print(f"Part 1: {solve(PLAN)}") # 47675 38 | print(f"Part 2: {solve(PLAN, True)}") # 122103860427465 39 | -------------------------------------------------------------------------------- /2023/21.py: -------------------------------------------------------------------------------- 1 | from itertools import count, pairwise 2 | 3 | with open("21.txt", "r") as file: 4 | data = file.read().strip() 5 | 6 | G = data.split("\n") 7 | START = next((i, j) for i, row in enumerate(G) for j, v in enumerate(row) if v == "S") 8 | N, M = len(G), len(G[0]) 9 | 10 | 11 | def step(q, pt2=False): 12 | nq = set() 13 | for _ in range(len(q)): 14 | i, j = q.pop() 15 | for ni, nj in ((i + 1, j), (i - 1, j), (i, j + 1), (i, j - 1)): 16 | if pt2 and G[ni % N][nj % M] != "#": 17 | nq.add((ni, nj)) 18 | elif 0 <= ni < N and 0 <= nj < M and G[ni][nj] != "#": 19 | nq.add((ni, nj)) 20 | return nq 21 | 22 | 23 | def nth_term(seq, n): 24 | # https://www.radfordmathematics.com/algebra/sequences-series/difference-method-sequences/quadratic-sequences.html 25 | d1, d2 = (b - a for a, b in pairwise(seq)) 26 | sd = d2 - d1 27 | x, y, z = sd, d1, seq[0] 28 | a = x >> 1 29 | b = y - (3 * a) 30 | c = z - b - a 31 | return (a * n * n) + (b * n) + c 32 | 33 | 34 | def part_one(): 35 | q = {START} 36 | for _ in range(64): 37 | q = step(q) 38 | return len(q) 39 | 40 | 41 | def part_two(): 42 | q = {START} 43 | seq = [] 44 | for i in count(1): 45 | q = step(q, pt2=True) 46 | if i % N == 26501365 % N: 47 | seq.append(len(q)) 48 | if len(seq) == 3: 49 | break 50 | return nth_term(seq, (26501365 // N) + 1) 51 | 52 | 53 | print(f"Part 1: {part_one()}") # 3722 54 | print(f"Part 2: {part_two()}") # 614864614526014 55 | -------------------------------------------------------------------------------- /2023/24.py: -------------------------------------------------------------------------------- 1 | from itertools import combinations 2 | 3 | import sympy 4 | 5 | with open("24.txt", "r") as file: 6 | data = file.read().strip() 7 | 8 | HAIL = [ 9 | (tuple(map(int, p.split(", "))), tuple(map(int, v.split(", ")))) 10 | for row in data.split("\n") 11 | for p, v in [row.split(" @ ")] 12 | ] 13 | 14 | 15 | def check_intersect(a, b): 16 | (apx, apy, apz), (avx, avy, avz) = a 17 | (bpx, bpy, bpz), (bvx, bvy, bvz) = b 18 | am = avy / avx 19 | ac = apy - am * apx 20 | bm = bvy / bvx 21 | bc = bpy - bm * bpx 22 | if am == bm: 23 | return False 24 | x = (ac - bc) / (bm - am) 25 | y = am * x + ac 26 | if ( 27 | (avx >= 0 and x < apx) 28 | or (avx < 0 and x > apx) 29 | or (bvx >= 0 and x < bpx) 30 | or (bvx < 0 and x > bpx) 31 | ): 32 | return False 33 | return 2e14 <= x <= 4e14 and 2e14 <= y <= 4e14 34 | 35 | 36 | def part_one(): 37 | return sum(check_intersect(a, b) for a, b in combinations(HAIL, 2)) 38 | 39 | 40 | def part_two(): 41 | x, v_x, y, v_y, z, v_z = sympy.symbols("x, v_x, y, v_y, z, v_z") 42 | eqs = [] 43 | for i, (p, v) in enumerate(HAIL[:3]): 44 | (px, py, pz), (vx, vy, vz) = p, v 45 | t = sympy.var(f"t{i}") 46 | eqs.append(sympy.Eq(x + v_x * t, px + vx * t)) 47 | eqs.append(sympy.Eq(y + v_y * t, py + vy * t)) 48 | eqs.append(sympy.Eq(z + v_z * t, pz + vz * t)) 49 | res = sympy.solve(eqs).pop() 50 | return sum(res[c] for c in (x, y, z)) 51 | 52 | 53 | print(f"Part 1: {part_one()}") # 20847 54 | print(f"Part 2: {part_two()}") # 908621716620524 55 | -------------------------------------------------------------------------------- /2023/25.py: -------------------------------------------------------------------------------- 1 | from math import prod 2 | 3 | import networkx as nx 4 | 5 | with open("25.txt", "r") as file: 6 | data = file.read().strip() 7 | 8 | 9 | def part_one(): 10 | G = nx.Graph() 11 | for row in data.split("\n"): 12 | u, vs = row.split(": ") 13 | for v in vs.split(" "): 14 | G.add_edge(u, v) 15 | cuts = nx.minimum_edge_cut(G) 16 | G.remove_edges_from(cuts) 17 | return prod(map(len, nx.connected_components(G))) 18 | 19 | 20 | print(f"Part 1: {part_one()}") # 552682 21 | -------------------------------------------------------------------------------- /2024/01.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | 3 | with open("01.txt", "r") as file: 4 | data = file.read().strip().splitlines() 5 | 6 | 7 | def parse_input(data): 8 | left, right = [], [] 9 | for row in data: 10 | l, r = row.split() 11 | left.append(int(l)) 12 | right.append(int(r)) 13 | return left, right 14 | 15 | 16 | def part_one(left, right): 17 | return sum(abs(a - b) for a, b in zip(sorted(left), sorted(right))) 18 | 19 | 20 | def part_two(left, right): 21 | freq = Counter(right) 22 | return sum(x * freq[x] for x in left) 23 | 24 | 25 | print(f"Part 1: {part_one(*parse_input(data))}") 26 | print(f"Part 2: {part_two(*parse_input(data))}") 27 | -------------------------------------------------------------------------------- /2024/02.py: -------------------------------------------------------------------------------- 1 | from itertools import pairwise 2 | 3 | with open("02.txt", "r") as file: 4 | data = file.read().strip().splitlines() 5 | 6 | 7 | REPORTS = [[int(x) for x in row.split()] for row in data] 8 | 9 | 10 | def check(report): 11 | a, b = report[:2] 12 | inc = (-1) ** (b < a) 13 | return all(1 <= inc * (b - a) <= 3 for a, b in pairwise(report)) 14 | 15 | 16 | def part_one(): 17 | return sum(check(report) for report in REPORTS) 18 | 19 | 20 | def part_two(): 21 | return sum( 22 | any(check(report[:i] + report[i + 1 :]) for i in range(len(report))) 23 | for report in REPORTS 24 | ) 25 | 26 | 27 | print(f"Part 1: {part_one()}") 28 | print(f"Part 2: {part_two()}") 29 | -------------------------------------------------------------------------------- /2024/03.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | with open("03.txt", "r") as file: 4 | data = file.read().strip() 5 | 6 | 7 | def part_one(s): 8 | return sum(int(x) * int(y) for x, y in re.findall(r"mul\((\d+),(\d+)\)", s)) 9 | 10 | 11 | def part_two(s): 12 | res = 0 13 | do = True 14 | for match in re.finditer(r"mul\((\d+),(\d+)\)|do\(\)|don't\(\)", s): 15 | if match.group(0) == "do()": 16 | do = True 17 | elif match.group(0) == "don't()": 18 | do = False 19 | elif do: 20 | res += int(match.group(1)) * int(match.group(2)) 21 | return res 22 | 23 | 24 | print(f"Part 1: {part_one(data)}") 25 | print(f"Part 2: {part_two(data)}") 26 | -------------------------------------------------------------------------------- /2024/04.py: -------------------------------------------------------------------------------- 1 | from itertools import product 2 | 3 | with open("04.txt", "r") as file: 4 | data = file.read().strip() 5 | 6 | GRID = data.splitlines() 7 | N, M = len(GRID), len(GRID[0]) 8 | 9 | 10 | def search_xmas(i, j): 11 | res = 0 12 | for di, dj in filter(any, product(range(-1, 2), repeat=2)): 13 | ni, nj = i, j 14 | xmas = 1 15 | for c in "MAS": 16 | ni += di 17 | nj += dj 18 | if 0 > ni or ni >= N or 0 > nj or nj >= M or GRID[ni][nj] != c: 19 | xmas = 0 20 | break 21 | res += xmas 22 | return res 23 | 24 | 25 | def search_x_mas(i, j): 26 | return ( 27 | 1 <= i < N - 1 28 | and 1 <= j < M - 1 29 | and GRID[i - 1][j - 1] + GRID[i][j] + GRID[i + 1][j + 1] in ("MAS", "SAM") 30 | and GRID[i - 1][j + 1] + GRID[i][j] + GRID[i + 1][j - 1] in ("MAS", "SAM") 31 | ) 32 | 33 | 34 | def part_one(): 35 | return sum( 36 | search_xmas(i, j) for i in range(N) for j in range(M) if GRID[i][j] == "X" 37 | ) 38 | 39 | 40 | def part_two(): 41 | return sum( 42 | search_x_mas(i, j) for i in range(N) for j in range(M) if GRID[i][j] == "A" 43 | ) 44 | 45 | 46 | print(f"Part 1: {part_one()}") 47 | print(f"Part 2: {part_two()}") 48 | -------------------------------------------------------------------------------- /2024/05.py: -------------------------------------------------------------------------------- 1 | import re 2 | from functools import cmp_to_key 3 | from itertools import pairwise 4 | 5 | 6 | def parse_input(): 7 | with open("05.txt", "r") as file: 8 | data = file.read() 9 | rules_raw, pages_raw = data.strip().split("\n\n") 10 | rules = {(int(x), int(y)) for x, y in re.findall(r"(\d+)\|(\d+)", rules_raw)} 11 | pages = [[int(x) for x in line.split(",")] for line in pages_raw.splitlines()] 12 | return rules, pages 13 | 14 | 15 | RULES, PAGES = parse_input() 16 | 17 | 18 | def part_one(): 19 | return sum( 20 | page[len(page) >> 1] 21 | for page in PAGES 22 | if all(pair in RULES for pair in pairwise(page)) 23 | ) 24 | 25 | 26 | def part_two(): 27 | unsorted = filter( 28 | lambda page: any(pair not in RULES for pair in pairwise(page)), PAGES 29 | ) 30 | return sum( 31 | sorted(page, key=cmp_to_key(lambda x, y: -1 if (x, y) in RULES else 1))[ 32 | len(page) >> 1 33 | ] 34 | for page in unsorted 35 | ) 36 | 37 | 38 | print(f"Part 1: {part_one()}") 39 | print(f"Part 2: {part_two()}") 40 | -------------------------------------------------------------------------------- /2024/06.py: -------------------------------------------------------------------------------- 1 | def parse_input(): 2 | with open("06.txt", "r") as file: 3 | data = file.read() 4 | grid = data.splitlines() 5 | return grid 6 | 7 | 8 | GRID = parse_input() 9 | GUARD = next( 10 | (i, j) for i, row in enumerate(GRID) for j, c in enumerate(row) if c == "^" 11 | ) 12 | N, M = len(GRID), len(GRID[0]) 13 | 14 | 15 | def find_path(): 16 | i, j = GUARD 17 | di, dj = -1, 0 18 | seen = set() 19 | while 0 <= i < N and 0 <= j < M: 20 | if 0 <= i + di < N and 0 <= j + dj < M and GRID[i + di][j + dj] == "#": 21 | di, dj = dj, -di 22 | else: 23 | seen.add((i, j)) 24 | i += di 25 | j += dj 26 | return seen 27 | 28 | 29 | def check_new(ni, nj): 30 | i, j = GUARD 31 | di, dj = -1, 0 32 | seen = set() 33 | while 0 <= i < N and 0 <= j < M: 34 | if (i, j, di, dj) in seen: 35 | return True 36 | if ( 37 | 0 <= i + di < N 38 | and 0 <= j + dj < M 39 | and (GRID[i + di][j + dj] == "#" or (i + di, j + dj) == (ni, nj)) 40 | ): 41 | di, dj = dj, -di 42 | else: 43 | seen.add((i, j, di, dj)) 44 | i += di 45 | j += dj 46 | return False 47 | 48 | 49 | def part_one(): 50 | return len(find_path()) 51 | 52 | 53 | def part_two(): 54 | path = find_path() 55 | return sum(check_new(i, j) for i, j in path if GRID[i][j] == ".") 56 | 57 | 58 | print(f"Part 1: {part_one()}") 59 | print(f"Part 2: {part_two()}") 60 | -------------------------------------------------------------------------------- /2024/07.py: -------------------------------------------------------------------------------- 1 | import re 2 | from functools import reduce 3 | from itertools import product 4 | from operator import add, mul 5 | 6 | 7 | def parse_input(): 8 | with open("07.txt", "r") as file: 9 | data = file.read() 10 | eqs = [tuple(map(int, re.findall(r"(\d+)", row))) for row in data.splitlines()] 11 | return eqs 12 | 13 | 14 | EQS = parse_input() 15 | 16 | 17 | def check_eq(eq, operators): 18 | test_val, nums = eq[0], eq[1:] 19 | for ops in product(operators, repeat=(len(nums) - 1)): 20 | if ( 21 | reduce(lambda acc, x: x[0](acc, x[1]), zip(ops, nums[1:]), nums[0]) 22 | == test_val 23 | ): 24 | return True 25 | return False 26 | 27 | 28 | def part_one(): 29 | return sum(eq[0] for eq in EQS if check_eq(eq, (add, mul))) 30 | 31 | 32 | def part_two(): 33 | def concat(a, b): 34 | a *= 10 ** len(str(b)) 35 | return a + b 36 | 37 | return sum(eq[0] for eq in EQS if check_eq(eq, (add, mul, concat))) 38 | 39 | 40 | print(f"Part 1: {part_one()}") 41 | print(f"Part 2: {part_two()}") 42 | -------------------------------------------------------------------------------- /2024/08.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from itertools import combinations 3 | 4 | 5 | def parse_input(): 6 | with open("08.txt", "r") as file: 7 | data = file.read() 8 | grid = data.splitlines() 9 | antennas = defaultdict(list) 10 | for i, row in enumerate(grid): 11 | for j, v in enumerate(row): 12 | if v != ".": 13 | antennas[v].append((i, j)) 14 | return antennas, len(grid), len(grid[0]) 15 | 16 | 17 | ANTENNAS, N, M = parse_input() 18 | 19 | 20 | def calc_antinodes(u, v, pt2=False): 21 | ui, uj = u 22 | vi, vj = v 23 | di = ui - vi 24 | dj = uj - vj 25 | res = {(ui, uj), (vi, vj)} if pt2 else set() 26 | while 0 <= (ui := ui + di) < N and 0 <= (uj := uj + dj) < M: 27 | res |= {(ui, uj)} 28 | if not pt2: 29 | break 30 | while 0 <= (vi := vi - di) < N and 0 <= (vj := vj - dj) < M: 31 | res |= {(vi, vj)} 32 | if not pt2: 33 | break 34 | return res 35 | 36 | 37 | def solve(pt2=False): 38 | return len( 39 | { 40 | antinode 41 | for locs in ANTENNAS.values() 42 | for u, v in combinations(locs, 2) 43 | for antinode in calc_antinodes(u, v, pt2) 44 | } 45 | ) 46 | 47 | 48 | def part_one(): 49 | return solve() 50 | 51 | 52 | def part_two(): 53 | return solve(pt2=True) 54 | 55 | 56 | print(f"Part 1: {part_one()}") 57 | print(f"Part 2: {part_two()}") 58 | -------------------------------------------------------------------------------- /2024/09.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | 3 | 4 | def parse_input(): 5 | with open("09.txt", "r") as file: 6 | data = file.read().strip() 7 | return list(map(int, data)) 8 | 9 | 10 | DISK_MAP = parse_input() 11 | 12 | 13 | def read_memory(): 14 | files, free = deque(), deque() 15 | pos = 0 16 | for i, v in enumerate(DISK_MAP): 17 | if i & 1 == 0: 18 | files.append([i >> 1, v, pos]) 19 | else: 20 | free.append([v, pos]) 21 | pos += v 22 | return files, free 23 | 24 | 25 | def part_one(): 26 | files, free = read_memory() 27 | layout = [] 28 | while files and free: 29 | layout.append(files.popleft()[:2]) 30 | space, _ = free.popleft() 31 | cur = 0 32 | while files and cur < space: 33 | fid, n, _ = files.pop() 34 | cur += n 35 | if cur > space: 36 | files.append((fid, cur - space, None)) 37 | layout.append((fid, n - (cur - space))) 38 | else: 39 | layout.append((fid, n)) 40 | 41 | res = 0 42 | pos = 0 43 | for fid, sz in layout: 44 | res += (2 * pos + sz - 1) * sz * fid >> 1 45 | pos += sz 46 | return res 47 | 48 | 49 | def part_two(): 50 | files, free = read_memory() 51 | for file in reversed(files): 52 | for space in free: 53 | if file[2] < space[1]: 54 | break 55 | if file[1] <= space[0]: 56 | file[2] = space[1] 57 | space[1] += file[1] 58 | space[0] -= file[1] 59 | break 60 | return sum((2 * pos + sz - 1) * sz * fid >> 1 for fid, sz, pos in files) 61 | 62 | 63 | print(f"Part 1: {part_one()}") 64 | print(f"Part 2: {part_two()}") 65 | -------------------------------------------------------------------------------- /2024/10.py: -------------------------------------------------------------------------------- 1 | def parse_input(): 2 | with open("10.txt", "r") as file: 3 | data = file.read() 4 | return [list(map(int, row)) for row in data.splitlines()] 5 | 6 | 7 | GRID = parse_input() 8 | N, M = len(GRID), len(GRID[0]) 9 | TRAILHEADS = [(i, j) for i in range(N) for j in range(M) if GRID[i][j] == 0] 10 | 11 | 12 | def dfs(i, j, pt2=False): 13 | if GRID[i][j] == 9: 14 | return 1 if pt2 else {(i, j)} 15 | res = 0 if pt2 else set() 16 | for ni, nj in ((i + 1, j), (i - 1, j), (i, j + 1), (i, j - 1)): 17 | if 0 <= ni < N and 0 <= nj < M and GRID[ni][nj] == GRID[i][j] + 1: 18 | if pt2: 19 | res += dfs(ni, nj, True) 20 | else: 21 | res |= dfs(ni, nj) 22 | return res 23 | 24 | 25 | def part_one(): 26 | return sum(len(dfs(i, j)) for i, j in TRAILHEADS) 27 | 28 | 29 | def part_two(): 30 | return sum(dfs(i, j, True) for i, j in TRAILHEADS) 31 | 32 | 33 | print(f"Part 1: {part_one()}") 34 | print(f"Part 2: {part_two()}") 35 | -------------------------------------------------------------------------------- /2024/11.py: -------------------------------------------------------------------------------- 1 | import re 2 | from functools import cache 3 | 4 | 5 | def parse_input(): 6 | with open("11.txt", "r") as file: 7 | data = file.read() 8 | return list(map(int, re.findall(r"(\d+)", data))) 9 | 10 | 11 | STONES = parse_input() 12 | 13 | 14 | @cache 15 | def dp(x, n): 16 | if n == 0: 17 | return 1 18 | if x == 0: 19 | return dp(1, n - 1) 20 | if (sz := len(str(x))) & 1 == 0: 21 | return dp(int(str(x)[: sz >> 1]), n - 1) + dp(int(str(x)[sz >> 1 :]), n - 1) 22 | return dp(x * 2024, n - 1) 23 | 24 | 25 | def part_one(): 26 | return sum(dp(x, 25) for x in STONES) 27 | 28 | 29 | def part_two(): 30 | return sum(dp(x, 75) for x in STONES) 31 | 32 | 33 | print(f"Part 1: {part_one()}") 34 | print(f"Part 2: {part_two()}") 35 | -------------------------------------------------------------------------------- /2024/13.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import numpy as np 4 | 5 | 6 | def parse_input(): 7 | with open("13.txt", "r") as file: 8 | data = file.read() 9 | pattern = r"Button A: X\+(\d+), Y\+(\d+)\nButton B: X\+(\d+), Y\+(\d+)\nPrize: X=(\d+), Y=(\d+)" 10 | return [list(map(int, x)) for x in re.findall(pattern, data)] 11 | 12 | 13 | MACHINES = parse_input() 14 | 15 | 16 | def calc_tokens(ax, ay, bx, by, px, py): 17 | A = np.array([[ax, bx], [ay, by]]) 18 | p = np.array([px, py]) 19 | x = np.linalg.solve(A, p).round() 20 | return int(x @ (3, 1)) if all(A @ x == p) else 0 21 | 22 | 23 | def part_one(): 24 | return sum(calc_tokens(*machine) for machine in MACHINES) 25 | 26 | 27 | def part_two(): 28 | return sum( 29 | calc_tokens( 30 | *machine[:4], machine[4] + 10000000000000, machine[5] + 10000000000000 31 | ) 32 | for machine in MACHINES 33 | ) 34 | 35 | 36 | print(f"Part 1: {part_one()}") 37 | print(f"Part 2: {part_two()}") 38 | -------------------------------------------------------------------------------- /2024/14.py: -------------------------------------------------------------------------------- 1 | import re 2 | from itertools import count 3 | from math import prod 4 | 5 | 6 | def parse_input(): 7 | with open("14.txt", "r") as file: 8 | data = file.read() 9 | pattern = r"p=(-?\d+),(-?\d+) v=(-?\d+),(-?\d+)" 10 | return [list(map(int, x)) for x in re.findall(pattern, data)] 11 | 12 | 13 | ROBOTS = parse_input() 14 | N, M = 103, 101 15 | 16 | 17 | def step(j, i, dj, di, t): 18 | di *= t 19 | dj *= t 20 | i = (i + di) % N 21 | j = (j + dj) % M 22 | return i, j 23 | 24 | 25 | def draw(positions): 26 | grid = [["."] * M for _ in range(N)] 27 | for i, j in positions: 28 | grid[i][j] = "\u2588" 29 | print("\n".join("".join(row) for row in grid)) 30 | 31 | 32 | def part_one(): 33 | quadrants = [0] * 4 34 | for i, j in (step(*robot, 100) for robot in ROBOTS): 35 | n = (N >> 1) + 1 36 | m = (M >> 1) + 1 37 | x, r = divmod(i, n) 38 | y, rr = divmod(j, m) 39 | if r == n - 1 or rr == m - 1: 40 | continue 41 | quadrants[x * 2 + y] += 1 42 | return prod(quadrants) 43 | 44 | 45 | def part_two(): 46 | for t in count(): 47 | cur = {step(*robot, t) for robot in ROBOTS} 48 | if len(cur) == len(ROBOTS): 49 | draw(cur) 50 | return t 51 | 52 | 53 | print(f"Part 1: {part_one()}") 54 | print(f"Part 2: {part_two()}") 55 | -------------------------------------------------------------------------------- /2024/18.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import deque 3 | from itertools import batched 4 | 5 | 6 | def parse_input(): 7 | with open("18.txt", "r") as file: 8 | data = file.read() 9 | return list(batched(map(int, re.findall(r"(\d+)", data)), 2)) 10 | 11 | 12 | BYTES = parse_input() 13 | N = M = 71 14 | 15 | 16 | def bfs(idx): 17 | q = deque([(0, 0)]) 18 | seen = {(0, 0)} | set(BYTES[:idx]) 19 | res = 0 20 | while q: 21 | for _ in range(len(q)): 22 | i, j = q.popleft() 23 | if (i, j) == (N - 1, M - 1): 24 | return res 25 | for ni, nj in ((i + 1, j), (i - 1, j), (i, j + 1), (i, j - 1)): 26 | if 0 <= ni < N and 0 <= nj < M and (ni, nj) not in seen: 27 | seen.add((ni, nj)) 28 | q.append((ni, nj)) 29 | res += 1 30 | return -1 31 | 32 | 33 | def part_one(): 34 | return bfs(1024) 35 | 36 | 37 | def part_two(): 38 | l, r = 1025, len(BYTES) - 1 39 | while l < r: 40 | m = l + (r - l >> 1) 41 | if bfs(m) == -1: 42 | r = m 43 | else: 44 | l = m + 1 45 | return ",".join(map(str, BYTES[l - 1])) 46 | 47 | 48 | print(f"Part 1: {part_one()}") 49 | print(f"Part 2: {part_two()}") 50 | -------------------------------------------------------------------------------- /2024/19.py: -------------------------------------------------------------------------------- 1 | from functools import cache 2 | 3 | 4 | def parse_input(): 5 | with open("19.txt", "r") as file: 6 | data = file.read() 7 | patterns, designs = data.split("\n\n") 8 | return patterns.split(", "), designs.splitlines() 9 | 10 | 11 | PATTERNS, DESIGNS = parse_input() 12 | 13 | 14 | @cache 15 | def dp(s, i=0, pt2=False): 16 | if i == len(s): 17 | return 1 18 | res = 0 19 | for pattern in PATTERNS: 20 | sz = len(pattern) 21 | if s[i : i + sz] == pattern: 22 | if pt2: 23 | res += dp(s, i + sz, pt2) 24 | else: 25 | res = res or dp(s, i + sz, pt2) 26 | return res 27 | 28 | 29 | def part_one(): 30 | return sum(map(dp, DESIGNS)) 31 | 32 | 33 | def part_two(): 34 | return sum(dp(x, pt2=True) for x in DESIGNS) 35 | 36 | 37 | print(f"Part 1: {part_one()}") 38 | print(f"Part 2: {part_two()}") 39 | -------------------------------------------------------------------------------- /2024/20.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | from itertools import combinations 3 | 4 | 5 | def parse_input(): 6 | with open("20.txt", "r") as file: 7 | data = file.read() 8 | grid = data.splitlines() 9 | s = e = None 10 | for i, row in enumerate(grid): 11 | for j, v in enumerate(row): 12 | if v == "S": 13 | s = (i, j) 14 | elif v == "E": 15 | e = (i, j) 16 | return grid, s, e 17 | 18 | 19 | GRID, S, E = parse_input() 20 | N, M = len(GRID), len(GRID[0]) 21 | 22 | 23 | def bfs(): 24 | q = deque([(*S, [S])]) 25 | seen = {S} 26 | while q: 27 | i, j, path = q.popleft() 28 | if (i, j) == E: 29 | return path 30 | for ni, nj in ((i + 1, j), (i - 1, j), (i, j + 1), (i, j - 1)): 31 | if ( 32 | 0 <= ni < N 33 | and 0 <= nj < M 34 | and GRID[ni][nj] != "#" 35 | and (ni, nj) not in seen 36 | ): 37 | seen.add((ni, nj)) 38 | q.append((ni, nj, path + [(ni, nj)])) 39 | 40 | 41 | PATH = bfs() 42 | 43 | 44 | def solve(cheat=2): 45 | path_length = len(PATH) - 1 46 | cost_start = {u: i for i, u in enumerate(PATH)} 47 | cost_end = {u: i for i, u in enumerate(reversed(PATH))} 48 | return sum( 49 | path_length - (cost_start[u] + cost_end[v] + dist) >= 100 50 | for u, v in combinations(PATH, 2) 51 | if (dist := abs(u[0] - v[0]) + abs(u[1] - v[1])) <= cheat 52 | ) 53 | 54 | 55 | def part_one(): 56 | return solve() 57 | 58 | 59 | def part_two(): 60 | return solve(20) 61 | 62 | 63 | print(f"Part 1: {part_one()}") 64 | print(f"Part 2: {part_two()}") 65 | -------------------------------------------------------------------------------- /2024/22.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from functools import reduce 3 | from itertools import accumulate, pairwise 4 | 5 | 6 | def parse_input(): 7 | with open("22.txt", "r") as file: 8 | data = file.read() 9 | return list(map(int, data.splitlines())) 10 | 11 | 12 | NUMS = parse_input() 13 | 14 | 15 | def evolve(n): 16 | n = ((n << 6) ^ n) & 0xFFFFFF 17 | n = ((n >> 5) ^ n) & 0xFFFFFF 18 | n = ((n << 11) ^ n) & 0xFFFFFF 19 | return n 20 | 21 | 22 | def part_one(): 23 | return sum(reduce(lambda acc, _: evolve(acc), range(2000), n) for n in NUMS) 24 | 25 | 26 | def part_two(): 27 | bananas = defaultdict(int) 28 | for n in NUMS: 29 | steps = list(accumulate(range(2000), lambda acc, _: evolve(acc), initial=n)) 30 | delta = [y % 10 - x % 10 for x, y in pairwise(steps)] 31 | seen = set() 32 | for i in range(len(steps) - 4): 33 | cur = tuple(delta[i : i + 4]) 34 | if cur in seen: 35 | continue 36 | seen.add(cur) 37 | bananas[cur] += steps[i + 4] % 10 38 | return max(bananas.values()) 39 | 40 | 41 | print(f"Part 1: {part_one()}") 42 | print(f"Part 2: {part_two()}") 43 | -------------------------------------------------------------------------------- /2024/23.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | 4 | def parse_input(): 5 | with open("23.txt", "r") as file: 6 | data = file.read() 7 | g = defaultdict(list) 8 | for line in data.splitlines(): 9 | u, v = line.split("-") 10 | g[u].append(v) 11 | g[v].append(u) 12 | return g 13 | 14 | 15 | G = parse_input() 16 | 17 | 18 | def part_one(): 19 | res = set() 20 | for u in filter(lambda x: x.startswith("t"), G.keys()): 21 | for v in filter(lambda x: x != u, G[u]): 22 | for w in filter(lambda x: x not in [u, v], G[u]): 23 | if w in G[v]: 24 | cur = tuple(sorted([u, v, w])) 25 | res.add(cur) 26 | return len(res) 27 | 28 | 29 | def part_two(): 30 | def bron_kerbosch(r, p, x, cur): 31 | if not p and not x: 32 | return max(r, cur, key=len) 33 | pivot = next(iter(p | x)) 34 | for v in list(p - set(G[pivot])): 35 | cur = max( 36 | cur, 37 | bron_kerbosch(r | {v}, p & set(G[v]), x & set(G[v]), cur), 38 | key=len, 39 | ) 40 | p -= {v} 41 | x |= {v} 42 | return cur 43 | 44 | return ",".join(sorted(bron_kerbosch(set(), set(G.keys()), set(), set()))) 45 | 46 | 47 | print(f"Part 1: {part_one()}") 48 | print(f"Part 2: {part_two()}") 49 | -------------------------------------------------------------------------------- /2024/25.py: -------------------------------------------------------------------------------- 1 | def parse_input(): 2 | with open("25.txt", "r") as file: 3 | data = file.read() 4 | locks, keys = [], [] 5 | for schematic in data.split("\n\n"): 6 | cur = {i for i, c in enumerate(schematic) if c == "#"} 7 | if schematic.startswith("#####"): 8 | locks.append(cur) 9 | else: 10 | keys.append(cur) 11 | return locks, keys 12 | 13 | 14 | LOCKS, KEYS = parse_input() 15 | 16 | 17 | def part_one(): 18 | return sum(not lock & key for lock in LOCKS for key in KEYS) 19 | 20 | 21 | print(f"Part 1: {part_one()}") 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Advent of Code 2 | 3 | My solutions/attempts to problems from [Advent of Code](https://adventofcode.com/) 4 | --------------------------------------------------------------------------------