├── .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 |

2 | 3 | **[2015](https://github.com/KanegaeGabriel/advent-of-code-2015) | [2016](https://github.com/KanegaeGabriel/advent-of-code-2016) | [2017](https://github.com/KanegaeGabriel/advent-of-code-2017) | [2018](https://github.com/KanegaeGabriel/advent-of-code-2018) | [2019](https://github.com/KanegaeGabriel/advent-of-code-2019) | 2020 | [2021](https://github.com/KanegaeGabriel/advent-of-code-2021)** 4 | 5 | Here lies my solutions to [Advent of Code 2020](https://adventofcode.com/2020), an Advent calendar full of programming puzzles from December 1st all the way to Christmas. 6 | 7 | This year I managed to be awake at every puzzle release (midnight EST/UTC-5, 2AM my time) and finished all them in the minutes/hours that followed! With that, I beat my last year's score, this time placing at 167th on the Global Leaderboard with 389 points across 6 leaderboard finishes! I also got top 500 on exactly half of the days, so that's cool as well! I did some pretty cools plots of my completion times over at [time-plots](time-plots) if you want to check those out. 8 | 9 | ## Inputs and Outputs 10 | 11 | All inputs are read from `inputs\inputXX.txt`, with `XX` being the zero-padded day. As per the creator's request, they are not available in this repository and should be downloaded directly from the event website. 12 | 13 | The only outputs for all days are exactly what should be pasted in the puzzle answer textbox, followed by the total runtime of both parts combined (via Python's `time.time()`), no more and no less. The only exception is when the answer is drawn on a grid-like formation, then that is printed instead of OCR. In some cases, helpful debugging code or other verbose messages are simply commented out, and can be manually toggled to better understand the code inner workings. 14 | 15 | ## Implementation Goals 16 | 17 | The solutions posted here are cleaned-up versions of the actual code written when aiming for the leaderboards. For all solutions, the main implementation goals were, in descending order: 18 | 19 | * **Readability:** Clean, readable, self-explanatory and commented code above all else. 20 | * **Input Generalization:** Should work not only for my input but for anyone's, with some assumptions made about it, which are noted when appropriate. 21 | * **Modularity:** Avoid duplicate code where possible, allowing for easy modification by making heavy use of classes and functions. 22 | * **Speed:** Use efficient algorithms, keeping runtime reasonably low without extreme micro-optimizations. 23 | * **Minimal Imports:** Refrain from `import`s besides utilities (`sys`, `time`) and basic standard libraries (`math`, `itertools`, `collections`). When the knowledge of functions and structures are considered vital to the problem solution (graphs, trees, linked lists, union-find, etc.), reimplement them. 24 | 25 | ## Thanks! 26 | 27 | Many thanks to [Eric Wastl](http://was.tl/), who creates Advent of Code, as well as to the amazing community over at [/r/adventofcode](https://www.reddit.com/r/adventofcode/)! 28 | -------------------------------------------------------------------------------- /16_ticket_translation.py: -------------------------------------------------------------------------------- 1 | ###################################### 2 | # --- Day 16: Ticket Translation --- # 3 | ###################################### 4 | 5 | import AOCUtils 6 | 7 | ###################################### 8 | 9 | rawInput = AOCUtils.loadInput(16) 10 | 11 | rawFields, rawMyTicket, rawNearbyTickets = "\n".join(rawInput).split("\n\n") 12 | rawFields = rawFields.strip().split("\n") 13 | rawMyTicket = rawMyTicket.strip().split("\n") 14 | rawNearbyTickets = rawNearbyTickets.strip().split("\n") 15 | 16 | fields = dict() 17 | for rawField in rawFields: 18 | field, rawRanges = rawField.split(": ") 19 | 20 | a, b = rawRanges.split(" or ") 21 | a = [int(i) for i in a.split("-")] 22 | b = [int(i) for i in b.split("-")] 23 | 24 | fields[field] = [a, b] 25 | 26 | myTicket = [int(i) for i in rawMyTicket[1].split(",")] 27 | 28 | nearbyTickets = [[int(i) for i in ticket.split(",")] for ticket in rawNearbyTickets[1:]] 29 | 30 | errorRate = 0 31 | validNearbyTickets = [] 32 | for ticket in nearbyTickets: 33 | isValidTicket = True 34 | for value in ticket: 35 | isValidValue = False 36 | for ranges in fields.values(): 37 | (sa, ea), (sb, eb) = ranges 38 | if sa <= value <= ea or sb <= value <= eb: 39 | isValidValue = True 40 | break 41 | 42 | if not isValidValue: 43 | errorRate += value 44 | isValidTicket = False 45 | 46 | if isValidTicket: 47 | validNearbyTickets.append(ticket) 48 | 49 | print("Part 1: {}".format(errorRate)) 50 | 51 | fieldsPossibleIndexes = dict() 52 | for field, ranges in fields.items(): 53 | fieldsPossibleIndexes[field] = set() 54 | 55 | (sa, ea), (sb, eb) = ranges 56 | for i in range(len(fields)): 57 | possible = True 58 | for ticket in validNearbyTickets: 59 | if not (sa <= ticket[i] <= ea or sb <= ticket[i] <= eb): 60 | possible = False 61 | break 62 | 63 | if possible: 64 | fieldsPossibleIndexes[field].add(i) 65 | 66 | # Sort by ascending amount of possible indexes 67 | fieldsPossibleIndexes = list(fieldsPossibleIndexes.items()) 68 | fieldsPossibleIndexes.sort(key=lambda x: len(x[1])) 69 | 70 | fieldIndexes = {field: None for field in fields} 71 | 72 | # Assume len(fieldsPossibleIndexes[0]) == 1, and each fieldPossibleIndexes 73 | # is a superset of the one before, with one more element 74 | for i in range(len(fieldsPossibleIndexes)): 75 | field, possibleIndexes = fieldsPossibleIndexes[i] 76 | if len(possibleIndexes) == 1: 77 | index = possibleIndexes.pop() 78 | 79 | fieldIndexes[field] = index 80 | 81 | # Remove determined index from all other possibilities 82 | for j in range(i+1, len(fieldsPossibleIndexes)): 83 | fieldsPossibleIndexes[j][1].discard(index) 84 | 85 | departureHash = 1 86 | for field, index in fieldIndexes.items(): 87 | if field.startswith("departure"): 88 | departureHash *= myTicket[index] 89 | 90 | print("Part 2: {}".format(departureHash)) 91 | 92 | AOCUtils.printTimeTaken() -------------------------------------------------------------------------------- /20_jurassic_jigsaw.py: -------------------------------------------------------------------------------- 1 | ################################### 2 | # --- Day 20: Jurassic Jigsaw --- # 3 | ################################### 4 | 5 | import AOCUtils 6 | from collections import deque 7 | 8 | class Tile: 9 | def __init__(self, tileID, image): 10 | self.tileID = tileID 11 | self.image = [list(l) for l in image] 12 | 13 | @property 14 | def imageWithoutBorders(self): 15 | return [row[1:-1] for row in self.image[1:-1]] 16 | 17 | @property 18 | def sides(self): 19 | up = self.image[0] 20 | down = self.image[-1] 21 | left = [self.image[i][0] for i in range(len(self.image[0]))] 22 | right = [self.image[i][-1] for i in range(len(self.image[0]))] 23 | 24 | return {"U": up, "D": down, "L": left, "R": right} 25 | 26 | def rot90CW(matrix): 27 | return [list(l) for l in zip(*matrix[::-1])] 28 | 29 | def flipH(matrix): 30 | return [row[::-1] for row in matrix] 31 | 32 | ################################### 33 | 34 | # Loooots of assumptions made for this one to work. Should be 35 | # good for all test cases, but it's very likely they were 36 | # VERY carefully picked for a bunch of reasons. 37 | 38 | rawTiles = AOCUtils.loadInput(20) 39 | 40 | rawTiles = "\n".join(rawTiles).split("\n\n") 41 | 42 | tiles = dict() 43 | for rawTile in rawTiles: 44 | rawTile = rawTile.split("\n") 45 | 46 | tileID = int(rawTile[0].split()[1][:-1]) 47 | tile = [l for l in rawTile[1:] if l] # Input has one extra newline at the end 48 | 49 | tiles[tileID] = Tile(tileID, tile) 50 | 51 | # Assume tiles have equal sizes and are all squares, 52 | # get size of one of them (doesn't matter which one) 53 | tileSize = len(tiles[list(tiles.keys())[0]].image) 54 | 55 | # Build tiles adjacency matrix 56 | tileConnections = dict() 57 | for tile1ID, tile1 in tiles.items(): 58 | for tile2ID, tile2 in tiles.items(): 59 | if tile1ID == tile2ID: continue 60 | 61 | for side1Direction, side1 in tile1.sides.items(): 62 | for side2Direction, side2 in tile2.sides.items(): 63 | if side1 in [side2, side2[::-1]]: 64 | if tile1ID not in tileConnections: 65 | tileConnections[tile1ID] = dict() 66 | tileConnections[tile1ID][side1Direction] = tile2ID 67 | 68 | # Assume there will be only one possible match/choice/placement for each tile 69 | cornerTiles = [tileID for tileID, conns in tileConnections.items() if len(conns) == 2] 70 | 71 | p1 = 1 72 | for tileID in cornerTiles: 73 | p1 *= tileID 74 | 75 | print("Part 1: {}".format(p1)) 76 | 77 | mov4 = {"R": (0, 1), "D": (1, 0), "L": (0, -1), "U": (-1, 0)} 78 | 79 | dirsOpposite = {"D": "U", "U": "D", "L": "R", "R": "L"} 80 | dirsRot90CW = {"D": "L", "U": "R", "L": "U", "R": "D"} 81 | dirsFlipH = {k: (v if k in "LR" else dirsOpposite[v]) for k, v in dirsOpposite.items()} 82 | 83 | # Figure out the correct tile placement (after tiles are 84 | # rotated/flipped), building a `grid` matrix made of tile IDs. 85 | # Assume grid is square (i.e. `len(tiles)` is a perfect square) 86 | gridSize = int(len(tiles) ** 0.5) 87 | grid = [[None for _ in range(gridSize)] for _ in range(gridSize)] 88 | 89 | # Pick any corner tile (doesn't matter which, as long 90 | # as `startPos` is set based on their neighbors) 91 | startTile = cornerTiles[0] 92 | if all(c in tileConnections[startTile] for c in "DR"): # Top-left 93 | startPos = (0, 0) 94 | elif all(c in tileConnections[startTile] for c in "DL"): # Top-right 95 | startPos = (0, len(grid)-1) 96 | elif all(c in tileConnections[startTile] for c in "UR"): # Bottom-left 97 | startPos = (len(grid)-1, 0) 98 | elif all(c in tileConnections[startTile] for c in "UL"): # Bottom-right 99 | startPos = (len(grid)-1, len(grid)-1) 100 | 101 | # BFS starting from the picked corner until the grid is filled 102 | queue = deque([(startPos, startTile)]) 103 | tilesPlaced = set() 104 | while queue: 105 | pos, tile = queue.popleft() 106 | 107 | if tile in tilesPlaced: continue 108 | tilesPlaced.add(tile) 109 | 110 | grid[pos[0]][pos[1]] = tile 111 | 112 | for direction, adjTile in tileConnections[tile].items(): 113 | # Loop through all possible rotations/flips until the matching one is found 114 | # Performing these actions yields all possible versions after each one: 115 | # nothing, rot90CW, rot90CW, rot90CW, flipH, rot90CW, rot90CW, rot90CW 116 | # TODO: Can be found directly instead of looping through all 8 117 | tries = 0 118 | while tiles[adjTile].sides[dirsOpposite[direction]] != tiles[tile].sides[direction]: 119 | if tries == 3: 120 | tiles[adjTile].image = flipH(tiles[adjTile].image) 121 | tileConnections[adjTile] = {dirsFlipH[k]: v for k, v in tileConnections[adjTile].items()} 122 | else: 123 | tiles[adjTile].image = rot90CW(tiles[adjTile].image) 124 | tileConnections[adjTile] = {dirsRot90CW[k]: v for k, v in tileConnections[adjTile].items()} 125 | tries += 1 126 | 127 | delta = mov4[direction] 128 | nxtPos = (pos[0]+delta[0], pos[1]+delta[1]) 129 | queue.append((nxtPos, adjTile)) 130 | 131 | # Merge the tiles row by row 132 | image = [] 133 | tileSizeWithoutBorder = tileSize - 2 134 | for rowIndex in range(gridSize * tileSizeWithoutBorder): 135 | gridColumn = rowIndex // tileSizeWithoutBorder 136 | tileRow = rowIndex % tileSizeWithoutBorder 137 | 138 | row = [] 139 | for i in range(gridSize): 140 | row += tiles[grid[gridColumn][i]].imageWithoutBorders[tileRow] 141 | image.append(row) 142 | 143 | # Print matrix of tile IDs 144 | # for i in range(len(grid)): print(grid[i]) 145 | 146 | # Print final image 147 | # print("\n".join("".join(row) for row in image)) 148 | 149 | # Print image but pretty (with borders, tile IDs and separators) 150 | # for rowIndex in range(gridSize * tileSize): 151 | # gridColumn = rowIndex // tileSize 152 | # tileRow = rowIndex % tileSize 153 | # if tileRow == 0: 154 | # print("-" * gridSize * (tileSize+1)) 155 | # tileIDs = [grid[gridColumn][i] for i in range(gridSize)] 156 | # print(" " + " ".join([str(s) for s in tileIDs])) 157 | # print("|".join("".join(tiles[grid[gridColumn][i]].image[tileRow]) for i in range(gridSize))) 158 | 159 | monster = [" # ", 160 | "# ## ## ###", 161 | " # # # # # # "] 162 | monsterRoughness = sum(row.count("#") for row in monster) 163 | 164 | # Assume the correct rotation/flip will be the only one with any monsters 165 | monsters = 0 166 | tries = 0 167 | # Loop through all possible rotations/flips until a monster is found 168 | while monsters == 0: 169 | for i in range(len(image) - len(monster) + 1): 170 | for j in range(len(image[0]) - len(monster[0]) + 1): 171 | roughness = 0 172 | # Assume a # can be part of more than one monster (although 173 | # I'm pretty sure this is not the case in any of the actual inputs) 174 | for mi in range(len(monster)): 175 | for mj in range(len(monster[0])): 176 | if monster[mi][mj] == "#" and image[i+mi][j+mj] == "#": 177 | roughness += 1 178 | 179 | monsters += int(roughness == monsterRoughness) 180 | 181 | # Performing these actions yields all possible versions after each one: 182 | # nothing, rot90CW, rot90CW, rot90CW, flipH, rot90CW, rot90CW, rot90CW 183 | if tries == 3: 184 | image = flipH(image) 185 | else: 186 | image = rot90CW(image) 187 | tries += 1 188 | 189 | totalRoughness = sum(row.count("#") for row in image) 190 | p2 = totalRoughness - (monsters * monsterRoughness) 191 | 192 | print("Part 2: {}".format(p2)) 193 | 194 | AOCUtils.printTimeTaken() --------------------------------------------------------------------------------