├── 2015 ├── day01.py ├── day02.py ├── day03.py ├── day04.py ├── day05.py ├── day06.py ├── day07.py ├── day08.py ├── day09.py ├── day10.py ├── day11.py ├── day12.py ├── day13.py ├── day14.py ├── day15.py ├── day16.py ├── day17.py ├── day18.py ├── day19.py ├── day20.py ├── day21.py ├── day22.py ├── day23.py ├── day24.py └── day25.py ├── 2016 ├── day01.py ├── day02.py ├── day03.py ├── day04.py ├── day05.py ├── day06.py ├── day07.py ├── day08.py ├── day09.py ├── day10.py ├── day11.py ├── day12.py ├── day13.py ├── day14.py ├── day15.py ├── day16.py ├── day17.py ├── day18.py ├── day19.py ├── day20.py ├── day21.py ├── day22.py ├── day23.py ├── day24.py ├── day25.py ├── starter.py └── utils.py ├── 2017 ├── day01.py ├── day02.py ├── day03.py ├── day04.py ├── day05.py ├── day06.py ├── day07.py ├── day08.py ├── day09.py ├── day10.py ├── day11.py ├── day12.py ├── day13.py ├── day14.py ├── day15.py ├── day16.py ├── day17.py ├── day18.py ├── day19.py ├── day20.py ├── day21.py ├── day22.py ├── day23.py ├── day24.py ├── day25.py ├── starter.py └── utils.py ├── 2018 ├── day01.py ├── day02.py ├── day03.py ├── day04.py ├── day05.py ├── day06.py ├── day07.py ├── day08.py ├── day09.py ├── day10.py ├── day11.py ├── day12.py ├── day13.py ├── day14.py ├── day15.py ├── day16.py ├── day17.py ├── day18.py ├── day19.py ├── day20.py ├── day21.py ├── day22.py ├── day24.py ├── day25.py ├── starter.py └── utils.py ├── 2019 ├── day01.py ├── day02.py ├── day03.py ├── day04.py ├── day05.py ├── day06.py ├── day07.py ├── day08.py ├── day09.py ├── day10.py ├── day11.py ├── day12.py ├── day13.py ├── day14.py ├── day15.py ├── day16.py ├── day17.py ├── day18.py ├── day19.py ├── day20.py ├── day21.py ├── day22.py ├── day23.py ├── day24.py ├── day25.py ├── intcode.py ├── starter.py └── utils.py ├── 2020 ├── day01.py ├── day02.py ├── day03.py ├── day04.py ├── day05.py ├── day06.py ├── day07.py ├── day08.py ├── day09.py ├── day10.py ├── day11.py ├── day12.py ├── day13.py ├── day14.py ├── day15.py ├── day16.py ├── day17.py ├── day18.py ├── day19.py ├── day20-solve.py ├── day20.py ├── day21.py ├── day22.py ├── day23.py ├── day24.py ├── day25.py ├── search.py ├── starter.py ├── utils.py └── vm.py ├── 2021 ├── day01.py ├── day02.py ├── day03.py ├── day04.py ├── day05.py ├── day06.py ├── day07.py ├── day08.py ├── day09.py ├── day10.py ├── day11.py ├── day12.py ├── day13.py ├── day14.py ├── day15.py ├── day16.py ├── day17.py ├── day18.py ├── day21.py ├── starter.py └── utils.py ├── 2022 ├── day01.py ├── day02.py ├── day03.py ├── day04.py ├── day05.py ├── day06.py ├── day07.py ├── day08.py ├── day09.py ├── day10.py ├── day11.py ├── day12.py ├── day13.py ├── day14.py ├── day15.py ├── day16.py ├── day17.py ├── day18.py ├── day19.py ├── day20.py ├── day21.py ├── day22.py ├── day23.py ├── day24.py ├── day25.py ├── starter.py └── utils.py ├── 2023 ├── day01.py ├── day02.py ├── day03.py ├── day04.py ├── day05.py ├── day06.py ├── day07.py ├── day08.py ├── day09.py ├── day10.py ├── day11.py ├── day12.py ├── day13.py ├── day14.py ├── day15.py ├── day16.py ├── day17.py ├── day18.py ├── day19.py ├── day20.py ├── day21.py ├── day22.py ├── day23.py ├── day24.py ├── day25.py ├── starter.py └── utils.py ├── 2024 ├── day01.py ├── day02.py ├── day03.py ├── day04.py ├── day05.py ├── day06.py ├── day07.py ├── day08.py ├── day09.py ├── day10.py ├── day11.py ├── day12.py ├── day13.py ├── day14.py ├── day16.py ├── day17.py ├── day18.py ├── day19.py ├── day20.py ├── day22.py ├── day23.py ├── day25.py ├── starter.py └── utils.py ├── .gitignore ├── LICENSE ├── README.md └── advent.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | inputs/ 3 | outputs/ 4 | wip/ 5 | vm.py 6 | search.py 7 | advent.py 8 | test.py 9 | *.sh 10 | -------------------------------------------------------------------------------- /2015/day01.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | floor = 0 4 | basement = None 5 | 6 | for line in fileinput.input(): 7 | for i, char in enumerate(line, start=1): 8 | if char == "(": 9 | floor += 1 10 | elif char == ")": 11 | floor -= 1 12 | 13 | if floor == -1 and basement is None: 14 | basement = i 15 | 16 | print "Santa's floor: %d" % floor 17 | print "Entered basement at character %d" % basement 18 | -------------------------------------------------------------------------------- /2015/day02.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | paper = 0 4 | ribbon = 0 5 | 6 | for line in fileinput.input(): 7 | l, w, h = [int(n) for n in line.strip().split('x')] 8 | a, b, c = l*w, w*h, h*l 9 | 10 | paper += 2*(a+b+c) + min(a, b, c) 11 | ribbon += (l*w*h) + 2*(l+w+h - max(l, w, h)) 12 | 13 | print "ft^2 of paper: %d" % paper 14 | print "Feet of ribbon: %d" % ribbon 15 | -------------------------------------------------------------------------------- /2015/day03.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | DIRS = { 4 | "^": (0, 1), 5 | ">": (1, 0), 6 | "v": (0, -1), 7 | "<": (-1, 0), 8 | } 9 | 10 | def visit_houses(path): 11 | house = (0, 0) 12 | for c in path: 13 | yield house 14 | house = tuple(map(sum, zip(house, DIRS[c]))) 15 | 16 | path = fileinput.input()[0].strip() 17 | 18 | year_1_houses = set(visit_houses(path)) 19 | year_2_houses = set(visit_houses(path[::2])) | set(visit_houses(path[1::2])) 20 | 21 | print "Houses visited in year 1: %d" % len(year_1_houses) 22 | print "Houses visited in year 2: %d" % len(year_2_houses) 23 | -------------------------------------------------------------------------------- /2015/day04.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | import hashlib 3 | 4 | secret_key = fileinput.input()[0].strip() 5 | i = 1 6 | 7 | hash1 = None 8 | hash2 = None 9 | 10 | print "Searching for hashes..." 11 | 12 | while True: 13 | s = secret_key + str(i) 14 | h = hashlib.md5(s).hexdigest() 15 | 16 | if h[:5] == "00000": 17 | hash1 = (s, h) if hash1 == None else hash1 18 | 19 | if h[:6] == "000000": 20 | hash2 = (s, h) if hash2 == None else hash2 21 | 22 | if hash1 and hash2: 23 | break 24 | else: 25 | i += 1 26 | 27 | print "Hashed %s to %s" % hash1 28 | print "Hashed %s to %s" % hash2 29 | -------------------------------------------------------------------------------- /2015/day05.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | import re 3 | 4 | VOWELS = {'a', 'e', 'i', 'o', 'u'} 5 | NAUGHTY = {'ab', 'cd', 'pq', 'xy'} 6 | 7 | nice_count_1 = 0 8 | nice_count_2 = 0 9 | 10 | for string in fileinput.input(): 11 | # Ruleset 1 12 | num_letters = len(string) 13 | num_vowels = 0 14 | double_letter = False 15 | 16 | for i, char in enumerate(string): 17 | if char in VOWELS: 18 | num_vowels += 1 19 | 20 | if i < num_letters - 1: 21 | if char + string[i+1] in NAUGHTY: 22 | break 23 | 24 | if char == string[i+1]: 25 | double_letter = True 26 | 27 | else: 28 | if double_letter and (num_vowels >= 3): 29 | nice_count_1 += 1 30 | 31 | 32 | # Ruleset 2 33 | overlap = False 34 | is_pair = False 35 | 36 | for i, char in enumerate(string): 37 | if i < len(string) - 2: 38 | if char == string[i+2]: 39 | overlap = True 40 | break 41 | 42 | if re.search(r'(..).*\1', string): 43 | is_pair = True 44 | 45 | 46 | if overlap and is_pair: 47 | nice_count_2 += 1 48 | 49 | 50 | print "Number of nice strings (first ruleset): %d" % nice_count_1 51 | print "Number of nice strings (second ruleset): %d" % nice_count_2 52 | -------------------------------------------------------------------------------- /2015/day06.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | import re 3 | 4 | instructions = [] 5 | 6 | for line in fileinput.input(): 7 | cmd, x1, y1, x2, y2 = re.match(r'(.+) (\d+),(\d+) through (\d+),(\d+)', line).groups() 8 | x1 = int(x1) 9 | x2 = int(x2) 10 | y1 = int(y1) 11 | y2 = int(y2) 12 | 13 | instructions.append((cmd, x1, x2, y1, y2)) 14 | 15 | 16 | def total_lights(instructions, brightness=False): 17 | lights = [[0 for x in range(1000)] for x in range(1000)] 18 | 19 | for cmd, x1, x2, y1, y2 in instructions: 20 | for x in range(x1, x2+1): 21 | for y in range(y1, y2+1): 22 | if cmd == "turn off": 23 | if brightness: 24 | lights[x][y] = max(0, lights[x][y] - 1) 25 | else: 26 | lights[x][y] = 0 27 | 28 | elif cmd == "turn on": 29 | if brightness: 30 | lights[x][y] += 1 31 | else: 32 | lights[x][y] = 1 33 | 34 | elif cmd == "toggle": 35 | if brightness: 36 | lights[x][y] += 2 37 | else: 38 | lights[x][y] ^= 1 39 | 40 | return sum(sum(x) for x in lights) 41 | 42 | 43 | print "Lights on: %d" % total_lights(instructions) 44 | print "Total brightness: %d" % total_lights(instructions, brightness=True) 45 | -------------------------------------------------------------------------------- /2015/day07.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | import re 3 | 4 | connections = {} 5 | 6 | # Set up dictionary of connections 7 | for line in fileinput.input(): 8 | rule, wire = re.search(r'(.*) -> (.*)', line).groups() 9 | value = None 10 | 11 | if len(rule.split()) == 1: 12 | value = (rule,) 13 | elif 'NOT' in rule: 14 | value = ('NOT', rule.split()[1]) 15 | else: 16 | value = (rule.split()[1], rule.split()[0], rule.split()[2]) 17 | 18 | connections[wire] = value 19 | 20 | connections2 = connections.copy() 21 | 22 | def follow(wire, c): 23 | rule = c[wire] 24 | val = None 25 | 26 | # Base case 27 | if len(rule) == 1: 28 | if rule[0].isdigit(): 29 | return int(rule[0]) 30 | else: 31 | return follow(rule[0], c) 32 | 33 | elif len(rule) == 2: 34 | return ~follow(rule[1], c) 35 | 36 | else: 37 | if rule[0] == 'AND': 38 | val = (int(rule[1]) if rule[1].isdigit() else follow(rule[1], c)) & (int(rule[2]) if rule[2].isdigit() else follow(rule[2], c)) 39 | elif rule[0] == 'OR': 40 | val = (int(rule[1]) if rule[1].isdigit() else follow(rule[1], c)) | (int(rule[2]) if rule[2].isdigit() else follow(rule[2], c)) 41 | elif rule[0] == 'LSHIFT': 42 | val = follow(rule[1], c) << int(rule[2]) 43 | elif rule[0] == 'RSHIFT': 44 | val = follow(rule[1], c) >> int(rule[2]) 45 | 46 | if type(val) is int: 47 | c[wire] = (str(val),) 48 | 49 | return val 50 | 51 | s = follow('a', connections) 52 | 53 | print "Signal to wire a: %d" % s 54 | 55 | connections2['b'] = (str(s), ) 56 | 57 | print "After overriding b to %s, signal to a is %d" % (s, follow('a', connections2)) 58 | -------------------------------------------------------------------------------- /2015/day08.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | import ast 3 | import re 4 | 5 | chars = 0 6 | lits = 0 7 | encoded_chars = 0 8 | 9 | for line in fileinput.input(): 10 | line = line.strip() # strip newline 11 | 12 | chars += len(line) 13 | lits += len(ast.literal_eval(line)) 14 | encoded_chars += (len(re.escape(line)) + 2 ) 15 | 16 | print "chars - literals = %d" % (chars - lits) 17 | print "encoded_chars - chars = %d" % (encoded_chars - chars) 18 | -------------------------------------------------------------------------------- /2015/day09.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import fileinput 3 | import itertools 4 | import re 5 | 6 | def parse_line(line): 7 | departure, arrival, distance = re.match(r'(\w+) to (\w+) = (\d+)', line).groups() 8 | return departure, arrival, int(distance) 9 | 10 | def sum_of_paths(d): 11 | # Yield sum of all possible routes 12 | for p in itertools.permutations(locations, len(locations)): 13 | yield sum(d[p[i]][p[i+1]] for i in range(len(p) - 1)) 14 | 15 | distances = {} 16 | 17 | # Parse input 18 | for line in fileinput.input(): 19 | frm, to, dist = parse_line(line) 20 | if distances.get(frm): 21 | distances[frm][to] = dist 22 | else: 23 | distances[frm] = {to: dist} 24 | 25 | if distances.get(to): 26 | distances[to][frm] = dist 27 | else: 28 | distances[to] = {frm: dist} 29 | 30 | locations = distances.keys() 31 | 32 | print "Shortest route: %d" % min(sum_of_paths(distances)) 33 | print "Longest route: %d" % max(sum_of_paths(distances)) 34 | -------------------------------------------------------------------------------- /2015/day10.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | string = fileinput.input()[0].strip() 4 | 5 | def look_and_say(s): 6 | occurences = 0 7 | number = s[0] 8 | ret = '' 9 | 10 | for n in s: 11 | if number == n: 12 | occurences += 1 13 | else: 14 | ret += str(occurences) + number 15 | number = n 16 | occurences = 1 17 | 18 | return ret + str(occurences) + number 19 | 20 | for _ in range(40): 21 | string = look_and_say(string) 22 | 23 | print "Length after 40 iterations: %d" % len(string) 24 | 25 | for _ in range(10): 26 | string = look_and_say(string) 27 | 28 | print "Length after 50 iterations: %d" % len(string) 29 | -------------------------------------------------------------------------------- /2015/day11.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | password = fileinput.input()[0].strip() 4 | 5 | def is_valid(p): 6 | if any(x in p for x in ['i', 'o', 'l']): 7 | return False 8 | 9 | straight = False 10 | pairs = set() 11 | 12 | for i in range(len(p) - 2): 13 | if ord(p[i]) == ord(p[i+1]) - 1 == ord(p[i+2]) - 2: 14 | straight = True 15 | if p[i] == p[i+1]: 16 | pairs.add(p[i]) 17 | 18 | if p[-1] == p[-2]: 19 | pairs.add(p[-1]) 20 | 21 | return straight and (len(pairs) >= 2) 22 | 23 | def next_pass(p): 24 | # Recursion is fantastic. 25 | if p == 'z': 26 | return 'a' 27 | elif p[-1] == 'z': 28 | return next_pass(p[:-1]) + 'a' 29 | else: 30 | return p[:-1] + chr(ord(p[-1]) + 1) 31 | 32 | 33 | print "Santa's current password is %s" % password 34 | 35 | for _ in range(2): 36 | while True: 37 | password = next_pass(password) 38 | if is_valid(password): 39 | print "His next password should be %s" % password 40 | break 41 | -------------------------------------------------------------------------------- /2015/day12.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | import re 3 | import json 4 | 5 | def no_red_sum(x): 6 | s = 0 7 | 8 | if type(x) == list: 9 | for y in x: 10 | if type(y) == int: 11 | s += y 12 | else: 13 | s += no_red_sum(y) 14 | elif type(x) == dict: 15 | for k in x.keys(): 16 | if x[k] == 'red': 17 | return 0 18 | s += no_red_sum(x[k]) 19 | elif type(x) == int: 20 | return x 21 | 22 | return s 23 | 24 | 25 | document = fileinput.input()[0] 26 | 27 | print "Sum of all numbers: %d" % sum(int(x) for x in re.findall('-?\d+', document)) 28 | 29 | j = json.loads(document) 30 | 31 | print "Sum of non-reds items: %d" % no_red_sum(j) 32 | -------------------------------------------------------------------------------- /2015/day13.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import fileinput 3 | import itertools 4 | import re 5 | 6 | def parse_line(line): 7 | p1, gl, diff, p2 = re.search(r'(\w+) .* (gain|lose) (\d+) .* (\w+).', line).groups() 8 | return p1, p2, int(diff) * (-1 if gl == 'lose' else 1) 9 | 10 | def happiness_deltas(d): 11 | # Not optimal because it includes cyclic permutations and reversals 12 | for p in itertools.permutations(d.keys(), len(d.keys())): 13 | total = 0 14 | for i in range(len(p)): 15 | total += d[p[i]][p[(i+1) % len(p)]] 16 | total += d[p[(i+1) % len(p)]][p[i]] 17 | 18 | yield total 19 | 20 | 21 | people = {} 22 | 23 | # Parse input 24 | for line in fileinput.input(): 25 | p1, p2, diff = parse_line(line) 26 | if p1 in people: 27 | people[p1][p2] = diff 28 | else: 29 | people[p1] = {p2: diff} 30 | 31 | 32 | print "Largest total change in happiness: %d" % max(happiness_deltas(people)) 33 | 34 | # Add self into list of keys for second part of problem 35 | for person in people.keys(): 36 | people[person]["iKevinY"] = 0 37 | 38 | people["iKevinY"] = {person: 0 for person in people.keys()} 39 | 40 | print "After adding me: %d" % max(happiness_deltas(people)) 41 | -------------------------------------------------------------------------------- /2015/day14.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | import re 3 | 4 | def fly(time, speed, fly_for, rest_for): 5 | cycle = fly_for + rest_for 6 | return speed * ( 7 | ((time // cycle) * fly_for) + # flying time * full cycles completed 8 | min(fly_for, time % cycle) # remaining incomplete flying time 9 | ) 10 | 11 | TOTAL_TIME = 2503 12 | 13 | reindeer = [[int(n) for n in re.findall(r'\d+', line)] for line in fileinput.input()] 14 | 15 | print "Winning reindeer distance: %d" % max(fly(TOTAL_TIME, *r) for r in reindeer) 16 | 17 | points = [0] * len(reindeer) 18 | 19 | for t in range(1, TOTAL_TIME + 1): 20 | positions = [fly(t, *r) for r in reindeer] 21 | for i, pos in enumerate(positions): 22 | if pos == max(positions): 23 | points[i] += 1 24 | 25 | print "Highest point total: %d" % max(points) 26 | 27 | -------------------------------------------------------------------------------- /2015/day15.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | import itertools 3 | import re 4 | 5 | def mul(vals): 6 | ret = 1 7 | for x in vals: 8 | ret *= x 9 | return ret 10 | 11 | ingredients = [[int(n) for n in re.findall(r'\w+ (-?\d+)', line)] for line in fileinput.input()] 12 | 13 | cookies = [] 14 | 15 | for combo in itertools.combinations_with_replacement(ingredients, 100): # 100 teaspoons of ingredients 16 | cookies.append([max(0, sum(c)) for c in tuple(zip(*combo))]) 17 | 18 | print "Best cookie total: %d" % max(mul(c[:-1]) for c in cookies) 19 | print "Best 500 calories: %d" % max(mul(c[:-1]) for c in cookies if c[-1] == 500) 20 | -------------------------------------------------------------------------------- /2015/day16.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | import re 3 | 4 | TICKER = { 5 | 'children': 3, 6 | 'cats': 7, 7 | 'samoyeds': 2, 8 | 'pomeranians': 3, 9 | 'akitas': 0, 10 | 'vizslas': 0, 11 | 'goldfish': 5, 12 | 'trees': 3, 13 | 'cars': 2, 14 | 'perfumes': 1, 15 | } 16 | 17 | sues = [{k: int(v) for k, v in re.findall(r'(\w+): (\d+)', line)} for line in fileinput.input()] 18 | 19 | def which_sue(sues, outdated=False): 20 | for i, sue in enumerate(sues, start=1): 21 | for key in sue: 22 | if outdated and (key in ('cats', 'trees')): 23 | if not (sue[key] > TICKER[key]): 24 | break 25 | elif outdated and (key in ('pomeranians', 'goldfish')): 26 | if not (sue[key] < TICKER[key]): 27 | break 28 | 29 | elif TICKER[key] != sue[key]: 30 | break 31 | else: 32 | return i 33 | else: 34 | return -1 35 | 36 | print "The gift was from Sue #%d." % which_sue(sues) 37 | print "With ranges gives Sue #%d." % which_sue(sues, outdated=True) 38 | -------------------------------------------------------------------------------- /2015/day17.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | import itertools 3 | import re 4 | 5 | LITRES = 150 6 | 7 | def permute_containers(containers, litres): 8 | for i in range(1, len(containers) + 1): 9 | for c in itertools.combinations(containers, i): 10 | if sum(c) == litres: 11 | yield c 12 | 13 | containers = [int(n) for n in fileinput.input()] 14 | permutations = list(permute_containers(containers, LITRES)) 15 | min_perm_len = min(len(p) for p in permutations) 16 | 17 | print "Containers: {}".format(containers) 18 | print "Litres of eggnog: %d" % LITRES 19 | 20 | print "Valid combinations: %d" % len(permutations) 21 | print "Minimal combinations: %d" % len([p for p in permutations if len(p) == min_perm_len]) 22 | -------------------------------------------------------------------------------- /2015/day18.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | import itertools 3 | import re 4 | 5 | def get_neighbours(x, y): 6 | square = [(a, b) 7 | for a in range(max(0, x-1), min(x+2, WIDTH)) 8 | for b in range(max(0, y-1), min(y+2, HEIGHT))] 9 | square.remove((x, y)) 10 | return square 11 | 12 | def next_state(grid, x, y, sticky_corners=False): 13 | if sticky_corners: 14 | if (x in (0, WIDTH-1)) and (y in (0, HEIGHT-1)): 15 | return True 16 | 17 | neighbours = get_neighbours(x, y) 18 | on_neighbours = sum(grid[b][a] for a, b in neighbours) 19 | 20 | if grid[y][x]: 21 | return int(on_neighbours in (2, 3)) 22 | else: 23 | return int(on_neighbours == 3) 24 | 25 | def next_grid(grid, sticky_corners=False): 26 | return [[next_state(grid, x, y, sticky_corners) for x in range(WIDTH)] for y in range(HEIGHT)] 27 | 28 | def count_lights(grid): 29 | return sum(sum(x for x in row) for row in grid) 30 | 31 | def print_grid(grid): 32 | for row in grid: 33 | print ''.join('#' if c else '.' for c in row) 34 | 35 | 36 | # Parse input 37 | rows = [line for line in fileinput.input()] 38 | LIGHTS = [[1 if c == '#' else 0 for c in row.strip()] for row in rows] 39 | 40 | # Define constants 41 | WIDTH = len(LIGHTS[0]) 42 | HEIGHT = len(LIGHTS) 43 | ITERATIONS = 100 44 | 45 | print "Simulating {}x{} grid of lights.".format(WIDTH, HEIGHT) 46 | 47 | grid = LIGHTS 48 | for _ in range(ITERATIONS): 49 | grid = next_grid(grid) 50 | 51 | print "Lights on after {} iterations: {}".format(ITERATIONS, count_lights(grid)) 52 | 53 | # Stick corners initially 54 | grid = LIGHTS 55 | for x in (0, WIDTH-1): 56 | for y in (0, HEIGHT-1): 57 | grid[y][x] = 1 58 | 59 | for _ in range(ITERATIONS): 60 | grid = next_grid(grid, sticky_corners=True) 61 | 62 | print "Lights on with stuck corners: {}".format(count_lights(grid)) 63 | -------------------------------------------------------------------------------- /2015/day19.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | import re 3 | 4 | REPLACEMENTS = [] 5 | ELECTRONS = [] 6 | MOLECULE = "" 7 | MOL_LEN = len(MOLECULE) 8 | 9 | for line in fileinput.input(): 10 | if "=>" in line: 11 | m = re.findall(r'\w+', line) 12 | if m[0] == "e": 13 | ELECTRONS.append(m[1]) 14 | else: 15 | REPLACEMENTS.append(m) 16 | else: 17 | MOLECULE = line.strip() 18 | 19 | 20 | def replace(old, new, s): 21 | for pos in (m.start() for m in re.finditer(old, s)): 22 | yield "{}{}{}".format(s[:pos], new, s[pos+len(old):]) 23 | 24 | 25 | single_replacements = set() 26 | 27 | for old, new in REPLACEMENTS: 28 | if old in MOLECULE: 29 | for new_mol in replace(old, new, MOLECULE): 30 | single_replacements.add(new_mol) 31 | 32 | print "Distinct molecules after one replacement:", len(single_replacements) 33 | 34 | 35 | elements = len(re.findall(r'[A-Z]', MOLECULE)) 36 | radon = len(re.findall(r'Rn', MOLECULE)) 37 | yttrium = len(re.findall(r'Y', MOLECULE)) 38 | 39 | # print "Elements:", elements 40 | # print "Rn/Ar:", radon, "* 2" 41 | # print "Y:", yttrium 42 | # print "|E| - (Rn + Ar) - 2*Y - 1 = n" 43 | 44 | print "Fewest steps from e to medicine:", elements - (2 * radon) - (2 * yttrium) - 1 45 | -------------------------------------------------------------------------------- /2015/day20.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | PRESENTS = int(fileinput.input()[0].strip()) 4 | CACHE = {} 5 | 6 | 7 | def factors(n): 8 | if n in CACHE: 9 | return CACHE[n] 10 | 11 | CACHE[n] = set() 12 | 13 | for i in range(1, int(n**0.5) + 1): 14 | if n % i == 0: 15 | CACHE[n] |= set([i, n//i]) 16 | 17 | return CACHE[n] 18 | 19 | 20 | house = 1 21 | while sum(factors(house)) * 10 < PRESENTS: 22 | house += 1 23 | 24 | print "Lowest house number #1:", house 25 | 26 | house = 1 27 | while sum(x for x in factors(house) if (x * 50 >= house)) * 11 < PRESENTS: 28 | house += 1 29 | 30 | print "Lowest house number #2:", house 31 | -------------------------------------------------------------------------------- /2015/day23.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | import re 3 | 4 | INS = [] 5 | 6 | for line in fileinput.input(): 7 | INS.append(line.strip()) 8 | 9 | pc = 0 10 | ra = 1 11 | rb = 0 12 | 13 | while 0 <= pc < len(INS): 14 | ins = INS[pc] 15 | print '{0: <16} a: {1}, b: {2}'.format(ins, ra, rb) 16 | 17 | if 'hlf' in ins: 18 | if 'a' in ins: 19 | ra /= 2 20 | else: 21 | rb /= 2 22 | 23 | elif 'tpl' in ins: 24 | if 'a' in ins: 25 | ra *= 3 26 | else: 27 | rb *= 3 28 | 29 | elif 'inc' in ins: 30 | if 'a' in ins: 31 | ra += 1 32 | else: 33 | rb += 1 34 | 35 | elif 'jmp' in ins: 36 | offset = int(re.findall('\d+', ins)[0]) * (-1 if '-' in ins else 1) 37 | pc += offset 38 | pc -= 1 # compensate for pc++ at end 39 | 40 | elif 'jie' in ins: 41 | offset = int(re.findall('\d+', ins)[0]) * (-1 if '-' in ins else 1) 42 | if 'a' in ins: 43 | if ra % 2 == 0: 44 | pc += offset 45 | pc -= 1 46 | else: 47 | if rb % 2 == 0: 48 | pc += offset 49 | pc -= 1 50 | 51 | elif 'jio' in ins: 52 | offset = int(re.findall('\d+', ins)[0]) * (-1 if '-' in ins else 1) 53 | if 'a' in ins: 54 | if ra == 1: 55 | pc += offset 56 | pc -= 1 57 | else: 58 | if rb == 1: 59 | pc += offset 60 | pc -= 1 61 | 62 | 63 | pc += 1 64 | 65 | print "halt\n--" 66 | print "Register A: %d" % ra 67 | print "Register B: %d" % rb 68 | -------------------------------------------------------------------------------- /2015/day24.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | import itertools 3 | 4 | PACKAGES = [] 5 | 6 | for line in fileinput.input(): 7 | PACKAGES.append(int(line)) 8 | 9 | PACKAGES.reverse() 10 | 11 | def quantum_package(packages, target_weight): 12 | for n in range(1, len(PACKAGES) - 2): 13 | found = False 14 | for p in itertools.combinations(PACKAGES, n): 15 | if sum(p) == target_weight: 16 | # print p 17 | yield p 18 | found = True 19 | 20 | if found: 21 | return 22 | 23 | def prod(it): 24 | p = 1 25 | for i in it: 26 | p *= i 27 | return p 28 | 29 | print "3-compartment package QE:", min(prod(c) for c in quantum_package(PACKAGES, sum(PACKAGES) / 3)) 30 | print "4-compartment package QE:", min(prod(c) for c in quantum_package(PACKAGES, sum(PACKAGES) / 4)) 31 | -------------------------------------------------------------------------------- /2015/day25.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | import re 3 | 4 | # ROW, COL = (int(x) for x in re.findall(r'\d+', fileinput.input()[0])) 5 | ROW, COL = 3010, 3019 6 | 7 | 8 | def next_code(c): 9 | return (c * 252533) % 33554393 10 | 11 | def code_no(row, col): 12 | # s = 1 13 | # for x in range(row + col - 1): 14 | # s += x 15 | 16 | # return s + col - 1 17 | 18 | return (((row + col - 1) * (row + col - 2)) / 2) + col 19 | 20 | code = 20151125 21 | 22 | # for x in range(code_no(ROW, COL) - 1): 23 | # code = next_code(code) 24 | 25 | 26 | print (code * pow(252533, code_no(ROW, COL) - 1, 33554393)) % 33554393 27 | -------------------------------------------------------------------------------- /2016/day01.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import Point 3 | 4 | DIRS = [ 5 | Point(0, 1), # north 6 | Point(1, 0), # east 7 | Point(0, -1), # south 8 | Point(-1, 0) # west 9 | ] 10 | 11 | seen = set() 12 | double_visit = '' 13 | 14 | pos = Point(0, 0) 15 | facing = 0 16 | 17 | for line in fileinput.input(): 18 | directions = line.split(', ') 19 | 20 | for d in directions: 21 | turn = d[0] 22 | steps = int(d[1:]) 23 | 24 | if turn == 'L': 25 | facing = (facing - 1) % 4 26 | else: 27 | facing = (facing + 1) % 4 28 | 29 | for _ in range(steps): 30 | pos += DIRS[facing] 31 | if not double_visit and pos in seen: 32 | double_visit = "First double-visit at ({}, {}) ({} blocks away)".format(pos.x, pos.y, pos.manhattan) 33 | else: 34 | seen.add(pos) 35 | 36 | print "Easter Bunny HQ is at ({}, {}) ({} blocks away)".format(pos.x, pos.y, pos.manhattan) 37 | print double_visit 38 | -------------------------------------------------------------------------------- /2016/day02.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import Point 3 | 4 | KEYPAD_1 = [ 5 | ['1', '2', '3'], 6 | ['4', '5', '6'], 7 | ['7', '8', '9'], 8 | ] 9 | 10 | KEYPAD_2 = [ 11 | ['_', '_', '1', '_', '_'], 12 | ['_', '2', '3', '4', '_'], 13 | ['5', '6', '7', '8', '9'], 14 | ['_', 'A', 'B', 'C', '_'], 15 | ['_', '_', 'D', '_', '_'], 16 | ] 17 | 18 | DIRS = { 19 | 'U': Point(0, -1), 20 | 'D': Point(0, 1), 21 | 'L': Point(-1, 0), 22 | 'R': Point(1, 0), 23 | } 24 | 25 | 26 | def move(keypad, pos, d): 27 | """Returns the (x, y) coordinate after (possibly) moving.""" 28 | new = pos + DIRS[d] 29 | 30 | if new.x < 0 or new.y < 0: 31 | return pos 32 | elif new.x >= len(keypad[0]) or new.y >= len(keypad): 33 | return pos 34 | elif keypad[new.y][new.x] == '_': 35 | return pos 36 | 37 | return new 38 | 39 | 40 | pos_1 = Point(1, 1) # start in middle 41 | pos_2 = Point(0, 2) # start in middle-left 42 | 43 | code_1 = '' 44 | code_2 = '' 45 | 46 | for line in fileinput.input(): 47 | for c in line.strip(): 48 | pos_1 = move(KEYPAD_1, pos_1, c) 49 | pos_2 = move(KEYPAD_2, pos_2, c) 50 | 51 | code_1 += KEYPAD_1[pos_1.y][pos_1.x] 52 | code_2 += KEYPAD_2[pos_2.y][pos_2.x] 53 | 54 | print "Theoretical bathroom code: {}".format(code_1) 55 | print "Actual bathroom code: {}".format(code_2) 56 | -------------------------------------------------------------------------------- /2016/day03.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import chunks 3 | 4 | 5 | def possible_tri(tri): 6 | t = sorted(tri) 7 | return t[0] + t[1] > t[2] 8 | 9 | 10 | triangles = [] 11 | 12 | for line in fileinput.input(): 13 | triangles.append([int(x) for x in line.split()]) 14 | 15 | print "Possible triangles: %i" % sum(possible_tri(t) for t in triangles) 16 | 17 | 18 | col_tris = [] 19 | 20 | for tris in chunks(triangles, 3): 21 | col_tris.extend(zip(*tris)) 22 | 23 | print "Column triangles: %i" % sum(possible_tri(t) for t in col_tris) 24 | -------------------------------------------------------------------------------- /2016/day04.py: -------------------------------------------------------------------------------- 1 | import re 2 | import fileinput 3 | from collections import Counter 4 | 5 | from utils import parse_line 6 | 7 | 8 | def decrypt(c, n): 9 | if c == '-': 10 | return ' ' 11 | 12 | return chr((((ord(c) - ord('a')) + n) % 26) + ord('a')) 13 | 14 | 15 | total_sector_id = 0 16 | north_pole_sector_id = None 17 | 18 | for line in fileinput.input(): 19 | name, sector, checksum = parse_line(r'(\S+)-(\d+)\[(\w{5})\]', line) 20 | sector = int(sector) 21 | 22 | real_name = ''.join(decrypt(c, sector) for c in name) 23 | 24 | if 'northpole' in real_name: 25 | north_pole_sector_id = sector 26 | 27 | occurences = Counter([x for x in name if x.isalpha()]) 28 | commons = sorted(occurences.most_common(), key=lambda (x, y): (-y, x)) 29 | 30 | if ''.join(zip(*commons)[0][:5]) == checksum: 31 | total_sector_id += sector 32 | 33 | 34 | print "Sum of sector IDs of real rooms: %i" % total_sector_id 35 | print "North Pole storage room sector: %i" % north_pole_sector_id 36 | -------------------------------------------------------------------------------- /2016/day05.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from hashlib import md5 3 | 4 | door_id = fileinput.input()[0].strip() 5 | 6 | pass_1 = '' 7 | pass_2 = [None for _ in range(8)] 8 | 9 | i = 0 10 | 11 | while len(pass_1) < 8 or None in pass_2: 12 | digest = md5(door_id + str(i)).hexdigest() 13 | 14 | if digest.startswith('00000'): 15 | # print "Hash found! %s (%08i)" % (digest, i) 16 | 17 | if len(pass_1) < 8: 18 | pass_1 += digest[5] 19 | 20 | pos = int(digest[5], 16) 21 | char = digest[6] 22 | 23 | if pos < 8 and pass_2[pos] is None: 24 | pass_2[pos] = char 25 | 26 | i += 1 27 | 28 | print "Password 1 is: %s" % pass_1 29 | print "Password 2 is: %s" % ''.join(pass_2) 30 | -------------------------------------------------------------------------------- /2016/day06.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import Counter 3 | 4 | messages = [line.strip() for line in fileinput.input()] 5 | message_1 = ''.join(Counter(c).most_common()[0][0] for c in zip(*messages)) 6 | message_2 = ''.join(Counter(c).most_common()[-1][0] for c in zip(*messages)) 7 | 8 | print "Error-corrected message: %s" % message_1 9 | print "Santa's original message: %s" % message_2 10 | -------------------------------------------------------------------------------- /2016/day07.py: -------------------------------------------------------------------------------- 1 | import re 2 | import fileinput 3 | 4 | 5 | def is_abba(seq): 6 | for i in range(len(seq) - 3): 7 | a, b, c, d = seq[i:i+4] 8 | if a == d and b == c and a != b: 9 | return True 10 | 11 | return False 12 | 13 | 14 | def find_abas(seq): 15 | for i in range(len(seq) - 2): 16 | a, b, c = seq[i:i+3] 17 | if a == c and a != b: 18 | yield a, b 19 | 20 | 21 | def parse_address(address): 22 | runs = re.findall(r'(\w+)', address.strip()) 23 | return runs[::2], runs[1::2] 24 | 25 | 26 | def supports_tls(address): 27 | sequences, hypernets = parse_address(address) 28 | 29 | if any(is_abba(s) for s in sequences): 30 | if not any(is_abba(h) for h in hypernets): 31 | return True 32 | 33 | return False 34 | 35 | 36 | def supports_ssl(address): 37 | sequences, hypernets = parse_address(address) 38 | 39 | for seq in sequences: 40 | for a, b in find_abas(seq): 41 | bab = b + a + b 42 | 43 | if any(bab in h for h in hypernets): 44 | return True 45 | 46 | return False 47 | 48 | 49 | ADDRESSES = [line.strip() for line in fileinput.input()] 50 | print "Number of TLS IPs: %i" % sum(supports_tls(a) for a in ADDRESSES) 51 | print "Number of SSL IPs: %i" % sum(supports_ssl(a) for a in ADDRESSES) 52 | -------------------------------------------------------------------------------- /2016/day08.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # import sys 3 | # import time 4 | import fileinput 5 | 6 | from utils import parse_line 7 | 8 | WIDTH = 50 9 | HEIGHT = 6 10 | SCREEN = [[False for _ in range(WIDTH)] for _ in range(HEIGHT)] 11 | 12 | # Make space for animated output 13 | # print '\n' * HEIGHT 14 | 15 | for line in fileinput.input(): 16 | if line.startswith('rect'): 17 | a, b = parse_line(r'rect (\d+)x(\d+)', line) 18 | 19 | for y in range(b): 20 | for x in range(a): 21 | SCREEN[y][x] = True 22 | 23 | else: 24 | rc, n, offset = parse_line(r'rotate (\w+) .=(\d+) by (\d+)', line) 25 | 26 | if rc == 'row': 27 | temp = SCREEN[n][:] 28 | for i, x in enumerate(temp): 29 | SCREEN[n][(offset+i) % WIDTH] = x 30 | 31 | else: 32 | temp = [row[n] for row in SCREEN] 33 | for i, x in enumerate(temp): 34 | SCREEN[(offset+i) % HEIGHT][n] = x 35 | 36 | 37 | # sys.stdout.write('\033[F' * HEIGHT) 38 | 39 | # for row in SCREEN: 40 | # print ''.join('█' if x else ' ' for x in row) 41 | 42 | # time.sleep(0.02) 43 | 44 | 45 | print '' 46 | for row in SCREEN: 47 | print ''.join('█' if x else ' ' for x in row) 48 | 49 | print "\nNumber of lit pixels: %i" % sum(sum(row) for row in SCREEN) 50 | -------------------------------------------------------------------------------- /2016/day09.py: -------------------------------------------------------------------------------- 1 | import re 2 | import fileinput 3 | 4 | marker_re = re.compile(r'(\d+)x(\d+)') 5 | 6 | 7 | def decompressed_len(data, improved=False): 8 | if not re.findall(marker_re, data): 9 | return len(data) 10 | 11 | length = 0 12 | 13 | take, repeat = (int(x) for x in re.findall(marker_re, data)[0]) 14 | 15 | # Find start and end indices of first marker 16 | start = data.index('(') 17 | end = data.index(')') 18 | 19 | length += start 20 | 21 | data = data[end+1:] 22 | to_repeat = data[:take] 23 | 24 | if '(' in to_repeat and improved: 25 | length += decompressed_len(to_repeat, improved) * repeat 26 | else: 27 | length += take * repeat 28 | 29 | return length + decompressed_len(data[take:], improved) 30 | 31 | 32 | payload = fileinput.input()[0].strip() 33 | 34 | print "Length of original format:", decompressed_len(payload) 35 | print "Length of improved format:", decompressed_len(payload, improved=True) 36 | -------------------------------------------------------------------------------- /2016/day10.py: -------------------------------------------------------------------------------- 1 | import re 2 | import fileinput 3 | from collections import defaultdict 4 | from utils import parse_line 5 | 6 | BOTS = defaultdict(list) 7 | OUTPUTS = defaultdict(list) 8 | INSTRUCTIONS = [line.strip() for line in fileinput.input()] 9 | 10 | while INSTRUCTIONS: 11 | temp = [] 12 | 13 | for line in INSTRUCTIONS: 14 | if 'value' in line: 15 | val, bot = [int(x) for x in re.findall(r'(\d+)', line)] 16 | BOTS[bot].append(val) 17 | 18 | else: 19 | bot, low_t, low, high_t, high = parse_line(r'bot (\d+) .+ (\w+) (\d+) .+ (\w+) (\d+)', line) 20 | 21 | if bot not in BOTS or len(BOTS[bot]) < 2: 22 | temp.append(line) 23 | continue 24 | 25 | if 17 in BOTS[bot] and 61 in BOTS[bot]: 26 | print "Bot #%i compares values 17 and 61." % bot 27 | 28 | l, h = sorted(BOTS[bot][:2]) 29 | BOTS[bot] = [] 30 | 31 | (BOTS if low_t == 'bot' else OUTPUTS)[low].append(l) 32 | (BOTS if high_t == 'bot' else OUTPUTS)[high].append(h) 33 | 34 | INSTRUCTIONS = temp 35 | temp = [] 36 | 37 | a, b, c = (OUTPUTS[i][0] for i in range(3)) 38 | print "Product of values in outputs 0-2 is %i." % (a * b * c) 39 | -------------------------------------------------------------------------------- /2016/day12.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | 4 | def reg_or_val(regs, x): 5 | try: 6 | return int(x) 7 | except ValueError: 8 | return regs[x] 9 | 10 | 11 | def emulate(program, a=0, b=0, c=0, d=0): 12 | pc = 0 13 | regs = {'a': a, 'b': b, 'c': c, 'd': d} 14 | 15 | while True: 16 | if pc >= len(program): 17 | return regs 18 | 19 | cmd, x, y = program[pc] 20 | 21 | if cmd == 'cpy': 22 | regs[y] = reg_or_val(regs, x) 23 | 24 | elif cmd == 'inc': 25 | regs[x] += 1 26 | 27 | elif cmd == 'dec': 28 | regs[x] -= 1 29 | 30 | elif cmd == 'jnz': 31 | x = reg_or_val(regs, x) 32 | 33 | if x != 0: 34 | pc += int(y) 35 | continue 36 | 37 | pc += 1 38 | 39 | 40 | PROGRAM = [] 41 | 42 | for line in fileinput.input(): 43 | # Add null argument to pad inc/dec instructions 44 | instruction = line + ' null' 45 | PROGRAM.append(instruction.split()[:3]) 46 | 47 | print "Value in register a:", emulate(PROGRAM)['a'] 48 | print "When setting c to 1:", emulate(PROGRAM, c=1)['a'] 49 | -------------------------------------------------------------------------------- /2016/day13.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | INPUT = int(fileinput.input()[0]) 4 | 5 | 6 | def is_open(x, y, seed=INPUT): 7 | """Open space if number of 1 bits is even, else wall.""" 8 | num = (x * x) + (3 * x) + (2 * x * y) + y + (y * y) + seed 9 | return bin(num)[2:].count('1') % 2 == 0 10 | 11 | 12 | def pathfind(a, b, c, d, max_steps=None): 13 | """ 14 | Returns length of the shortest path from (a, b) to (c, d) 15 | and the number of visited locations along the way. 16 | """ 17 | 18 | deltas = [(0, 1), (1, 0), (0, -1), (-1, 0)] 19 | horizon = [(a, b)] 20 | seen = set() 21 | steps = 0 22 | 23 | while horizon: 24 | new_horizon = [] 25 | for x, y in horizon: 26 | if x == c and y == d: 27 | return steps, len(seen) 28 | 29 | seen.add((x, y)) 30 | 31 | for dx, dy in deltas: 32 | nx, ny = x + dx, y + dy 33 | if is_open(nx, ny) and nx >= 0 and ny >= 0 and (nx, ny) not in seen: 34 | new_horizon.append((nx, ny)) 35 | 36 | if steps == max_steps: 37 | break 38 | 39 | horizon = new_horizon 40 | steps += 1 41 | 42 | return steps, len(seen) 43 | 44 | 45 | print "Fewest steps to reach 31, 39 from 1, 1:", pathfind(1, 1, 31, 39)[0] 46 | print "Distinct locations reached in 50 steps:", pathfind(1, 1, -1, -1, max_steps=50)[1] 47 | -------------------------------------------------------------------------------- /2016/day14.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import fileinput 3 | from hashlib import md5 4 | from collections import defaultdict 5 | 6 | from utils import memoize 7 | 8 | 9 | @memoize 10 | def salty_md5(salt, i): 11 | return md5('{}{}'.format(salt, i)).hexdigest() 12 | 13 | 14 | @memoize 15 | def stretched_md5(salt, i): 16 | h = salty_md5(salt, i) 17 | for _ in range(2016): 18 | h = md5(h).hexdigest() 19 | 20 | return h 21 | 22 | 23 | def first_triple(s): 24 | for i in range(len(s) - 2): 25 | a, b, c = s[i:i+3] 26 | if a == b == c: 27 | return a 28 | 29 | 30 | def all_quintuples(s): 31 | for i in range(len(s) - 4): 32 | a, b, c, d, e = s[i:i+5] 33 | if a == b == c == d == e: 34 | yield a 35 | 36 | 37 | def find_64th_pad_key(salt, hash_fn): 38 | valid_keys = 0 39 | triples = {} 40 | quintuples = defaultdict(set) 41 | i = 0 42 | 43 | while True: 44 | digest = hash_fn(salt, i) 45 | triple = first_triple(digest) 46 | 47 | if triple is not None: 48 | triples[i] = triple 49 | 50 | for quint in all_quintuples(digest): 51 | quintuples[i].add(quint) 52 | 53 | if (i - 1000) in triples: 54 | n = i - 1000 55 | triple = triples[n] 56 | for j in range(n + 1, n + 1001): 57 | if triple in quintuples[j]: 58 | valid_keys += 1 59 | sys.stdout.write('.') 60 | sys.stdout.flush() 61 | 62 | if valid_keys >= 64: 63 | return n 64 | 65 | break 66 | 67 | i += 1 68 | 69 | 70 | if __name__ == "__main__": 71 | SALT = fileinput.input()[0].strip() 72 | print "Index of 64th one-time pad key", find_64th_pad_key(SALT, salty_md5) 73 | print "Index of key-stretched pad key", find_64th_pad_key(SALT, stretched_md5) 74 | -------------------------------------------------------------------------------- /2016/day15.py: -------------------------------------------------------------------------------- 1 | import re 2 | import fileinput 3 | from utils import parse_line, mul 4 | 5 | 6 | def button_timing(discs): 7 | """Returns the first time at which the button should be pressed.""" 8 | largest_disc = max(discs) 9 | stride, initial_pos = largest_disc 10 | disc_num = discs.index(largest_disc) + 1 11 | 12 | i = (stride - initial_pos - disc_num) % stride 13 | 14 | while True: 15 | for time, (positions, initial) in enumerate(discs, start=i+1): 16 | if (initial + time) % positions != 0: 17 | break 18 | else: 19 | return i 20 | 21 | i += stride 22 | 23 | # https://stackoverflow.com/a/9758173/239076 24 | def egcd(a, b): 25 | if a == 0: 26 | return (b, 0, 1) 27 | else: 28 | g, y, x = egcd(b % a, a) 29 | return (g, x - (b // a) * y, y) 30 | 31 | def modinv(a, m): 32 | g, x, y = egcd(a, m) 33 | if g != 1: 34 | raise Exception('modular inverse does not exist') 35 | else: 36 | return x % m 37 | 38 | def chinese_remainder_theorem(discs): 39 | M = mul(d[0] for d in discs) 40 | x = 0 41 | 42 | for i, (size, initial) in enumerate(discs, start=1): 43 | # Disc #2 has 17 positions; at time=0, it is at position 15. 44 | # => x \equiv (17 - 15 - 2) (mod 17) 45 | M_i = (M / size) 46 | x += (size - initial - i) * M_i * modinv(M_i, size) 47 | 48 | return x 49 | 50 | 51 | DISCS = [] 52 | DISC_RE = re.compile(r'Disc #\d+ has (\d+) positions; at time=0, it is at position (\d+).') 53 | 54 | for line in fileinput.input(): 55 | disc = parse_line(DISC_RE, line.strip()) 56 | DISCS.append(disc) 57 | 58 | # print "Timing to press button:", button_timing(DISCS) 59 | # print "Timing with added disc:", button_timing(DISCS + [[11, 0]]) 60 | print "Timing to press button:", chinese_remainder_theorem(DISCS) 61 | print "Timing with added disc:", chinese_remainder_theorem(DISCS + [[11, 0]]) 62 | -------------------------------------------------------------------------------- /2016/day16.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | 4 | def generate_data(data, size): 5 | def dragon(a): 6 | b = ''.join('1' if c == '0' else '0' for c in a) 7 | return a + '0' + b[::-1] 8 | 9 | while len(data) < size: 10 | data = dragon(data) 11 | 12 | return data[:size] 13 | 14 | 15 | def checksum(s): 16 | def checksum_iteration(s): 17 | tmp = [] 18 | for a, b in zip(s[::2], s[1::2]): 19 | tmp.append('1' if a == b else '0') 20 | 21 | return ''.join(tmp) 22 | 23 | res = checksum_iteration(s) 24 | 25 | while len(res) % 2 == 0: 26 | res = checksum_iteration(res) 27 | 28 | return res 29 | 30 | 31 | if __name__ == '__main__': 32 | data = fileinput.input()[0].strip() 33 | print 'Checksum #1:', checksum(generate_data(data, 272)) 34 | print 'Checksum #2:', checksum(generate_data(data, 35651584)) 35 | -------------------------------------------------------------------------------- /2016/day17.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from hashlib import md5 3 | from itertools import compress 4 | 5 | 6 | DIRS = { 7 | 'U': (0, -1), 8 | 'D': (0, 1), 9 | 'L': (-1, 0), 10 | 'R': (1, 0), 11 | } 12 | 13 | 14 | def open_doors(digest): 15 | """Returns a 4-tuple of booleans representing whether doors UDLR are open.""" 16 | return [x in 'bcdef' for x in digest[:4]] 17 | 18 | 19 | def pathfind(salt): 20 | horizon = [(0, 0, '')] 21 | 22 | while horizon: 23 | x, y, path = horizon.pop(0) 24 | digest = md5(salt + path).hexdigest() 25 | 26 | for door in compress('UDLR', open_doors(digest)): 27 | dx, dy = DIRS[door] 28 | nx, ny = x + dx, y + dy 29 | 30 | if (0 <= nx < 4) and (0 <= ny < 4): 31 | if nx == 3 and ny == 3: 32 | yield path + door 33 | else: 34 | horizon.append((nx, ny, path + door)) 35 | 36 | 37 | if __name__ == '__main__': 38 | salt = fileinput.input()[0].strip() 39 | paths = sorted(pathfind(salt), key=len) 40 | print 'Shortest path:', paths[0] 41 | print 'Longest path length:', len(paths[-1]) 42 | -------------------------------------------------------------------------------- /2016/day18.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | 4 | def is_trap(row, i): 5 | """Helper function to read out-of-bounds tiles as safe.""" 6 | if 0 <= i < len(row): 7 | return row[i] 8 | 9 | return False 10 | 11 | 12 | def predict_next_row(row): 13 | """Given a row, returns the predicted following row.""" 14 | return [is_trap(row, i-1) ^ is_trap(row, i+1) for i in range(len(row))] 15 | 16 | 17 | if __name__ == '__main__': 18 | # Represent traps as True and safe tiles as False. 19 | # The number of safe tiles per row is len(row) - sum(row). 20 | row = [c == '^' for c in fileinput.input()[0].strip()] 21 | row_len = len(row) 22 | safe_tiles = row_len - sum(row) 23 | 24 | for _ in range(40 - 1): 25 | row = predict_next_row(row) 26 | safe_tiles += row_len - sum(row) 27 | 28 | print "Number of safe tiles in first 40 rows:", safe_tiles 29 | 30 | for _ in range(400000 - 40): 31 | row = predict_next_row(row) 32 | safe_tiles += row_len - sum(row) 33 | 34 | print "Number of safe tiles in all 400000 rows:", safe_tiles 35 | -------------------------------------------------------------------------------- /2016/day19.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import deque 3 | 4 | 5 | def adjacent_elves(num_elves): 6 | elves = deque(range(1, num_elves + 1)) 7 | 8 | while len(elves) > 1: 9 | elves.rotate(-1) 10 | elves.popleft() 11 | 12 | return elves[0] 13 | 14 | 15 | def circular_elves(num_elves): 16 | left = deque(range(1, (num_elves // 2) + 1)) 17 | right = deque(range((num_elves // 2) + 1, num_elves + 1)) 18 | 19 | while len(left) + len(right) > 1: 20 | # Remove the middle Elf in the circle 21 | right.popleft() 22 | 23 | # Move an Elf from between deques 24 | right.append(left.popleft()) 25 | 26 | # Rebalance the deques if uneven 27 | if len(right) - len(left) == 2: 28 | left.append(right.popleft()) 29 | 30 | return right[0] 31 | 32 | 33 | if __name__ == '__main__': 34 | num_elves = int(fileinput.input()[0]) 35 | print "Winning Elf in part 1:", adjacent_elves(num_elves) 36 | print "Winning Elf in part 2:", circular_elves(num_elves) 37 | -------------------------------------------------------------------------------- /2016/day20.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import parse_line 3 | 4 | MAX_IP = 2**32 - 1 5 | 6 | 7 | def allowed_ips(blacklist, max_ip=MAX_IP): 8 | """Generator over all allowed IPs given a blacklist.""" 9 | i = 0 10 | 11 | for start, end in sorted(blacklist): 12 | if i < start: 13 | for n in range(i, start): 14 | yield n 15 | 16 | if i <= end: 17 | i = end + 1 18 | 19 | if i <= max_ip: 20 | for n in range(i, max_ip + 1): 21 | yield n 22 | 23 | 24 | if __name__ == '__main__': 25 | blacklist = [] 26 | 27 | for line in fileinput.input(): 28 | start, end = parse_line(r'(\d+)-(\d+)', line) 29 | blacklist.append((start, end)) 30 | 31 | whitelist = list(allowed_ips(blacklist)) 32 | 33 | print "Lowest-valued whitelisted IP:", whitelist[0] 34 | print "Number of allowed addresses:", len(whitelist) 35 | -------------------------------------------------------------------------------- /2016/starter.py: -------------------------------------------------------------------------------- 1 | import os # NOQA 2 | import sys # NOQA 3 | import re # NOQA 4 | import math # NOQA 5 | import fileinput 6 | from collections import Counter, deque, namedtuple # NOQA 7 | from itertools import count, product, permutations, combinations, combinations_with_replacement # NOQA 8 | 9 | from utils import parse_line, mul, factors, memoize, primes, new_table, Point # NOQA 10 | 11 | # Itertools Functions: 12 | # product('ABCD', repeat=2) AA AB AC AD BA BB BC BD CA CB CC CD DA DB DC DD 13 | # permutations('ABCD', 2) AB AC AD BA BC BD CA CB CD DA DB DC 14 | # combinations('ABCD', 2) AB AC AD BC BD CD 15 | # combinations_with_replacement('ABCD', 2) AA AB AC AD BB BC BD CC CD DD 16 | 17 | total = 0 18 | result = [] 19 | table = new_table(None, width=2, height=4) 20 | 21 | for i, line in enumerate(fileinput.input()): 22 | line = line.strip() 23 | 24 | # data = [x for x in line.split(', ')] 25 | # data = [x for x in line] 26 | # data = [int(x) for x in line.split()] 27 | # data = re.findall(r'(\w+)', line) 28 | data = parse_line(r'', line) 29 | 30 | if i == 0: 31 | print(data) 32 | -------------------------------------------------------------------------------- /2017/day01.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | digits = [int(x) for x in fileinput.input()[0].strip()] 4 | dlen = len(digits) 5 | half = dlen // 2 6 | 7 | fst_total = 0 8 | snd_total = 0 9 | 10 | for i in range(len(digits)): 11 | if digits[i] == digits[(i + 1) % dlen]: 12 | fst_total += digits[i] 13 | 14 | if digits[i] == digits[(i + half) % dlen]: 15 | snd_total += digits[i] 16 | 17 | print "Solution to first captcha:", fst_total 18 | print "Solution to second captcha:", snd_total 19 | -------------------------------------------------------------------------------- /2017/day02.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | checksum = 0 4 | result = 0 5 | 6 | for line in fileinput.input(): 7 | nums = sorted((int(x) for x in line.split()), reverse=True) 8 | 9 | checksum += nums[0] - nums[-1] 10 | 11 | for i in range(len(nums)): 12 | for j in range(i + 1, len(nums)): 13 | if nums[i] % nums[j] == 0: 14 | result += nums[i] // nums[j] 15 | 16 | print "Spreadsheet checksum:", checksum 17 | print "Spreadsheet result:", result 18 | -------------------------------------------------------------------------------- /2017/day03.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from itertools import count 3 | 4 | from utils import Point, DIRS 5 | 6 | target = int(fileinput.input()[0]) 7 | pos = Point(0, 0) 8 | seen = {pos: 1} 9 | facing = 1 10 | 11 | answer_1 = None 12 | answer_2 = None 13 | 14 | for i in count(start=2): 15 | pos = pos + DIRS[facing] 16 | 17 | value = sum(seen.get(p, 0) for p in pos.neighbours_8()) 18 | seen[pos] = value 19 | 20 | if i == target and not answer_1: 21 | answer_1 = pos.manhattan 22 | 23 | if value > target and not answer_2: 24 | answer_2 = value 25 | 26 | # Check if we should move straight or turn left next 27 | if (pos + DIRS[(facing - 1) % 4] not in seen): 28 | facing = (facing - 1) % 4 29 | 30 | if (answer_1 is not None) and (answer_2 is not None): 31 | break 32 | 33 | print "Step to access port:", answer_1 34 | print "First value larger than puzzle input:", answer_2 35 | -------------------------------------------------------------------------------- /2017/day04.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | fst_valids = 0 4 | snd_valids = 0 5 | 6 | for line in fileinput.input(): 7 | passphrase = [x for x in line.split()] 8 | anagrams = [''.join(sorted(p)) for p in passphrase] 9 | 10 | if len(set(passphrase)) == len(passphrase): 11 | fst_valids += 1 12 | 13 | if len(set(anagrams)) == len(anagrams): 14 | snd_valids += 1 15 | 16 | print "Valid passphrases under first policy:", fst_valids 17 | print "Valid passphrases under second policy:", snd_valids 18 | -------------------------------------------------------------------------------- /2017/day05.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | INSTR = [] 4 | 5 | for line in fileinput.input(): 6 | INSTR.append(int(line)) 7 | 8 | # Part 1 9 | instr = INSTR[:] 10 | steps = 0 11 | i = 0 12 | 13 | while 0 <= i < len(instr): 14 | jump_to = instr[i] + i 15 | instr[i] += 1 16 | i = jump_to 17 | steps += 1 18 | 19 | print "Steps to reach exit (part 1):", steps 20 | 21 | # Part 2 22 | instr = INSTR[:] 23 | steps = 0 24 | i = 0 25 | 26 | while 0 <= i < len(instr): 27 | jump_to = instr[i] + i 28 | instr[i] += (-1 if instr[i] >= 3 else 1) 29 | i = jump_to 30 | steps += 1 31 | 32 | print "Steps to reach exit (part 2):", steps 33 | -------------------------------------------------------------------------------- /2017/day06.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from itertools import count 3 | 4 | banks = [int(n) for n in fileinput.input()[0].split()] 5 | seen = {} 6 | 7 | for i in count(start=1): 8 | m = max(banks) 9 | idx = banks.index(m) 10 | banks[idx] = 0 11 | 12 | for j in range(1, m + 1): 13 | banks[(idx + j) % len(banks)] += 1 14 | 15 | t = tuple(banks) 16 | if t in seen: 17 | break 18 | 19 | seen[t] = i 20 | 21 | print "Number of redistribution cycles:", i 22 | print "Length of infinite loop cycle:", i - seen[t] 23 | -------------------------------------------------------------------------------- /2017/day08.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import defaultdict 3 | 4 | REGS = defaultdict(int) 5 | HIGHEST_VAL = 0 6 | 7 | for line in fileinput.input(): 8 | a, op, x, _, b, eq, y = line.strip().split() 9 | 10 | if eval('REGS["{}"] {} {}'.format(b, eq, y)): 11 | REGS[a] += int(x) * (1 if op == 'inc' else -1) 12 | HIGHEST_VAL = max(HIGHEST_VAL, REGS[a]) 13 | 14 | print "Largest value in any register:", max(REGS.values()) 15 | print "Highest value held in registers:", HIGHEST_VAL 16 | -------------------------------------------------------------------------------- /2017/day09.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | score = 0 4 | depth = 0 5 | garb_chars = 0 6 | 7 | in_garbage = False 8 | skip = False 9 | 10 | for c in fileinput.input()[0].strip(): 11 | if skip: 12 | skip = False 13 | 14 | elif c == '!': 15 | skip = True 16 | 17 | elif in_garbage: 18 | if c == '>': 19 | in_garbage = False 20 | else: 21 | garb_chars += 1 22 | 23 | else: 24 | if c == '{': 25 | depth += 1 26 | elif c == '}': 27 | score += depth 28 | depth -= 1 29 | elif c == '<': 30 | in_garbage = True 31 | 32 | print "Total score for all groups:", score 33 | print "Non-canceled garbage characters:", garb_chars 34 | -------------------------------------------------------------------------------- /2017/day10.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | 4 | def knot_hash(elems, lengths, pos=0, skip=0): 5 | for l in lengths: 6 | for i in range(l // 2): 7 | x = (pos + i) % len(elems) 8 | y = (pos + l - i - 1) % len(elems) 9 | elems[x], elems[y] = elems[y], elems[x] 10 | 11 | pos = pos + l + skip % len(elems) 12 | skip += 1 13 | 14 | return elems, pos, skip 15 | 16 | 17 | # Read puzzle input 18 | line = fileinput.input()[0].strip() 19 | 20 | # Part 1 21 | try: 22 | lengths = [int(x) for x in line.split(',')] 23 | elems = knot_hash(range(0, 256), lengths)[0] 24 | print "Product of first two items in list:", elems[0] * elems[1] 25 | except ValueError: 26 | print "Skipping part 1 (can't parse puzzle input into ints)" 27 | 28 | # Part 2 29 | lengths = [ord(x) for x in line] + [17, 31, 73, 47, 23] 30 | elems = range(0, 256) 31 | pos = 0 32 | skip = 0 33 | 34 | # Perform 64 rounds of Knot Hash 35 | for _ in range(64): 36 | elems, pos, skip = knot_hash(elems, lengths, pos, skip) 37 | 38 | # Convert from sparse hash to dense hash 39 | sparse = elems 40 | dense = [] 41 | 42 | for i in range(16): 43 | res = 0 44 | for j in range(0, 16): 45 | res ^= sparse[(i * 16) + j] 46 | 47 | dense.append(res) 48 | 49 | print "Knot Hash of puzzle input:", ''.join('%02x' % x for x in dense) 50 | -------------------------------------------------------------------------------- /2017/day11.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import Counter 3 | 4 | # Represent hex coordinates in 3 dimensions 5 | HEX_DIRS = { 6 | 'n': (1, -1, 0), 7 | 'ne': (1, 0, -1), 8 | 'se': (0, 1, -1), 9 | 's': (-1, 1, 0), 10 | 'sw': (-1, 0, 1), 11 | 'nw': (0, -1, 1), 12 | } 13 | 14 | 15 | def hex_distance(x, y, z): 16 | """Returns a given hex point's distance from the origin.""" 17 | return (abs(x) + abs(y) + abs(z)) // 2 18 | 19 | 20 | x = y = z = 0 21 | furthest = 0 22 | 23 | for move in fileinput.input()[0].strip().split(','): 24 | nx, ny, nz = HEX_DIRS[move] 25 | x += nx 26 | y += ny 27 | z += nz 28 | furthest = max(furthest, hex_distance(x, y, z)) 29 | 30 | print "Fewest steps to reach child process:", hex_distance(x, y, z) 31 | print "Furthest distance from start:", furthest 32 | -------------------------------------------------------------------------------- /2017/day12.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | MAPPINGS = {} 4 | 5 | for line in fileinput.input(): 6 | x, y = line.strip().split(' <-> ') 7 | MAPPINGS[x] = [pid for pid in y.split(', ')] 8 | 9 | groups = {} 10 | seen_ids = set() 11 | group_0 = None 12 | 13 | for pid in MAPPINGS.keys(): 14 | if pid in seen_ids: 15 | continue 16 | 17 | connected = set() 18 | queue = MAPPINGS[pid][:] 19 | 20 | while queue: 21 | nid = queue.pop() 22 | connected.add(nid) 23 | 24 | if nid == '0': 25 | group_0 = pid 26 | 27 | for n in MAPPINGS[nid]: 28 | if n not in seen_ids: 29 | queue.append(n) 30 | seen_ids.add(n) 31 | 32 | groups[pid] = connected 33 | 34 | print "Programs in group containing PID 0:", len(groups[group_0]) 35 | print "Total number of program groups:", len(groups) 36 | -------------------------------------------------------------------------------- /2017/day13.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from itertools import count 3 | 4 | 5 | def scanner(height, time): 6 | """Return the position of a given scanner height at a given time.""" 7 | offset = time % ((height - 1) * 2) 8 | 9 | if offset > height - 1: 10 | return 2 * (height - 1) 11 | else: 12 | return offset 13 | 14 | 15 | HEIGHTS = {} 16 | 17 | for line in fileinput.input(): 18 | depth, rnge = [int(x) for x in line.strip().split(': ')] 19 | HEIGHTS[depth] = rnge 20 | 21 | # Part 1 22 | severity = 0 23 | for pos in HEIGHTS: 24 | if scanner(HEIGHTS[pos], pos) == 0: 25 | severity += pos * HEIGHTS[pos] 26 | 27 | print "Severity of whole trip:", severity 28 | 29 | # Part 2 30 | for delay in count(): 31 | for pos in HEIGHTS: 32 | if scanner(HEIGHTS[pos], delay + pos) == 0: 33 | break 34 | else: 35 | print "Minimal delay to not get caught:", delay 36 | break 37 | -------------------------------------------------------------------------------- /2017/day14.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import Point, knot_hash 3 | 4 | 5 | KEY_STRING = fileinput.input()[0].strip() 6 | DISK = set() 7 | 8 | for x in range(128): 9 | hash_hex = knot_hash(KEY_STRING + '-' + str(x)) 10 | hash_bits = ''.join('{0:04b}'.format(int(x, 16)) for x in hash_hex) 11 | for y, b in enumerate(hash_bits): 12 | if b == '1': 13 | DISK.add(Point(x, y)) 14 | 15 | regions = 0 16 | seen = set() 17 | 18 | for p in DISK: 19 | if p in seen: 20 | continue 21 | 22 | queue = [p] 23 | while queue: 24 | np = queue.pop() 25 | seen.add(np) 26 | for n in np.neighbours_4(): 27 | if n in DISK and n not in seen: 28 | queue.append(n) 29 | 30 | regions += 1 31 | 32 | print "Number of used squares:", len(DISK) 33 | print "Number of regions:", regions 34 | -------------------------------------------------------------------------------- /2017/day15.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | SEEDS = [int(l.split()[-1]) for l in fileinput.input()] 4 | 5 | FACTOR_A = 16807 6 | FACTOR_B = 48271 7 | MODULO = 2**31 - 1 8 | 9 | # Part 1 10 | gen_a, gen_b = SEEDS 11 | count = 0 12 | 13 | for i in range(40000000): 14 | gen_a = (gen_a * FACTOR_A) % MODULO 15 | gen_b = (gen_b * FACTOR_B) % MODULO 16 | 17 | if (gen_a & 0xffff) == (gen_b & 0xffff): 18 | count += 1 19 | 20 | print "Judge's first count:", count 21 | 22 | 23 | # Part 2 24 | gen_a, gen_b = SEEDS 25 | count = 0 26 | 27 | for i in range(5000000): 28 | gen_a = (gen_a * FACTOR_A) % MODULO 29 | while gen_a % 4 != 0: 30 | gen_a = (gen_a * FACTOR_A) % MODULO 31 | 32 | gen_b = (gen_b * FACTOR_B) % MODULO 33 | while gen_b % 8 != 0: 34 | gen_b = (gen_b * FACTOR_B) % MODULO 35 | 36 | if (gen_a & 0xffff) == (gen_b & 0xffff): 37 | count += 1 38 | 39 | print "Judge's second count:", count 40 | -------------------------------------------------------------------------------- /2017/day16.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import deque 3 | 4 | 5 | def perform_dance(progs, moves): 6 | for m in moves: 7 | if m[0] == 's': 8 | progs.rotate(int(m[1:])) 9 | 10 | else: 11 | a, b = m[1:].split('/') 12 | 13 | if m[0] == 'x': 14 | a = int(a) 15 | b = int(b) 16 | else: 17 | # deque.index() only supported in Python 3.5+ 18 | progs_lst = list(progs) 19 | a = progs_lst.index(a) 20 | b = progs_lst.index(b) 21 | 22 | progs[a], progs[b] = progs[b], progs[a] 23 | 24 | 25 | MOVES = fileinput.input()[0].strip().split(',') 26 | PROGS = [chr(x) for x in range(ord('a'), ord('p') + 1)] 27 | 28 | # Part 1 29 | progs = deque(PROGS) 30 | perform_dance(progs, MOVES) 31 | print "Order of programs after first dance:", ''.join(progs) 32 | 33 | # Part 2 34 | progs = deque(PROGS) 35 | order = ''.join(progs) 36 | past_orders = [] 37 | cycle_len = 0 38 | 39 | while order not in past_orders: 40 | past_orders.append(order) 41 | perform_dance(progs, MOVES) 42 | order = ''.join(progs) 43 | cycle_len += 1 44 | 45 | # Now we know the length of the cycle, so just compute the order 46 | # after the billionth dance using modular arithmetic. 47 | idx = 1000000000 % cycle_len 48 | print "Order of programs after one billion dances:", past_orders[idx] 49 | -------------------------------------------------------------------------------- /2017/day17.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | INC = int(fileinput.input()[0]) 4 | 5 | # Part 1 6 | spinlock = [0] 7 | pos = 0 8 | 9 | for i in range(1, 2017 + 1): 10 | pos = (pos + INC) % len(spinlock) 11 | spinlock.insert((pos + 1), i) 12 | pos += 1 13 | 14 | val = spinlock[spinlock.index(2017) + 1] 15 | print "Value after 2017 in small buffer:", val 16 | 17 | # Part 2 18 | snd_elem = None 19 | lock_len = 1 20 | pos = 0 21 | 22 | for i in range(1, 50000000 + 1): 23 | pos = (pos + INC) % lock_len 24 | 25 | if pos == 0: 26 | snd_elem = i 27 | 28 | lock_len += 1 29 | pos += 1 30 | 31 | print "Value after 0 in large buffer:", snd_elem 32 | -------------------------------------------------------------------------------- /2017/day18.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import deque, defaultdict 3 | 4 | 5 | def duet_program(instrs, snd_q, rcv_q, pid=None): 6 | regs = defaultdict(int) 7 | if pid is not None: 8 | regs['p'] = pid 9 | 10 | pc = 0 11 | sends = 0 12 | 13 | while 0 <= pc < len(instrs): 14 | op, x, y = instrs[pc] 15 | y = regs[y] if y.isalpha() else int(y) 16 | 17 | if op == 'snd': 18 | snd_q.appendleft(regs[x] if x.isalpha() else int(x)) 19 | sends += 1 20 | elif op == 'set': 21 | regs[x] = y 22 | elif op == 'add': 23 | regs[x] += y 24 | elif op == 'mul': 25 | regs[x] *= y 26 | elif op == 'mod': 27 | regs[x] %= y 28 | elif op == 'rcv': 29 | if len(rcv_q) > 0: 30 | regs[x] = rcv_q.pop() 31 | else: 32 | yield op, sends 33 | continue 34 | elif op == 'jgz': 35 | if (regs[x] if x.isalpha() else int(x)) > 0: 36 | pc += y 37 | continue 38 | 39 | pc += 1 40 | 41 | raise StopIteration 42 | 43 | 44 | # Read puzzle input 45 | INSTRS = [] 46 | 47 | for line in fileinput.input(): 48 | instr = line.strip() + ' foo' 49 | op, x, y = instr.split()[:3] 50 | INSTRS.append((op, x, y)) 51 | 52 | 53 | # Part 1 54 | snd_queue = deque() 55 | program = duet_program(INSTRS, snd_queue, deque()) 56 | next(program) 57 | 58 | print "Value of recovered frequency:", snd_queue[0] 59 | 60 | 61 | # Part 2 62 | queue_0 = deque() 63 | queue_1 = deque() 64 | 65 | program_0 = duet_program(INSTRS, queue_1, queue_0, pid=0) 66 | program_1 = duet_program(INSTRS, queue_0, queue_1, pid=1) 67 | 68 | last_op_0 = None 69 | last_op_1 = None 70 | 71 | while not (last_op_0 == last_op_1 == 'rcv' and len(queue_0) == len(queue_1) == 0): 72 | last_op_0, _ = next(program_0) 73 | last_op_1, sends = next(program_1) 74 | 75 | print "Number of times program 1 sent a value:", sends 76 | -------------------------------------------------------------------------------- /2017/day19.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import Point, DIRS 3 | 4 | 5 | DIAGRAM = {} 6 | 7 | for y, line in enumerate(fileinput.input()): 8 | for x, c in enumerate(line.strip('\n')): 9 | if y == 0 and c == '|': 10 | start = Point(x, y) 11 | 12 | DIAGRAM[Point(x, y)] = c 13 | 14 | d = 0 15 | pos = start 16 | 17 | steps = 0 18 | letters = [] 19 | 20 | while True: 21 | tile = DIAGRAM.get(pos, ' ') 22 | 23 | if tile.isalpha(): 24 | letters.append(tile) 25 | 26 | elif tile == '+': 27 | # Determine whether to turn left or right 28 | right_tile = DIAGRAM.get(pos + DIRS[(d + 1) % 4], ' ') 29 | if right_tile == ' ': 30 | d = (d - 1) % 4 31 | else: 32 | d = (d + 1) % 4 33 | 34 | elif tile == ' ': 35 | break 36 | 37 | pos = pos + DIRS[d] 38 | steps += 1 39 | 40 | print "Letters seen in path:", ''.join(letters) 41 | print "Total number of steps:", steps 42 | -------------------------------------------------------------------------------- /2017/day22.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import Point, DIRS 3 | 4 | 5 | def simulate_activity(x, y, facing, grid, mapping): 6 | node = grid.get((x, y), '.') 7 | new_node, df = mapping[node] 8 | 9 | # Update node 10 | grid[x, y] = new_node 11 | 12 | # Update direction and position 13 | nf = (facing + df) % 4 14 | x += DIRS[nf].x 15 | y += DIRS[nf].y 16 | 17 | return x, y, nf, new_node == '#' 18 | 19 | 20 | # Maps node transformations and turn-direction 21 | PART_1_MAP = { 22 | '#': ('.', -1), 23 | '.': ('#', 1), 24 | } 25 | 26 | PART_2_MAP = { 27 | '.': ('W', 1), 28 | 'W': ('#', 0), 29 | '#': ('F', -1), 30 | 'F': ('.', 2), 31 | } 32 | 33 | GRID = {} 34 | HEIGHT = 0 35 | WIDTH = 0 36 | 37 | # Read puzzle input 38 | for y, line in enumerate(fileinput.input()): 39 | for x, c in enumerate(line.strip()): 40 | GRID[x, y] = c 41 | 42 | WIDTH = x + 1 43 | HEIGHT += 1 44 | 45 | # Part 1 46 | x = WIDTH // 2 47 | y = HEIGHT // 2 48 | facing = 2 49 | grid = GRID.copy() 50 | infections_caused = 0 51 | 52 | for _ in range(10000): 53 | x, y, facing, caused = simulate_activity(x, y, facing, grid, PART_1_MAP) 54 | if caused: 55 | infections_caused += 1 56 | 57 | print "Infections caused in Part 1:", infections_caused 58 | 59 | # Part 2 60 | x = WIDTH // 2 61 | y = HEIGHT // 2 62 | facing = 2 63 | grid = GRID.copy() 64 | infections_caused = 0 65 | 66 | for _ in range(10000000): 67 | x, y, facing, caused = simulate_activity(x, y, facing, grid, PART_2_MAP) 68 | if caused: 69 | infections_caused += 1 70 | 71 | print "Infections caused in Part 2:", infections_caused 72 | -------------------------------------------------------------------------------- /2017/day24.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import defaultdict, deque 3 | 4 | 5 | def bridge_bfs(i, port): 6 | def bridge_strength(ports): 7 | """Returns the strength of a bridge given its ports.""" 8 | return sum(ports) * 2 - ports[0] - ports[-1] 9 | 10 | # (port_index, port_list, seen_indices) 11 | horizon = deque([(i, [port], set([i]))]) 12 | 13 | strongest = 0 14 | 15 | while horizon: 16 | longest = 0 17 | 18 | for _ in range(len(horizon)): 19 | i, ports, seen = horizon.popleft() 20 | seen.add(i) 21 | 22 | bridge = BRIDGES[i] 23 | port = bridge[0] if bridge[1] == ports[-1] else bridge[1] 24 | 25 | next_ports = ports + [port] 26 | strength = bridge_strength(next_ports) 27 | 28 | # See if we can incorporate any `X/X` components 29 | strength += sum(2 * p for p in BRIDGE_EXTRAS if p in next_ports) 30 | 31 | strongest = max(strongest, strength) 32 | longest = max(longest, strength) 33 | 34 | for p in BRIDGE_PORTS[port]: 35 | if p not in seen: 36 | horizon.append((p, list(next_ports), set(seen))) 37 | 38 | return strongest, longest 39 | 40 | 41 | BRIDGES = [] 42 | BRIDGE_PORTS = defaultdict(list) 43 | BRIDGE_EXTRAS = [] # tracks components of type `X/X` 44 | 45 | for line in fileinput.input(): 46 | x, y = (int(x) for x in line.split('/')) 47 | 48 | if x != y: 49 | i = len(BRIDGES) 50 | BRIDGES.append((x, y)) 51 | BRIDGE_PORTS[x].append(i) 52 | BRIDGE_PORTS[y].append(i) 53 | else: 54 | BRIDGE_EXTRAS.append(x) 55 | 56 | for i in BRIDGE_PORTS[0]: 57 | strongest, longest = bridge_bfs(i, 0) 58 | print "Strength of strongest bridge:", strongest 59 | print "Strength of longest bridge:", longest 60 | -------------------------------------------------------------------------------- /2017/day25.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import defaultdict 3 | 4 | from utils import parse_line 5 | 6 | PROG = {} 7 | 8 | # Parse puzzle input 9 | for i, line in enumerate(fileinput.input()): 10 | if i == 0: 11 | STATE = parse_line(r'Begin in state (\w+).', line)[0] 12 | elif i == 1: 13 | STEPS = int(parse_line(r'Perform a diagnostic checksum after (\d+) steps.', line)[0]) 14 | 15 | if i <= 2: 16 | continue 17 | 18 | if i % 10 == 3: 19 | start = parse_line(r'In state (\w+):', line)[0] 20 | elif i % 10 == 5: 21 | write_0 = parse_line(r' - Write the value (\d+).', line)[0] 22 | elif i % 10 == 6: 23 | move_0 = parse_line(r' - Move one slot to the (\w+).', line)[0] 24 | move_0 = 1 if move_0 == 'right' else -1 25 | elif i % 10 == 7: 26 | next_0 = parse_line(r' - Continue with state (\w+).', line)[0] 27 | elif i % 10 == 9: 28 | write_1 = parse_line(r' - Write the value (\d+).', line)[0] 29 | elif i % 10 == 0: 30 | move_1 = parse_line(r' - Move one slot to the (\w+).', line)[0] 31 | move_1 = 1 if move_1 == 'right' else -1 32 | elif i % 10 == 1: 33 | next_1 = parse_line(r' - Continue with state (\w+).', line)[0] 34 | PROG[start] = ((write_0, move_0, next_0), (write_1, move_1, next_1)) 35 | 36 | 37 | TAPE = defaultdict(int) 38 | CURSOR = 0 39 | 40 | for _ in range(STEPS): 41 | write, dx, ns = PROG[STATE][TAPE[CURSOR]] 42 | TAPE[CURSOR] = write 43 | CURSOR += dx 44 | STATE = ns 45 | 46 | print "Diagnostic checksum:", sum(x for x in TAPE.values()) 47 | -------------------------------------------------------------------------------- /2017/starter.py: -------------------------------------------------------------------------------- 1 | import os # NOQA 2 | import sys # NOQA 3 | import re # NOQA 4 | import math # NOQA 5 | import fileinput 6 | from collections import Counter, defaultdict, deque, namedtuple # NOQA 7 | from itertools import count, product, permutations, combinations, combinations_with_replacement # NOQA 8 | 9 | from utils import parse_line, mul, all_unique, factors, memoize, primes # NOQA 10 | from utils import new_table, transposed, rotated # NOQA 11 | from utils import md5, sha256, knot_hash # NOQA 12 | from utils import Point, DIRS, DIRS_4, DIRS_8 # NOQA 13 | 14 | # Itertools Functions: 15 | # product('ABCD', repeat=2) AA AB AC AD BA BB BC BD CA CB CC CD DA DB DC DD 16 | # permutations('ABCD', 2) AB AC AD BA BC BD CA CB CD DA DB DC 17 | # combinations('ABCD', 2) AB AC AD BC BD CD 18 | # combinations_with_replacement('ABCD', 2) AA AB AC AD BB BC BD CC CD DD 19 | 20 | total = 0 21 | result = [] 22 | table = new_table(None, width=2, height=4) 23 | 24 | for i, line in enumerate(fileinput.input()): 25 | line = line.strip() 26 | data = parse_line(r'', line) 27 | 28 | if i == 0: 29 | print(data) 30 | -------------------------------------------------------------------------------- /2018/day01.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import fileinput 3 | from itertools import count 4 | 5 | FREQ = 0 6 | SEEN = set([0]) 7 | TAPE = [] 8 | 9 | for line in fileinput.input(): 10 | TAPE.append(int(line)) 11 | 12 | for i in count(): 13 | for n in TAPE: 14 | FREQ += n 15 | if FREQ in SEEN: 16 | print "First duplicate frequency:", FREQ 17 | sys.exit() 18 | else: 19 | SEEN.add(FREQ) 20 | 21 | if i == 0: 22 | print "First resulting frequency:", FREQ 23 | -------------------------------------------------------------------------------- /2018/day02.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import Counter 3 | 4 | BOXES = [line.strip() for line in fileinput.input()] 5 | 6 | DOUBLES = 0 7 | TRIPLES = 0 8 | COMMON = None 9 | 10 | for box_1 in BOXES: 11 | doubles = 0 12 | triples = 0 13 | 14 | for char, count in Counter(box_1).items(): 15 | if count == 2: 16 | doubles += 1 17 | elif count == 3: 18 | triples += 1 19 | 20 | if doubles > 0: 21 | DOUBLES += 1 22 | 23 | if triples > 0: 24 | TRIPLES += 1 25 | 26 | for box_2 in BOXES: 27 | if box_1 == box_2: 28 | continue 29 | 30 | diffs = 0 31 | 32 | for i in range(len(box_1)): 33 | if box_1[i] != box_2[i]: 34 | diffs += 1 35 | 36 | if diffs == 1: 37 | COMMON = ''.join(a for a, b in zip(box_1, box_2) if a == b) 38 | 39 | 40 | print "Checksum for list of box IDs:", DOUBLES * TRIPLES 41 | print "Common letters for right IDs:", COMMON 42 | -------------------------------------------------------------------------------- /2018/day03.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import Counter, defaultdict 3 | 4 | from utils import parse_line 5 | 6 | SQUARES = Counter() 7 | CLAIMS = defaultdict(list) 8 | 9 | for i, line in enumerate(fileinput.input()): 10 | _id, x, y, w, h = parse_line(r'#(\d+) @ (\d+),(\d+): (\d+)x(\d+)', line) 11 | 12 | for i in range(x, x + w): 13 | for j in range(y, y + h): 14 | SQUARES[(i, j)] += 1 15 | CLAIMS[_id].append((i, j)) 16 | 17 | print "Square inches of fabric within multiple claims:", sum(n > 1 for c, n in SQUARES.items()) 18 | 19 | for _id in CLAIMS: 20 | for pos in CLAIMS[_id]: 21 | if SQUARES[pos] != 1: 22 | break 23 | else: 24 | print "ID of only non-overlapping claim:", _id 25 | -------------------------------------------------------------------------------- /2018/day04.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import Counter, defaultdict 3 | 4 | from utils import parse_nums 5 | 6 | 7 | # Read problem input 8 | EVENTS = sorted(parse_nums(line, negatives=False) for line in fileinput.input()) 9 | 10 | # Process events 11 | SHIFTS = Counter() 12 | GUARDS = defaultdict(Counter) 13 | curr_id = None 14 | sleep_min = None 15 | 16 | for record in EVENTS: 17 | if len(record) == 6: # shift begins 18 | curr_id = int(record[-1]) 19 | 20 | elif sleep_min is None: # falls asleep 21 | sleep_min = record[-1] 22 | 23 | else: # wakes up 24 | minute = record[-1] 25 | SHIFTS[curr_id] += minute - sleep_min 26 | for m in range(sleep_min, minute): 27 | GUARDS[curr_id][m] += 1 28 | 29 | sleep_min = None 30 | 31 | # Compute Part 1 answer 32 | guard_id = SHIFTS.most_common(1)[0][0] 33 | minute = GUARDS[guard_id].most_common(1)[0][0] 34 | print "Strategy 1 checksum:", guard_id * minute 35 | 36 | # Compute Part 2 answer 37 | guard_id, (minute, _) = max(((i, c.most_common(1)[0]) for i, c in GUARDS.items()), key=lambda x: x[1][1]) 38 | print "Strategy 2 checksum:", guard_id * minute 39 | -------------------------------------------------------------------------------- /2018/day05.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from string import ascii_lowercase, ascii_uppercase 3 | 4 | 5 | def reduce_polymer(poly, omit=None): 6 | if omit: 7 | poly = poly.replace(omit.upper(), '').replace(omit.lower(), '') 8 | else: 9 | poly = poly.replace('', '') 10 | 11 | while True: 12 | for (x, X) in zip(ascii_lowercase, ascii_uppercase): 13 | new = poly.replace(x + X, '').replace(X + x, '') 14 | if len(new) < len(poly): 15 | improved = True 16 | poly = new 17 | break 18 | else: 19 | return len(poly) 20 | 21 | 22 | POLYMER = fileinput.input()[0].strip() 23 | 24 | 25 | print "Units remaining after reaction:", reduce_polymer(POLYMER) 26 | print "Best length after omitting one:", min(reduce_polymer(POLYMER, x) for x in ascii_lowercase) 27 | -------------------------------------------------------------------------------- /2018/day06.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import Counter 3 | 4 | from utils import Point, DIRS_4, parse_nums 5 | 6 | 7 | COORDS = [] 8 | 9 | for line in fileinput.input(): 10 | x, y = parse_nums(line) 11 | COORDS.append((x, y)) 12 | 13 | X, Y = zip(*COORDS) 14 | 15 | ASSIGNS = {} 16 | REGIONS = Counter() 17 | SAFETY = Counter() 18 | 19 | for y in range(min(Y), max(Y)): 20 | for x in range(min(X), max(X)): 21 | p = Point(x, y) 22 | total_distance = 0 23 | to_point = {} 24 | 25 | for (xx, yy) in COORDS: 26 | q = Point(xx, yy) 27 | dist = p.to_manhattan(q) 28 | to_point[q] = dist 29 | total_distance += dist 30 | 31 | SAFETY[p] = total_distance 32 | 33 | dists = list(sorted(to_point.items(), key=lambda x: x[1])) 34 | if dists[0][1] < dists[1][1]: 35 | REGIONS[dists[0][0]] += 1 36 | ASSIGNS[p] = dists[0][0] 37 | else: 38 | ASSIGNS[p] = Point(-1, -1) 39 | 40 | for p, n in REGIONS.most_common(): 41 | for d in DIRS_4: 42 | q = p + d 43 | while q in ASSIGNS: 44 | if ASSIGNS[q] != p: 45 | # Not infinite 46 | break 47 | 48 | q += d 49 | else: 50 | # Confirmed infinite 51 | break 52 | 53 | else: 54 | # This is the largest non-infinite region 55 | print "Size of Part 1 region:", n 56 | break 57 | 58 | 59 | print "Size of Part 2 region:", sum(n < 10000 for n in SAFETY.values()) 60 | -------------------------------------------------------------------------------- /2018/day08.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | 4 | def parse_node(nodes, i=0, part_2=False): 5 | value = 0 6 | start = i 7 | num_children, num_meta = nodes[i:i+2] 8 | child_vals = [] 9 | 10 | i += 2 11 | 12 | for _ in range(num_children): 13 | j, val = parse_node(nodes, i, part_2) 14 | child_vals.append(val) 15 | if not part_2: 16 | value += val 17 | i += j 18 | 19 | for j in range(num_meta): 20 | if part_2 and num_children > 0: 21 | try: 22 | value += child_vals[nodes[i] - 1] 23 | except IndexError: 24 | pass 25 | else: 26 | value += nodes[i] 27 | 28 | i += 1 29 | 30 | return i - start, value 31 | 32 | 33 | nodes = [int(x) for x in fileinput.input()[0].split()] 34 | 35 | print "Sum of metadata entries:", parse_node(nodes)[1] 36 | print "Value of the root node:", parse_node(nodes, part_2=True)[1] 37 | -------------------------------------------------------------------------------- /2018/day09.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import parse_nums 3 | 4 | 5 | class Node: 6 | def __init__(self, val, n=None, p=None): 7 | self.val = val 8 | self.next = n 9 | self.prev = p 10 | 11 | 12 | PLAYERS, MARBLES = parse_nums(fileinput.input()[0]) 13 | scores = [0] * PLAYERS 14 | player = 0 15 | 16 | curr = Node(0) 17 | curr.next = curr 18 | curr.prev = curr 19 | 20 | for m in range(1, MARBLES * 100): 21 | if m % 23 == 0: 22 | scores[player] += m 23 | for _ in range(7): 24 | curr = curr.prev 25 | 26 | scores[player] += curr.val 27 | curr.prev.next = curr.next 28 | curr.next.prev = curr.prev 29 | curr = curr.prev.next 30 | else: 31 | curr = curr.next 32 | node = Node(m, p=curr, n=curr.next) 33 | curr.next.prev = node 34 | curr.next = node 35 | curr = node 36 | 37 | player = (player + 1) % PLAYERS 38 | 39 | if m == MARBLES: 40 | print "Winning elf's score:", max(scores) 41 | 42 | print "Newer winning score:", max(scores) 43 | -------------------------------------------------------------------------------- /2018/day10.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import parse_nums 3 | 4 | 5 | INF = 1e9 6 | POINTS = [] 7 | 8 | for line in fileinput.input(): 9 | x, y, dx, dy = parse_nums(line) 10 | POINTS.append([x, y, dx, dy]) 11 | 12 | best_x = INF 13 | best_y = INF 14 | seconds = 0 15 | 16 | while True: 17 | min_x = INF 18 | max_x = -INF 19 | min_y = INF 20 | max_y = -INF 21 | 22 | for i, (x, y, dx, dy) in enumerate(POINTS): 23 | min_x = min(min_x, x) 24 | max_x = max(max_x, x) 25 | min_y = min(min_y, y) 26 | max_y = max(max_y, y) 27 | 28 | POINTS[i][0] += dx 29 | POINTS[i][1] += dy 30 | 31 | diff_x = max_x - min_x 32 | diff_y = max_y - min_y 33 | 34 | improved = False 35 | 36 | if diff_x < best_x: 37 | best_x = diff_x 38 | improved = True 39 | if diff_y < best_y: 40 | best_y = diff_y 41 | improved = True 42 | 43 | if not improved: 44 | break 45 | 46 | seconds += 1 47 | 48 | # Reverse by 2 timesteps to reassemble message 49 | for i, (x, y, dx, dy) in enumerate(POINTS): 50 | POINTS[i][0] -= dx * 2 51 | POINTS[i][1] -= dy * 2 52 | 53 | 54 | points = {(x, y) for x, y, dx, dy in POINTS} 55 | xs, ys = zip(*points) 56 | min_x, max_x = sorted(xs)[0], sorted(xs)[-1] 57 | min_y, max_y = sorted(ys)[0], sorted(ys)[-1] 58 | 59 | for y in range(min_y, max_y + 1): 60 | print ''.join('#' if (x, y) in points else '.' for x in range(min_x, max_x + 1)) 61 | 62 | print "Seconds until the above message is formed:", seconds - 1 63 | -------------------------------------------------------------------------------- /2018/day11.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | 4 | SIZE = 300 5 | 6 | 7 | def power_level(x, y, serial): 8 | rack_id = x + 10 9 | return (((rack_id * y + serial) * rack_id // 100) % 10) - 5 10 | 11 | 12 | def power_search(partials, conv_size): 13 | best_power = 0 14 | best_x = 0 15 | best_y = 0 16 | 17 | for y in range(1, SIZE - conv_size + 1): 18 | for x in range(1, SIZE - conv_size + 1): 19 | xx = x + conv_size 20 | yy = y + conv_size 21 | power = PARTIALS[yy][xx] + PARTIALS[y][x] - PARTIALS[y][xx] - PARTIALS[yy][x] 22 | 23 | if power > best_power: 24 | best_power = power 25 | best_x = x + 1 26 | best_y = y + 1 27 | 28 | return best_power, best_x, best_y, conv_size, 29 | 30 | 31 | SERIAL = int(fileinput.input()[0]) 32 | PARTIALS = [[0 for _ in range(SIZE + 1)] for _ in range(SIZE + 1)] 33 | 34 | for y in range(1, SIZE + 1): 35 | for x in range(1, SIZE + 1): 36 | PARTIALS[y][x] = power_level(x, y, SERIAL) + PARTIALS[y][x-1] + PARTIALS[y-1][x] - PARTIALS[y-1][x-1] 37 | 38 | print "Coordinate of most powerful 3x3 grid: {1},{2}".format(*power_search(PARTIALS, 3)) 39 | print "Identifier of the largest total power: {1},{2},{3}".format(*max(power_search(PARTIALS, n) for n in range(1, SIZE))) 40 | -------------------------------------------------------------------------------- /2018/day12.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | 4 | INITIAL = None 5 | RULES = {} 6 | GENS = 200 # 200 generations is safe for the automata to converge 7 | TARGET_GEN = 50000000000 8 | 9 | for i, line in enumerate(fileinput.input()): 10 | if i == 0: 11 | INITIAL = line.split()[-1] 12 | elif i >= 2: 13 | a, _, b = line.split() 14 | RULES[a] = b 15 | 16 | 17 | state = ['.'] * GENS + list(INITIAL) + ['.'] * GENS 18 | pot_sums = [] 19 | 20 | for it in range(GENS): 21 | amt = sum(i - GENS for i, p in enumerate(state) if p == '#') 22 | pot_sums.append(amt) 23 | 24 | next_state = ['.'] * len(state) 25 | for i in range(len(state) - 2): 26 | next_state[i + 2] = RULES.get(''.join(state[i:i+5]), '.') 27 | 28 | state = next_state 29 | 30 | print "Sum of plants after 20 generations:", pot_sums[20] 31 | 32 | growth_per_gen = pot_sums[-1] - pot_sums[-2] 33 | remaining_gens = TARGET_GEN - GENS + 1 34 | print "Sum after 50000000000 generations:", growth_per_gen * remaining_gens + pot_sums[-1] 35 | -------------------------------------------------------------------------------- /2018/day14.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from itertools import count 3 | 4 | 5 | def slice_to_str(slice): 6 | return ''.join(str(n) for n in slice) 7 | 8 | 9 | INPUT = int(fileinput.input()[0]) 10 | input_str = str(INPUT) 11 | input_len = len(input_str) 12 | 13 | recipes = [3, 7] 14 | recipes_len = 2 15 | elf_1 = 0 16 | elf_2 = 1 17 | 18 | part_1 = None 19 | part_2 = None 20 | 21 | while part_1 is None or part_2 is None: 22 | score = recipes[elf_1] + recipes[elf_2] 23 | if score >= 10: 24 | recipes.append(score // 10) 25 | recipes.append(score % 10) 26 | recipes_len += 2 27 | else: 28 | recipes.append(score) 29 | recipes_len += 1 30 | 31 | elf_1 = (elf_1 + recipes[elf_1] + 1) % recipes_len 32 | elf_2 = (elf_2 + recipes[elf_2] + 1) % recipes_len 33 | 34 | if part_1 is None and recipes_len > INPUT + 10: 35 | part_1 = slice_to_str(recipes[INPUT:INPUT+10]) 36 | 37 | if part_2 is None: 38 | if score >= 10 and slice_to_str(recipes[-1-input_len:-1]) == input_str: 39 | part_2 = recipes_len - input_len - 1 40 | 41 | if slice_to_str(recipes[-input_len:]) == input_str: 42 | part_2 = recipes_len - input_len 43 | 44 | print "Scores of recipes after puzzle input:", part_1 45 | print "Number of recipes before puzzle input:", part_2 46 | -------------------------------------------------------------------------------- /2018/day18.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import defaultdict 3 | from itertools import count 4 | 5 | from utils import Point 6 | 7 | 8 | GRID = {} 9 | 10 | for y, line in enumerate(fileinput.input()): 11 | for x, t in enumerate(line.strip()): 12 | GRID[Point(x, y)] = t 13 | 14 | DIM = y + 1 15 | 16 | 17 | seen = defaultdict(list) 18 | resources = {} 19 | period = None 20 | last_period = None 21 | 22 | for minute in count(): 23 | tiles = GRID.values() 24 | trees = tiles.count('|') 25 | yards = tiles.count('#') 26 | 27 | seen[trees, yards].append(minute) 28 | resources[minute] = trees, yards 29 | 30 | if minute == 10: 31 | print "Resource value after 10 minutes:", trees * yards 32 | 33 | # Wait until we see periodic behaviour in the resource values observed 34 | if len(seen[trees, yards]) > 3: 35 | poss_period = minute - seen[trees, yards][-2] 36 | if poss_period == last_period: 37 | period = poss_period 38 | last_period = poss_period 39 | else: 40 | last_period = None 41 | 42 | if period is not None: 43 | if (1000000000 - minute) % period == 0: 44 | print "Resoruce value after 1000000000 minutes:", trees * yards 45 | break 46 | 47 | next_grid = {} 48 | 49 | for y in range(DIM): 50 | for x in range(DIM): 51 | p = Point(x, y) 52 | neighs = [GRID.get(q) for q in Point(x, y).neighbours_8()] 53 | 54 | if GRID.get(p) == '.': 55 | next_grid[p] = '|' if neighs.count('|') >= 3 else '.' 56 | elif GRID.get(p) == '|': 57 | next_grid[p] = '#' if neighs.count('#') >= 3 else '|' 58 | else: 59 | next_grid[p] = '#' if neighs.count('#') >= 1 and neighs.count('|') >= 1 else '.' 60 | 61 | GRID = next_grid 62 | -------------------------------------------------------------------------------- /2018/day20.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import defaultdict 3 | from utils import Point, DIRS_4 4 | 5 | 6 | REGEX = fileinput.input()[0].strip().replace('$', '').replace('^', '') 7 | DIRS = {a: b for a, b in zip('NESW', DIRS_4)} 8 | 9 | # Build graph of facility 10 | graph = defaultdict(set) 11 | start = Point(0, 0) 12 | curr = start 13 | stk = [] 14 | 15 | for d in REGEX: 16 | if d in DIRS: 17 | nx = curr + DIRS[d] 18 | graph[curr].add(nx) 19 | graph[nx].add(curr) 20 | curr = nx 21 | elif d == '(': 22 | stk.append(curr) 23 | elif d == '|': 24 | curr = stk[-1] 25 | elif d == ')': 26 | curr = stk.pop() 27 | 28 | seen = set() 29 | horizon = [start] 30 | dist = {} 31 | 32 | # BFS to solve for minimum distances to rooms 33 | depth = 0 34 | while horizon: 35 | next_horizon = [] 36 | 37 | for h in horizon: 38 | if h in seen: 39 | continue 40 | 41 | dist[h] = depth 42 | seen.add(h) 43 | 44 | for g in graph[h]: 45 | next_horizon.append(g) 46 | 47 | depth += 1 48 | horizon = next_horizon 49 | 50 | 51 | print "Largest number of doors passed through:", max(d for d in dist.values()) 52 | print "Rooms passing through at least 1000 doors:", sum(d >= 1000 for d in dist.values()) 53 | -------------------------------------------------------------------------------- /2018/day25.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import parse_nums 3 | 4 | 5 | def dist(x, y): 6 | return sum(abs(a - b) for a, b in zip(x, y)) 7 | 8 | 9 | POINTS = [tuple(parse_nums(line)) for line in fileinput.input()] 10 | 11 | 12 | cliques = [[g] for g in POINTS] 13 | last_len = None 14 | 15 | while True: 16 | if len(cliques) == last_len: 17 | break 18 | 19 | last_len = len(cliques) 20 | 21 | next_cliques = [] 22 | merged = set() 23 | 24 | for i in range(len(cliques)): 25 | next_clique = cliques[i][:] 26 | 27 | if i in merged: 28 | continue 29 | 30 | for j in range(i + 1, len(cliques)): 31 | for dd in cliques[j]: 32 | if (any(dist(cc, dd) <= 3 for cc in cliques[i])): 33 | next_clique.extend(cliques[j]) 34 | merged.add(j) 35 | break 36 | 37 | next_cliques.append(next_clique) 38 | 39 | cliques = next_cliques 40 | 41 | print "Number of constellations:", len(cliques) 42 | -------------------------------------------------------------------------------- /2018/starter.py: -------------------------------------------------------------------------------- 1 | import os # NOQA 2 | import sys # NOQA 3 | import re # NOQA 4 | import math # NOQA 5 | import fileinput 6 | from string import ascii_uppercase, ascii_lowercase # NOQA 7 | from collections import Counter, defaultdict, deque, namedtuple # NOQA 8 | from itertools import count, product, permutations, combinations, combinations_with_replacement # NOQA 9 | 10 | from utils import parse_line, parse_nums, mul, all_unique, factors, memoize, primes # NOQA 11 | from utils import new_table, transposed, rotated # NOQA 12 | from utils import md5, sha256, knot_hash # NOQA 13 | from utils import VOWELS, CONSONANTS # NOQA 14 | from utils import Point, DIRS, DIRS_4, DIRS_8 # NOQA 15 | 16 | # Itertools Functions: 17 | # product('ABCD', repeat=2) AA AB AC AD BA BB BC BD CA CB CC CD DA DB DC DD 18 | # permutations('ABCD', 2) AB AC AD BA BC BD CA CB CD DA DB DC 19 | # combinations('ABCD', 2) AB AC AD BC BD CD 20 | # combinations_with_replacement('ABCD', 2) AA AB AC AD BB BC BD CC CD DD 21 | 22 | total = 0 23 | result = [] 24 | table = new_table(None, width=2, height=4) 25 | 26 | for i, line in enumerate(fileinput.input()): 27 | line = line.strip() 28 | nums = parse_nums(line) 29 | data = parse_line(r'', line) 30 | 31 | if i == 0: 32 | print(data) 33 | -------------------------------------------------------------------------------- /2019/day01.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | def fuel(n): 4 | return (n // 3) - 2 5 | 6 | PART_1 = 0 7 | PART_2 = 0 8 | 9 | for line in fileinput.input(): 10 | n = int(line) 11 | PART_1 += fuel(n) 12 | while fuel(n) > 0: 13 | n = fuel(n) 14 | PART_2 += n 15 | 16 | print "Total fuel for Part 1:", PART_1 17 | print "Total fuel for Part 2:", PART_2 18 | -------------------------------------------------------------------------------- /2019/day02.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | TARGET = 19690720 4 | NOUN_1 = 12 5 | VERB_1 = 2 6 | 7 | TAPE = [int(x) for x in fileinput.input()[0].split(',')] 8 | 9 | for noun in range(100): 10 | for verb in range(100): 11 | tape = TAPE[:] 12 | pc = 0 13 | tape[1] = noun 14 | tape[2] = verb 15 | 16 | while pc < len(tape): 17 | opcode = tape[pc] 18 | if opcode == 1: 19 | tape[tape[pc + 3]] = tape[tape[pc + 1]] + tape[tape[pc + 2]] 20 | pc += 4 21 | elif opcode == 2: 22 | tape[tape[pc + 3]] = tape[tape[pc + 1]] * tape[tape[pc + 2]] 23 | pc += 4 24 | elif opcode == 99: 25 | result = tape[0] 26 | if result == TARGET: 27 | print "100 * noun + verb =", 100 * noun + verb 28 | elif noun == NOUN_1 and verb == VERB_1: 29 | print "Value in position 0 for Part 1:", result 30 | 31 | break 32 | -------------------------------------------------------------------------------- /2019/day03.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | seen = {} 4 | ints = {} 5 | 6 | for wire, line in enumerate(fileinput.input()): 7 | commands = line.split(',') 8 | x = y = 0 9 | steps = 1 10 | 11 | for cmd in commands: 12 | direction = cmd[0] 13 | dist = int(cmd[1:]) 14 | 15 | for _ in range(dist): 16 | if direction == 'U': 17 | y -= 1 18 | elif direction == 'L': 19 | x -= 1 20 | elif direction == 'R': 21 | x += 1 22 | else: 23 | y += 1 24 | 25 | if (x, y) in seen and wire == 1 and (x, y) not in ints: 26 | ints[(x, y)] = steps + seen[(x, y)] 27 | 28 | if wire == 0 and (x, y) not in seen: 29 | seen[x, y] = steps 30 | 31 | steps += 1 32 | 33 | print "Distance to closest intersection:", min(abs(a) + abs(b) for a, b in ints.keys()) 34 | print "Fewest steps to intersection:", min(n for n in ints.values()) 35 | -------------------------------------------------------------------------------- /2019/day04.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | password_range = fileinput.input()[0] 4 | start, end = (int(x) for x in password_range.split('-')) 5 | 6 | def increasing(n): 7 | s = [int(c) for c in str(n)] 8 | if s[0] <= s[1] <= s[2] <= s[3] <= s[4] <= s[5]: 9 | return True 10 | 11 | def part_1_valid(n): 12 | if not increasing(n): 13 | return False 14 | 15 | s = [int(c) for c in str(n)] 16 | if s[0] == s[1] or s[1] == s[2] or s[2] == s[3] or s[3] == s[4] or s[4] == s[5]: 17 | return True 18 | 19 | return False 20 | 21 | def part_2_valid(n): 22 | if not increasing(n): 23 | return False 24 | 25 | s = [int(c) for c in str(n)] 26 | if ((s[0] == s[1] and s[1] != s[2]) or 27 | (s[1] == s[2] and s[1] != s[0] and s[1] != s[3]) or 28 | (s[2] == s[3] and s[2] != s[1] and s[2] != s[4]) or 29 | (s[3] == s[4] and s[3] != s[2] and s[3] != s[5]) or 30 | (s[4] == s[5] and s[4] != s[3])): 31 | return True 32 | 33 | return False 34 | 35 | print "Part 1 passwords:", sum(part_1_valid(n) for n in range(start, end + 1)) 36 | print "Part 2 passwords:", sum(part_2_valid(n) for n in range(start, end + 1)) 37 | -------------------------------------------------------------------------------- /2019/day06.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import defaultdict 3 | 4 | 5 | def bfs(graph, start, end=None): 6 | dist = -1 7 | seen = set() 8 | frontier = [start] 9 | while frontier: 10 | new_frontier = [] 11 | for _ in range(len(frontier)): 12 | p = frontier.pop() 13 | for c in graph[p]: 14 | if c == end: 15 | return dist 16 | if c not in seen: 17 | new_frontier.append(c) 18 | seen.add(c) 19 | 20 | frontier = new_frontier 21 | dist += 1 22 | 23 | return dist 24 | 25 | 26 | orbits = defaultdict(set) 27 | undirected = defaultdict(set) 28 | planets = set() 29 | 30 | for line in fileinput.input(): 31 | a, b = line.strip().split(')') 32 | 33 | orbits[b].add(a) 34 | undirected[b].add(a) 35 | undirected[a].add(b) 36 | planets.add(a) 37 | planets.add(b) 38 | 39 | print "Total orbits:", sum(bfs(orbits, p) for p in planets) 40 | print "Minimum orbital transfers:", bfs(undirected, 'YOU', 'SAN') 41 | -------------------------------------------------------------------------------- /2019/day08.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import Counter 3 | 4 | WIDTH = 25 5 | HEIGHT = 6 6 | IMAGE = fileinput.input()[0].strip() 7 | 8 | layers = [] 9 | layer_size = WIDTH * HEIGHT 10 | num_layers = len(IMAGE) / layer_size 11 | 12 | final_image = [[None for _ in range(WIDTH)] for _ in range(HEIGHT)] 13 | 14 | 15 | for i in range(num_layers): 16 | layer = Counter() 17 | for y in range(HEIGHT): 18 | for x in range(WIDTH): 19 | idx = (i * layer_size) + (y * WIDTH + x) 20 | c = IMAGE[idx] 21 | layer[c] += 1 22 | 23 | if final_image[y][x] is None: 24 | if c != '2': 25 | final_image[y][x] = '#' if c == '1' else ' ' 26 | 27 | layers.append(layer) 28 | 29 | fewest_zeros = min(layers, key=lambda l: l['0']) 30 | print "Image checksum:", fewest_zeros['1'] * fewest_zeros['2'] 31 | print 32 | 33 | for row in final_image: 34 | print ''.join(row) 35 | -------------------------------------------------------------------------------- /2019/day09.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | from intcode import emulate 4 | 5 | # Read input 6 | TAPE = [int(x) for x in fileinput.input()[0].split(',')] 7 | 8 | print "BOOST keycode:", next(emulate(TAPE, [1])) 9 | print "Coordinates of distress signal:", next(emulate(TAPE, [2])) 10 | -------------------------------------------------------------------------------- /2019/day10.py: -------------------------------------------------------------------------------- 1 | import math 2 | import fileinput 3 | from collections import deque, defaultdict 4 | 5 | from utils import Point 6 | 7 | 8 | # Read problem input 9 | asteroids = set() 10 | 11 | for y, line in enumerate(fileinput.input()): 12 | for x, c in enumerate(line.strip()): 13 | if c == '#': 14 | asteroids.add(Point(x, y)) 15 | 16 | 17 | # Part 1 18 | detections = { 19 | a: len(set(a.angle(o) for o in (asteroids - set([a])))) 20 | for a in asteroids 21 | } 22 | 23 | station, num = max(detections.items(), key=lambda x: x[1]) 24 | print "Number of detections:", num 25 | 26 | 27 | # Part 2 28 | asteroids.remove(station) 29 | 30 | # Construct a circular list that is sorted by increasing angle, 31 | # and contains sorted lists of the asteroids at that angle from 32 | # the station (going from furthest to closest). We rotate in the 33 | # positive-radian direction as a hack, since the y-axis coordinates 34 | # actually increase going downwards (as opposed to upwards). 35 | by_angle = defaultdict(list) 36 | for a in asteroids: 37 | by_angle[station.angle(a)].append(a) 38 | 39 | for k, v in by_angle.items(): 40 | v.sort(key=station.dist, reverse=True) 41 | 42 | queue = deque(sorted(by_angle.items())) 43 | 44 | # Rotate the queue to the starting angle 45 | start = math.pi / 2 46 | while abs(queue[0][0] - start) > 1e-6: 47 | queue.rotate(-1) 48 | 49 | # Iterate through the circular list, vaporizing the closest asteroid 50 | # at that angle, then moving to the next angle in the rotation. 51 | for i in range(200): 52 | at_angle = queue[0][1] 53 | vaporized = at_angle.pop() 54 | if not at_angle: 55 | queue.popleft() 56 | else: 57 | queue.rotate(-1) 58 | 59 | print "200th asteroid checksum:", vaporized.x * 100 + vaporized.y 60 | -------------------------------------------------------------------------------- /2019/day11.py: -------------------------------------------------------------------------------- 1 | import time 2 | import fileinput 3 | from collections import defaultdict 4 | 5 | from intcode import emulate 6 | 7 | 8 | def simulate_robot(tape, start=0): 9 | grid = {} 10 | grid[0, 0] = start 11 | 12 | facing = 0 13 | x, y = 0, 0 14 | 15 | inputs = [start] 16 | robot = emulate(tape[:], inputs) 17 | 18 | try: 19 | while True: 20 | grid[x, y] = next(robot) 21 | if next(robot) == 0: 22 | facing = (facing - 1) % 4 23 | else: 24 | facing = (facing + 1) % 4 25 | 26 | if facing == 0: 27 | y += 1 28 | elif facing == 1: 29 | x += 1 30 | elif facing == 2: 31 | y -= 1 32 | elif facing == 3: 33 | x -= 1 34 | 35 | inputs.append(grid.get((x, y), 0)) 36 | except StopIteration: 37 | return grid 38 | 39 | 40 | # Read input 41 | TAPE = [int(x) for x in fileinput.input()[0].split(',')] 42 | 43 | # Part 1 44 | grid = simulate_robot(TAPE, 0) 45 | print "Number of painted tiles:", len(grid.keys()) 46 | 47 | 48 | # Part 2 49 | grid = simulate_robot(TAPE, 1) 50 | 51 | print "Registration ID:\n" 52 | xs, ys = zip(*grid.keys()) 53 | for y in reversed(range(min(ys), max(ys) + 1)): 54 | print ''.join( 55 | '#' if grid.get((x, y)) else ' ' 56 | for x in range(min(xs), max(xs) + 1)) 57 | -------------------------------------------------------------------------------- /2019/day12.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from itertools import count 3 | 4 | from utils import parse_nums 5 | 6 | 7 | def gcd(a, b): 8 | """Compute the greatest common divisor of a and b""" 9 | while b > 0: 10 | a, b = b, a % b 11 | return a 12 | 13 | 14 | def lcm(a, b, *args): 15 | """Compute the lowest common multiple of a and b""" 16 | return a * b / gcd(a, b) 17 | 18 | 19 | positions = [] 20 | initial = [] 21 | velocities = [[0] * 3 for _ in range(4)] 22 | 23 | for i, line in enumerate(fileinput.input()): 24 | line = line.strip() 25 | nums = parse_nums(line) 26 | positions.append(list(nums)) 27 | initial.append(list(nums)) 28 | 29 | CYCLES = [None, None, None] 30 | 31 | for step in count(start=1): 32 | # Update velocities 33 | for x in range(4): 34 | for y in range(x + 1, 4): 35 | for d in range(3): 36 | if positions[x][d] < positions[y][d]: 37 | velocities[x][d] += 1 38 | velocities[y][d] -= 1 39 | elif positions[x][d] > positions[y][d]: 40 | velocities[x][d] -= 1 41 | velocities[y][d] += 1 42 | 43 | # Update positions 44 | for x in range(4): 45 | for d in range(3): 46 | positions[x][d] += velocities[x][d] 47 | 48 | if step == 1000: 49 | energy = 0 50 | 51 | for pos, vel in zip(positions, velocities): 52 | energy += sum(abs(p) for p in pos) * sum(abs(v) for v in vel) 53 | 54 | print "Total energy after 1000 steps:", energy 55 | 56 | for d in range(3): 57 | if CYCLES[d] is not None: 58 | continue 59 | 60 | for m in range(4): 61 | if positions[m][d] != initial[m][d]: 62 | break 63 | if velocities[m][d] != 0: 64 | break 65 | else: 66 | CYCLES[d] = step 67 | 68 | if all(CYCLES): 69 | print "Steps for full cycle:", lcm(lcm(CYCLES[0], CYCLES[1]), CYCLES[2]) 70 | break 71 | -------------------------------------------------------------------------------- /2019/day13.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import Counter 3 | 4 | from intcode import emulate 5 | 6 | 7 | # Read input 8 | TAPE = [int(x) for x in fileinput.input()[0].split(',')] 9 | TAPE[0] = 2 10 | 11 | inputs = [0] 12 | game = emulate(TAPE, inputs) 13 | grid = {} 14 | ball_x = 0 15 | padd_x = 0 16 | score = None 17 | 18 | try: 19 | while True: 20 | x = next(game) 21 | y = next(game) 22 | n = next(game) 23 | 24 | # Score output 25 | if x == -1 and y == 0: 26 | if score is None: 27 | print "Blocks at start of game:", Counter(grid.values())[2] 28 | score = n 29 | else: 30 | grid[x, y] = n 31 | if n == 3: 32 | padd_x = x 33 | elif n == 4: 34 | ball_x = x 35 | 36 | if ball_x < padd_x: 37 | inputs.append(-1) 38 | elif ball_x > padd_x: 39 | inputs.append(1) 40 | else: 41 | inputs.append(0) 42 | 43 | except StopIteration: 44 | print "Score after last block is broken:", score 45 | -------------------------------------------------------------------------------- /2019/day16.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | 4 | PATTERN = [0, 1, 0, -1] 5 | SIGNAL = [int(s) for s in fileinput.input()[0].strip()] 6 | 7 | 8 | def fft(signal): 9 | def coeff(idx, digit): 10 | r = ((idx + 1) // (digit + 1)) % 4 11 | return PATTERN[r] 12 | 13 | for _ in range(100): 14 | for i in range(len(signal)): 15 | res = 0 16 | for j, d in enumerate(signal): 17 | res += coeff(j, i) * d 18 | 19 | signal[i] = abs(res) % 10 20 | 21 | return signal 22 | 23 | 24 | def fft_backhalf(signal): 25 | # What pattern? 26 | for _ in range(100): 27 | partial = 0 28 | for i in reversed(range(len(signal) // 2, len(signal))): 29 | partial += signal[i] 30 | signal[i] = partial % 10 31 | 32 | return signal 33 | 34 | 35 | part_1 = ''.join(str(n) for n in fft(SIGNAL[:]))[:8] 36 | print "First 8 digits of FFT(input):", part_1 37 | 38 | offset = int(''.join(str(n) for n in SIGNAL[:7])) 39 | part_2 = ''.join(str(n) for n in fft_backhalf(SIGNAL * 10000)[offset:offset+8]) 40 | print "Offset digits of FFT(input * 10000):", part_2 41 | -------------------------------------------------------------------------------- /2019/day17.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | from intcode import emulate 4 | from utils import Point 5 | 6 | 7 | # Read problem input 8 | TAPE = [int(x) for x in fileinput.input()[0].split(',')] 9 | 10 | 11 | # Part 1 12 | vm = emulate(TAPE[:], []) 13 | board = [] 14 | 15 | try: 16 | while True: 17 | resp = next(vm) 18 | board.append(chr(resp)) 19 | except StopIteration: 20 | pass 21 | 22 | signal = [a for a in ''.join(board).split()] 23 | 24 | height = len(signal) 25 | width = len(signal[0]) 26 | 27 | grid = {} 28 | 29 | for y in range(height): 30 | for x in range(width): 31 | grid[Point(x, y)] = signal[y][x] 32 | if signal[y][x] == '^': 33 | START = Point(x, y) 34 | 35 | alignment = 0 36 | for p in grid: 37 | if grid.get(p, ' ') != '#': 38 | continue 39 | 40 | for np in p.neighbours_4(): 41 | if grid.get(np, ' ') != '#': 42 | break 43 | else: 44 | alignment += p.x * p.y 45 | 46 | print "Sum of alignment parameters:", alignment 47 | 48 | 49 | # Part 2 50 | main = "A,B,A,C,A,B,C,B,C,B" 51 | fn_a = "L,10,R,8,L,6,R,6" 52 | fn_b = "L,8,L,8,R,8" 53 | fn_c = "R,8,L,6,L,10,L,10" 54 | camera = "n" 55 | 56 | instructions = [] 57 | for ins in (main, fn_a, fn_b, fn_c, camera): 58 | for c in ins: 59 | instructions.append(ord(c)) 60 | instructions.append(ord('\n')) 61 | 62 | TAPE[0] = 2 63 | for c in emulate(TAPE[:], instructions[::-1]): 64 | try: 65 | print chr(c), 66 | except Exception: 67 | print "Dust collected:", c 68 | -------------------------------------------------------------------------------- /2019/day19.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import fileinput 3 | 4 | from utils import memoize 5 | from intcode import emulate 6 | 7 | 8 | @memoize 9 | def affected(x, y): 10 | if x < 0 or y < 0: 11 | return 0 12 | 13 | vm = emulate(TAPE, [y, x]) 14 | return next(vm) 15 | 16 | 17 | # Read problem input 18 | TAPE = [int(x) for x in fileinput.input()[0].split(',')] 19 | 20 | 21 | part_1 = sum(affected(x, y) for y in range(50) for x in range(50)) 22 | print "Points affected by tractor beam:", part_1 23 | 24 | # Arbitrary starting point for my input that skips past the incontinuity 25 | x = 37 26 | y = 49 27 | 28 | while True: 29 | while affected(x, y): 30 | y += 1 31 | 32 | assert affected(x, y) == 0 33 | 34 | while not affected(x, y): 35 | x += 1 36 | 37 | while affected(x, y + 1): 38 | y += 1 39 | 40 | assert affected(x, y) == 1 41 | assert affected(x + 1, y) == 1 42 | assert affected(x - 1, y) == 0 43 | assert affected(x, y + 1) == 0 44 | assert affected(x, y - 1) == 1 45 | 46 | if affected(x, y - 99) == 1 and affected(x + 99, y) == 1: 47 | for yy in range(y - 99, y + 1): 48 | for xx in range(x, x + 100): 49 | if not affected(xx, yy): 50 | break 51 | else: 52 | continue 53 | 54 | break 55 | else: 56 | print "100x100 point checksum:", x * 10000 + (y - 99) 57 | sys.exit() 58 | -------------------------------------------------------------------------------- /2019/day21.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from intcode import emulate 3 | 4 | TAPE = [int(x) for x in fileinput.input()[0].split(',')] 5 | 6 | walking = """\ 7 | OR D J 8 | NOT C T 9 | AND T J 10 | NOT A T 11 | OR T J 12 | WALK 13 | """ 14 | 15 | running = """\ 16 | NOT C T 17 | OR T J 18 | NOT A T 19 | OR T J 20 | NOT B T 21 | OR T J 22 | AND D J 23 | AND H J 24 | NOT A T 25 | OR T J 26 | RUN 27 | """ 28 | 29 | for instructions in (walking, running): 30 | program = [ord(c) for c in instructions] 31 | vm = emulate(TAPE, program[::-1]) 32 | try: 33 | while True: 34 | resp = next(vm) 35 | chr(resp), 36 | except Exception as e: 37 | print "{} hull damage: {}".format(instructions.split()[-1], resp) 38 | -------------------------------------------------------------------------------- /2019/day23.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import fileinput 5 | import threading 6 | from collections import deque 7 | 8 | from intcode import emulate 9 | 10 | 11 | TAPE = [int(x) for x in fileinput.input()[0].split(',')] 12 | 13 | VMS = 50 14 | PACKET_QUEUES = [deque([i]) for i in range(VMS)] 15 | NAT = [None, None] 16 | 17 | 18 | def start_nic(pid): 19 | vm = emulate(TAPE, PACKET_QUEUES[pid]) 20 | 21 | while True: 22 | addr = next(vm) 23 | x = next(vm) 24 | y = next(vm) 25 | 26 | if addr == 255: 27 | NAT[0] = x 28 | NAT[1] = y 29 | else: 30 | PACKET_QUEUES[addr].extendleft([x, y]) 31 | 32 | 33 | def start_nat(): 34 | first_nat = False 35 | seen = set() 36 | 37 | while True: 38 | if NAT[1] is not None: 39 | if not first_nat: 40 | print "First Y value sent to NAT:", NAT[1] 41 | first_nat = True 42 | 43 | if not any(PACKET_QUEUES): 44 | x, y = NAT 45 | if y in seen: 46 | print "Value delivered by NAT twice in a row:", y 47 | os._exit(1) 48 | seen.add(y) 49 | 50 | PACKET_QUEUES[0].extendleft(NAT) 51 | NAT[0] = None 52 | NAT[1] = None 53 | 54 | time.sleep(0.1) 55 | 56 | 57 | threads = [] 58 | 59 | sys.stdout.write("Starting NICs and NAT") 60 | for pid in range(VMS): 61 | t = threading.Thread(target=start_nic, args=(pid,)) 62 | threads.append(t) 63 | t.start() 64 | sys.stdout.write(".") 65 | sys.stdout.flush() 66 | 67 | t = threading.Thread(target=start_nat) 68 | threads.append(t) 69 | t.start() 70 | sys.stdout.write("!\n") 71 | 72 | for t in threads: 73 | t.join() 74 | -------------------------------------------------------------------------------- /2019/day25.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import fileinput 3 | from itertools import combinations 4 | 5 | from intcode import emulate 6 | 7 | TAS = """\ 8 | south 9 | take space law space brochure 10 | south 11 | 12 | take mouse 13 | west 14 | north 15 | north 16 | take wreath 17 | south 18 | south 19 | east 20 | 21 | south 22 | take astrolabe 23 | south 24 | take mug 25 | north 26 | north 27 | 28 | north 29 | west 30 | take sand 31 | north 32 | take manifold 33 | south 34 | west 35 | take monolith 36 | west 37 | """ 38 | 39 | items = ['space law space brochure', 'mouse', 'sand', 'wreath', 'manifold', 'astrolabe', 'mug', 'monolith'] 40 | 41 | for i in range(1, len(items) + 1): 42 | for comb in combinations(items, i): 43 | for item in items: 44 | TAS += "drop {}\n".format(item) 45 | 46 | for c in comb: 47 | TAS += "take {}\n".format(c) 48 | 49 | TAS += 'west\n' 50 | 51 | 52 | TAPE = [int(x) for x in fileinput.input()[0].split(',')] 53 | 54 | vm = emulate(TAPE, [ord(c) for c in TAS][::-1]) 55 | try: 56 | while True: 57 | resp = chr(next(vm)) 58 | sys.stdout.write(resp) 59 | 60 | except Exception as e: 61 | pass 62 | -------------------------------------------------------------------------------- /2019/starter.py: -------------------------------------------------------------------------------- 1 | import os # NOQA 2 | import sys # NOQA 3 | import re # NOQA 4 | import math # NOQA 5 | import fileinput 6 | from string import ascii_uppercase, ascii_lowercase # NOQA 7 | from collections import Counter, defaultdict, deque, namedtuple # NOQA 8 | from itertools import count, product, permutations, combinations, combinations_with_replacement # NOQA 9 | 10 | from utils import parse_line, parse_nums, mul, all_unique, factors, memoize, primes # NOQA 11 | from utils import chunks, gcd, lcm, print_grid, min_max_xy # NOQA 12 | from utils import new_table, transposed, rotated # NOQA 13 | from utils import md5, sha256, knot_hash # NOQA 14 | from utils import VOWELS, CONSONANTS # NOQA 15 | from utils import Point, DIRS, DIRS_4, DIRS_8 # NOQA 16 | 17 | # Itertools Functions: 18 | # product('ABCD', repeat=2) AA AB AC AD BA BB BC BD CA CB CC CD DA DB DC DD 19 | # permutations('ABCD', 2) AB AC AD BA BC BD CA CB CD DA DB DC 20 | # combinations('ABCD', 2) AB AC AD BC BD CD 21 | # combinations_with_replacement('ABCD', 2) AA AB AC AD BB BC BD CC CD DD 22 | 23 | # It's Intcode time again! 24 | # from intcode import emulate 25 | # TAPE = [int(x) for x in fileinput.input()[0].split(',')] 26 | # TAPE += [0] * 100000 27 | # GLOBAL_INPUTS = [0] 28 | # vm = emulate(TAPE, 0, GLOBAL_INPUTS) 29 | # try: 30 | # resp = next(vm) 31 | # except StopIteration: 32 | # pass 33 | 34 | total = 0 35 | result = [] 36 | table = new_table(None, width=2, height=4) 37 | 38 | for i, line in enumerate(fileinput.input()): 39 | line = line.strip() 40 | nums = parse_nums(line) 41 | data = parse_line(r'', line) 42 | 43 | if i == 0: 44 | print(data) 45 | -------------------------------------------------------------------------------- /2020/day01.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | nums = [int(x) for x in fileinput.input()] 4 | 5 | part_1 = None 6 | part_2 = None 7 | 8 | for m in nums: 9 | for n in nums: 10 | for o in nums: 11 | if m + n == 2020: 12 | part_1 = m * n 13 | elif m + n + o == 2020: 14 | part_2 = m * n * o 15 | 16 | print "Part 1:", part_1 17 | print "Part 2:", part_2 18 | -------------------------------------------------------------------------------- /2020/day02.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import parse_line 3 | 4 | part_1 = 0 5 | part_2 = 0 6 | 7 | for line in fileinput.input(): 8 | start, end, letter, pwd = parse_line(r'(\d+)-(\d+) (\w+): (\w+)', line) 9 | 10 | if start <= pwd.count(letter) <= end: 11 | part_1 += 1 12 | 13 | if (pwd[start-1] == letter) ^ (pwd[end-1] == letter): 14 | part_2 += 1 15 | 16 | print "Part 1:", part_1 17 | print "Part 2:", part_2 18 | -------------------------------------------------------------------------------- /2020/day03.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | 4 | grid = [line.strip() for line in fileinput.input()] 5 | 6 | max_y = len(grid) 7 | max_x = len(grid[0]) 8 | 9 | part_1 = 0 10 | part_2 = 1 11 | 12 | for xs, ys in ((1, 1), (3, 1), (5, 1), (7, 1), (1, 2)): 13 | x = 0 14 | y = 0 15 | tot = 0 16 | 17 | while y < max_y: 18 | if grid[y][x] == '#': 19 | tot += 1 20 | 21 | x = (x + xs) % max_x 22 | y += ys 23 | 24 | part_2 *= tot 25 | 26 | if (xs, ys) == (3, 1): 27 | part_1 = tot 28 | 29 | print part_1 30 | print part_2 31 | -------------------------------------------------------------------------------- /2020/day04.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import OrderedDict 3 | 4 | def validate_hgt(hgt): 5 | if hgt.endswith('cm'): 6 | return 150 <= int(hgt[:-2]) <= 193 7 | elif hgt.endswith('in'): 8 | return 59 <= int(hgt[:-2]) <= 76 9 | 10 | return False 11 | 12 | VALIDATION_FNS = { 13 | 'byr': lambda v: 1920 <= int(v) <= 2002, 14 | 'iyr': lambda v: 2010 <= int(v) <= 2020, 15 | 'eyr': lambda v: 2020 <= int(v) <= 2030, 16 | 'hgt': validate_hgt, 17 | 'hcl': lambda v: len(v) == 7 and v[0] == '#' and all(c in 'abcdef1234567890' for c in v[1:]), 18 | 'ecl': lambda v: v in ['amb', 'blu', 'brn', 'gry', 'grn', 'hzl', 'oth'], 19 | 'pid': lambda v: len(v) == 9 and all(c in '1234567890' for c in v), 20 | } 21 | 22 | # Parse problem input 23 | passports = [] 24 | port = OrderedDict() 25 | 26 | for line in fileinput.input(): 27 | line = line.strip() 28 | if not line: 29 | passports.append(port) 30 | port = OrderedDict() 31 | else: 32 | parts = [x.split(':') for x in line.split()] 33 | for a, b in parts: 34 | port[a] = b 35 | 36 | passports.append(port) 37 | 38 | part_1 = 0 39 | part_2 = 0 40 | 41 | for p in passports: 42 | if all(k in p for k in VALIDATION_FNS): 43 | part_1 += 1 44 | 45 | if all(VALIDATION_FNS.get(k, lambda x: True)(v) for k, v in p.items()): 46 | part_2 += 1 47 | 48 | print part_1 49 | print part_2 50 | 51 | -------------------------------------------------------------------------------- /2020/day05.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import Counter 3 | 4 | 5 | seen = set() 6 | max_seat_id = 0 7 | min_row = 1000 8 | max_row = 0 9 | 10 | for line in fileinput.input(): 11 | row = int(line[:7].replace('F', '0').replace('B', '1'), 2) 12 | col = int(line[7:10].replace('L', '0').replace('R', '1'), 2) 13 | 14 | min_row = min(row, min_row) 15 | max_row = max(row, max_row) 16 | max_seat_id = max(row * 8 + col, max_seat_id) 17 | seen.add((row, col)) 18 | 19 | print "Highest seat ID:", max_seat_id 20 | 21 | for row in range(min_row + 1, max_row - 1): 22 | for col in range(8): 23 | if (row, col) not in seen: 24 | print "Your seat ID:", row * 8 + col 25 | -------------------------------------------------------------------------------- /2020/day06.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from string import ascii_lowercase 3 | 4 | data = ''.join([line for line in fileinput.input()]) 5 | groups = [g.split('\n') for g in data.split('\n\n')] 6 | 7 | part_1 = 0 8 | part_2 = 0 9 | 10 | for group in groups: 11 | anyone = set() 12 | everyone = set(ascii_lowercase) 13 | 14 | for person in group: 15 | anyone |= set(person) 16 | everyone &= set(person) 17 | 18 | part_1 += len(anyone) 19 | part_2 += len(everyone) 20 | 21 | print part_1 22 | print part_2 23 | -------------------------------------------------------------------------------- /2020/day07.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import defaultdict 3 | from utils import parse_line 4 | 5 | GOAL = 'shiny gold' 6 | 7 | graph = defaultdict(set) 8 | 9 | for i, line in enumerate(fileinput.input()): 10 | line = line.strip() 11 | color, rest = parse_line(r'(\w+ \w+) bags contain (.+)+', line) 12 | rest = rest.replace('.', '').split(',') 13 | 14 | for r in rest: 15 | if r != 'no other bags': 16 | parts = r.split() 17 | new_color = ' '.join(parts[1:3]) 18 | graph[color].add((new_color, int(parts[0]))) 19 | 20 | 21 | def dfs_1(node): 22 | if node == GOAL: 23 | return True 24 | 25 | return any(dfs_1(n) for n, _ in graph[node]) 26 | 27 | 28 | def dfs_2(node, count=1): 29 | tot = 1 30 | 31 | for nxt, amt in graph[node]: 32 | tot += dfs_2(nxt, amt) 33 | 34 | return count * tot 35 | 36 | part_1 = 0 37 | 38 | keys = graph.keys() 39 | 40 | for bag in keys: 41 | print bag 42 | if dfs_1(bag): 43 | part_1 += 1 44 | print "good" 45 | 46 | print part_1 - 1 47 | 48 | 49 | print dfs_2(GOAL) - 1 50 | -------------------------------------------------------------------------------- /2020/day08.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from copy import deepcopy 3 | from utils import parse_nums 4 | 5 | TAPE = [] 6 | 7 | for line in fileinput.input(): 8 | ins = line.split(' ')[0] 9 | TAPE.append([ins, parse_nums(line)]) 10 | 11 | 12 | def emulate(tape, pc=0, acc=0): 13 | """Returns (acc, pc, loop_detected)""" 14 | seen = set() 15 | while pc < len(tape): 16 | if pc in seen: 17 | return (acc, pc, True) 18 | else: 19 | seen.add(pc) 20 | 21 | ins, ops = tape[pc] 22 | 23 | if ins == 'acc': 24 | acc += ops[0] 25 | pc += 1 26 | elif ins == 'jmp': 27 | pc += ops[0] 28 | else: 29 | pc += 1 30 | 31 | return (acc, pc, False) 32 | 33 | 34 | print "ACC at repeated instruction:", emulate(TAPE)[0] 35 | 36 | for i in range(len(TAPE)): 37 | tape = deepcopy(TAPE) 38 | 39 | if tape[i][0] == 'acc': 40 | continue 41 | else: 42 | if tape[i][0] == 'jmp': 43 | tape[i][0] = 'nop' 44 | else: # ins == 'nop' 45 | tape[i][0] = 'jmp' 46 | 47 | acc, pc, loop_detected = emulate(tape) 48 | if not loop_detected: 49 | print "ACC with modified termination:", acc 50 | -------------------------------------------------------------------------------- /2020/day09.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from itertools import permutations 3 | 4 | SEQ = [int(x) for x in fileinput.input()] 5 | LEN = 25 6 | 7 | for i in range(LEN, len(SEQ)): 8 | for x, y in permutations(SEQ[i-LEN:i], 2): 9 | if x + y == SEQ[i]: 10 | break 11 | else: 12 | INVALID = SEQ[i] 13 | print "Part 1:", INVALID 14 | break 15 | 16 | for n in range(2, len(SEQ)): 17 | tot = 0 18 | for i in range(len(SEQ)-n): 19 | tot = sum(SEQ[i:i+n]) 20 | if tot == INVALID: 21 | print "Part 2:", min(SEQ[i:i+n]) + max(SEQ[i:i+n]) 22 | 23 | -------------------------------------------------------------------------------- /2020/day10.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import Counter 3 | from utils import memoize 4 | 5 | NUMS = [int(x) for x in fileinput.input()] 6 | 7 | NUMS.append(0) 8 | NUMS.append(max(NUMS) + 3) 9 | NUMS.sort() 10 | 11 | deltas = Counter() 12 | curr = 0 13 | 14 | for n in NUMS: 15 | deltas[n - curr] += 1 16 | curr = n 17 | 18 | print "Part 1:", deltas[1] * deltas[3] 19 | 20 | 21 | @memoize 22 | def dp(i): 23 | if i >= len(NUMS) - 1: 24 | return 1 25 | 26 | ways = 0 27 | 28 | if i + 1 < len(NUMS) and NUMS[i + 1] - NUMS[i] <= 3: 29 | ways += dp(i + 1) 30 | 31 | if i + 2 < len(NUMS) and NUMS[i + 2] - NUMS[i] <= 3: 32 | ways += dp(i + 2) 33 | 34 | if i + 3 < len(NUMS) and NUMS[i + 3] - NUMS[i] <= 3: 35 | ways += dp(i + 3) 36 | 37 | return ways 38 | 39 | print "Part 2:", dp(0) 40 | -------------------------------------------------------------------------------- /2020/day12.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import Point, DIRS 3 | 4 | DIR_MAP = 'NESW' 5 | 6 | p1 = Point(0, 0) 7 | p2 = Point(0, 0) 8 | wp = Point(10, 1) 9 | 10 | facing = 1 # start at 1 to match DIRS 11 | 12 | for line in fileinput.input(): 13 | d = line[0] 14 | n = int(line[1:]) 15 | 16 | if d in DIR_MAP: 17 | delta = DIRS[DIR_MAP.index(d)] * n 18 | p1 += delta 19 | wp += delta 20 | 21 | elif d == 'L' or d == 'R': 22 | turns = n // 90 23 | if d == 'L': 24 | turns = 4 - turns 25 | 26 | facing = (facing + turns) % 4 27 | 28 | wp = wp.rotate(turns) 29 | 30 | elif d == 'F': 31 | p1 += DIRS[facing] * n 32 | p2 += wp * n 33 | 34 | print "Part 1:", p1.manhattan 35 | print "Part 2:", p2.manhattan 36 | -------------------------------------------------------------------------------- /2020/day16.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import defaultdict 3 | from utils import parse_nums, mul, transposed, resolve_mapping 4 | 5 | fields = {} 6 | tickets = [] 7 | 8 | for line in fileinput.input(): 9 | nums = parse_nums(line) 10 | if nums: 11 | if len(nums) == 4: 12 | fields[line.split(':')[0]] = [abs(x) for x in nums] 13 | else: 14 | tickets.append(nums) 15 | 16 | valid_tickets = [] 17 | part_1 = 0 18 | 19 | for ticket in tickets[1:]: 20 | for n in ticket: 21 | for a, b, c, d in fields.values(): 22 | if a <= n <= b or c <= n <= d: 23 | break 24 | 25 | else: 26 | part_1 += n 27 | break 28 | else: 29 | valid_tickets.append(ticket) 30 | 31 | print "Part 1:", part_1 32 | 33 | poss = defaultdict(set) 34 | 35 | for i, col in enumerate(transposed(valid_tickets)): 36 | for field, (a, b, c, d) in fields.items(): 37 | if all(a <= n <= b or c <= n <= d for n in col): 38 | poss[field].add(i) 39 | 40 | resolved = resolve_mapping(poss) 41 | 42 | departures = [resolved[field] for field in resolved if 'departure' in field] 43 | print "Part 2:", mul(tickets[0][i] for i in departures) 44 | -------------------------------------------------------------------------------- /2020/day17.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import defaultdict 3 | from itertools import product 4 | 5 | grid_1 = defaultdict(lambda: '.') 6 | grid_2 = defaultdict(lambda: '.') 7 | 8 | for y, line in enumerate(fileinput.input()): 9 | for x, c in enumerate(line.strip()): 10 | grid_1[x, y, 0] = c 11 | grid_2[x, y, 0, 0] = c 12 | 13 | max_dim = max(x, y) 14 | 15 | 16 | for i in range(6): 17 | new_grid_1 = defaultdict(lambda: '.') 18 | new_grid_2 = defaultdict(lambda: '.') 19 | 20 | for x, y, z, w in product(range(-i - 1, max_dim + i + 1), repeat=4): 21 | neighs_1 = 0 22 | neighs_2 = 0 23 | 24 | for dx, dy, dz, dw in product(range(-1, 2), repeat=4): 25 | if dx == 0 and dy == 0 and dz == 0 and dw == 0: 26 | continue 27 | 28 | if grid_1[x + dx, y + dy, z + dz] == '#' and dw == 0: 29 | neighs_1 += 1 30 | if grid_2[x + dx, y + dy, z + dz, w + dw] == '#': 31 | neighs_2 += 1 32 | 33 | 34 | if grid_1[x, y, z] == '#' and (neighs_1 == 2 or neighs_1 == 3): 35 | new_grid_1[x, y, z] = '#' 36 | 37 | elif grid_1[x, y, z] == '.' and neighs_1 == 3: 38 | new_grid_1[x, y, z] = '#' 39 | 40 | if grid_2[x, y, z, w] == '#' and (neighs_2 == 2 or neighs_2 == 3): 41 | new_grid_2[x, y, z, w] = '#' 42 | 43 | elif grid_2[x, y, z, w] == '.' and neighs_2 == 3: 44 | new_grid_2[x, y, z, w] = '#' 45 | 46 | grid_1 = new_grid_1 47 | grid_2 = new_grid_2 48 | 49 | 50 | print "Part 1:", sum(c == '#' for c in grid_1.values()) 51 | print "Part 2:", sum(c == '#' for c in grid_2.values()) 52 | 53 | -------------------------------------------------------------------------------- /2020/day19.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import defaultdict 3 | from utils import parse_nums, memoize 4 | 5 | @memoize 6 | def resolve(r): 7 | if type(rules[r]) == str: 8 | return [rules[r]] 9 | 10 | matches = [] 11 | for subrule in rules[r]: 12 | submatches = [''] 13 | for n in subrule: 14 | new = [] 15 | for m in resolve(n): 16 | for existing in submatches: 17 | new.append(existing + m) 18 | 19 | submatches = new 20 | 21 | matches.extend(submatches) 22 | 23 | return matches 24 | 25 | rules = defaultdict(list) 26 | messages = [] 27 | 28 | for line in fileinput.input(): 29 | line = line.strip() 30 | nums = parse_nums(line) 31 | 32 | if nums: 33 | parts = line.split(": ")[1].split(" | ") 34 | r = nums[0] 35 | for p in parts: 36 | if '"' in p: 37 | rules[r] = p[1] 38 | else: 39 | rules[r].append([int(x) for x in p.split(' ')]) 40 | 41 | elif line: 42 | messages.append(line) 43 | 44 | 45 | pl = len(resolve(42)[0]) 46 | 47 | part_1 = 0 48 | part_2 = 0 49 | 50 | for line in messages: 51 | if line in resolve(0): 52 | part_1 += 1 53 | 54 | orig_line = line 55 | a = 0 56 | b = 0 57 | 58 | while line[:pl] in resolve(42): 59 | line = line[pl:] 60 | a += 1 61 | 62 | while line[:pl] in resolve(31): 63 | line = line[pl:] 64 | b += 1 65 | 66 | if a > b and b > 0 and not line: 67 | print orig_line 68 | part_2 += 1 69 | 70 | print "Part 1:", part_1 71 | print "Part 2:", part_2 72 | -------------------------------------------------------------------------------- /2020/day21.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import resolve_mapping 3 | from collections import Counter, defaultdict, deque, namedtuple # NOQA 4 | 5 | foods = [] 6 | 7 | for y, line in enumerate(fileinput.input()): 8 | line = line.strip() 9 | 10 | ingredients, allergens = line.split(' (') 11 | allergens = allergens.replace(')', '').replace(',', '').split(' ')[1:] 12 | 13 | foods.append((ingredients.split(' '), allergens)) 14 | 15 | ing_to_all = defaultdict(set) 16 | all_to_ing = defaultdict(set) 17 | 18 | for ingredients, allergens in foods: 19 | for i in ingredients: 20 | for a in allergens: 21 | ing_to_all[i].add(a) 22 | all_to_ing[a].add(i) 23 | 24 | candidates = defaultdict(set) 25 | resolved_ing = set() 26 | 27 | for a in all_to_ing: 28 | poss = [] 29 | for ingredients, allergens in foods: 30 | if a in allergens: 31 | poss.append(ingredients) 32 | 33 | if poss: 34 | resolve = set(poss[0]) 35 | for p in poss[1:]: 36 | resolve &= set(p) 37 | 38 | for r in resolve: 39 | candidates[a].add(r) 40 | resolved_ing.add(r) 41 | else: 42 | print "empty poss", a 43 | 44 | good = set(ing_to_all) - resolved_ing 45 | print len(good) 46 | 47 | tot = 0 48 | for ingredients, allergens in foods: 49 | for i in ingredients: 50 | if i in good: 51 | tot += 1 52 | 53 | 54 | print tot 55 | 56 | resolved = resolve_mapping(candidates) 57 | 58 | l = [] 59 | for r in sorted(resolved): 60 | l.append(resolved[r]) 61 | 62 | print ','.join(l) 63 | -------------------------------------------------------------------------------- /2020/day23.py: -------------------------------------------------------------------------------- 1 | NUM_CUPS = 1000000 2 | ITERS = 10000000 3 | 4 | class Cup: 5 | def __init__(self, n): 6 | self.n = n 7 | 8 | def __repr__(self): 9 | return "{} -> ({}) -> {}".format(self.prev.n, self.n, self.next.n) 10 | 11 | labeling = [int(x) for x in raw_input()] 12 | cup_ptrs = {} 13 | 14 | for i in range(1, NUM_CUPS + 1): 15 | cup_ptrs[i] = Cup(i) 16 | 17 | curr_cup = None 18 | prev = None 19 | for n in labeling: 20 | if curr_cup is None: 21 | curr_cup = cup_ptrs[n] 22 | prev = curr_cup 23 | else: 24 | cup_ptrs[n].prev = prev 25 | prev.next = cup_ptrs[n] 26 | prev = cup_ptrs[n] 27 | 28 | for n in range(len(labeling) + 1, NUM_CUPS + 1): 29 | cup_ptrs[n].prev = prev 30 | prev.next = cup_ptrs[n] 31 | prev = cup_ptrs[n] 32 | 33 | curr_cup.prev = prev 34 | prev.next = curr_cup 35 | 36 | for move in range(ITERS): 37 | curr = curr_cup.n 38 | 39 | # Pick up three cups 40 | a = curr_cup.next 41 | b = a.next 42 | c = b.next 43 | 44 | # Cup spacing adjusted 45 | a.prev.next = c.next 46 | c.next.prev = a.prev 47 | 48 | dval = curr - 1 49 | if dval == 0: 50 | dval = NUM_CUPS 51 | while dval == a.n or dval == b.n or dval == c.n: 52 | dval -= 1 53 | if dval == 0: 54 | dval = NUM_CUPS 55 | 56 | dest_cup = cup_ptrs[dval] 57 | tmp_cup = dest_cup.next 58 | 59 | dest_cup.next = a 60 | a.prev = dest_cup 61 | 62 | c.next = tmp_cup 63 | tmp_cup.prev = c 64 | 65 | # Move current cup to the next cup. 66 | curr_cup = curr_cup.next 67 | 68 | print cup_ptrs[1].next.n * cup_ptrs[1].next.next.n 69 | -------------------------------------------------------------------------------- /2020/day24.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import defaultdict 3 | 4 | HEX_DIRS = { 5 | 'e': (1, -1, 0), 6 | 'ne': (1, 0, -1), 7 | 'se': (0, -1, 1), 8 | 'w': (-1, 1, 0), 9 | 'sw': (-1, 0, 1), 10 | 'nw': (0, 1, -1), 11 | } 12 | 13 | def hex_neighbours(x, y, z): 14 | for dx, dy, dz in HEX_DIRS.values(): 15 | yield x + dx, y + dy, z + dz 16 | 17 | 18 | TILES = defaultdict(bool) 19 | 20 | for line in fileinput.input(): 21 | x = 0 22 | y = 0 23 | z = 0 24 | curr = '' 25 | 26 | for c in line: 27 | curr += c 28 | if curr in HEX_DIRS: 29 | dx, dy, dz = HEX_DIRS[curr] 30 | x += dx 31 | y += dy 32 | z += dz 33 | curr = '' 34 | 35 | TILES[x, y, z] = not TILES[x, y, z] 36 | 37 | print "Part 1:", sum(TILES.values()) 38 | 39 | for day in range(1, 100 + 1): 40 | new_tiles = defaultdict(bool) 41 | 42 | for old_pos in TILES.keys(): 43 | for pos in hex_neighbours(*old_pos): 44 | neighs = 0 45 | for n in hex_neighbours(*pos): 46 | if TILES[n]: 47 | neighs += 1 48 | 49 | if TILES[pos] and (neighs == 0 or neighs > 2): 50 | pass 51 | elif not TILES[pos] and neighs == 2: 52 | new_tiles[pos] = True 53 | else: 54 | if TILES[pos]: 55 | new_tiles[pos] = True 56 | 57 | TILES = new_tiles 58 | 59 | print "Part 2:", sum(TILES.values()) 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /2020/day25.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import fileinput 3 | 4 | 5 | MOD = 20201227 6 | KEYS = [int(n) for n in fileinput.input()] 7 | 8 | card_pub = KEYS[0] 9 | door_pub = KEYS[1] 10 | 11 | 12 | for loop_size in range(1, MOD + 1): 13 | val = pow(7, loop_size, MOD) 14 | if val == card_pub: 15 | print pow(door_pub, loop_size, MOD) 16 | break 17 | elif val == door_pub: 18 | print pow(card_pub, loop_size, MOD) 19 | break 20 | -------------------------------------------------------------------------------- /2020/search.py: -------------------------------------------------------------------------------- 1 | def bfs(start, graph): 2 | from collections import deque 3 | 4 | max_depth = 0 5 | depths = {} 6 | horizon = deque([(start, 0)]) # node, depth 7 | seen = {start: None} 8 | 9 | # TODO 10 | def is_goal(node): 11 | return graph.get(node) == 'G' 12 | 13 | def gen_neighbours(node): 14 | for n in node.neighbours_4(): 15 | if graph.get(n, ' ') != ' ': 16 | yield n 17 | 18 | 19 | while horizon: 20 | node, depth = horizon.popleft() # pop() for DFS 21 | depths[node] = depth 22 | 23 | if depth > max_depth: 24 | max_depth = depth 25 | print "BFS @ {}; depth={}, horizon={}, seen={}".format(node, depth, len(horizon), len(seen)) 26 | 27 | for new in gen_neighbours(node): 28 | if new in seen: 29 | continue 30 | 31 | seen[new] = node 32 | horizon.append((new, depth + 1)) 33 | 34 | if is_goal(new): 35 | print "FOUND GOAL", new, depth + 1 36 | path = [] 37 | curr = new 38 | while curr is not None: 39 | path.append(curr) 40 | curr = seen[curr] 41 | for p in reversed(path): 42 | print p 43 | pass 44 | print "FOUND GOAL", new, depth + 1 45 | return depth + 1 46 | 47 | print "FLOOD FILL COMPLETE, max_depth={}, seen={}".format(max_depth, len(seen)) 48 | return depths 49 | 50 | start = Point(0, 0) 51 | bfs(start, board) 52 | -------------------------------------------------------------------------------- /2020/starter.py: -------------------------------------------------------------------------------- 1 | import os # NOQA 2 | import sys # NOQA 3 | import re # NOQA 4 | import math # NOQA 5 | import copy # NOQA 6 | import fileinput 7 | from string import ascii_uppercase, ascii_lowercase # NOQA 8 | from collections import Counter, defaultdict, deque, namedtuple # NOQA 9 | from itertools import count, product, permutations, combinations, combinations_with_replacement # NOQA 10 | 11 | from utils import parse_line, parse_nums, mul, all_unique, factors, memoize, primes, resolve_mapping # NOQA 12 | from utils import chunks, gcd, lcm, print_grid, min_max_xy # NOQA 13 | from utils import new_table, transposed, rotated # NOQA 14 | from utils import md5, sha256, knot_hash # NOQA 15 | from utils import VOWELS, CONSONANTS # NOQA 16 | from utils import Point, DIRS, DIRS_4, DIRS_8 # NOQA # N (0, 1) -> E (1, 0) -> S (0, -1) -> W (-1, 0) 17 | 18 | # Itertools Functions: 19 | # product('ABCD', repeat=2) AA AB AC AD BA BB BC BD CA CB CC CD DA DB DC DD 20 | # permutations('ABCD', 2) AB AC AD BA BC BD CA CB CD DA DB DC 21 | # combinations('ABCD', 2) AB AC AD BC BD CD 22 | # combinations_with_replacement('ABCD', 2) AA AB AC AD BB BC BD CC CD DD 23 | 24 | tot = 0 25 | res = [] 26 | board = {} 27 | table = new_table(None, width=2, height=4) 28 | 29 | # Uncomment for multi-group style inputs. :c 30 | # data = ''.join([line for line in fileinput.input()]) 31 | # groups = [g.split('\n') for g in data.split('\n\n')] 32 | 33 | for y, line in enumerate(fileinput.input()): 34 | line = line.strip() 35 | nums = parse_nums(line) 36 | data = parse_line(r'', line) 37 | 38 | for x, c in enumerate(line): 39 | board[Point(x, y)] = c 40 | 41 | if y == 0: 42 | print(data) 43 | 44 | -------------------------------------------------------------------------------- /2021/day01.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | 4 | readings = [int(line) for line in fileinput.input()] 5 | 6 | part_1 = 0 7 | part_2 = 0 8 | 9 | 10 | for i in range(1, len(readings)): 11 | if readings[i] > readings[i-1]: 12 | part_1 += 1 13 | 14 | for i in range(3, len(readings)): 15 | if readings[i] > readings[i-3]: 16 | part_2 += 1 17 | 18 | 19 | print "Part 1:", part_1 20 | print "Part 2:", part_2 21 | -------------------------------------------------------------------------------- /2021/day02.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | pos = 0 4 | aim = 0 5 | part_1_depth = 0 6 | part_2_depth = 0 7 | 8 | for line in fileinput.input(): 9 | ins, num = line.split() 10 | num = int(num) 11 | 12 | if ins == 'forward': 13 | pos += num 14 | part_2_depth += (aim * num) 15 | elif ins == 'down': 16 | part_1_depth += num 17 | aim += num 18 | elif ins == "up": 19 | part_1_depth -= num 20 | aim -= num 21 | 22 | print "Part 1:", pos * part_1_depth 23 | print "Part 2:", pos * part_2_depth 24 | -------------------------------------------------------------------------------- /2021/day03.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import Counter 3 | 4 | 5 | report = [line.strip() for line in fileinput.input()] 6 | 7 | 8 | # Part 1 9 | gamma = '' 10 | epsilon = '' 11 | for i in range(len(report[0])): 12 | c = Counter(r[i] for r in report) 13 | gamma += c.most_common()[0][0] 14 | epsilon += c.most_common()[1][0] 15 | 16 | gamma = int(gamma, 2) 17 | epsilon = int(epsilon, 2) 18 | 19 | print "Part 1:", gamma * epsilon 20 | 21 | 22 | # Part 2 23 | def part_2(report, idx, oxygen=True): 24 | # Figure out what the most/least common bit is. 25 | c = Counter(r[idx] for r in report) 26 | keep = '1' if oxygen else '0' 27 | comm = c.most_common() 28 | 29 | if comm[0][1] != comm[1][1]: 30 | if oxygen: 31 | keep = comm[0][0] 32 | else: 33 | keep = comm[1][0] 34 | 35 | valid = [r for r in report if r[idx] == keep] 36 | 37 | if len(valid) == 1: 38 | return valid[0] 39 | 40 | return part_2(valid, idx + 1, oxygen=oxygen) 41 | 42 | 43 | o2 = int(part_2(report, 0, oxygen=True), 2) 44 | co2 = int(part_2(report, 0, oxygen=False), 2) 45 | 46 | print "Part 2:", o2 * co2 47 | -------------------------------------------------------------------------------- /2021/day04.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import OrderedDict 3 | 4 | 5 | # Read input 6 | data = ''.join([line for line in fileinput.input()]) 7 | groups = [g.split('\n') for g in data.split('\n\n')] 8 | 9 | order = groups[0] 10 | order = [int(x) for x in order[0].split(',')] 11 | 12 | boards = groups[1:] 13 | 14 | 15 | BOARDS = [] 16 | for b in boards: 17 | board = [[int(x) for x in line.split()] for line in b] 18 | BOARDS.append(board) 19 | 20 | MARKS = [[[False for _ in range(5)] for _ in range(5)] for _ in range(len(BOARDS))] 21 | 22 | 23 | def is_win(marks): 24 | for line in marks: 25 | if all(line): 26 | return True 27 | 28 | for line in zip(*marks): 29 | if all(line): 30 | return True 31 | 32 | return False 33 | 34 | 35 | scores = OrderedDict() 36 | 37 | for n in order: 38 | for board, marks in zip(BOARDS, MARKS): 39 | for y, line in enumerate(board): 40 | for x, val in enumerate(line): 41 | if n == val: 42 | marks[y][x] = True 43 | 44 | # Check for winners 45 | for i, b in enumerate(MARKS): 46 | if is_win(b) and i not in scores: 47 | unmarked = 0 48 | for y, line in enumerate(MARKS[i]): 49 | for x, val in enumerate(line): 50 | if not val: 51 | unmarked += BOARDS[i][y][x] 52 | 53 | scores[i] = unmarked * n 54 | 55 | print "Part 1:", scores.values()[0] 56 | print "Part 2:", scores.values()[-1] 57 | -------------------------------------------------------------------------------- /2021/day05.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import Counter 3 | from utils import parse_nums 4 | 5 | lines = [] 6 | 7 | for line in fileinput.input(): 8 | nums = parse_nums(line) 9 | lines.append(nums) 10 | 11 | part_1 = Counter() 12 | part_2 = Counter() 13 | 14 | for (x1, y1, x2, y2) in lines: 15 | # Horizontal or vertical 16 | if x1 == x2 or y1 == y2: 17 | if x1 > x2: 18 | x1, x2 = x2, x1 19 | 20 | if y1 > y2: 21 | y1, y2 = y2, y1 22 | 23 | for y in range(y1, y2 + 1): 24 | for x in range(x1, x2 + 1): 25 | part_1[x, y] += 1 26 | part_2[x, y] += 1 27 | 28 | # Diagonal 29 | else: 30 | delta = abs(x2 - x1) 31 | x, y = x1, y1 32 | for n in range(delta + 1): 33 | part_2[x, y] += 1 34 | if x2 > x1: 35 | x += 1 36 | else: 37 | x -= 1 38 | 39 | if y2 > y1: 40 | y += 1 41 | else: 42 | y -= 1 43 | 44 | print "Part 1:", sum(1 for v in part_1.values() if v > 1) 45 | print "Part 2:", sum(1 for v in part_2.values() if v > 1) 46 | -------------------------------------------------------------------------------- /2021/day06.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import Counter 3 | 4 | lanternfish = [int(x) for x in fileinput.input()[0].split(',')] 5 | 6 | state = Counter() 7 | 8 | for n in lanternfish: 9 | state[n] += 1 10 | 11 | for i in range(1, 256 + 1): 12 | next_state = Counter() 13 | for age, count in state.items(): 14 | if age == 0: 15 | next_state[6] += count 16 | next_state[8] += count 17 | else: 18 | next_state[age - 1] += count 19 | 20 | state = next_state 21 | 22 | if i == 80: 23 | print "Part 1:", sum(state.values()) 24 | 25 | print "Part 2:", sum(state.values()) 26 | 27 | -------------------------------------------------------------------------------- /2021/day07.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | 4 | CRABS = [int(x) for x in fileinput.input()[0].split(',')] 5 | 6 | part_1 = part_2 = 10000000000000 7 | 8 | for pos in range(max(CRABS) + 1): 9 | part_1_fuel = 0 10 | part_2_fuel = 0 11 | 12 | for c in CRABS: 13 | part_1_fuel += abs(pos - c) 14 | 15 | delta = abs(pos - c) 16 | part_2_fuel += ((delta + 1) * delta) // 2 17 | 18 | part_1 = min(part_1, part_1_fuel) 19 | part_2 = min(part_2, part_2_fuel) 20 | 21 | print "Part 1:", part_1 22 | print "Part 2:", part_2 23 | -------------------------------------------------------------------------------- /2021/day09.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import Counter, defaultdict, deque, namedtuple # NOQA 3 | from utils import Point, mul 4 | 5 | # Read in problem input. 6 | WIDTH = HEIGHT = 0 7 | BOARD = {} 8 | 9 | for y, line in enumerate(fileinput.input()): 10 | line = line.strip() 11 | for x, c in enumerate(line): 12 | BOARD[Point(x, y)] = int(c) 13 | WIDTH = x + 1 14 | 15 | HEIGHT = y + 1 16 | 17 | 18 | LOW_POINTS = set() 19 | part_1 = 0 20 | 21 | # Solve part 1. 22 | for y in range(HEIGHT): 23 | for x in range(WIDTH): 24 | p = Point(x, y) 25 | val = BOARD[p] 26 | 27 | for n in p.neighbours_4(): 28 | if n in BOARD: 29 | if BOARD[n] <= val: 30 | break 31 | else: 32 | LOW_POINTS.add(p) 33 | part_1 += (val + 1) 34 | 35 | print "Part 1:", part_1 36 | 37 | 38 | # Solve part 2. 39 | def compute_basin_size(board, start): 40 | """Return the set of all explored points from a given point.""" 41 | seen = set() 42 | 43 | def _dfs(node): 44 | val = board[node] 45 | seen.add(node) 46 | for n in node.neighbours_4(): 47 | if n in board and n not in seen: 48 | if val <= board[n] and board[n] != 9: 49 | _dfs(n) 50 | 51 | _dfs(start) 52 | return len(seen) 53 | 54 | BASIN_SIZES = {} 55 | for p in LOW_POINTS: 56 | BASIN_SIZES[p] = compute_basin_size(BOARD, p) 57 | 58 | print "Part 2:", mul(sorted(BASIN_SIZES.values(), reverse=True)[:3]) 59 | 60 | -------------------------------------------------------------------------------- /2021/day10.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | RIGHT_TO_LEFT = { 4 | '}': '{', 5 | ']': '[', 6 | ')': '(', 7 | '>': '<', 8 | } 9 | 10 | LEFT_TO_RIGHT = {v: k for k, v in RIGHT_TO_LEFT.items()} 11 | 12 | ILLEGAL_POINTS = { 13 | ')': 3, 14 | ']': 57, 15 | '}': 1197, 16 | '>': 25137, 17 | } 18 | 19 | PART_2_POINTS = { 20 | ')': 1, 21 | ']': 2, 22 | '}': 3, 23 | '>': 4, 24 | } 25 | 26 | part_1 = 0 27 | part_2_scores = [] 28 | 29 | for line in fileinput.input(): 30 | stk = [] 31 | is_incomplete = True 32 | 33 | for c in line.strip(): 34 | if c in RIGHT_TO_LEFT.values(): 35 | stk.append(c) 36 | else: 37 | p = stk.pop() 38 | if RIGHT_TO_LEFT[c] != p: 39 | is_incomplete = False 40 | part_1 += ILLEGAL_POINTS[c] 41 | 42 | if is_incomplete: 43 | score = 0 44 | for s in stk[::-1]: 45 | score *= 5 46 | score += PART_2_POINTS[LEFT_TO_RIGHT[s]] 47 | 48 | part_2_scores.append(score) 49 | 50 | print "Part 1:", part_1 51 | print "Part 2:", sorted(part_2_scores)[len(part_2_scores)//2] 52 | -------------------------------------------------------------------------------- /2021/day11.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import fileinput 3 | from utils import Point 4 | 5 | board = {} 6 | for y, line in enumerate(fileinput.input()): 7 | for x, c in enumerate(line.strip()): 8 | board[Point(x, y)] = int(c) 9 | 10 | 11 | part_1 = 0 12 | 13 | for i in range(10000000): 14 | next_state = copy.copy(board) 15 | 16 | for pos, val in next_state.items(): 17 | next_state[pos] = val + 1 18 | 19 | flashed = set() 20 | while True: # when steady state 21 | updated = False 22 | 23 | for pos, val in next_state.items(): 24 | # Flash! 25 | if next_state[pos] > 9 and pos not in flashed: 26 | # Increment all 8 neighbours 27 | for n in pos.neighbours_8(): 28 | if n not in next_state: 29 | continue 30 | 31 | next_state[n] = next_state[n] + 1 32 | 33 | flashed.add(pos) 34 | part_1 += 1 35 | updated = True 36 | 37 | if not updated: 38 | break 39 | 40 | if len(flashed) == len(next_state): 41 | print "Part 2:", i + 1 42 | break 43 | 44 | # Reset all >9s to 0 45 | for pos, val in next_state.items(): 46 | if val > 9: 47 | next_state[pos] = 0 48 | 49 | board = next_state 50 | 51 | if i == 99: 52 | print "Part 1:", part_1 53 | -------------------------------------------------------------------------------- /2021/day12.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import fileinput 3 | from collections import Counter, defaultdict 4 | 5 | 6 | graph = defaultdict(set) 7 | smalls = set() 8 | 9 | for line in fileinput.input(): 10 | a, b = line.strip().split('-') 11 | 12 | graph[a].add(b) 13 | graph[b].add(a) 14 | 15 | 16 | def dfs(start, extra_time=False): 17 | num_paths = [0] 18 | def _dfs(node, seen=None, doubled=False): 19 | if seen is None: 20 | seen = Counter({'start': 1 if extra_time else 0}) 21 | 22 | if node == 'end': 23 | num_paths[0] += 1 24 | return 25 | 26 | # If small cave, check if part 1/2 and skip accordingly. 27 | if node == node.lower(): 28 | if (doubled or not extra_time) and seen[node] >= 1: 29 | return 30 | elif not doubled and seen[node] >= 2: 31 | return 32 | 33 | seen[node] += 1 34 | if node == node.lower() and node not in ('start', 'end') and seen[node] >= 2: 35 | doubled = True 36 | 37 | for n in graph[node]: 38 | _dfs(n, copy.deepcopy(seen), doubled=doubled) 39 | 40 | _dfs(start) 41 | return num_paths[0] 42 | 43 | 44 | print "Part 1:", dfs('start') 45 | print "Part 2:", dfs('start', extra_time=True) 46 | -------------------------------------------------------------------------------- /2021/day13.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import print_grid, Point 3 | 4 | 5 | DOTS = {} 6 | FOLDS = [] 7 | 8 | 9 | # Read problem input 10 | data = ''.join([line for line in fileinput.input()]) 11 | groups = [g.split('\n') for g in data.split('\n\n')] 12 | 13 | for line in groups[0]: 14 | x, y = [int(x) for x in line.split(',')] 15 | DOTS[Point(x, y)] = '#' 16 | 17 | for line in groups[1]: 18 | axis, n = line.split()[2].split('=') 19 | FOLDS.append((axis, int(n))) 20 | 21 | for i, (axis, n) in enumerate(FOLDS): 22 | new_dots = {} 23 | 24 | for p in DOTS: 25 | if axis == 'x': 26 | if p.x < n: 27 | new_dots[p] = '#' 28 | else: 29 | dx = abs(n - p.x) 30 | nx = n - dx 31 | new_dots[Point(nx, p.y)] = '#' 32 | else: 33 | if p.y < n: 34 | new_dots[p] = '#' 35 | else: 36 | dy = abs(n - p.y) 37 | ny = n - dy 38 | new_dots[Point(p.x, ny)] = '#' 39 | 40 | DOTS = new_dots 41 | 42 | if i == 0: 43 | print "Part 1:", len(DOTS) 44 | 45 | print "Part 2:\n", '\n'.join(print_grid(DOTS, quiet=True)) 46 | -------------------------------------------------------------------------------- /2021/day14.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import Counter, defaultdict, deque, namedtuple # NOQA 3 | 4 | # Read problem input. 5 | data = ''.join([line for line in fileinput.input()]) 6 | groups = [g.split('\n') for g in data.split('\n\n')] 7 | 8 | 9 | POLYMER = groups[0][0] 10 | MAPPING = {} 11 | 12 | for line in groups[1]: 13 | a, b = line.split(' -> ') 14 | MAPPING[a] = b 15 | 16 | 17 | # COUNTS maps bigrams to their counts in the underlying polymer. 18 | COUNTS = Counter() 19 | 20 | 21 | for i in range(len(POLYMER)-1): 22 | COUNTS[POLYMER[i:i+2]] += 1 23 | 24 | 25 | for iteration in range(1, 40 + 1): 26 | new_counts = Counter() 27 | 28 | for bigram, num in COUNTS.items(): 29 | insertion = MAPPING[bigram] 30 | 31 | new_counts[bigram[0] + insertion] += num 32 | new_counts[insertion + bigram[1]] += num 33 | 34 | COUNTS = new_counts 35 | 36 | if iteration in (10, 40): 37 | # Compute counts of individual letters from bigram counts. 38 | letter_counts = Counter() 39 | 40 | for bigram, num in COUNTS.items(): 41 | letter_counts[bigram[0]] += num 42 | letter_counts[bigram[1]] += num 43 | 44 | frequencies = zip(*letter_counts.most_common())[1] 45 | most = (frequencies[0] + 1) // 2 46 | least = (frequencies[-1] + 1) // 2 47 | print "Part 1:" if iteration == 10 else "Part 2:", most - least 48 | -------------------------------------------------------------------------------- /2021/day15.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | import heapq 3 | from utils import Point 4 | 5 | 6 | def shortest_path(graph, start, end): 7 | """Returns the shortest path from start to end in graph.""" 8 | horizon = [(0, start)] 9 | seen = set() 10 | 11 | while horizon: 12 | depth, node = heapq.heappop(horizon) 13 | 14 | if node == end: 15 | return depth 16 | elif node in seen: 17 | continue 18 | 19 | seen.add(node) 20 | 21 | for neigh in node.neighbours_4(): 22 | if neigh not in graph: 23 | continue 24 | 25 | heapq.heappush(horizon, (depth + graph[neigh], neigh)) 26 | 27 | return -1 28 | 29 | 30 | CAVE = {} 31 | 32 | # Read problem input and solve part 1. 33 | for y, line in enumerate(fileinput.input()): 34 | for x, c in enumerate(line.strip()): 35 | CAVE[Point(x, y)] = int(c) 36 | WIDTH = x + 1 37 | 38 | HEIGHT = y + 1 39 | 40 | print "Part 1:", shortest_path(CAVE, Point(0, 0), Point(x, y)) 41 | 42 | 43 | # Replicate CAVE tiles to solve part 2. 44 | for y in range(HEIGHT*5): 45 | for x in range(WIDTH*5): 46 | if Point(x, y) not in CAVE: 47 | dx = x // WIDTH 48 | dy = y // HEIGHT 49 | 50 | # Need to subtract one, take modulo, then add one because we 51 | # are dealing with the range 1-9, not 0-8. 52 | val = (CAVE[Point(x % WIDTH, y % HEIGHT)] + (dx + dy)) 53 | CAVE[Point(x, y)] = ((val - 1) % 9) + 1 54 | 55 | print "Part 2:", shortest_path(CAVE, Point(0, 0), Point(x, y)) 56 | -------------------------------------------------------------------------------- /2021/day17.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import parse_nums 3 | 4 | 5 | def solve(vel_x=0, vel_y=0): 6 | x = 0 7 | y = 0 8 | max_y = 0 9 | 10 | while x <= END_X and y >= END_Y - 30: 11 | x += vel_x 12 | y += vel_y 13 | max_y = max(max_y, y) 14 | 15 | if vel_x > 0: 16 | vel_x -= 1 17 | elif vel_x < 0: 18 | vel_x += 1 19 | 20 | vel_y -= 1 21 | 22 | # We are in the target zone. 23 | if START_X <= x <= END_X and START_Y <= y <= END_Y: 24 | return max_y 25 | 26 | 27 | # Parse problem input. 28 | START_X, END_X, START_Y, END_Y = parse_nums(fileinput.input()[0]) 29 | 30 | # Compute viable start velocities. 31 | viables = [solve(x, y) for y in range(START_Y, -START_Y + 1) for x in range(END_X + 1)] 32 | viables = [v for v in viables if v is not None] 33 | 34 | print "Part 1:", max(viables) 35 | print "Part 2:", sum(1 for v in viables) 36 | -------------------------------------------------------------------------------- /2021/starter.py: -------------------------------------------------------------------------------- 1 | import os # NOQA 2 | import sys # NOQA 3 | import re # NOQA 4 | import math # NOQA 5 | import copy # NOQA 6 | import fileinput 7 | from string import ascii_uppercase, ascii_lowercase # NOQA 8 | from collections import Counter, defaultdict, deque, namedtuple # NOQA 9 | from itertools import count, product, permutations, combinations, combinations_with_replacement # NOQA 10 | 11 | from utils import parse_line, parse_nums, mul, all_unique, factors, memoize, primes, resolve_mapping # NOQA 12 | from utils import chunks, gcd, lcm, print_grid, min_max_xy # NOQA 13 | from utils import new_table, transposed, rotated # NOQA 14 | from utils import md5, sha256, knot_hash # NOQA 15 | from utils import VOWELS, CONSONANTS # NOQA 16 | from utils import Point, DIRS, DIRS_4, DIRS_8 # NOQA # N (0, 1) -> E (1, 0) -> S (0, -1) -> W (-1, 0) 17 | 18 | # Itertools Functions: 19 | # product('ABCD', repeat=2) AA AB AC AD BA BB BC BD CA CB CC CD DA DB DC DD 20 | # permutations('ABCD', 2) AB AC AD BA BC BD CA CB CD DA DB DC 21 | # combinations('ABCD', 2) AB AC AD BC BD CD 22 | # combinations_with_replacement('ABCD', 2) AA AB AC AD BB BC BD CC CD DD 23 | 24 | tot = 0 25 | res = [] 26 | board = {} 27 | table = new_table(None, width=2, height=4) 28 | 29 | # # Uncomment for multi-group style inputs. :c 30 | # data = ''.join([line for line in fileinput.input()]) 31 | # groups = [g.split('\n') for g in data.split('\n\n')] 32 | 33 | for y, line in enumerate(fileinput.input()): 34 | line = line.strip() 35 | nums = parse_nums(line) 36 | data = parse_line(r'', line) 37 | 38 | for x, c in enumerate(line): 39 | board[Point(x, y)] = c 40 | 41 | if y == 0: 42 | print(data) 43 | 44 | -------------------------------------------------------------------------------- /2022/day01.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | elves = [] 4 | elf = 0 5 | for line in fileinput.input(): 6 | if line.strip(): 7 | elf += int(line) 8 | else: 9 | # New line -> new elf. 10 | elves.append(elf) 11 | elf = 0 12 | 13 | elves.append(elf) 14 | 15 | print("Part 1:", max(elves)) 16 | print("Part 2:", sum(sorted(elves)[-3:])) 17 | -------------------------------------------------------------------------------- /2022/day02.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | wins = { 4 | 'A': 'Y', 5 | 'B': 'Z', 6 | 'C': 'X', 7 | } 8 | 9 | losses = { 10 | 'B': 'X', 11 | 'C': 'Y', 12 | 'A': 'Z', 13 | } 14 | 15 | draw = { 16 | 'A': 'X', 17 | 'B': 'Y', 18 | 'C': 'Z', 19 | } 20 | 21 | score = { 22 | 'X': 1, 23 | 'Y': 2, 24 | 'Z': 3, 25 | } 26 | 27 | INPUT = [line.strip() for line in fileinput.input()] 28 | 29 | part_1 = 0 30 | for line in INPUT: 31 | op, us = line.split(' ') 32 | 33 | part_1 += score[us] 34 | if wins[op] == us: 35 | part_1 += 6 36 | elif losses[op] == us: 37 | part_1 += 0 38 | else: 39 | part_1 += 3 40 | 41 | print("Part 1:", part_1) 42 | 43 | part_2 = 0 44 | for line in INPUT: 45 | op, outcome = line.split(' ') 46 | if outcome == 'X': 47 | us = losses[op] 48 | part_2 += score[us] 49 | elif outcome == 'Y': 50 | us = draw[op] 51 | part_2 += score[us] + 3 52 | else: 53 | us = wins[op] 54 | part_2 += score[us] + 6 55 | 56 | print("Part 2:", part_2) 57 | 58 | -------------------------------------------------------------------------------- /2022/day03.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import chunks, parts 3 | 4 | INPUT = [line.strip() for line in fileinput.input()] 5 | 6 | # Part 1 7 | part_1 = 0 8 | for line in INPUT: 9 | n = len(line) 10 | fst, snd = parts(line, 2) 11 | 12 | common = set(fst) & set(snd) 13 | 14 | for c in common: 15 | if c == c.lower(): 16 | part_1 += ord(c) - ord('a') + 1 17 | else: 18 | part_1 += ord(c) - ord('A') + 27 19 | 20 | print("Part 1:", part_1) 21 | 22 | # Part 2 23 | part_2 = 0 24 | for a, b, c in chunks(INPUT, 3): 25 | common = set(a) & set(b) & set(c) 26 | 27 | for c in common: 28 | if c == c.lower(): 29 | part_2 += ord(c) - ord('a') + 1 30 | else: 31 | part_2 += ord(c) - ord('A') + 27 32 | 33 | print("Part 2:", part_2) 34 | -------------------------------------------------------------------------------- /2022/day04.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | 4 | part_1 = 0 5 | part_2 = 0 6 | 7 | for line in fileinput.input(): 8 | elf_1, elf_2 = line.strip().split(',') 9 | a, b = [int(x) for x in elf_1.split('-')] 10 | c, d = [int(x) for x in elf_2.split('-')] 11 | 12 | # Part 1 13 | if a <= c <= b and a <= d <= b: 14 | part_1 += 1 15 | elif c <= a <= d and c <= b <= d: 16 | part_1 += 1 17 | 18 | 19 | # Part 2 20 | if a <= c <= b or a <= d <= b: 21 | part_2 += 1 22 | elif c <= a <= d or c <= b <= d: 23 | part_2 += 1 24 | 25 | print("Part 1:", part_1) 26 | print("Part 2:", part_2) 27 | -------------------------------------------------------------------------------- /2022/day05.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | import copy 3 | from utils import parse_nums 4 | 5 | INPUT = ''.join(fileinput.input()) 6 | board, moves = INPUT.split('\n\n') 7 | 8 | board = board.splitlines() 9 | bottom = board[-1] 10 | num_of_stacks = max(int(x) for x in bottom.split()) 11 | STACKS = [[] for _ in range(num_of_stacks)] 12 | 13 | for line in board[::-1]: 14 | for i, crate in enumerate(line[1::4]): 15 | if crate.isupper(): 16 | STACKS[i].append(crate) 17 | 18 | MOVES = [parse_nums(line) for line in moves.splitlines()] 19 | 20 | 21 | def simulate(part_2=False): 22 | stacks = copy.deepcopy(STACKS) 23 | for amt, frm, to in MOVES: 24 | frm -= 1 25 | to -= 1 26 | 27 | crates_to_move = [] 28 | for _ in range(amt): 29 | crate = stacks[frm].pop() 30 | crates_to_move.append(crate) 31 | 32 | if not part_2: 33 | crates_to_move.reverse() 34 | 35 | stacks[to].extend(crates_to_move) 36 | 37 | return ''.join(s[-1] for s in stacks) 38 | 39 | print("Part 1:", simulate()) 40 | print("Part 2:", simulate(part_2=True)) 41 | -------------------------------------------------------------------------------- /2022/day06.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | stream = fileinput.input()[0].strip() 4 | 5 | part_1 = None 6 | part_2 = None 7 | for i in range(len(stream)): 8 | window_1 = stream[i:i+4] 9 | window_2 = stream[i:i+14] 10 | if len(window_1) == len(set(window_1)) and not part_1: 11 | part_1 = i + 4 12 | if len(window_2) == len(set(window_2)) and not part_2: 13 | part_2 = i + 14 14 | break 15 | 16 | print("Part 1:", part_1) 17 | print("Part 2:", part_2) 18 | -------------------------------------------------------------------------------- /2022/day07.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | 4 | class File: 5 | def __init__(self, name, size=0): 6 | self.name = name 7 | self.size = size 8 | self.children = [] 9 | self.parent = None 10 | 11 | FS = File('/') 12 | pwd = FS 13 | 14 | for line in fileinput.input(): 15 | if line.startswith('$'): 16 | parts = line.split() 17 | cmd = parts[1] 18 | if cmd == 'cd': 19 | directory = parts[2] 20 | if directory == '..': 21 | pwd = pwd.parent 22 | else: # cd-ing into a new directory 23 | file = File(directory) 24 | file.parent = pwd 25 | pwd.children.append(file) 26 | pwd = file 27 | 28 | # The current line is telling us something about 29 | # either a file and its size, or a new directory. 30 | else: 31 | size, name = line.split() 32 | 33 | # Only care if we are looking at a file. 34 | if size != 'dir': 35 | file = File(name, int(size)) 36 | pwd.children.append(file) 37 | 38 | 39 | SIZES = {} 40 | 41 | def dir_size(file): 42 | """Return the size of the given file.""" 43 | if not file.children: 44 | return file.size 45 | 46 | total_size = 0 47 | for child in file.children: 48 | total_size += dir_size(child) 49 | 50 | SIZES[file] = total_size 51 | return total_size 52 | 53 | # Perform DFS to seed `SIZES`. 54 | USED = dir_size(FS) 55 | 56 | # Part 1 57 | print("Part 1:", sum(s for s in SIZES.values() if s < 100000)) 58 | 59 | # Part 2 60 | AVAILABLE = 70000000 61 | NEED = 30000000 62 | UNUSED = AVAILABLE - USED 63 | 64 | for size in sorted(SIZES.values()): 65 | if UNUSED + size >= NEED: 66 | print("Part 2:", size) 67 | break 68 | 69 | -------------------------------------------------------------------------------- /2022/day08.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import mul 3 | 4 | DIRS = [ 5 | (0, 1), 6 | (0, -1), 7 | (-1, 0), 8 | (1, 0), 9 | ] 10 | 11 | # Parse problem input. 12 | grid = {} 13 | for y, line in enumerate(fileinput.input()): 14 | for x, c in enumerate(line.strip()): 15 | grid[x, y] = int(c) 16 | 17 | # Check what trees are visible. 18 | visibles = set() 19 | scenic_scores = {} 20 | 21 | for tree in grid: 22 | scores = [] 23 | for dx, dy in DIRS: 24 | x, y = tree 25 | score = 0 26 | while (x, y) in grid: 27 | nx = x + dx 28 | ny = y + dy 29 | 30 | # Are we at the edge yet? 31 | if (nx, ny) not in grid: 32 | visibles.add(tree) 33 | break 34 | elif grid[nx, ny] >= grid[tree]: 35 | score += 1 36 | break 37 | 38 | x = nx 39 | y = ny 40 | score += 1 41 | 42 | scores.append(score) 43 | 44 | scenic_scores[tree] = mul(scores) 45 | 46 | print("Part 1:", len(visibles)) 47 | print("Part 2:", max(scenic_scores.values())) 48 | -------------------------------------------------------------------------------- /2022/day09.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import Point 3 | 4 | 5 | def sign(x, y): 6 | if x > y: 7 | return 1 8 | elif x < y: 9 | return -1 10 | else: 11 | return 0 12 | 13 | 14 | def update(head, tail): 15 | """ 16 | Given the current position of head and tail, 17 | return the new tail position. 18 | """ 19 | 20 | # This occurs if we have moved diagonally away from the tail, 21 | # and it is far enough for the tail to come and chase us. 22 | if abs(head.x - tail.x) + abs(head.y - tail.y) > 2: 23 | tail += Point(sign(head.x, tail.x), sign(head.y, tail.y)) 24 | 25 | # Check if we need to move horizontally/vertically. 26 | elif (head.x == tail.x or head.y == tail.y) and (abs(head.x - tail.x) + abs(head.y - tail.y) > 1): 27 | tail += Point(sign(head.x, tail.x), sign(head.y, tail.y)) 28 | 29 | return tail 30 | 31 | 32 | mapping = { 33 | "U": Point(0, 1), 34 | "R": Point(1, 0), 35 | "D": Point(0, -1), 36 | "L": Point(-1, 0), 37 | } 38 | 39 | INSTRUCTIONS = [line.strip() for line in fileinput.input()] 40 | 41 | # Solve problem. 42 | for part, bridge_len in ((1, 2), (2, 10)): 43 | bridge = [Point(0, 0) for _ in range(bridge_len)] 44 | seen = set() 45 | 46 | for line in INSTRUCTIONS: 47 | d, amt = line.split() 48 | amt = int(amt) 49 | 50 | for _ in range(amt): 51 | # First, move the head in the given direction. 52 | bridge[0] += mapping[d] 53 | for i in range(1, bridge_len): 54 | bridge[i] = update(bridge[i-1], bridge[i]) 55 | seen.add(bridge[-1]) 56 | 57 | print(f"Part {part}: {len(seen)}") 58 | 59 | -------------------------------------------------------------------------------- /2022/day10.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | # Initialize emulator. 4 | cycle = 1 5 | X = 1 6 | 7 | WIDTH = 40 8 | HEIGHT = 6 9 | 10 | def draw_pixel(): 11 | pos = ((cycle - 1) % WIDTH) + 1 12 | if X <= pos < X + 3: 13 | print("#", end="") 14 | else: 15 | print(".", end="") 16 | 17 | if pos == WIDTH: 18 | print() 19 | 20 | 21 | # Part 1 22 | interesting_cycle = [20, 60, 100, 140, 180, 220] 23 | signal_strengths = [] 24 | 25 | for line in fileinput.input(): 26 | ins = line.split() 27 | op = ins[0] 28 | 29 | if op == 'noop': 30 | draw_pixel() 31 | if cycle in interesting_cycle: 32 | signal_strengths.append(X * cycle) 33 | cycle += 1 34 | 35 | elif op == 'addx': 36 | arg = int(ins[1]) 37 | 38 | # Process first cycle of draw_pixelx. 39 | draw_pixel() 40 | if cycle in interesting_cycle: 41 | signal_strengths.append(X * cycle) 42 | cycle += 1 43 | 44 | # Process second cycle of draw_pixelx. 45 | draw_pixel() 46 | X += arg 47 | if cycle in interesting_cycle: 48 | signal_strengths.append(X * cycle) 49 | cycle += 1 50 | 51 | print() 52 | print("Part 1:", sum(signal_strengths)) 53 | -------------------------------------------------------------------------------- /2022/day11.py: -------------------------------------------------------------------------------- 1 | import os, copy, fileinput 2 | from collections import Counter, deque 3 | from utils import parse_nums, mul 4 | 5 | 6 | def simulate(monkeys, num_turns, part_2=False): 7 | monkeys = copy.deepcopy(monkeys) 8 | mod = mul(m[2] for m in monkeys) 9 | counts = Counter() 10 | 11 | for turn in range(num_turns): 12 | for i, (items, operation, div, true, false) in enumerate(monkeys): 13 | for _ in range(len(items)): 14 | counts[i] += 1 15 | item = monkeys[i][0].popleft() 16 | op, val = operation.split()[-2:] 17 | if val == 'old': 18 | val = item 19 | else: 20 | val = int(val) 21 | 22 | if op == "*": 23 | item *= val 24 | elif op == "+": 25 | item += val 26 | 27 | if part_2: 28 | item %= mod 29 | else: 30 | item //= 3 31 | 32 | next_monkey = true if item % div == 0 else false 33 | monkeys[next_monkey][0].append(item) 34 | 35 | inspections = list(sorted(counts.values(), reverse=True)) 36 | return inspections[0] * inspections[1] 37 | 38 | 39 | 40 | # Parse input. 41 | INPUT = "".join(fileinput.input()) 42 | 43 | MONKEYS = [] 44 | for monkey in INPUT.split(os.linesep * 2): 45 | m = monkey.splitlines() 46 | items = deque(parse_nums(m[1])) 47 | operation = m[2].split(': ')[1] 48 | div = parse_nums(m[3])[0] 49 | true = parse_nums(m[4])[0] 50 | false = parse_nums(m[5])[0] 51 | MONKEYS.append([items, operation, div, true, false]) 52 | 53 | # Solve problem. 54 | print("Part 1:", simulate(MONKEYS, 20)) 55 | print("Part 2:", simulate(MONKEYS, 10000, part_2=True)) 56 | -------------------------------------------------------------------------------- /2022/day12.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import deque 3 | from utils import Point 4 | 5 | # Parse input. 6 | board = {} 7 | for y, line in enumerate(fileinput.input()): 8 | for x, c in enumerate(line.strip()): 9 | board[Point(x, y)] = c 10 | if c == 'S': 11 | start = Point(x, y) 12 | board[start] = 'a' 13 | elif c == 'E': 14 | end = Point(x, y) 15 | board[end] = 'z' 16 | 17 | 18 | def bfs(start): 19 | horizon = deque([(start, 0)]) 20 | seen = set() 21 | while horizon: 22 | p, depth = horizon.popleft() 23 | if p in seen: 24 | continue 25 | elif p == end: 26 | return depth 27 | 28 | seen.add(p) 29 | 30 | for n in p.neighbours(): # returns the 4 adjacent points 31 | if n not in board: 32 | continue 33 | if (ord(board[n]) - 1 <= ord(board[p])): 34 | horizon.append((n, depth + 1)) 35 | 36 | return 1e9 37 | 38 | 39 | print("Part 1:", bfs(start)) 40 | print("Part 2:", min(bfs(s) for s in board if board[s] == 'a')) 41 | -------------------------------------------------------------------------------- /2022/day13.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | 4 | def right_order(x, y): 5 | if type(x) == int and type(y) == int: 6 | if int(x) < int(y): 7 | return True 8 | elif int(x) > int(y): 9 | return False 10 | else: 11 | return None 12 | 13 | if type(x) == int: 14 | x = [x] 15 | if type(y) == int: 16 | y = [y] 17 | 18 | if len(x) == len(y) == 0: 19 | return None 20 | elif len(x) == 0: 21 | return True 22 | elif len(y) == 0: 23 | return False 24 | 25 | x, *xs = x 26 | y, *ys = y 27 | 28 | ret = right_order(x, y) 29 | if ret is None: 30 | return right_order(xs, ys) 31 | return ret 32 | 33 | 34 | # Solve part 1 while parsing input. 35 | packets = [] 36 | pair = [] 37 | part_1 = 0 38 | idx = 1 39 | for line in fileinput.input(): 40 | if not line.strip(): 41 | if right_order(*pair): 42 | part_1 += idx 43 | pair = [] 44 | idx += 1 45 | else: 46 | packet = eval(line.strip()) 47 | pair.append(packet) 48 | packets.append(packet) 49 | 50 | # Final pair because no trailing newline. 51 | if right_order(*pair): 52 | part_1 += idx 53 | 54 | print("Part 1:", part_1) 55 | 56 | 57 | # Solve Part 2. 58 | packets.insert(0, [[2]]) 59 | packets.insert(0, [[6]]) 60 | 61 | mapping = {} 62 | for i, p1 in enumerate(packets): 63 | count = 0 64 | for j, p2 in enumerate(packets): 65 | if i == j: 66 | continue 67 | 68 | if right_order(p2, p1): 69 | count += 1 70 | 71 | mapping[i] = count 72 | 73 | two = mapping[0] + 1 74 | six = mapping[1] + 1 75 | print("Part 2:", two*six) 76 | -------------------------------------------------------------------------------- /2022/day21.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import fileinput 3 | from collections import defaultdict 4 | 5 | from utils import topsort 6 | 7 | 8 | GRAPH = defaultdict(set) 9 | YELLS = {} 10 | 11 | for line in fileinput.input(): 12 | monkey, rest = line.strip().split(': ') 13 | if rest.isnumeric(): 14 | YELLS[monkey] = int(rest) 15 | else: 16 | d1, op, d2 = rest.split(' ') 17 | YELLS[monkey] = (d1, op, d2) 18 | GRAPH[monkey].add(d1) 19 | GRAPH[monkey].add(d2) 20 | 21 | ORDERING = list(reversed(topsort(GRAPH))) 22 | 23 | 24 | def simulate(yells, humn=None): 25 | if humn: 26 | yells['humn'] = humn 27 | d1, _, d2 = yells['root'] 28 | yells['root'] = (d1, "==", d2) 29 | 30 | for monkey in ORDERING: 31 | if type(yells[monkey]) != int: 32 | d1, op, d2 = yells[monkey] 33 | yells[monkey] = eval(f"{yells[d1]} {op} {yells[d2]}") 34 | 35 | return int(yells['root']) 36 | 37 | 38 | print("Part 1:", simulate(copy.deepcopy(YELLS))) 39 | 40 | # For my problem input, we analyze some values of the two monkeys that 41 | # the `root` monkey depends on, and how it changes depends on `humn`. 42 | # 43 | # humn=3 d1=72750855862944 d2=31522134274080 44 | # humn=8 d1=72750855862880 d2=31522134274080 45 | # 46 | # When `humn` increases by 5, `d1` decreases by 64, and `d2` is constant. 47 | # Therefore, we need to solve the following equation: 48 | # 49 | # humn = 3 + (72750855862944 - 31522134274080) * (5 / 64) 50 | # = 3220993874133 51 | 52 | HUMN = 3220993874133 53 | part_2 = simulate(copy.deepcopy(YELLS), humn=HUMN); assert part_2 == 1 54 | print("Part 2:", HUMN) 55 | 56 | 57 | -------------------------------------------------------------------------------- /2022/day25.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import Counter 3 | from itertools import zip_longest 4 | 5 | SNAFU_DIGITS = { 6 | '2': 2, 7 | '1': 1, 8 | '0': 0, 9 | '-': -1, 10 | '=': -2, 11 | } 12 | REVERSE_DIGITS = {v: k for k, v in SNAFU_DIGITS.items()} 13 | 14 | 15 | # Read problem input and solve problem. 16 | nums = [line.strip() for line in fileinput.input()] 17 | remainder = Counter() 18 | part_1 = '' 19 | 20 | for i, place in enumerate(zip_longest(*(reversed(n) for n in nums), fillvalue='0')): 21 | count = sum(SNAFU_DIGITS[c] for c in place) + remainder[i] 22 | while not -2 <= count <= 2: 23 | if count < 0: 24 | remainder[i+1] -= 1 25 | count += 5 26 | else: 27 | remainder[i+1] += 1 28 | count -= 5 29 | 30 | part_1 = REVERSE_DIGITS[count] + part_1 31 | 32 | print("Part 1:", part_1) 33 | -------------------------------------------------------------------------------- /2022/starter.py: -------------------------------------------------------------------------------- 1 | import os, sys, re, math, copy, fileinput 2 | from string import ascii_uppercase, ascii_lowercase 3 | from collections import Counter, defaultdict, deque, namedtuple 4 | from itertools import count, product, permutations, combinations, combinations_with_replacement 5 | 6 | import advent 7 | from utils import parse_line, parse_nums, mul, all_unique, factors, memoize, primes, resolve_mapping 8 | from utils import chunks, parts, gcd, lcm, print_grid, min_max_xy 9 | from utils import new_table, transposed, rotated, firsts, lasts 10 | from utils import md5, sha256, VOWELS, CONSONANTS 11 | from utils import Point, DIRS, DIRS_4, DIRS_8, N, NE, E, SE, S, SW, W, NW 12 | # Itertools Functions: 13 | # product('ABCD', repeat=2) AA AB AC AD BA BB BC BD CA CB CC CD DA DB DC DD 14 | # permutations('ABCD', 2) AB AC AD BA BC BD CA CB CD DA DB DC 15 | # combinations_with_replacement('ABCD', 2) AA AB AC AD BB BC BD CC CD DD 16 | # combinations('ABCD', 2) AB AC AD BC BD CD 17 | 18 | # day .lines .nlines(negs=True) .pars .npars(negs=True) .board .pboard .tboard 19 | 20 | tot = 0 21 | res = [] 22 | 23 | day = advent.Day(year=2022, day=0) 24 | 25 | -------------------------------------------------------------------------------- /2023/day01.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | DIGITS = {str(i): i for i in range(1, 10)} 4 | 5 | DIGIT_WORDS = { 6 | 'one': 1, 7 | 'two': 2, 8 | 'three': 3, 9 | 'four': 4, 10 | 'five': 5, 11 | 'six': 6, 12 | 'seven': 7, 13 | 'eight': 8, 14 | 'nine': 9, 15 | } 16 | 17 | 18 | def calibration(line, dictionary): 19 | start = None 20 | end = None 21 | for i, c in enumerate(line): 22 | for key, val in dictionary.items(): 23 | if line[i:].startswith(key): 24 | if start is None: 25 | start = val 26 | end = val 27 | 28 | return start * 10 + end 29 | 30 | 31 | part_1 = 0 32 | part_2 = 0 33 | 34 | for line in fileinput.input(): 35 | part_1 += calibration(line, DIGITS) 36 | part_2 += calibration(line, DIGITS | DIGIT_WORDS) 37 | 38 | print("Part 1:", part_1) 39 | print("Part 2:", part_2) 40 | 41 | -------------------------------------------------------------------------------- /2023/day02.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import Counter 3 | 4 | from utils import mul 5 | 6 | PART_1_LIMITS = { 7 | 'red': 12, 8 | 'green': 13, 9 | 'blue': 14, 10 | } 11 | 12 | part_1 = 0 13 | part_2 = 0 14 | 15 | for line in fileinput.input(): 16 | game, log = line.strip().split(': ') 17 | game_id = int(game[5:]) 18 | 19 | part_1_poss = True 20 | part_2_mins = Counter() 21 | 22 | for turn in log.split('; '): 23 | for entry in turn.split(', '): 24 | n, color = entry.split(' ') 25 | n = int(n) 26 | if n > PART_1_LIMITS[color]: 27 | part_1_poss = False 28 | part_2_mins[color] = max(part_2_mins[color], n) 29 | 30 | if part_1_poss: 31 | part_1 += game_id 32 | 33 | part_2 += mul(part_2_mins.values()) 34 | 35 | print("Part 1:", part_1) 36 | print("Part 2:", part_2) 37 | -------------------------------------------------------------------------------- /2023/day03.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | SYMBOLS = {} 4 | PARTS = [] 5 | 6 | # Parse input. 7 | for y, line in enumerate(fileinput.input()): 8 | num = '' 9 | for x, c in enumerate(line): 10 | if not c.isdigit() and c != '.' and c != '\n': 11 | SYMBOLS[x, y] = c 12 | 13 | if c.isdigit(): 14 | if not num: 15 | start = x 16 | num += c 17 | elif num: 18 | PARTS.append((int(num), y, x - len(num), x - 1)) 19 | num = '' 20 | 21 | # Solve problem. 22 | part_1_seen = set() 23 | part_2 = 0 24 | 25 | for x, y in SYMBOLS: 26 | adj = [] 27 | for i, (n, yy, start, end) in enumerate(PARTS): 28 | if yy - 1 <= y <= yy + 1 and start - 1 <= x <= end + 1: 29 | part_1_seen.add(i) 30 | adj.append(n) 31 | 32 | if len(adj) == 2 and SYMBOLS[x, y] == '*': 33 | part_2 += adj[0] * adj[1] 34 | 35 | 36 | print("Part 1:", sum(PARTS[i][0] for i in part_1_seen)) 37 | print("Part 2:", part_2) 38 | -------------------------------------------------------------------------------- /2023/day04.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import Counter 3 | 4 | 5 | part_1 = 0 6 | part_2_cards = Counter() 7 | 8 | for line in fileinput.input(): 9 | card, rest = line.split(': ') 10 | card_id = int(card[5:]) 11 | 12 | winning, your = rest.split(' | ') 13 | winning = set(int(n) for n in winning.split()) 14 | your = [int(n) for n in your.split()] 15 | 16 | # Add the "original" scratchcard as one of the copies. 17 | part_2_cards[card_id] += 1 18 | 19 | matches = 0 20 | for n in your: 21 | if n in winning: 22 | matches += 1 23 | 24 | if matches > 0: 25 | part_1 += 2 ** (matches - 1) 26 | 27 | for copy_id in range(card_id + 1, card_id + matches + 1): 28 | part_2_cards[copy_id] += part_2_cards[card_id] 29 | 30 | print("Part 1:", part_1) 31 | print("Part 2:", sum(part_2_cards.values())) 32 | 33 | -------------------------------------------------------------------------------- /2023/day06.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import mul 3 | 4 | 5 | def solve(time, record): 6 | num_wins = 0 7 | for hold_time in range(time + 1): 8 | total_distance = hold_time * (time - hold_time) 9 | if total_distance > record: 10 | num_wins += 1 11 | 12 | return num_wins 13 | 14 | 15 | # Parse problem input. 16 | for i, line in enumerate(fileinput.input()): 17 | if i == 0: 18 | times = [int(x) for x in line.split()[1:]] 19 | else: 20 | distances = [int(x) for x in line.split()[1:]] 21 | 22 | 23 | # Solve problem. 24 | print("Part 1:", mul(solve(time, record) for time, record in zip(times, distances))) 25 | 26 | part_2_time = int(''.join(str(n) for n in times)) 27 | part_2_distance = int(''.join(str(n) for n in distances)) 28 | print("Part 2:", solve(part_2_time, part_2_distance)) 29 | 30 | -------------------------------------------------------------------------------- /2023/day07.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import Counter 3 | 4 | 5 | def rank(hand, part_2=False): 6 | # Special-case JJJJJ; other hands Js just become the most common card. 7 | if part_2 and hand != 'JJJJJ': 8 | no_jokers = Counter(c for c in hand if c != 'J').most_common() 9 | hand = hand.replace('J', no_jokers[0][0]) 10 | 11 | cmn = Counter(hand).most_common() 12 | 13 | # Five of a Kind 14 | if cmn[0][1] == 5: 15 | return 6 16 | 17 | # Four of a Kind 18 | elif cmn[0][1] == 4: 19 | return 5 20 | 21 | # Full House / Three of a Kind (based on second-most common card) 22 | elif cmn[0][1] == 3: 23 | return 2 + cmn[1][1] 24 | 25 | # Two Pair / One Pair (based on second-most common card) 26 | elif cmn[0][1] == 2: 27 | return cmn[1][1] 28 | 29 | return 0 30 | 31 | 32 | def tiebreak(hand, part_2=False): 33 | return tuple(card_score(c, part_2) for c in hand) 34 | 35 | 36 | def card_score(card, part_2=False): 37 | if part_2: 38 | return 'J23456789TQKA'.index(card) 39 | else: 40 | return '23456789TJQKA'.index(card) 41 | 42 | 43 | # Parse problem input. 44 | hands = [] 45 | for line in fileinput.input(): 46 | hand, bid = line.split() 47 | hands.append((hand, int(bid))) 48 | 49 | 50 | # Solve part 1. 51 | part_1 = 0 52 | for i, (hand, bid) in enumerate(sorted(hands, key=lambda h: (rank(h[0]), tiebreak(h[0]))), start=1): 53 | part_1 += i * bid 54 | 55 | print("Part 1:", part_1) 56 | 57 | 58 | # Solve part 2. 59 | part_2 = 0 60 | for i, (hand, bid) in enumerate(sorted(hands, key=lambda h: (rank(h[0], part_2=True), tiebreak(h[0], part_2=True))), start=1): 61 | part_2 += i * bid 62 | 63 | print("Part 2:", part_2) 64 | -------------------------------------------------------------------------------- /2023/day08.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from itertools import cycle 3 | from functools import reduce 4 | from utils import parse_line, lcm 5 | 6 | 7 | # Parse input. 8 | elements = {} 9 | for i, line in enumerate(fileinput.input()): 10 | if i == 0: 11 | instructions = line.strip() 12 | elif i >= 2: 13 | node, left, right = parse_line(r'(.+) = \((.+), (.+)\)', line) 14 | elements[node] = (left, right) 15 | 16 | # Solve part 1. 17 | curr = 'AAA' 18 | for i, ins in enumerate(cycle(instructions), start=1): 19 | if ins == 'L': 20 | curr = elements[curr][0] 21 | else: 22 | curr = elements[curr][1] 23 | 24 | if curr == 'ZZZ': 25 | print("Part 1:", i) 26 | break 27 | 28 | # Solve part 2. 29 | starts = [n for n in elements if n.endswith('A')] 30 | cycles = [] 31 | for start in starts: 32 | curr = start 33 | seen = {} 34 | 35 | for i, ins in enumerate(cycle(instructions), start=1): 36 | if ins == 'L': 37 | curr = elements[curr][0] 38 | else: 39 | curr = elements[curr][1] 40 | 41 | if curr.endswith('Z'): 42 | if curr in seen: 43 | cycles.append(i - seen[curr]) 44 | break 45 | 46 | seen[curr] = i 47 | 48 | i += 1 49 | 50 | print("Part 2:", int(reduce(lcm, cycles, 1))) 51 | 52 | -------------------------------------------------------------------------------- /2023/day09.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | 4 | part_1 = 0 5 | part_2 = 0 6 | 7 | for line in fileinput.input(): 8 | row = [int(x) for x in line.split()] 9 | 10 | # Build up triangle. 11 | triangle = [row[:]] 12 | 13 | while any(n != 0 for n in triangle[-1]): 14 | row = [] 15 | for a, b in zip(triangle[-1], triangle[-1][1:]): 16 | row.append(b - a) 17 | 18 | triangle.append(row) 19 | 20 | triangle.reverse() 21 | 22 | # Extrapolate right column. 23 | right = [0] 24 | for *_, n in triangle[1:]: 25 | right.append(right[-1] + n) 26 | 27 | part_1 += right[-1] 28 | 29 | # Extrapolate left column. 30 | left = [0] 31 | for n, *_ in triangle[1:]: 32 | left.append(n - left[-1]) 33 | 34 | part_2 += left[-1] 35 | 36 | 37 | print("Part 1:", part_1) 38 | print("Part 2:", part_2) 39 | 40 | -------------------------------------------------------------------------------- /2023/day11.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from itertools import combinations 3 | 4 | 5 | # Parse problem input. 6 | GALAXIES = set() 7 | 8 | for y, line in enumerate(fileinput.input()): 9 | for x, c in enumerate(line.strip()): 10 | if c == '#': 11 | GALAXIES.add((x, y)) 12 | 13 | # Find "expanded" rows and columns. 14 | empty_row = set(range(y)) 15 | empty_col = set(range(x)) 16 | 17 | for (gx, gy) in GALAXIES: 18 | empty_col.discard(gx) 19 | empty_row.discard(gy) 20 | 21 | # Solve problem. 22 | part_1 = 0 23 | part_2 = 0 24 | 25 | for (sx, sy), (ex, ey) in combinations(GALAXIES, 2): 26 | crossed = 0 27 | for y in range(min(sy, ey), max(sy, ey) + 1): 28 | if y in empty_row: 29 | crossed += 1 30 | 31 | for x in range(min(sx, ex), max(sx, ex) + 1): 32 | if x in empty_col: 33 | crossed += 1 34 | 35 | # Raw distance is the Manhattan distance between the pair. 36 | raw_distance = abs(sy - ey) + abs(sx - ex) 37 | part_1 += raw_distance + crossed * 1 38 | part_2 += raw_distance + crossed * 999999 39 | 40 | print("Part 1:", part_1) 41 | print("Part 2:", part_2) 42 | 43 | -------------------------------------------------------------------------------- /2023/day12.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import memoize 3 | 4 | @memoize 5 | def ways(spring, groups): 6 | # Base cases 7 | if len(spring) == 0: 8 | if not groups: 9 | return 1 10 | else: 11 | return 0 12 | 13 | # trust_the_natural_recursion.jpg 14 | if spring[0] == '#': 15 | if not groups: 16 | return 0 17 | 18 | if any(c == '.' for c in spring[:groups[0]]): 19 | return 0 20 | 21 | if len(spring) == groups[0]: 22 | if len(groups) == 1: 23 | return 1 24 | else: 25 | return 0 26 | 27 | if len(spring) < groups[0]: 28 | return 0 29 | 30 | # If the character after this "group" is a `#`, we have a mismatch. 31 | if spring[groups[0]] == '#': 32 | return 0 33 | 34 | return ways(spring[groups[0]+1:], groups[1:]) 35 | 36 | 37 | elif spring[0] == '.': 38 | return ways(spring[1:], groups) 39 | 40 | elif spring[0] == '?': 41 | return ways('#' + spring[1:], groups) + ways('.' + spring[1:], groups) 42 | 43 | 44 | part_1 = 0 45 | part_2 = 0 46 | 47 | for line in fileinput.input(): 48 | spring, groups = line.strip().split() 49 | groups = tuple(int(n) for n in groups.split(',')) 50 | part_1 += ways(spring, groups) 51 | part_2 += ways('?'.join(spring for _ in range(5)), groups * 5) 52 | 53 | print("Part 1:", part_1) 54 | print("Part 2:", part_2) 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /2023/day13.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import transposed 3 | 4 | 5 | def find_reflections(pattern): 6 | """Returns all possible reflections for pattern.""" 7 | sols = [] 8 | 9 | # First try solving as-is, then transpose for vertical reflections. 10 | for multiplier in [100, 1]: 11 | for y in range(len(pattern) - 1): 12 | bad = False 13 | for a, b in zip(reversed(pattern[:y+1]), pattern[y+1:]): 14 | if a != b: 15 | bad = True 16 | break 17 | 18 | if not bad: 19 | sols.append((y + 1) * multiplier) 20 | 21 | pattern = transposed(pattern) 22 | 23 | return sols 24 | 25 | 26 | def unsmudge(pattern, reflection): 27 | for y in range(len(pattern)): 28 | for x in range(len(pattern[0])): 29 | cell = pattern[y][x] 30 | pattern[y][x] = '.' if cell == '#' else '#' 31 | 32 | for new_reflection in find_reflections(pattern): 33 | if new_reflection != reflection: 34 | return new_reflection 35 | 36 | pattern[y][x] = cell 37 | 38 | 39 | part_1 = 0 40 | part_2 = 0 41 | 42 | for pattern in ''.join(fileinput.input()).split('\n\n'): 43 | pattern = [list(x) for x in pattern.splitlines()] 44 | 45 | reflection = solve(pattern)[0] 46 | part_1 += reflection 47 | part_2 += unsmudge(pattern, reflection) 48 | 49 | print("Part 1:", part_1) 50 | print("Part 2:", part_2) 51 | 52 | -------------------------------------------------------------------------------- /2023/day15.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import defaultdict 3 | 4 | 5 | def HASH(code): 6 | val = 0 7 | for c in code: 8 | val += ord(c) 9 | val *= 17 10 | val %= 256 11 | return val 12 | 13 | 14 | # Parse problem input. 15 | STEPS = next(fileinput.input()).strip().split(',') 16 | 17 | # Solve part 1. 18 | print("Part 1:", sum(HASH(step) for step in STEPS)) 19 | 20 | # Solve part 2. 21 | BOXES = defaultdict(list) 22 | 23 | for step in STEPS: 24 | if step[-1] == '-': 25 | label = step[:-1] 26 | box = HASH(label) 27 | BOXES[box] = [lens for lens in BOXES[box] if lens[0] != label] 28 | 29 | else: 30 | focal = int(step[-1]) 31 | label = step[:-2] 32 | box = HASH(label) 33 | 34 | for i, (other_label, other_focal) in enumerate(BOXES[box]): 35 | if other_label == label: 36 | BOXES[box][i] = (label, focal) 37 | break 38 | else: 39 | BOXES[box].append((label, focal)) 40 | 41 | lenses = {} 42 | for box_num, box in BOXES.items(): 43 | for slot_num, (label, focal) in enumerate(box, start=1): 44 | lenses[label] = (box_num + 1) * slot_num * focal 45 | 46 | print("Part 2:", sum(lenses.values())) 47 | -------------------------------------------------------------------------------- /2023/day16.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import Point, DIRS, N, E, S, W 3 | 4 | 5 | # Parse problem input. 6 | GRID = {} 7 | for y, line in enumerate(fileinput.input()): 8 | for x, c in enumerate(line.strip()): 9 | GRID[Point(x, y)] = c 10 | 11 | MAX_X = x 12 | MAX_Y = y 13 | 14 | 15 | def simulate_light(grid, start, initial_d): 16 | photons = [(start, initial_d)] 17 | seen = set() 18 | while photons: 19 | photon, d = photons.pop() 20 | 21 | if photon not in grid: 22 | continue 23 | 24 | if (photon, d) in seen: 25 | continue 26 | 27 | seen.add((photon, d)) 28 | 29 | if grid[photon] == '.': 30 | photons.append((photon + d, d)) 31 | elif grid[photon] == '/': 32 | # ~d takes N <-> E and S <-> W 33 | photons.append((photon + ~d, ~d)) 34 | elif grid[photon] == '\\': 35 | # -~d takes N <-> W and S <-> E 36 | photons.append((photon + -~d, -~d)) 37 | elif grid[photon] == '|': 38 | if d in (N, S): 39 | photons.append((photon + d, d)) 40 | else: 41 | photons.append((photon + N, N)) 42 | photons.append((photon + S, S)) 43 | elif grid[photon] == '-': 44 | if d in (E, W): 45 | photons.append((photon + d, d)) 46 | else: 47 | photons.append((photon + E, E)) 48 | photons.append((photon + W, W)) 49 | 50 | return len(set(p for p, d in seen)) 51 | 52 | 53 | def part_2_starts(grid): 54 | for x in range(MAX_X): 55 | yield Point(x, 0), N 56 | yield Point(x, MAX_Y - 1), S 57 | 58 | for y in range(MAX_Y): 59 | yield Point(0, y), E 60 | yield Point(MAX_X - 1, y), W 61 | 62 | 63 | print("Part 1:", simulate_light(GRID, Point(0, 0), E)) 64 | print("Part 2:", max(simulate_light(GRID, p, d) for p, d in part_2_starts(GRID))) 65 | -------------------------------------------------------------------------------- /2023/day18.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import Point, N, S, E, W 3 | 4 | DIR_MAPPING = { 5 | 'U': N, 6 | 'D': S, 7 | 'L': W, 8 | 'R': E, 9 | 10 | '0': E, 11 | '1': S, 12 | '2': W, 13 | '3': N, 14 | } 15 | 16 | def discrete_polygon_area(points): 17 | # Use shoelace formula to compute internal area. 18 | area = 0 19 | 20 | for a, b in zip(points, points[1:] + [points[0]]): 21 | area += (b.x + a.x) * (b.y - a.y) 22 | 23 | area = int(abs(area / 2.0)) 24 | 25 | # Calculate perimeter. 26 | perimeter = sum(a.dist_manhattan(b) for a, b in zip(points, points[1:] + [points[0]])) 27 | 28 | # Account for outer perimeter strip in final area computation. 29 | return area + (perimeter // 2) + 1 30 | 31 | 32 | # Parse problem input. 33 | p1_pos = Point(0, 0) 34 | p2_pos = Point(0, 0) 35 | 36 | p1_points = [p1_pos] 37 | p2_points = [p2_pos] 38 | 39 | for line in fileinput.input(): 40 | direction, distance, hex_code = line.split() 41 | p1_dir = DIR_MAPPING[direction] 42 | p1_dist = int(distance) 43 | 44 | p2_dir = DIR_MAPPING[hex_code[-2]] 45 | p2_dist = int(hex_code[2:-2], 16) 46 | 47 | p1_pos += p1_dir * p1_dist 48 | p2_pos += p2_dir * p2_dist 49 | 50 | p1_points.append(p1_pos) 51 | p2_points.append(p2_pos) 52 | 53 | 54 | # Solve problem. 55 | print("Part 1:", discrete_polygon_area(p1_points)) 56 | print("Part 2:", discrete_polygon_area(p2_points)) 57 | -------------------------------------------------------------------------------- /2023/day25.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import mul 3 | 4 | try: 5 | import networkx as nx 6 | except ImportError: 7 | print("Solution requires NetworkX (`pip install networkx`)") 8 | import sys 9 | sys.exit() 10 | 11 | # Parse problem input. 12 | graph = nx.Graph() 13 | 14 | for line in fileinput.input(): 15 | src, rest = line.strip().split(': ') 16 | graph.add_node(src) 17 | for dest in rest.split(): 18 | graph.add_edge(src, dest) 19 | 20 | # Solve part 1. 21 | for a, b in nx.minimum_edge_cut(graph): 22 | graph.remove_edge(a, b) 23 | 24 | print("Part 1:", mul(len(c) for c in nx.connected_components(graph))) 25 | -------------------------------------------------------------------------------- /2023/starter.py: -------------------------------------------------------------------------------- 1 | import os, sys, re, math, copy, fileinput 2 | from string import ascii_uppercase, ascii_lowercase 3 | from collections import Counter, defaultdict, deque, namedtuple 4 | from itertools import count, product, permutations, combinations, combinations_with_replacement 5 | 6 | import advent 7 | from utils import parse_line, parse_nums, mul, all_unique, factors, memoize, primes, resolve_mapping 8 | from utils import chunks, parts, gcd, lcm, print_grid, min_max_xy 9 | from utils import new_table, transposed, rotated, firsts, lasts 10 | from utils import md5, sha256, VOWELS, CONSONANTS, HASH, polygon_perimeter, polygon_area 11 | from utils import Point, DIRS, DIRS_4, DIRS_8, N, NE, E, SE, S, SW, W, NW 12 | # Itertools Functions: 13 | # product('ABCD', repeat=2) AA AB AC AD BA BB BC BD CA CB CC CD DA DB DC DD 14 | # permutations('ABCD', 2) AB AC AD BA BC BD CA CB CD DA DB DC 15 | # combinations_with_replacement('ABCD', 2) AA AB AC AD BB BC BD CC CD DD 16 | # combinations('ABCD', 2) AB AC AD BC BD CD 17 | 18 | # day .lines .nlines(negs=True) .pars .npars(negs=True) .board .pboard .tboard 19 | 20 | tot = 0 21 | ans = '' 22 | res = [] 23 | 24 | day = advent.Day(year=2023, day=0) 25 | 26 | -------------------------------------------------------------------------------- /2024/day01.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | lefts = [] 4 | rights = [] 5 | 6 | for line in fileinput.input(): 7 | a, b = line.strip().split() 8 | lefts.append(int(a)) 9 | rights.append(int(b)) 10 | 11 | lefts.sort() 12 | rights.sort() 13 | 14 | print("Part 1:", sum(abs(l - r) for l, r in zip(lefts, rights))) 15 | print("Part 2:", sum(l * rights.count(l) for l in lefts)) 16 | -------------------------------------------------------------------------------- /2024/day02.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | 3 | def is_safe(levels): 4 | increasing = all((a < b) for a, b in zip(levels, levels[1:])) 5 | decreasing = all((a > b) for a, b in zip(levels, levels[1:])) 6 | good_deltas = all((abs(a - b) <= 3) for a, b in zip(levels, levels[1:])) 7 | 8 | return (increasing or decreasing) and good_deltas 9 | 10 | def gen_part_2(levels): 11 | yield levels 12 | for i in range(len(levels)): 13 | yield levels[:i] + levels[i+1:] 14 | 15 | 16 | reports = [] 17 | 18 | for line in fileinput.input(): 19 | reports.append([int(n) for n in line.split()]) 20 | 21 | print("Part 1:", sum(is_safe(r) for r in reports)) 22 | print("Part 2:", sum(any(is_safe(r) for r in gen_part_2(report)) for report in reports)) 23 | -------------------------------------------------------------------------------- /2024/day03.py: -------------------------------------------------------------------------------- 1 | import re 2 | import fileinput 3 | 4 | 5 | MUL_RE = r"mul\((\d+),(\d+)\)" 6 | DONT_DO_RE = r"don\'t\(\).*?(do\(\)|$)" # match a `do()` or end of string 7 | 8 | 9 | def count_muls(program): 10 | return sum(int(x) * int(y) for x, y in re.findall(MUL_RE, program)) 11 | 12 | 13 | program = ''.join(line.strip() for line in fileinput.input()) 14 | print("Part 1:", count_muls(program)) 15 | print("Part 2:", count_muls(re.sub(DONT_DO_RE, '', program))) 16 | 17 | -------------------------------------------------------------------------------- /2024/day04.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import Point, DIRS_8, NW, NE, SW, SE 3 | 4 | BOARD = {} 5 | X_LOCS = set() 6 | A_LOCS = set() 7 | 8 | for y, line in enumerate(fileinput.input()): 9 | for x, c in enumerate(line.strip()): 10 | p = Point(x, y) 11 | BOARD[p] = c 12 | if c == 'X': 13 | X_LOCS.add(p) 14 | elif c == 'A': 15 | A_LOCS.add(p) 16 | 17 | 18 | # Solve part 1. 19 | part_1 = 0 20 | for loc in X_LOCS: 21 | for d in DIRS_8: 22 | if (BOARD.get(loc + d) == 'M' 23 | and BOARD.get(loc + d * 2) == 'A' 24 | and BOARD.get(loc + d * 3) == 'S'): 25 | part_1 += 1 26 | 27 | print("Part 1:", part_1) 28 | 29 | 30 | # Solve part 2. 31 | part_2 = 0 32 | for loc in A_LOCS: 33 | diag_1 = set([BOARD.get(loc + NW), BOARD.get(loc + SE)]) 34 | diag_2 = set([BOARD.get(loc + NE), BOARD.get(loc + SW)]) 35 | 36 | if diag_1 == diag_2 == set(['M', 'S']): 37 | part_2 += 1 38 | 39 | print("Part 2:", part_2) 40 | -------------------------------------------------------------------------------- /2024/day05.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import defaultdict 3 | 4 | from utils import parse_nums 5 | 6 | 7 | def is_unordered(graph, update): 8 | """Returns -1 if ordered, else the index of the first bad page.""" 9 | for i, n in enumerate(update): 10 | if any(n in graph[m] for m in update[i+1:]): 11 | return i 12 | 13 | return -1 14 | 15 | def mid(lst): 16 | return lst[(len(lst) - 1) // 2] 17 | 18 | 19 | # Parse problem input. 20 | on_updates = False 21 | graph = defaultdict(set) 22 | updates = [] 23 | for line in fileinput.input(): 24 | if on_updates: 25 | updates.append(parse_nums(line)) 26 | else: 27 | if line.strip(): 28 | x, y = parse_nums(line) 29 | graph[x].add(y) 30 | else: 31 | on_updates = True 32 | 33 | part_1 = 0 34 | part_2 = 0 35 | 36 | for update in updates: 37 | if is_unordered(graph, update) == -1: 38 | part_1 += mid(update) 39 | else: 40 | # Sort based on how many pages in `updates` should come after a given page. 41 | afters = {} 42 | for i, n in enumerate(update): 43 | afters[n] = len(graph[n] & (set(update) - set([n]))) 44 | 45 | update.sort(key=afters.get) 46 | 47 | part_2 += mid(update) 48 | 49 | print("Part 1:", part_1) 50 | print("Part 2:", part_2) 51 | -------------------------------------------------------------------------------- /2024/day06.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import Point, N, S, E, W 3 | 4 | 5 | DIRS = [S, E, N, W] 6 | 7 | def simulate(board, start, obs=None): 8 | BOARD[obs] = '#' 9 | p = start 10 | d = 0 11 | seen = set() 12 | 13 | while True: 14 | if (p, d) in seen: 15 | # Loop detected. 16 | BOARD[obs] = '.' 17 | return None 18 | 19 | seen.add((p, d)) 20 | 21 | np = p + DIRS[d] 22 | if BOARD.get(np) == '#': 23 | d = (d + 1) % 4 24 | elif BOARD.get(np) is None: 25 | break 26 | else: 27 | p += DIRS[d] 28 | 29 | BOARD[obs] = '.' 30 | return set(p for p, _ in seen) 31 | 32 | # Read problem input. 33 | BOARD = {} 34 | for y, line in enumerate(fileinput.input()): 35 | for x, c in enumerate(line.strip()): 36 | p = Point(x, y) 37 | BOARD[p] = c 38 | if c == '^': 39 | START = p 40 | 41 | # Solve part 1. 42 | seen = simulate(BOARD, START) 43 | print("Part 1:", len(seen)) 44 | 45 | # Solve part 2. 46 | candidates = set() 47 | for x in seen: 48 | for n in x.neighbours(): 49 | candidates.add(n) 50 | 51 | part_2 = 0 52 | for p in candidates: 53 | if p not in seen or BOARD.get(p) != '.': 54 | continue 55 | if simulate(BOARD, START, obs=p) is None: 56 | part_2 += 1 57 | 58 | print("Part 2:", part_2) 59 | 60 | -------------------------------------------------------------------------------- /2024/day07.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | import itertools 3 | 4 | from utils import parse_nums 5 | 6 | 7 | def solve(target, operands, part_2=False): 8 | ops = ['+', '*'] 9 | if part_2: 10 | ops.append('||') 11 | 12 | for operators in itertools.product(ops, repeat=(len(operands))): 13 | ans = operands[0] 14 | for operand, operator in zip(operands[1:], operators): 15 | if operator == '+': 16 | ans += operand 17 | elif operator == '*': 18 | ans *= operand 19 | else: 20 | ans = int('{}{}'.format(ans, operand)) 21 | 22 | if int(ans) == target: 23 | return target 24 | 25 | return 0 26 | 27 | part_1 = 0 28 | part_2 = 0 29 | 30 | for line in fileinput.input(): 31 | target, *operands = parse_nums(line) 32 | part_1 += solve(target, operands) 33 | part_2 += solve(target, operands, part_2=True) 34 | 35 | print("Part 1:", part_1) 36 | print("Part 2:", part_2) 37 | -------------------------------------------------------------------------------- /2024/day08.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from itertools import permutations 3 | from collections import defaultdict 4 | 5 | from utils import Point 6 | 7 | 8 | # Parse problem input. 9 | BOARD = {} 10 | ANTENNAE = defaultdict(set) 11 | for y, line in enumerate(fileinput.input()): 12 | for x, c in enumerate(line.strip()): 13 | p = Point(x, y) 14 | BOARD[p] = c 15 | if c != '.': 16 | ANTENNAE[c].add(p) 17 | 18 | # Solve problem. 19 | part_1_antinodes = set() 20 | part_2_antinodes = set() 21 | 22 | for freq, locs in ANTENNAE.items(): 23 | for a, b in permutations(locs, 2): 24 | for i in range(len(line)): 25 | antinode = b + (b - a) * i 26 | part_2_antinodes.add(antinode) 27 | if i == 1: 28 | part_1_antinodes.add(antinode) 29 | 30 | print("Part 1:", len(part_1_antinodes & set(BOARD))) 31 | print("Part 2:", len(part_2_antinodes & set(BOARD))) 32 | -------------------------------------------------------------------------------- /2024/day10.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import deque 3 | 4 | from utils import Point 5 | 6 | 7 | def hike(board, trailhead): 8 | horizon = deque([trailhead]) 9 | seen = set() 10 | 11 | nines = set() 12 | trails = 0 13 | 14 | while horizon: 15 | curr = horizon.pop() 16 | if curr in seen: 17 | continue 18 | 19 | if board.get(curr) == 9: 20 | trails += 1 21 | nines.add(curr) 22 | 23 | for neigh in curr.neighbours(): 24 | if board.get(neigh, 0) == board.get(curr) + 1: 25 | horizon.appendleft(neigh) 26 | 27 | return len(nines), trails 28 | 29 | 30 | # Read problem input. 31 | BOARD = {} 32 | TRAILHEADS = set() 33 | 34 | for y, line in enumerate(fileinput.input()): 35 | for x, c in enumerate(line.strip()): 36 | p = Point(x, y) 37 | BOARD[p] = int(c) 38 | if BOARD[p] == 0: 39 | TRAILHEADS.add(p) 40 | 41 | 42 | # Solve problem. 43 | part_1 = 0 44 | part_2 = 0 45 | for trailhead in TRAILHEADS: 46 | scores, trails = hike(BOARD, trailhead) 47 | part_1 += scores 48 | part_2 += trails 49 | 50 | print("Part 1:", part_1) 51 | print("Part 2:", part_2) 52 | -------------------------------------------------------------------------------- /2024/day11.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import Counter 3 | 4 | 5 | stones = Counter(int(n) for n in fileinput.input()[0].split()) 6 | 7 | for blink in range(1, 76): 8 | new_stones = Counter() 9 | for stone, count in stones.items(): 10 | stone_str = str(stone) 11 | 12 | if stone == 0: 13 | new_stones[1] += count 14 | elif len(stone_str) % 2 == 0: 15 | mid = len(stone_str) // 2 16 | left, right = stone_str[:mid], stone_str[mid:] 17 | new_stones[int(left)] += count 18 | new_stones[int(right)] += count 19 | else: 20 | new_stones[stone * 2024] += count 21 | 22 | stones = new_stones 23 | 24 | if blink == 25: 25 | print("Part 1:", sum(stones.values())) 26 | elif blink == 75: 27 | print("Part 2:", sum(stones.values())) 28 | -------------------------------------------------------------------------------- /2024/day13.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from utils import parse_nums 3 | 4 | from z3 import Optimize, Int, And, sat 5 | 6 | 7 | PART_2_FACTOR = 10000000000000 8 | 9 | part_1 = 0 10 | part_2 = 0 11 | 12 | 13 | INPUT = ''.join(fileinput.input()) 14 | for block in INPUT.split('\n\n'): 15 | lines = block.split('\n') 16 | ax, ay = parse_nums(lines[0]) 17 | bx, by = parse_nums(lines[1]) 18 | px, py = parse_nums(lines[2]) 19 | 20 | a = Int('a') 21 | b = Int('b') 22 | 23 | for part in range(1, 3): 24 | o = Optimize() 25 | o.add(And( 26 | a >= 0, 27 | b >= 0, 28 | a * ax + b * bx == px + (PART_2_FACTOR if part == 2 else 0), 29 | a * ay + b * by == py + (PART_2_FACTOR if part == 2 else 0), 30 | )) 31 | 32 | o.minimize(a * 3 + b) 33 | 34 | if o.check() == sat: 35 | m = o.model() 36 | result = m[a].as_long() * 3 + m[b].as_long() 37 | if part == 1: 38 | part_1 += result 39 | else: 40 | part_2 += result 41 | 42 | print("Part 1:", part_1) 43 | print("Part 2:", part_2) 44 | -------------------------------------------------------------------------------- /2024/day14.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import fileinput 3 | from collections import Counter 4 | 5 | from utils import parse_nums, mul 6 | 7 | 8 | MAX_X = 101 9 | MAX_Y = 103 10 | TREE_CRITERIA = 15 11 | 12 | 13 | # Parse problem input. 14 | robots = [] 15 | for line in fileinput.input(): 16 | px, py, vx, vy = parse_nums(line) 17 | robots.append((px, py, vx, vy)) 18 | 19 | 20 | # Simulate robots. 21 | for iteration in range(1, 10000): 22 | new_robots = [] 23 | for px, py, vx, vy in robots: 24 | nx = (px + vx) % MAX_X 25 | ny = (py + vy) % MAX_Y 26 | new_robots.append((nx, ny, vx, vy)) 27 | 28 | robots = new_robots 29 | 30 | # The "Christmas tree" is nicely outlined by a border, so if we 31 | # find a contiguous line of robots, it's probably the tree. 32 | picture = set((px, py) for px, py, _, _ in robots) 33 | 34 | contig = 0 35 | max_contig = 0 36 | 37 | for y in range(MAX_Y): 38 | for x in range(MAX_X): 39 | if (x, y) in picture: 40 | contig += 1 41 | max_contig = max(contig, max_contig) 42 | else: 43 | contig = 0 44 | 45 | if max_contig > TREE_CRITERIA: 46 | print("Part 2:", iteration) 47 | sys.exit() 48 | 49 | 50 | if iteration == 100: 51 | quadrants = Counter() 52 | for px, py, _, _ in robots: 53 | if px < MAX_X // 2 and py < MAX_Y // 2: 54 | quadrants[1] += 1 55 | elif px < MAX_X // 2 and py > MAX_Y // 2: 56 | quadrants[2] += 1 57 | elif px > MAX_X // 2 and py < MAX_Y // 2: 58 | quadrants[3] += 1 59 | elif px > MAX_X // 2 and py > MAX_Y // 2: 60 | quadrants[4] += 1 61 | 62 | print("Part 1:", mul(quadrants.values())) 63 | -------------------------------------------------------------------------------- /2024/day16.py: -------------------------------------------------------------------------------- 1 | import heapq 2 | import fileinput 3 | from collections import defaultdict 4 | 5 | from utils import Point, DIRS, E 6 | 7 | 8 | # Read problem input. 9 | BOARD = {} 10 | for y, line in enumerate(fileinput.input()): 11 | for x, c in enumerate(line.strip()): 12 | p = Point(x, y) 13 | BOARD[p] = '#' if c == '#' else '.' 14 | if c == 'S': 15 | START = p 16 | elif c == 'E': 17 | END = p 18 | 19 | 20 | # Best-first search around the maze. 21 | horizon = [(0, START, E, [])] 22 | seen = {} 23 | 24 | best_score = 1e9 25 | best_paths = defaultdict(list) 26 | 27 | while horizon: 28 | score, pos, d, path = heapq.heappop(horizon) 29 | 30 | if seen.get((pos, d), 1e9) < score: 31 | continue 32 | 33 | if pos == END: 34 | best_score = min(best_score, score) 35 | best_paths[score].append(path + [pos]) 36 | 37 | seen[pos, d] = score 38 | 39 | for dd in DIRS: 40 | np = pos + dd 41 | if dd == -d: 42 | continue 43 | 44 | if dd == d and BOARD.get(np) == '.': 45 | heapq.heappush(horizon, (score + 1, np, dd, [pos] + path)) 46 | elif BOARD.get(np) == '.': 47 | heapq.heappush(horizon, (score + 1001, np, dd, [pos] + path)) 48 | 49 | nice_tiles = set() 50 | for path in best_paths[best_score]: 51 | nice_tiles |= set(path) 52 | 53 | print("Part 1:", best_score) 54 | print("Part 2:", len(nice_tiles)) 55 | 56 | -------------------------------------------------------------------------------- /2024/day18.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import deque 3 | 4 | from utils import Point, parse_nums 5 | 6 | 7 | def bfs(size, corruptions, time, start, end): 8 | horizon = deque([(start, 0)]) 9 | seen = set() 10 | while horizon: 11 | pos, dist = horizon.popleft() 12 | if pos in seen: 13 | continue 14 | 15 | seen.add(pos) 16 | 17 | if pos == END: 18 | return dist 19 | 20 | for n in pos.neighbours(): 21 | is_corrupted = corruptions.get(n, 1e6) 22 | if is_corrupted < time: 23 | continue 24 | 25 | if 0 <= n.x <= size and 0 <= n.y <= size: 26 | horizon.append((n, dist + 1)) 27 | 28 | 29 | SIZE = 70 30 | 31 | # Read problem input. 32 | BYTES = [] 33 | CORRUPTIONS = {} 34 | for i, line in enumerate(fileinput.input()): 35 | BYTES.append(line) 36 | x, y = parse_nums(line) 37 | CORRUPTIONS[Point(x, y)] = i 38 | 39 | START = Point(0, 0) 40 | END = Point(SIZE, SIZE) 41 | 42 | # Solve problem. 43 | for i in range(len(BYTES)): 44 | dist = bfs(SIZE, CORRUPTIONS, i, Point(0, 0), Point(SIZE, SIZE)) 45 | if i == 1024: 46 | print("Part 1:", dist) 47 | if dist is None: 48 | print("Part 2:", BYTES[i-1]) 49 | break 50 | -------------------------------------------------------------------------------- /2024/day19.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | import functools 3 | 4 | 5 | @functools.lru_cache() 6 | def num_designs(towel, patterns): 7 | poss = 0 8 | 9 | for pattern in patterns: 10 | l = len(pattern) 11 | if towel == pattern: 12 | poss += 1 13 | elif towel[:l] == pattern: 14 | sub_poss = num_designs(towel[l:], patterns) 15 | if sub_poss: 16 | poss += sub_poss 17 | 18 | return poss 19 | 20 | 21 | # Read problem input. 22 | TOWELS = [] 23 | for line in fileinput.input(): 24 | if ',' in line: 25 | PATTERNS = frozenset(line.strip().split(', ')) 26 | elif line.strip(): 27 | TOWELS.append(line.strip()) 28 | 29 | 30 | # Solve problem. 31 | designs = [num_designs(towel, PATTERNS) for towel in TOWELS] 32 | print("Part 1:", sum(1 for d in designs if d)) 33 | print("Part 2:", sum(designs)) 34 | -------------------------------------------------------------------------------- /2024/day20.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from collections import deque, Counter 3 | from itertools import permutations 4 | 5 | from utils import Point 6 | 7 | 8 | def bfs(board, start, end): 9 | horizon = deque([(start, 0)]) 10 | seen = {} 11 | while horizon: 12 | pos, dist = horizon.popleft() 13 | if pos in seen: 14 | continue 15 | 16 | seen[pos] = dist 17 | 18 | if pos == END: 19 | return seen 20 | 21 | for n in pos.neighbours(): 22 | if BOARD.get(n) == '#' or n not in BOARD: 23 | continue 24 | horizon.append((n, dist + 1)) 25 | 26 | 27 | def num_cheat_paths(distances, max_cheat_dist): 28 | cheat_saves = Counter() 29 | for start, end in permutations(distances, 2): 30 | if distances[end] < distances[start]: 31 | continue 32 | 33 | cheat_dist = abs(start.dist_manhattan(end)) 34 | 35 | if cheat_dist > max_cheat_dist: 36 | continue 37 | 38 | saved = distances[end] - distances[start] - cheat_dist 39 | cheat_saves[saved] += 1 40 | 41 | num_cheats = 0 42 | for cheat_dist, count in cheat_saves.items(): 43 | if cheat_dist >= 100: 44 | num_cheats += count 45 | 46 | return num_cheats 47 | 48 | 49 | # Read problem input. 50 | BOARD = {} 51 | for y, line in enumerate(fileinput.input()): 52 | for x, c in enumerate(line.strip()): 53 | p = Point(x, y) 54 | if c == '#': 55 | BOARD[p] = '#' 56 | else: 57 | BOARD[p] = '.' 58 | if c == 'S': 59 | START = p 60 | elif c == 'E': 61 | END = p 62 | 63 | 64 | # Solve problem. 65 | distances = bfs(BOARD, START, END) 66 | 67 | print("Part 1:", num_cheat_paths(distances, 2)) 68 | print("Part 2:", num_cheat_paths(distances, 20)) 69 | -------------------------------------------------------------------------------- /2024/day23.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | import networkx as nx 3 | 4 | 5 | # Read problem input. 6 | G = nx.Graph() 7 | 8 | for line in fileinput.input(): 9 | a, b = line.strip().split('-') 10 | if a not in G: 11 | G.add_node(a) 12 | if b not in G: 13 | G.add_node(b) 14 | 15 | G.add_edge(a, b) 16 | 17 | 18 | # Solve problem. 19 | part_1 = 0 20 | largest_clique = [] 21 | 22 | for clique in nx.enumerate_all_cliques(G): 23 | if len(clique) > len(largest_clique): 24 | largest_clique = clique 25 | 26 | if len(clique) == 3 and any(n.startswith('t') for n in clique): 27 | part_1 += 1 28 | 29 | print("Part 1:", part_1) 30 | print("Part 2:", ','.join(n for n in sorted(largest_clique))) 31 | -------------------------------------------------------------------------------- /2024/day25.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | from itertools import product 3 | 4 | from utils import transposed 5 | 6 | 7 | KEYS = [] 8 | LOCKS = [] 9 | 10 | block = [] 11 | for line in fileinput.input(): 12 | if line == '\n': 13 | if block[0][0] == '#': 14 | KEYS.append(transposed(block)) 15 | else: 16 | LOCKS.append(transposed(block)) 17 | block = [] 18 | else: 19 | block.append(line.strip()) 20 | 21 | # The last image is a lock. 22 | LOCKS.append(block) 23 | 24 | # Solve problem. 25 | KEYS = [[pin.count('#') for pin in key] for key in KEYS] 26 | LOCKS = [[pin.count('#') for pin in lock] for lock in LOCKS] 27 | print("Part 1:", sum(1 if all(a + b <= 7 for a, b in zip(key, lock)) else 0 for key, lock in product(KEYS, LOCKS))) 28 | -------------------------------------------------------------------------------- /2024/starter.py: -------------------------------------------------------------------------------- 1 | import os, sys, re, math, copy, fileinput 2 | from string import ascii_uppercase, ascii_lowercase 3 | from collections import Counter, defaultdict, deque, namedtuple 4 | from itertools import count, product, permutations, combinations, combinations_with_replacement 5 | 6 | import advent 7 | from utils import parse_line, parse_nums, mul, all_unique, factors, memoize, primes, resolve_mapping 8 | from utils import chunks, parts, gcd, lcm, print_grid, min_max_xy 9 | from utils import new_table, transposed, rotated, firsts, lasts 10 | from utils import md5, sha256, VOWELS, CONSONANTS, HASH, polygon_perimeter, polygon_area 11 | from utils import Point, DIRS, DIRS_4, DIRS_8, N, NE, E, SE, S, SW, W, NW 12 | # Itertools Functions: 13 | # product('ABCD', repeat=2) AA AB AC AD BA BB BC BD CA CB CC CD DA DB DC DD 14 | # permutations('ABCD', 2) AB AC AD BA BC BD CA CB CD DA DB DC 15 | # combinations_with_replacement('ABCD', 2) AA AB AC AD BB BC BD CC CD DD 16 | # combinations('ABCD', 2) AB AC AD BC BD CD 17 | 18 | # day .lines .nlines(negs=True) .pars .npars(negs=True) .board .pboard .tboard 19 | 20 | tot = 0 21 | ans = '' 22 | res = [] 23 | 24 | day = advent.Day(year=2024, day=0) 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Kevin Yap 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iKevinY/advent 2 | 3 | My [Advent of Code](https://adventofcode.com) solutions. 4 | 5 | 6 | ## Test Runner 7 | 8 | Under the request of the Advent of Code team, puzzle inputs and outputs 9 | are not being committed to this repo. However, I have written a small 10 | test runner for the puzzles. 11 | 12 | The runner assumes that the input for year `YYYY`, day `DD` is stored 13 | in `YYYY/inputs/DD.txt`, and that the expected output is present in 14 | `YYYY/outputs/DD.txt`. If both files are present, the file will be 15 | tested against the input; it passes if all lines in the output file 16 | are printed to stdout during the execution of the program. 17 | 18 | ## License 19 | 20 | Code in the `2015`, `2016`, `2017`, and `2018` directories are licensed 21 | under the [MIT License](LICENSE). 22 | --------------------------------------------------------------------------------