├── .gitattributes ├── aoc20.png ├── time-plots ├── 2020.png └── 2020-cleanup.png ├── 02_password_philosophy.py ├── 06_custom_customs.py ├── 10_adapter_array.py ├── 05_binary_boarding.py ├── 03_toboggan_trajectory.py ├── 09_encoding_error.py ├── 25_combo_breaker.py ├── 01_report_repair.py ├── AOCUtils.py ├── 15_rambunctious_recitation.py ├── LICENSE ├── 07_handy_haversacks.py ├── 14_docking_data.py ├── 12_rain_risk.py ├── 08_handheld_halting.py ├── 17_conway_cubes.py ├── 23_crab_cups.py ├── 24_lobby_layout.py ├── 04_password_processing.py ├── 18_operation_order.py ├── 13_shuttle_search.py ├── 22_crab_combat.py ├── 11_seating_system.py ├── 19_monster_messages.py ├── 21_allergen_assessment.py ├── README.md ├── 16_ticket_translation.py └── 20_jurassic_jigsaw.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ipynb linguist-vendored 2 | -------------------------------------------------------------------------------- /aoc20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabekanegae/advent-of-code-2020/HEAD/aoc20.png -------------------------------------------------------------------------------- /time-plots/2020.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabekanegae/advent-of-code-2020/HEAD/time-plots/2020.png -------------------------------------------------------------------------------- /time-plots/2020-cleanup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabekanegae/advent-of-code-2020/HEAD/time-plots/2020-cleanup.png -------------------------------------------------------------------------------- /02_password_philosophy.py: -------------------------------------------------------------------------------- 1 | ###################################### 2 | # --- Day 2: Password Philosophy --- # 3 | ###################################### 4 | 5 | import AOCUtils 6 | 7 | ###################################### 8 | 9 | rawPasswords = AOCUtils.loadInput(2) 10 | 11 | p1 = 0 12 | p2 = 0 13 | for rawPassword in rawPasswords: 14 | counts, c, password = rawPassword.split() 15 | a, b = [int(i) for i in counts.split("-")] 16 | c = c[0] 17 | 18 | if a <= password.count(c) <= b: 19 | p1 += 1 20 | if (password[a-1] == c) ^ (password[b-1] == c): 21 | p2 += 1 22 | 23 | print("Part 1: {}".format(p1)) 24 | 25 | print("Part 2: {}".format(p2)) 26 | 27 | AOCUtils.printTimeTaken() -------------------------------------------------------------------------------- /06_custom_customs.py: -------------------------------------------------------------------------------- 1 | ################################## 2 | # --- Day 6: Binary Boarding --- # 3 | ################################## 4 | 5 | import AOCUtils 6 | 7 | ################################## 8 | 9 | rawAnswers = AOCUtils.loadInput(6) 10 | 11 | for i in range(len(rawAnswers)): 12 | if rawAnswers[i] == "": rawAnswers[i] = "\n" 13 | 14 | rawGroups = " ".join(rawAnswers).split("\n") 15 | 16 | groups = [[set(answer) for answer in rawGroup.split()] for rawGroup in rawGroups] 17 | 18 | p1 = sum(len(set.union(*group)) for group in groups) 19 | print("Part 1: {}".format(p1)) 20 | 21 | p2 = sum(len(set.intersection(*group)) for group in groups) 22 | print("Part 2: {}".format(p2)) 23 | 24 | AOCUtils.printTimeTaken() -------------------------------------------------------------------------------- /10_adapter_array.py: -------------------------------------------------------------------------------- 1 | ################################# 2 | # --- Day 10: Adapter Array --- # 3 | ################################# 4 | 5 | import AOCUtils 6 | 7 | ################################# 8 | 9 | adapters = AOCUtils.loadInput(10) 10 | 11 | adapters.sort() 12 | 13 | j1, j3 = 0, 0 14 | cur = 0 15 | for n in adapters: 16 | delta = n - cur 17 | 18 | j1 += int(delta == 1) 19 | j3 += int(delta == 3) 20 | 21 | cur += delta 22 | 23 | j3 += 1 24 | 25 | print("Part 1: {}".format(j1 * j3)) 26 | 27 | memo = [0] * (max(adapters) + 1) 28 | memo[0] = 1 29 | 30 | for n in adapters: 31 | l1 = memo[n-1] if n-1 >= 0 else 0 32 | l2 = memo[n-2] if n-2 >= 0 else 0 33 | l3 = memo[n-3] if n-3 >= 0 else 0 34 | 35 | memo[n] = l1 + l2 + l3 36 | 37 | print("Part 2: {}".format(memo[-1])) 38 | 39 | AOCUtils.printTimeTaken() -------------------------------------------------------------------------------- /05_binary_boarding.py: -------------------------------------------------------------------------------- 1 | ################################## 2 | # --- Day 5: Binary Boarding --- # 3 | ################################## 4 | 5 | import AOCUtils 6 | 7 | ################################## 8 | 9 | boardingPasses = AOCUtils.loadInput(5) 10 | 11 | seatIDs = [] 12 | for boardingPass in boardingPasses: 13 | lo, hi = 0, (2 ** len(boardingPass)) - 1 14 | for c in boardingPass: 15 | mid = (lo + hi) // 2 16 | if c in "FL": 17 | hi = mid 18 | elif c in "BR": 19 | lo = mid 20 | 21 | seatID = hi 22 | seatIDs.append(seatID) 23 | 24 | print("Part 1: {}".format(max(seatIDs))) 25 | 26 | allSeats = set(range(min(seatIDs), max(seatIDs) + 1)) 27 | missingSeats = allSeats - set(seatIDs) # Assume len(missingSeats) == 1 28 | 29 | print("Part 2: {}".format(missingSeats.pop())) 30 | 31 | AOCUtils.printTimeTaken() -------------------------------------------------------------------------------- /03_toboggan_trajectory.py: -------------------------------------------------------------------------------- 1 | ###################################### 2 | # --- Day 3: Toboggan Trajectory --- # 3 | ###################################### 4 | 5 | import AOCUtils 6 | 7 | def countTrees(forest, deltaW, deltaH): 8 | h, w = len(forest), len(forest[0]) 9 | 10 | trees = 0 11 | 12 | curH, curW = 0, 0 13 | for curH in range(0, h, deltaH): 14 | trees += int(forest[curH][curW] == "#") 15 | 16 | curW = (curW + deltaW) % w 17 | 18 | return trees 19 | 20 | ###################################### 21 | 22 | forest = AOCUtils.loadInput(3) 23 | 24 | p1 = countTrees(forest, 3, 1) 25 | 26 | print("Part 1: {}".format(p1)) 27 | 28 | p2 = countTrees(forest, 1, 1) 29 | p2 *= countTrees(forest, 3, 1) 30 | p2 *= countTrees(forest, 5, 1) 31 | p2 *= countTrees(forest, 7, 1) 32 | p2 *= countTrees(forest, 1, 2) 33 | 34 | print("Part 2: {}".format(p2)) 35 | 36 | AOCUtils.printTimeTaken() -------------------------------------------------------------------------------- /09_encoding_error.py: -------------------------------------------------------------------------------- 1 | ################################# 2 | # --- Day 9: Encoding Error --- # 3 | ################################# 4 | 5 | import AOCUtils 6 | from itertools import combinations 7 | 8 | ################################# 9 | 10 | data = AOCUtils.loadInput(9) 11 | 12 | for i in range(25, len(data)): 13 | if data[i] not in [sum(l) for l in combinations(data[i-25:i], 2)]: 14 | p1 = data[i] 15 | break 16 | 17 | print("Part 1: {}".format(p1)) 18 | 19 | i = 0 20 | j = 1 21 | cumSum = data[i] 22 | while i < len(data): 23 | cumSum += data[j] # cumSum == sum(data[i:j+1]) 24 | 25 | if cumSum == p1: 26 | p2 = min(data[i:j+1]) + max(data[i:j+1]) 27 | break 28 | 29 | if cumSum > p1 or j == len(data) - 1: 30 | i += 1 31 | j = i 32 | cumSum = data[i] 33 | 34 | j += 1 35 | 36 | print("Part 2: {}".format(p2)) 37 | 38 | AOCUtils.printTimeTaken() -------------------------------------------------------------------------------- /25_combo_breaker.py: -------------------------------------------------------------------------------- 1 | ################################# 2 | # --- Day 25: Combo Breaker --- # 3 | ################################# 4 | 5 | import AOCUtils 6 | 7 | def brutePrivate(public, e, n): 8 | # (e ^ private) % n = public 9 | 10 | private = 1 11 | genPublic = 1 12 | while True: 13 | genPublic = (genPublic * e) % n 14 | if genPublic == public: 15 | return private 16 | private += 1 17 | 18 | return private 19 | 20 | ################################# 21 | 22 | cardPublic, doorPublic = AOCUtils.loadInput(25) 23 | 24 | n = 20201227 25 | e = 7 26 | 27 | doorPrivate = brutePrivate(doorPublic, e, n) 28 | doorKey = pow(cardPublic, doorPrivate, n) 29 | 30 | cardPrivate = brutePrivate(cardPublic, e, n) 31 | cardKey = pow(doorPublic, cardPrivate, n) 32 | 33 | if doorKey == cardKey: 34 | print("Part 1: {}".format(doorKey)) 35 | 36 | AOCUtils.printTimeTaken() -------------------------------------------------------------------------------- /01_report_repair.py: -------------------------------------------------------------------------------- 1 | ################################ 2 | # --- Day 1: Report Repair --- # 3 | ################################ 4 | 5 | import AOCUtils 6 | 7 | def twoSum(counts, target): 8 | for i, ct in counts.items(): 9 | if target-i in counts: 10 | return i * (target-i) 11 | 12 | def threeSum(counts, target): 13 | for i in counts: 14 | twoSumResult = twoSum(counts, target-i) 15 | if twoSumResult is not None: 16 | return i * twoSumResult 17 | 18 | ################################ 19 | 20 | report = AOCUtils.loadInput(1) 21 | 22 | target = 2020 23 | 24 | reportCounts = dict() 25 | for i in report: 26 | if i not in reportCounts: 27 | reportCounts[i] = 0 28 | reportCounts[i] += 1 29 | 30 | print("Part 1: {}".format(twoSum(reportCounts, target))) 31 | 32 | print("Part 2: {}".format(threeSum(reportCounts, target))) 33 | 34 | AOCUtils.printTimeTaken() 35 | -------------------------------------------------------------------------------- /AOCUtils.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | import os 3 | 4 | _startTime = None 5 | 6 | def loadInput(day): 7 | global _startTime 8 | 9 | day = str(day) 10 | filename = "input" + day.zfill(2) + ".txt" 11 | filepath = os.path.join("inputs", filename) 12 | 13 | with open(filepath) as f: 14 | content = [l.rstrip("\n") for l in f.readlines()] 15 | 16 | _startTime = time() 17 | 18 | if len(content) == 1: 19 | try: 20 | return int(content[0]) 21 | except: 22 | try: 23 | return [int(i) for i in content[0].split()] 24 | except: 25 | return content[0] 26 | else: 27 | try: 28 | return [int(i) for i in content] 29 | except: 30 | return content 31 | 32 | def printTimeTaken(): 33 | global _startTime 34 | _endTime = time() 35 | 36 | print("Time: {:.3f}s".format(_endTime - _startTime)) -------------------------------------------------------------------------------- /15_rambunctious_recitation.py: -------------------------------------------------------------------------------- 1 | ########################################### 2 | # --- Day 15: Rambunctious Recitation --- # 3 | ########################################### 4 | 5 | import AOCUtils 6 | 7 | def memoryGame(start, n): 8 | seen = dict() 9 | last = None 10 | 11 | for i in range(n): 12 | if i < len(start): 13 | speak = start[i] 14 | elif len(seen[last]) < 2: 15 | speak = 0 16 | else: 17 | speak = seen[last][-1] - seen[last][-2] 18 | 19 | if speak not in seen: 20 | seen[speak] = [] 21 | seen[speak].append(i) 22 | last = speak 23 | 24 | return speak 25 | 26 | ########################################### 27 | 28 | start = AOCUtils.loadInput(15) 29 | 30 | start = [int(i) for i in start.split(",")] 31 | 32 | print("Part 1: {}".format(memoryGame(start, 2020))) 33 | 34 | print("Part 2: {}".format(memoryGame(start, 30000000))) 35 | 36 | AOCUtils.printTimeTaken() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Gabriel Kanegae 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /07_handy_haversacks.py: -------------------------------------------------------------------------------- 1 | ################################### 2 | # --- Day 7: Handy Haversacks --- # 3 | ################################### 4 | 5 | import AOCUtils 6 | 7 | def containsBag(bags, outerBag, goal): 8 | return outerBag == goal or any(containsBag(bags, bag, goal) for bag in bags[outerBag]) 9 | 10 | def countBagsInside(bags, outerBag): 11 | return sum(n * (countBagsInside(bags, bag) + 1) for bag, n in bags[outerBag].items()) 12 | 13 | ################################### 14 | 15 | rawBags = AOCUtils.loadInput(7) 16 | 17 | bags = dict() 18 | for rawBag in rawBags: 19 | color, rawContents = rawBag.split(" contain ") 20 | rawContents = rawContents.rstrip(".").split(", ") 21 | 22 | color = color.replace("bags", "bag") 23 | 24 | contents = dict() 25 | if rawContents[0] != "no other bags": 26 | for rawContent in rawContents: 27 | rawContent = rawContent.split() 28 | 29 | contentAmount = int(rawContent[0]) 30 | contentColor = " ".join(rawContent[1:]) 31 | 32 | contentColor = contentColor.replace("bags", "bag") 33 | contents[contentColor] = contentAmount 34 | 35 | bags[color] = contents 36 | 37 | target = "shiny gold bag" 38 | 39 | p1 = sum(containsBag(bags, bag, target) for bag in bags) - 1 40 | print("Part 1: {}".format(p1)) 41 | 42 | p2 = countBagsInside(bags, target) 43 | print("Part 2: {}".format(p2)) 44 | 45 | AOCUtils.printTimeTaken() -------------------------------------------------------------------------------- /14_docking_data.py: -------------------------------------------------------------------------------- 1 | ################################ 2 | # --- Day 14: Docking Data --- # 3 | ################################ 4 | 5 | import AOCUtils 6 | 7 | ################################ 8 | 9 | program = AOCUtils.loadInput(14) 10 | 11 | mem = dict() 12 | mask = None 13 | 14 | for instruction in program: 15 | var, data = instruction.split(" = ") 16 | if var == "mask": 17 | mask = data 18 | elif var.startswith("mem"): 19 | idx = var[4:-1] 20 | 21 | data = list(bin(int(data))[2:].zfill(36)) 22 | for i, c in enumerate(mask): 23 | if c != "X": 24 | data[i] = c 25 | data = int("".join(data), 2) 26 | 27 | mem[idx] = data 28 | 29 | print("Part 1: {}".format(sum(mem.values()))) 30 | 31 | mem = dict() 32 | mask = None 33 | 34 | for instruction in program: 35 | var, data = instruction.split(" = ") 36 | if var == "mask": 37 | mask = data 38 | elif var.startswith("mem"): 39 | idx = var[4:-1] 40 | 41 | idx = list(bin(int(idx))[2:].zfill(36)) 42 | for i, c in enumerate(mask): 43 | if c == "1": 44 | idx[i] = c 45 | 46 | floating = [i for i, c in enumerate(mask) if c == "X"] 47 | n = len(floating) 48 | for bits in range(2**n): 49 | bits = bin(bits)[2:].zfill(n) 50 | 51 | newIdx = idx[:] 52 | for i, b in zip(floating, bits): 53 | newIdx[i] = b 54 | 55 | newIdx = int("".join(newIdx), 2) 56 | mem[newIdx] = int(data) 57 | 58 | print("Part 2: {}".format(sum(mem.values()))) 59 | 60 | AOCUtils.printTimeTaken() -------------------------------------------------------------------------------- /12_rain_risk.py: -------------------------------------------------------------------------------- 1 | ############################# 2 | # --- Day 12: Rain Risk --- # 3 | ############################# 4 | 5 | import AOCUtils 6 | 7 | moves = {"E": (1, 0), 8 | "N": (0, 1), 9 | "W": (-1, 0), 10 | "S": (0, -1)} 11 | 12 | ############################# 13 | 14 | navigation = AOCUtils.loadInput(12) 15 | 16 | pos = (0, 0) 17 | facing = 0 18 | 19 | for inst in navigation: 20 | action, n = inst[0], int(inst[1:]) 21 | 22 | if action in moves: 23 | delta = moves[action] 24 | pos = (pos[0] + (delta[0] * n), pos[1] + (delta[1] * n)) 25 | elif action == "L": 26 | facing = (facing + (n // 90)) % 4 27 | elif action == "R": 28 | facing = (facing - (n // 90)) % 4 29 | elif action == "F": 30 | delta = list(moves.values())[facing] 31 | pos = (pos[0] + (delta[0] * n), pos[1] + (delta[1] * n)) 32 | 33 | print("Part 1: {}".format(abs(pos[0]) + abs(pos[1]))) 34 | 35 | pos = (0, 0) 36 | waypoint = (10, 1) 37 | 38 | for inst in navigation: 39 | action, n = inst[0], int(inst[1:]) 40 | 41 | if action in moves: 42 | delta = moves[action] 43 | waypoint = (waypoint[0] + (delta[0] * n), waypoint[1] + (delta[1] * n)) 44 | elif action == "L": 45 | for _ in range((n // 90) % 4): 46 | waypoint = (-waypoint[1], waypoint[0]) 47 | elif action == "R": 48 | for _ in range((n // 90) % 4): 49 | waypoint = (waypoint[1], -waypoint[0]) 50 | elif action == "F": 51 | pos = (pos[0] + (waypoint[0] * n), pos[1] + (waypoint[1] * n)) 52 | 53 | print("Part 2: {}".format(abs(pos[0]) + abs(pos[1]))) 54 | 55 | AOCUtils.printTimeTaken() -------------------------------------------------------------------------------- /08_handheld_halting.py: -------------------------------------------------------------------------------- 1 | ################################### 2 | # --- Day 8: Handheld Halting --- # 3 | ################################### 4 | 5 | import AOCUtils 6 | 7 | class VM: 8 | def __init__(self, program): 9 | self.program = program[:] 10 | 11 | self.pc = 0 12 | self.acc = 0 13 | 14 | self.seen = set() 15 | self.looped = False 16 | 17 | def run(self): 18 | while self.pc < len(self.program): 19 | if self.pc in self.seen: 20 | self.looped = True 21 | break 22 | 23 | self.seen.add(self.pc) 24 | 25 | inst, n = self.program[self.pc].split() 26 | n = int(n) 27 | 28 | if inst == "acc": 29 | self.acc += n 30 | elif inst == "jmp": 31 | self.pc += n-1 32 | elif inst == "nop": 33 | pass 34 | 35 | self.pc += 1 36 | 37 | if self.pc >= len(program): 38 | self.looped = False 39 | 40 | ################################### 41 | 42 | program = AOCUtils.loadInput(8) 43 | 44 | vm = VM(program) 45 | vm.run() 46 | 47 | print("Part 1: {}".format(vm.acc)) 48 | 49 | for i in range(len(program)): 50 | if "jmp" in program[i]: 51 | variation = program[:] 52 | variation[i] = program[i].replace("jmp", "nop") 53 | elif "nop" in program[i]: 54 | variation = program[:] 55 | variation[i] = program[i].replace("nop", "jmp") 56 | else: 57 | continue 58 | 59 | vm = VM(variation) 60 | vm.run() 61 | 62 | if not vm.looped: 63 | print("Part 2: {}".format(vm.acc)) 64 | break 65 | 66 | AOCUtils.printTimeTaken() -------------------------------------------------------------------------------- /17_conway_cubes.py: -------------------------------------------------------------------------------- 1 | ################################ 2 | # --- Day 17: Conway Cubes --- # 3 | ################################ 4 | 5 | import AOCUtils 6 | from itertools import combinations 7 | 8 | # Assume dimensions >= 2 9 | def conwayCubes(rawGrid, dimensions, cycles=6): 10 | allMoves = set(list(combinations([-1, 0, 1]*dimensions, dimensions))) 11 | nullMove = set([tuple([0]*dimensions)]) 12 | moves = list(allMoves - nullMove) 13 | 14 | active = set() 15 | for x in range(len(rawGrid)): 16 | for y in range(len(rawGrid[0])): 17 | if rawGrid[x][y] == "#": 18 | pos = tuple([x, y] + [0]*(dimensions - 2)) 19 | active.add(pos) 20 | 21 | for cycle in range(cycles): 22 | toBeUpdated = set(active) 23 | for pos in active: 24 | for delta in moves: 25 | neighbor = tuple(k+d for k, d in zip(pos, delta)) 26 | toBeUpdated.add(neighbor) 27 | 28 | newActive = set(active) 29 | for pos in toBeUpdated: 30 | neighbors = 0 31 | for delta in moves: 32 | neighbor = tuple(k+d for k, d in zip(pos, delta)) 33 | neighbors += int(neighbor in active) 34 | 35 | if pos in active and neighbors not in [2, 3]: 36 | newActive.remove(pos) 37 | elif pos not in active and neighbors == 3: 38 | newActive.add(pos) 39 | 40 | active = newActive 41 | 42 | return len(active) 43 | 44 | ################################ 45 | 46 | rawGrid = AOCUtils.loadInput(17) 47 | 48 | print("Part 1: {}".format(conwayCubes(rawGrid, 3))) 49 | 50 | print("Part 2: {}".format(conwayCubes(rawGrid, 4))) 51 | 52 | AOCUtils.printTimeTaken() -------------------------------------------------------------------------------- /23_crab_cups.py: -------------------------------------------------------------------------------- 1 | ############################# 2 | # --- Day 23: Crab Cups --- # 3 | ############################# 4 | 5 | import AOCUtils 6 | 7 | class Node: 8 | def __init__(self, data): 9 | self.data = data 10 | self.next = None 11 | 12 | def crabCups(cups, moves): 13 | nodes = [Node(k) for k in cups] 14 | for i in range(len(cups)-1): 15 | nodes[i].next = nodes[i+1] 16 | nodes[len(cups)-1].next = nodes[0] 17 | 18 | nodeLookup = {node.data: node for node in nodes} 19 | 20 | cur = nodes[0].data 21 | for _ in range(moves): 22 | p = nodeLookup[cur] 23 | 24 | a = nodeLookup[cur].next 25 | b = nodeLookup[cur].next.next 26 | c = nodeLookup[cur].next.next.next 27 | 28 | nodeLookup[cur].next = c.next 29 | 30 | dest = cur 31 | while True: 32 | dest -= 1 33 | 34 | if dest < 1: 35 | dest = len(cups) 36 | 37 | if dest not in [a.data, b.data, c.data]: 38 | break 39 | 40 | c.next = nodeLookup[dest].next 41 | nodeLookup[dest].next = a 42 | nodeLookup[dest].next.next = b 43 | nodeLookup[dest].next.next.next = c 44 | 45 | cur = nodeLookup[cur].next.data 46 | 47 | return nodeLookup[1].next 48 | 49 | ############################# 50 | 51 | rawCups = AOCUtils.loadInput(23) 52 | 53 | cups1 = [int(i) for i in str(rawCups)] 54 | p = crabCups(cups1, 100) 55 | 56 | p1 = [] 57 | for _ in range(8): 58 | p1.append(str(p.data)) 59 | p = p.next 60 | 61 | print("Part 1: {}".format("".join(p1))) 62 | 63 | cups2 = cups1 + list(range(len(cups1)+1, 1000000+1)) 64 | p = crabCups(cups2, 10000000) 65 | 66 | print("Part 2: {}".format(p.data * p.next.data)) 67 | 68 | AOCUtils.printTimeTaken() -------------------------------------------------------------------------------- /24_lobby_layout.py: -------------------------------------------------------------------------------- 1 | ################################ 2 | # --- Day 24: Lobby Layout --- # 3 | ################################ 4 | 5 | import AOCUtils 6 | 7 | def splitTokens(s, tokens): 8 | tokens = set(tokens) 9 | 10 | i = 0 11 | j = 0 12 | out = [] 13 | while j < len(s): 14 | if s[i:j] in tokens: 15 | out.append(s[i:j]) 16 | i = j 17 | j += 1 18 | out.append(s[i:]) 19 | 20 | return out 21 | 22 | # Y axis rotated 30 deg (L/R is W/E) 23 | directions = {"nw": (1, -1), "w": (0, -1), "ne": (1, 0), 24 | "sw": (-1, 0), "e": (0, 1), "se": (-1, 1)} 25 | 26 | ################################ 27 | 28 | paths = AOCUtils.loadInput(24) 29 | 30 | blackTiles = set() 31 | for path in paths: 32 | cur = (0, 0) 33 | for direction in splitTokens(path, directions.keys()): 34 | delta = directions[direction] 35 | cur = (cur[0]+delta[0], cur[1]+delta[1]) 36 | 37 | if cur not in blackTiles: 38 | blackTiles.add(cur) 39 | else: 40 | blackTiles.remove(cur) 41 | 42 | print("Part 1: {}".format(len(blackTiles))) 43 | 44 | for _ in range(100): 45 | toBeUpdated = set(blackTiles) 46 | for tile in blackTiles: 47 | for delta in directions.values(): 48 | neighbor = (tile[0]+delta[0], tile[1]+delta[1]) 49 | toBeUpdated.add(neighbor) 50 | 51 | newBlackTiles = set(blackTiles) 52 | for tile in toBeUpdated: 53 | blackNeighbors = 0 54 | for delta in directions.values(): 55 | neighbor = (tile[0]+delta[0], tile[1]+delta[1]) 56 | blackNeighbors += int(neighbor in blackTiles) 57 | 58 | if tile in blackTiles and (blackNeighbors == 0 or blackNeighbors > 2): 59 | newBlackTiles.remove(tile) 60 | elif tile not in blackTiles and blackNeighbors == 2: 61 | newBlackTiles.add(tile) 62 | 63 | blackTiles = newBlackTiles 64 | 65 | print("Part 2: {}".format(len(blackTiles))) 66 | 67 | AOCUtils.printTimeTaken() -------------------------------------------------------------------------------- /04_password_processing.py: -------------------------------------------------------------------------------- 1 | ###################################### 2 | # --- Day 4: Passport Processing --- # 3 | ###################################### 4 | 5 | import AOCUtils 6 | 7 | decChars = set("0123456789") 8 | hexChars = set("0123456789abcdef") 9 | 10 | checks1 = [ 11 | lambda pp: all(field in pp for field in ["byr", "iyr", "eyr", "hgt", "hcl", "ecl", "pid"]) 12 | ] 13 | 14 | checks2 = [ 15 | lambda pp: set(pp["byr"]) <= decChars and 1920 <= int(pp["byr"]) <= 2002, 16 | lambda pp: set(pp["iyr"]) <= decChars and 2010 <= int(pp["iyr"]) <= 2020, 17 | lambda pp: set(pp["eyr"]) <= decChars and 2020 <= int(pp["eyr"]) <= 2030, 18 | lambda pp: set(pp["hgt"][:-2]) <= decChars and \ 19 | ((pp["hgt"][-2:] == "cm" and 150 <= int(pp["hgt"][:-2]) <= 193) or \ 20 | (pp["hgt"][-2:] == "in" and 59 <= int(pp["hgt"][:-2]) <= 76)), 21 | lambda pp: len(pp["hcl"]) == 7 and pp["hcl"][0] == "#" and set(pp["hcl"][1:]) <= hexChars, 22 | lambda pp: pp["ecl"] in ["amb", "blu", "brn", "gry", "grn", "hzl", "oth"], 23 | lambda pp: len(pp["pid"]) == 9 and set(pp["pid"]) <= decChars 24 | ] 25 | 26 | def isValid1(passport): 27 | return all(check(passport) for check in checks1) 28 | 29 | def isValid2(passport): 30 | return isValid1(passport) and all(check(passport) for check in checks2) 31 | 32 | ###################################### 33 | 34 | rawInput = AOCUtils.loadInput(4) 35 | 36 | for i in range(len(rawInput)): 37 | if rawInput[i] == "": rawInput[i] = "\n" 38 | 39 | rawPassports = " ".join(rawInput).split(" \n ") 40 | 41 | passports = [] 42 | for rawPassport in rawPassports: 43 | passport = dict() 44 | for kvp in rawPassport.split(): 45 | k, v = kvp.split(":") 46 | passport[k] = v 47 | 48 | passports.append(passport) 49 | 50 | p1 = sum(isValid1(passport) for passport in passports) 51 | print("Part 1: {}".format(p1)) 52 | 53 | p2 = sum(isValid2(passport) for passport in passports) 54 | print("Part 2: {}".format(p2)) 55 | 56 | AOCUtils.printTimeTaken() -------------------------------------------------------------------------------- /18_operation_order.py: -------------------------------------------------------------------------------- 1 | ################################### 2 | # --- Day 18: Operation Order --- # 3 | ################################### 4 | 5 | import AOCUtils 6 | 7 | # Special int subclass for Part 1: for + and * to have the same precedence, 8 | # replace all '*' with '-' but change __sub__ behavior to __mul__. 9 | class int1(int): 10 | repl = {"*": "-"} 11 | 12 | def __add__(self, other): return int1(super().__add__(other)) 13 | def __sub__(self, other): return int1(super().__mul__(other)) 14 | 15 | # Special int subclass for Part 2: for + to have a higher precedence than *, 16 | # swap both '*' and '+' but swap their behaviors as well. 17 | class int2(int): 18 | repl = {"*": "+", "+": "*"} 19 | 20 | def __add__(self, other): return int2(super().__mul__(other)) 21 | def __mul__(self, other): return int2(super().__add__(other)) 22 | 23 | def splitTokens(expr): 24 | splitExpr = [] 25 | i = 0 26 | j = 0 27 | while j < len(expr): 28 | if not expr[j].isdigit(): 29 | splitExpr.append(expr[j]) 30 | j += 1 31 | else: 32 | while j < len(expr) and expr[j].isdigit(): j += 1 33 | splitExpr.append(expr[i:j]) 34 | 35 | i = j 36 | 37 | return splitExpr 38 | 39 | def specialEval(expr, cls): 40 | expr = expr.replace(" ", "") 41 | 42 | # Replace operations according to cls 43 | replacedExpr = list(expr) 44 | for i in range(len(replacedExpr)): 45 | for old, new in cls.repl.items(): 46 | if expr[i] == old: replacedExpr[i] = new 47 | expr = "".join(replacedExpr) 48 | 49 | # expr.split(), but keep digits together 50 | expr = splitTokens(expr) 51 | 52 | # Replace numbers with instances of cls 53 | for i in range(len(expr)): 54 | if expr[i].isdigit(): 55 | expr[i] = "{}({})".format(cls.__name__, expr[i]) 56 | 57 | return eval("".join(expr)) 58 | 59 | ################################### 60 | 61 | homework = AOCUtils.loadInput(18) 62 | 63 | p1 = sum(specialEval(expr, int1) for expr in homework) 64 | print("Part 1: {}".format(p1)) 65 | 66 | p2 = sum(specialEval(expr, int2) for expr in homework) 67 | print("Part 2: {}".format(p2)) 68 | 69 | AOCUtils.printTimeTaken() -------------------------------------------------------------------------------- /13_shuttle_search.py: -------------------------------------------------------------------------------- 1 | ################################## 2 | # --- Day 13: Shuttle Search --- # 3 | ################################## 4 | 5 | import AOCUtils 6 | 7 | # Modular inverse of n (assumes mod is prime, uses Euler's Theorem) 8 | def modinv(n, mod): 9 | return pow(n, mod-2, mod) 10 | 11 | # Get smallest x (i.e. unique in mod N) that satisfies a list 12 | # of linear congruences (assumes all elements in mods are coprime) 13 | def chineseRemainderTheorem(mods, remainders): 14 | # Given linear congruences x%3 = 2, x%5 = 3, x%7 = 2, 15 | # x = chineseRemainderTheorem([3, 5, 7], [2, 3, 2]) 16 | 17 | N = 1 18 | for m in mods: 19 | N *= m 20 | 21 | x = sum(r * N//m * modinv(N//m, m) for m, r in zip(mods, remainders)) 22 | return x % N 23 | 24 | ################################## 25 | 26 | notes = AOCUtils.loadInput(13) 27 | 28 | arrivedAtBusStop = int(notes[0]) 29 | schedule = notes[1].split(",") 30 | 31 | nextArrivals = [] 32 | for i, busID in enumerate(schedule): 33 | if busID == "x": continue 34 | busInterval = int(busID) 35 | 36 | sinceLastArrival = arrivedAtBusStop % busInterval 37 | untilNextArrival = busInterval - sinceLastArrival 38 | 39 | nextArrivals.append((untilNextArrival, busInterval)) 40 | 41 | nextArrivals.sort() 42 | nextArrival = nextArrivals[0] 43 | 44 | print("Part 1: {}".format(nextArrival[0] * nextArrival[1])) 45 | 46 | # Builds a list of linear congruences as x%mod = remainder 47 | # Example with schedule=[67,7,x,59,61]: 48 | # (t+0) % 67 = 0 -> t % 67 = (67-0)%67 -> t % 67 = 0 49 | # (t+1) % 7 = 0 -> t % 7 = (7-1)%7 -> t % 7 = 6 50 | # (t+3) % 59 = 0 -> t % 59 = (59-3)%59 -> t % 59 = 56 51 | # (t+4) % 61 = 0 -> t % 61 = (61-4)%61 -> t % 61 = 57 52 | # mods = [67, 7, 59, 61] 53 | # remainders = [0, 6, 56, 57] 54 | 55 | mods = [] 56 | remainders = [] 57 | for i, busID in enumerate(schedule): 58 | if busID == "x": continue 59 | busInterval = int(busID) 60 | 61 | mod = busInterval 62 | remainder = (busInterval - i) % busInterval 63 | 64 | mods.append(mod) 65 | remainders.append(remainder) 66 | 67 | print("Part 2: {}".format(chineseRemainderTheorem(mods, remainders))) 68 | 69 | AOCUtils.printTimeTaken() -------------------------------------------------------------------------------- /22_crab_combat.py: -------------------------------------------------------------------------------- 1 | ############################### 2 | # --- Day 22: Crab Combat --- # 3 | ############################### 4 | 5 | import AOCUtils 6 | from collections import deque 7 | 8 | def getScore(deck): 9 | return sum((i+1) * card for i, card in zip(range(len(deck)), reversed(deck))) 10 | 11 | def playGame1(rawPlayer1, rawPlayer2): 12 | player1 = deque(rawPlayer1) 13 | player2 = deque(rawPlayer2) 14 | 15 | while player1 and player2: 16 | top1 = player1.popleft() 17 | top2 = player2.popleft() 18 | 19 | p1Wins = (top1 > top2) 20 | 21 | if p1Wins: 22 | player1.append(top1) 23 | player1.append(top2) 24 | else: 25 | player2.append(top2) 26 | player2.append(top1) 27 | 28 | return getScore(player1), getScore(player2) 29 | 30 | def playGame2(rawPlayer1, rawPlayer2): 31 | player1 = deque(rawPlayer1) 32 | player2 = deque(rawPlayer2) 33 | 34 | seen = set() 35 | while player1 and player2: 36 | top1 = player1.popleft() 37 | top2 = player2.popleft() 38 | 39 | state = (tuple(player1), tuple(player2)) 40 | 41 | if state in seen: 42 | p1Wins = True 43 | else: 44 | seen.add(state) 45 | 46 | if len(player1) >= top1 and len(player2) >= top2: 47 | recursiveCopy1 = list(player1)[:top1] 48 | recursiveCopy2 = list(player2)[:top2] 49 | 50 | score1, score2 = playGame2(recursiveCopy1, recursiveCopy2) 51 | 52 | p1Wins = (score1 > score2) 53 | else: 54 | p1Wins = (top1 > top2) 55 | 56 | if p1Wins: 57 | player1.append(top1) 58 | player1.append(top2) 59 | else: 60 | player2.append(top2) 61 | player2.append(top1) 62 | 63 | return getScore(player1), getScore(player2) 64 | 65 | ############################### 66 | 67 | rawDecks = AOCUtils.loadInput(22) 68 | 69 | rawPlayer1, rawPlayer2 = "\n".join(rawDecks).split("\n\n") 70 | rawPlayer1 = [int(i) for i in rawPlayer1.split("\n")[1:]] 71 | rawPlayer2 = [int(i) for i in rawPlayer2.split("\n")[1:]] 72 | 73 | print("Part 1: {}".format(max(playGame1(rawPlayer1, rawPlayer2)))) 74 | 75 | print("Part 2: {}".format(max(playGame2(rawPlayer1, rawPlayer2)))) 76 | 77 | AOCUtils.printTimeTaken() -------------------------------------------------------------------------------- /11_seating_system.py: -------------------------------------------------------------------------------- 1 | ################################## 2 | # --- Day 11: Seating System --- # 3 | ################################## 4 | 5 | import AOCUtils 6 | 7 | moves8 = [(0, 1), (1, 0), (0, -1), (-1, 0), 8 | (1, 1), (1, -1), (-1, 1), (-1, -1)] 9 | 10 | ################################## 11 | 12 | originalSeats = [list(l) for l in AOCUtils.loadInput(11)] 13 | 14 | seats = [l[:] for l in originalSeats] 15 | h, w = len(seats), len(seats[0]) 16 | 17 | hasChanged = True 18 | while hasChanged: 19 | newSeats = [l[:] for l in seats] 20 | hasChanged = False 21 | for a in range(h): 22 | for b in range(w): 23 | n = 0 24 | for da, db in moves8: 25 | if da == 0 and db == 0: continue 26 | 27 | if 0 <= a+da < h and 0 <= b+db < w: 28 | if seats[a+da][b+db] == "#": 29 | n += 1 30 | 31 | if seats[a][b] == "L" and n == 0: 32 | newSeats[a][b] = "#" 33 | hasChanged = True 34 | elif seats[a][b] == "#" and n >= 4: 35 | newSeats[a][b] = "L" 36 | hasChanged = True 37 | 38 | seats = newSeats 39 | 40 | p1 = sum(l.count("#") for l in seats) 41 | print("Part 1: {}".format(p1)) 42 | 43 | seats = [l[:] for l in originalSeats] 44 | h, w = len(seats), len(seats[0]) 45 | 46 | hasChanged = True 47 | while hasChanged: 48 | hasChanged = False 49 | newSeats = [l[:] for l in seats] 50 | for a in range(h): 51 | for b in range(w): 52 | n = 0 53 | for da, db in moves8: 54 | ca, cb = a, b 55 | while 0 <= ca+da < h and 0 <= cb+db < w: 56 | ca += da 57 | cb += db 58 | 59 | if seats[ca][cb] == "#": 60 | n += 1 61 | 62 | if seats[ca][cb] != ".": 63 | break 64 | 65 | if seats[a][b] == "L" and n == 0: 66 | newSeats[a][b] = "#" 67 | hasChanged = True 68 | elif seats[a][b] == "#" and n >= 5: 69 | newSeats[a][b] = "L" 70 | hasChanged = True 71 | 72 | seats = newSeats 73 | 74 | p2 = sum(l.count("#") for l in seats) 75 | print("Part 2: {}".format(p2)) 76 | 77 | AOCUtils.printTimeTaken() -------------------------------------------------------------------------------- /19_monster_messages.py: -------------------------------------------------------------------------------- 1 | #################################### 2 | # --- Day 19: Monster Messages --- # 3 | #################################### 4 | 5 | import AOCUtils 6 | import re 7 | 8 | def checkRule(rule, line): 9 | return re.match("^"+rule+"$", line) is not None 10 | 11 | def buildDirectRule(baseRules, rule): 12 | if rule in "ab|": 13 | return rule 14 | else: 15 | return [buildDirectRule(baseRules, t) for t in baseRules[rule]] 16 | 17 | def parseRule(rule): 18 | if rule == ["a"] or rule == ["b"] or rule == "|": 19 | return rule[0] 20 | else: 21 | strRule = [parseRule(c) for c in rule] 22 | strRule = "".join(strRule) 23 | return "(?:" + strRule + ")" 24 | 25 | #################################### 26 | 27 | rawInput = AOCUtils.loadInput(19) 28 | 29 | rawRules, rawMessages = "\n".join(rawInput).split("\n\n") 30 | rawRules = rawRules.split("\n") 31 | messages = rawMessages.split("\n") 32 | 33 | baseRules = dict() 34 | for rawRule in rawRules: 35 | ruleID, rule = rawRule.split(": ") 36 | rule = [c.replace("\"", "") for c in rule.split()] 37 | 38 | baseRules[ruleID] = rule 39 | 40 | # Get token references, building a direct recursive array of arrays down until terminal symbols 41 | directRules = {ruleID: [buildDirectRule(baseRules, c) for c in rule] for ruleID, rule in baseRules.items()} 42 | 43 | # Serialize the recursive arrays enclosing them in parentheses (resulting in a regex-ready pattern) 44 | parsedRules = {ruleID: parseRule(rule) for ruleID, rule in directRules.items()} 45 | 46 | p1 = sum(checkRule(parsedRules["0"], message) for message in messages) 47 | print("Part 1: {}".format(p1)) 48 | 49 | # '8: 42 | 42 8' === '(42)+' 50 | # '11: 42 31 | 42 11 31' === '(42){n}(31){n}', n >= 1 51 | 52 | modifiedRule = parsedRules["0"] 53 | modifiedRule = modifiedRule.replace(parsedRules["42"], parsedRules["42"]+"!") 54 | 55 | # Replace all '(42)(31)' with '(42){n}(31){n}' 56 | modifiedRule = modifiedRule.replace(parsedRules["42"]+"!"+parsedRules["31"], parsedRules["42"]+"X"+parsedRules["31"]+"X") 57 | 58 | # Replace all '(42)' (without a following (31)) with '(42)+' 59 | modifiedRule = modifiedRule.replace(parsedRules["42"]+"!", parsedRules["42"]+"+") 60 | 61 | # Will check rule 11's n up until N, assuming it doesn't occur more than N times 62 | N = 10 63 | 64 | p2 = 0 65 | for n in range(1, N): 66 | p2 += sum(checkRule(modifiedRule.replace("X", "{"+str(n)+"}"), message) for message in messages) 67 | print("Part 2: {}".format(p2)) 68 | 69 | AOCUtils.printTimeTaken() -------------------------------------------------------------------------------- /21_allergen_assessment.py: -------------------------------------------------------------------------------- 1 | ####################################### 2 | # --- Day 21: Allergen Assessment --- # 3 | ####################################### 4 | 5 | import AOCUtils 6 | 7 | ####################################### 8 | 9 | rawFoods = AOCUtils.loadInput(21) 10 | 11 | foods = [] 12 | for rawFood in rawFoods: 13 | rawIngredients, rawAllergies = rawFood.split(" (contains ") 14 | rawAllergies = rawAllergies[:-1].split(", ") 15 | rawIngredients = rawIngredients.split() 16 | 17 | allergies = set(rawAllergies) 18 | ingredients = set(rawIngredients) 19 | 20 | foods.append((ingredients, allergies)) 21 | 22 | allIngredients = [] 23 | for ingredients, _ in foods: 24 | allIngredients += ingredients 25 | 26 | # Make a list of list of ingredients that make a recipe with each allergy 27 | possibleCauses = dict() 28 | for ingredients, allergies in foods: 29 | for allergy in allergies: 30 | if allergy not in possibleCauses: 31 | possibleCauses[allergy] = [] 32 | possibleCauses[allergy].append(ingredients) 33 | 34 | # Narrow down the possibilities by taking intersections 35 | # of ingredients of recipes that cause each allergy 36 | for allergy in possibleCauses: 37 | possibleCauses[allergy] = set.intersection(*possibleCauses[allergy]) 38 | 39 | mayCauseAllergy = set.union(*possibleCauses.values()) 40 | cantCauseAllergy = set(allIngredients) - mayCauseAllergy 41 | 42 | p1 = sum(ingredients in cantCauseAllergy for ingredients in allIngredients) 43 | print("Part 1: {}".format(p1)) 44 | 45 | possibleCausesList = list(possibleCauses.items()) 46 | 47 | # Assume the correct answer can be found by cascading 48 | # the correct answers from before 49 | allergyCauses = dict() 50 | for i in range(len(possibleCausesList)): 51 | # Always get the ingredient with the smallest amount of possibilities 52 | # i.e. sort by descending set length (so it can be later popped in O(1)) 53 | if len(possibleCausesList[-1][1]) != 1: 54 | possibleCausesList.sort(key=lambda x: len(x[1]), reverse=True) 55 | 56 | # Assume len(possible) == 1, i.e. there's only one answer 57 | allergy, possible = possibleCausesList[-1] 58 | cause = possible.pop() 59 | 60 | allergyCauses[allergy] = cause 61 | 62 | possibleCausesList.pop() # Remove allergy that had its cause identified 63 | 64 | # Remove determined cause from all other possibilities 65 | for j in range(len(possibleCausesList)): 66 | possibleCausesList[j][1].discard(cause) 67 | 68 | allergyCausesList = list(allergyCauses.items()) 69 | allergyCausesList.sort(key=lambda x: x[0]) 70 | 71 | p2 = ",".join(cause for _, cause in allergyCausesList) 72 | print("Part 2: {}".format(p2)) 73 | 74 | AOCUtils.printTimeTaken() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
