├── 2015 ├── 2015-README.md ├── advent10.py ├── advent11.py ├── advent12.py ├── advent13.py ├── advent14.py ├── advent15.py ├── advent16.py ├── advent17.py ├── advent18.py ├── advent19.py ├── advent21.py ├── advent22.py ├── advent23.py ├── advent24.py ├── advent25.py ├── advent6.py ├── advent7.py ├── advent8.py └── advent9.py ├── 2016 ├── 2016-README.md ├── puzzle01.py ├── puzzle02.py ├── puzzle03.py ├── puzzle04.py ├── puzzle05.py ├── puzzle06.py ├── puzzle07.py ├── puzzle08.py ├── puzzle09.py ├── puzzle10.py ├── puzzle11.py ├── puzzle12.py ├── puzzle13.py ├── puzzle14.py ├── puzzle15.py ├── puzzle16.py ├── puzzle17.py ├── puzzle18.py ├── puzzle19.py ├── puzzle20.py ├── puzzle21.py ├── puzzle22.py ├── puzzle23.py ├── puzzle24.py └── puzzle25.py ├── 2017 ├── Day 01.ipynb ├── Day 02.ipynb ├── Day 03.ipynb ├── Day 04.ipynb ├── Day 05.ipynb ├── Day 06.ipynb ├── Day 07.ipynb ├── Day 08.ipynb ├── Day 09.ipynb ├── Day 10.ipynb ├── Day 11.ipynb ├── Day 12.ipynb ├── Day 13.ipynb ├── Day 14.ipynb ├── Day 15.ipynb ├── Day 16.ipynb ├── Day 17.ipynb ├── Day 18.ipynb ├── Day 19.ipynb ├── Day 20.ipynb ├── Day 21.ipynb ├── Day 22.ipynb ├── Day 23.ipynb ├── Day 24.ipynb ├── Day 25.ipynb ├── knothash.py └── ruff.toml ├── 2018 ├── Day 01.ipynb ├── Day 02.ipynb ├── Day 03.ipynb ├── Day 04.ipynb ├── Day 05.ipynb ├── Day 06.ipynb ├── Day 07.ipynb ├── Day 08.ipynb ├── Day 09.ipynb ├── Day 10.ipynb ├── Day 11.ipynb ├── Day 12.ipynb ├── Day 13.ipynb ├── Day 14.ipynb ├── Day 15.ipynb ├── Day 16.ipynb ├── Day 17.ipynb ├── Day 18.ipynb ├── Day 19.ipynb ├── Day 20.ipynb ├── Day 21.ipynb ├── Day 22.ipynb ├── Day 23.ipynb ├── Day 24.ipynb └── Day 25.ipynb ├── 2019 ├── Day 01.ipynb ├── Day 02.ipynb ├── Day 03.ipynb ├── Day 04.ipynb ├── Day 05.ipynb ├── Day 06.ipynb ├── Day 07.ipynb ├── Day 08.ipynb ├── Day 09.ipynb ├── Day 10.ipynb ├── Day 11.ipynb ├── Day 12.ipynb ├── Day 13.ipynb ├── Day 14.ipynb ├── Day 15.ipynb ├── Day 16.ipynb ├── Day 17.ipynb ├── Day 18.ipynb ├── Day 19.ipynb ├── Day 20.ipynb ├── Day 21.ipynb ├── Day 22.ipynb ├── Day 23.ipynb ├── Day 24.ipynb ├── Day 25.ipynb ├── aoc201913-breakout.gif ├── intcode.py └── ruff.toml ├── 2020 ├── Day 01.ipynb ├── Day 02.ipynb ├── Day 03.ipynb ├── Day 04.ipynb ├── Day 05.ipynb ├── Day 06.ipynb ├── Day 07.ipynb ├── Day 08.ipynb ├── Day 09.ipynb ├── Day 10.ipynb ├── Day 11.ipynb ├── Day 12.ipynb ├── Day 13.ipynb ├── Day 14.ipynb ├── Day 15.ipynb ├── Day 16.ipynb ├── Day 17.ipynb ├── Day 18.ipynb ├── Day 19.ipynb ├── Day 20.ipynb ├── Day 21.ipynb ├── Day 22.ipynb ├── Day 23.ipynb ├── Day 24.ipynb ├── Day 25.ipynb └── day7-full.svg ├── 2021 ├── Day 01.ipynb ├── Day 02.ipynb ├── Day 03.ipynb ├── Day 04.ipynb ├── Day 05.ipynb ├── Day 06.ipynb ├── Day 07.ipynb ├── Day 08.ipynb ├── Day 09.ipynb ├── Day 10.ipynb ├── Day 11.ipynb ├── Day 12.ipynb ├── Day 13.ipynb ├── Day 14.ipynb ├── Day 15.ipynb ├── Day 16.ipynb ├── Day 17.ipynb ├── Day 18.ipynb ├── Day 19.ipynb ├── Day 20.ipynb ├── Day 21.ipynb ├── Day 22.ipynb ├── Day 23.ipynb ├── Day 24.ipynb └── Day 25.ipynb ├── 2022 ├── Day 01.ipynb ├── Day 02.ipynb ├── Day 03.ipynb ├── Day 04.ipynb ├── Day 05.ipynb ├── Day 06.ipynb ├── Day 07.ipynb ├── Day 08.ipynb ├── Day 09.ipynb ├── Day 10.ipynb ├── Day 11.ipynb ├── Day 12.ipynb ├── Day 13.ipynb ├── Day 14.ipynb ├── Day 15.ipynb ├── Day 16.ipynb ├── Day 17.ipynb ├── Day 18.ipynb ├── Day 19.ipynb ├── Day 20.ipynb ├── Day 21.ipynb ├── Day 22.ipynb ├── Day 23.ipynb ├── Day 24.ipynb └── Day 25.ipynb ├── 2023 ├── Day 01.ipynb ├── Day 02.ipynb ├── Day 03.ipynb ├── Day 04.ipynb ├── Day 05.ipynb ├── Day 06.ipynb ├── Day 07.ipynb ├── Day 08.ipynb ├── Day 09.ipynb ├── Day 10.ipynb ├── Day 11.ipynb ├── Day 12.ipynb ├── Day 13.ipynb ├── Day 14.ipynb ├── Day 15.ipynb ├── Day 16.ipynb ├── Day 17.ipynb ├── Day 18.ipynb └── Day 19.ipynb ├── .flake8 ├── .gitignore ├── LICENSE ├── README.md ├── jupyter_notebook_config.py ├── poetry.lock ├── pyproject.toml ├── rust ├── 2018 │ ├── Cargo.toml │ └── src │ │ ├── day01.rs │ │ ├── day02.rs │ │ ├── day03.rs │ │ └── lib.rs ├── 2019 │ ├── Cargo.toml │ └── src │ │ ├── day01.rs │ │ └── lib.rs ├── 2020 │ ├── Cargo.toml │ └── src │ │ ├── day01.rs │ │ ├── day02.rs │ │ ├── day03.rs │ │ ├── day05.rs │ │ └── lib.rs └── 2021 │ ├── Cargo.toml │ └── src │ ├── day18.rs │ └── lib.rs ├── start └── stop /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203,E501,W503 3 | max-line-length = 80 4 | max-complexity = 12 5 | select = B,C,E,F,W,B9 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | */inputs/ 2 | 3 | rust/input/ 4 | rust/*/input/ 5 | rust/*/target/ 6 | rust/*/Cargo.lock 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | env/ 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | .hypothesis/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # dotenv 90 | .env 91 | 92 | # virtualenv 93 | .venv 94 | venv/ 95 | ENV/ 96 | 97 | # Spyder project settings 98 | .spyderproject 99 | .spyproject 100 | 101 | # Rope project settings 102 | .ropeproject 103 | 104 | # mkdocs documentation 105 | /site 106 | 107 | # mypy 108 | .mypy_cache/ 109 | 110 | # jupyter notebook server logs 111 | jupyter_notebook.log 112 | -------------------------------------------------------------------------------- /2015/2015-README.md: -------------------------------------------------------------------------------- 1 | # 2015 code 2 | 3 | I found my 2015 Python files (or rather, most of them), the first year of AoC. I did solve all 25 days then, but didn't keep the code for days 1-4 and 20, I think I solved them using an interactive Python session. 4 | 5 | These are provided as-is without the input files or validation if they still work. -------------------------------------------------------------------------------- /2015/advent10.py: -------------------------------------------------------------------------------- 1 | # --- Day 10: Elves Look, Elves Say --- 2 | from itertools import groupby 3 | from functools import reduce 4 | 5 | 6 | def looksay(digits): 7 | return ''.join(['{}{}'.format(len(list(group)), d) 8 | for d, group in groupby(digits)]) 9 | 10 | 11 | assert looksay('1') == '11' 12 | assert looksay('11') == '21' 13 | assert looksay('21') == '1211' 14 | assert looksay('1211') == '111221' 15 | assert looksay('111221') == '312211' 16 | 17 | 18 | if __name__ == '__main__': 19 | res1 = reduce(lambda prev, i: looksay(prev), range(40), '1321131112') 20 | print('Part 1:', len(res1)) 21 | # iterate an *additional* 10 times 22 | res2 = reduce(lambda prev, i: looksay(prev), range(10), res1) 23 | print('Part 2:', len(res2)) 24 | -------------------------------------------------------------------------------- /2015/advent11.py: -------------------------------------------------------------------------------- 1 | from string import ascii_lowercase 2 | from itertools import chain, permutations, product 3 | import bisect 4 | import re 5 | import time 6 | 7 | 8 | letters = ''.join([l for l in ascii_lowercase if l not in 'ilo']) 9 | # 21 valid 3-letter consecutive sequences 10 | sequences = [''.join(seq) 11 | for seq in zip( 12 | ascii_lowercase, ascii_lowercase[1:], ascii_lowercase[2:]) 13 | if not any(c in 'ilo' for c in seq)] 14 | 15 | 16 | def generate_next(password): 17 | # no z, this is deliberate! The KeyError signals we need to recurse 18 | next_map = {a: b for a, b in zip(letters, letters[1:])} 19 | # Technically speaking, we would need to account for i, l and o too, 20 | # since Santa may have used those in the old password 21 | next_map.update({'i': 'j', 'l': 'm', 'o': 'p'}) 22 | 23 | def increment(pw): 24 | try: 25 | return pw[:-1] + next_map[pw[-1]] 26 | except KeyError: 27 | return increment(pw[:-1]) + 'a' 28 | 29 | has_seq = re.compile(r'({})'.format('|'.join(sequences))) 30 | has_doubles = re.compile(r'(.)\1.*(.)\2') 31 | 32 | while True: 33 | password = increment(password) 34 | if bool(has_seq.search(password) and has_doubles.search(password)): 35 | return password 36 | 37 | 38 | def generate_all(): 39 | # There are only 15 ^ 5 possible 3-character consequtive sequences in the 40 | # passwords. Combined with the requirement there are 2 doubled letter 41 | # pairs, limits the total to 6712592 (6.7 million) different passwords 42 | doubled = [l + l for l in letters] 43 | 44 | print('Generating all possible passwords') 45 | start = time.time() 46 | all_passwords = sorted(set(chain( 47 | # no overlap between sequence and doubled letters 48 | (''.join(c) 49 | for p in product(sequences, letters, doubled, doubled) 50 | for c in permutations(p)), 51 | # last letter of sequence doubled 52 | (''.join(c) 53 | for p in product((s + s[-1] for s in sequences), 54 | doubled, *[letters] * 2) 55 | for c in permutations(p)), 56 | # first letter of sequence doubled 57 | (''.join(c) 58 | for p in product((s[0] + s for s in sequences), 59 | doubled, *[letters] * 2) 60 | for c in permutations(p)), 61 | # first and last letter of sequence doubled 62 | (''.join(c) 63 | for p in product((s[0] + s + s[-1] for s in sequences), 64 | *[letters] * 3) 65 | for c in permutations(p)), 66 | ))) 67 | print('Done generating all {} possible passwords in {:.3f} seconds'.format( 68 | len(all_passwords), time.time() - start)) 69 | return all_passwords 70 | 71 | 72 | if __name__ == '__main__': 73 | import sys 74 | old_password = sys.argv[-1] 75 | 76 | if '--gen' in sys.argv: 77 | all_passwords = generate_all() 78 | new_password = all_passwords[ 79 | bisect.bisect_right(all_passwords, old_password)] 80 | print('Part 1:', new_password) 81 | next_password = all_passwords[ 82 | bisect.bisect_right(all_passwords, new_password)] 83 | print('Part 2:', next_password) 84 | else: 85 | new_password = generate_next(old_password) 86 | print('Part 1:', new_password) 87 | next_password = generate_next(new_password) 88 | print('Part 2:', next_password) 89 | -------------------------------------------------------------------------------- /2015/advent12.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | 4 | 5 | with open(sys.argv[-1]) as json_data: 6 | data = json.load(json_data) 7 | 8 | 9 | def sum_recurse(d, skip_value=None): 10 | if isinstance(d, list): 11 | return sum(sum_recurse(e, skip_value) for e in d) 12 | if isinstance(d, dict): 13 | if skip_value and skip_value in d.values(): 14 | return 0 15 | return sum(sum_recurse(e, skip_value) for e in d.values()) 16 | if isinstance(d, (int, float)): 17 | return d 18 | return 0 19 | 20 | 21 | print(sum_recurse(data)) 22 | print(sum_recurse(data, 'red')) 23 | -------------------------------------------------------------------------------- /2015/advent13.py: -------------------------------------------------------------------------------- 1 | from itertools import islice, permutations 2 | import re 3 | 4 | 5 | def circular_window(seq, n=2): 6 | it = iter(seq + seq[:n - 1]) 7 | result = tuple(islice(it, n)) 8 | yield result 9 | for elem in it: 10 | result = result[1:] + (elem,) 11 | yield result 12 | 13 | 14 | def calculate_happiness_change(graph, order): 15 | return sum(graph[m][l] + graph[m][r] 16 | for l, m, r in circular_window(order, 3)) 17 | 18 | 19 | def find_max_happiness_change(graph): 20 | return max(calculate_happiness_change(graph, o) 21 | for o in permutations(graph)) 22 | 23 | 24 | def read_input(fileobj): 25 | linepattern = re.compile( 26 | r'(\w+) would (gain|lose) (\d+) happiness units ' 27 | r'by sitting next to (\w+).') 28 | graph = {} 29 | for line in fileobj: 30 | match = linepattern.search(line) 31 | if not match: 32 | continue 33 | name1, direction, change, name2 = match.groups() 34 | change = int(change) if direction == 'gain' else -int(change) 35 | graph.setdefault(name1, {})[name2] = change 36 | return graph 37 | 38 | 39 | if __name__ == '__main__': 40 | import os.path 41 | import sys 42 | filename = sys.argv[-1] 43 | with open(filename) as inf: 44 | graph = read_input(inf) 45 | print('Part 1:', find_max_happiness_change(graph)) 46 | 47 | for name in graph: 48 | graph[name]['Myself'] = 0 49 | graph['Myself'] = dict.fromkeys(graph, 0) 50 | print('Part 2:', find_max_happiness_change(graph)) 51 | 52 | if '--graph' in sys.argv: 53 | dirname, basename = os.path.split(filename) 54 | output = os.path.join(dirname, os.path.splitext(basename)[0] + '.dot') 55 | order = max((o for o in permutations(graph)), 56 | key=lambda o: calculate_happiness_change(graph, o)) 57 | with open(output, 'w') as df: 58 | df.write('graph advent_seating {\nlayout="circo";\n') 59 | for name, toright in circular_window(order, 2): 60 | df.write( 61 | 'n{0} [shape=circle, label="{0}", width=1]\n' 62 | 'n{0} -- n{1} ' 63 | '[headlabel="{2}", taillabel="{3}", ' 64 | 'labeldistance=2];\n'.format( 65 | name, toright, 66 | graph[name][toright], graph[toright][name])) 67 | df.write('}\n') 68 | print('Written graph to', output) 69 | -------------------------------------------------------------------------------- /2015/advent15.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from functools import reduce 3 | from itertools import product 4 | from operator import mul 5 | 6 | 7 | Ingredient = namedtuple( 8 | 'Ingredient', 'capacity durability flavor texture calories') 9 | 10 | 11 | def read_ingredients(fileobj): 12 | results = {} 13 | for line in fileobj: 14 | name, __, properties = line.strip().partition(': ') 15 | properties = {name: int(val) 16 | for prop in properties.split(',') 17 | for name, val in (prop.split(),)} 18 | results[name] = Ingredient(**properties) 19 | return results 20 | 21 | 22 | def optimise_for_score(ingredients, target_calories=None): 23 | max_score = 0 24 | for combo in product(range(100), repeat=len(ingredients) - 1): 25 | if sum(combo) > 100: 26 | continue 27 | combo += ((100 - sum(combo)),) 28 | if target_calories is not None: 29 | calories = sum(ingr.calories * factor 30 | for factor, ingr in zip( 31 | combo, ingredients.values())) 32 | if calories != target_calories: 33 | continue 34 | scores = [sum(ingr[p] * factor 35 | for factor, ingr in zip(combo, ingredients.values())) 36 | for p in range(4)] 37 | if any(s <= 0 for s in scores): 38 | continue 39 | score = reduce(mul, scores) 40 | if score > max_score: 41 | max_score = score 42 | return max_score 43 | 44 | 45 | if __name__ == '__main__': 46 | import sys 47 | 48 | filename = sys.argv[-1] 49 | with open(filename) as ingrf: 50 | ingr = read_ingredients(ingrf) 51 | score = optimise_for_score(ingr) 52 | print('Part 1:', score) 53 | score = optimise_for_score(ingr, 500) 54 | print('Part 2:', score) 55 | -------------------------------------------------------------------------------- /2015/advent16.py: -------------------------------------------------------------------------------- 1 | import os 2 | import operator 3 | 4 | HERE = os.path.dirname(os.path.abspath(__file__)) 5 | 6 | clues = { 7 | 'children': 3, 8 | 'cats': 7, 9 | 'samoyeds': 2, 10 | 'pomeranians': 3, 11 | 'akitas': 0, 12 | 'vizslas': 0, 13 | 'goldfish': 5, 14 | 'trees': 3, 15 | 'cars': 2, 16 | 'perfumes': 1, 17 | } 18 | 19 | 20 | def parse_aunts(fileobj): 21 | for line in fileobj: 22 | name, __, props = line.partition(':') 23 | props = {name.strip(): int(count) 24 | for entry in props.split(',') 25 | for name, count in (entry.split(':'),)} 26 | yield name.strip(), props 27 | 28 | 29 | with open(os.path.join(HERE, 'inputs/input16.txt')) as fileobj: 30 | aunts = list(parse_aunts(fileobj)) 31 | 32 | aunt = next(name for name, props in aunts 33 | if all(props[k] == clues[k] 34 | for k in props.keys() & clues.keys())) 35 | print('Part 1:', aunt) 36 | 37 | comps = { 38 | 'cats': operator.gt, 39 | 'trees': operator.gt, 40 | 'pomeranians': operator.lt, 41 | 'goldfish': operator.lt, 42 | } 43 | aunt = next(name for name, props in aunts 44 | if all(comps.get(k, operator.eq)(props[k], clues[k]) 45 | for k in props.keys() & clues.keys())) 46 | print('Part 2:', aunt) 47 | -------------------------------------------------------------------------------- /2015/advent17.py: -------------------------------------------------------------------------------- 1 | from itertools import combinations 2 | 3 | 4 | def combos_for_size(containers, size): 5 | return sum( 6 | 1 7 | for count in range(len(containers)) 8 | for c in combinations(containers, count + 1) 9 | if sum(c) == size) 10 | 11 | 12 | def combos_for_min_size(containers, size): 13 | min_count = next( 14 | count 15 | for count in range(len(containers)) 16 | for c in combinations(containers, count + 1) 17 | if sum(c) == size) 18 | return sum( 19 | 1 20 | for c in combinations(containers, min_count + 1) 21 | if sum(c) == size) 22 | 23 | 24 | if __name__ == '__main__': 25 | import sys 26 | 27 | filename = sys.argv[-1] 28 | with open(filename) as inputfile: 29 | containers = [int(l) for l in inputfile if l.strip()] 30 | 31 | print('Part 1:', combos_for_size(containers, 150)) 32 | print('Part 2:', combos_for_min_size(containers, 150)) 33 | -------------------------------------------------------------------------------- /2015/advent18.py: -------------------------------------------------------------------------------- 1 | from functools import reduce 2 | from itertools import chain, product 3 | 4 | 5 | def animate(lights): 6 | new_state = [] 7 | for x, row in enumerate(lights): 8 | new_state.append([]) 9 | for y, light in enumerate(row): 10 | count = sum( 11 | lights[x + dx][y + dy] 12 | for dx, dy in product(range(-1, 2), repeat=2) 13 | if (dx or dy) and 14 | 0 <= x + dx < len(lights) and 0 <= y + dy < len(row)) 15 | new_state[-1].append(count in {2, 3} if light else count == 3) 16 | return new_state 17 | 18 | 19 | def animate_stuck(lights): 20 | state = animate(lights) 21 | state[0][0] = state[0][-1] = state[-1][0] = state[-1][-1] = True 22 | return state 23 | 24 | 25 | def read_display(fileobj): 26 | return [[l == '#' for l in line.strip()] for line in fileobj] 27 | 28 | 29 | if __name__ == '__main__': 30 | import sys 31 | filename = sys.argv[-1] 32 | with open(filename) as f: 33 | lights = read_display(f) 34 | endstate = reduce(lambda l, i: animate(l), range(100), lights) 35 | result = sum(chain.from_iterable(endstate)) 36 | print('Part 1:', result) 37 | 38 | endstate = reduce(lambda l, i: animate_stuck(l), range(100), lights) 39 | result = sum(chain.from_iterable(endstate)) 40 | print('Part 2:', result) 41 | -------------------------------------------------------------------------------- /2015/advent19.py: -------------------------------------------------------------------------------- 1 | import re 2 | from itertools import takewhile 3 | from random import shuffle 4 | 5 | 6 | def one_step_substitute(molecule, replacements): 7 | for i, atom in enumerate(molecule): 8 | for subst in replacements.get(atom, []): 9 | yield ''.join(molecule[:i] + [subst] + molecule[i + 1:]) 10 | 11 | 12 | def random_replace(molecule, replacements): 13 | # to hell with finding the right order of substitutions by analysis; 14 | # there is only one solution and the *quickest* way to that solution 15 | # is trying out random substition orderings. 16 | molecule = ''.join(molecule) 17 | reversed = [(v, k) for k in replacements for v in replacements[k]] 18 | steps = 0 19 | target = molecule 20 | while target != 'e': 21 | changed = False 22 | for repl, source in reversed: 23 | if repl in target: 24 | target = target.replace(repl, source, 1) 25 | steps += 1 26 | changed = True 27 | if not changed: 28 | target = molecule 29 | shuffle(reversed) 30 | steps = 0 31 | return steps 32 | 33 | 34 | def read_input(fileobj, _atom=re.compile('[A-Z][a-z]*')): 35 | fileobj = iter(fileobj) 36 | replacements = {} 37 | for line in takewhile(lambda l: l.strip(), fileobj): 38 | source, __, target = line.strip().partition(' => ') 39 | replacements.setdefault(source, []).append(target) 40 | molecule = next(fileobj).strip() 41 | return replacements, _atom.findall(molecule) 42 | 43 | 44 | if __name__ == '__main__': 45 | import sys 46 | filename = sys.argv[-1] 47 | with open(filename) as f: 48 | replacements, molecule = read_input(f) 49 | count = len(set(one_step_substitute(molecule, replacements))) 50 | print('Part 1:', count) 51 | 52 | steps = random_replace(molecule, replacements) 53 | print('Part 2:', steps) 54 | -------------------------------------------------------------------------------- /2015/advent21.py: -------------------------------------------------------------------------------- 1 | from itertools import product, combinations 2 | from collections import namedtuple 3 | import re 4 | 5 | Item = namedtuple('Item', 'name cost damage armour') 6 | conv = lambda n: int(n) if n.isdigit() else n 7 | load = lambda lines: [Item(*map(conv, re.split(r'\s{2,}', l))) for l in lines] 8 | 9 | weapons = load('''\ 10 | Dagger 8 4 0 11 | Shortsword 10 5 0 12 | Warhammer 25 6 0 13 | Longsword 40 7 0 14 | Greataxe 74 8 0 15 | '''.splitlines()) 16 | 17 | # Armour is optional, include a noop. 18 | armour = load('''\ 19 | Leather 13 0 1 20 | Chainmail 31 0 2 21 | Splintmail 53 0 3 22 | Bandedmail 75 0 4 23 | Platemail 102 0 5 24 | None 0 0 0 25 | '''.splitlines()) 26 | 27 | # Rings are optional; we pick between 0 and 2 so include 2 noops. 28 | rings = load('''\ 29 | Damage +1 25 1 0 30 | Damage +2 50 2 0 31 | Damage +3 100 3 0 32 | Defense +1 20 0 1 33 | Defense +2 40 0 2 34 | Defense +3 80 0 3 35 | None 0 0 0 36 | None 0 0 0 37 | '''.splitlines()) 38 | 39 | 40 | def attack(weapon, rings, boss_armour): 41 | return max(1, weapon.damage + sum(r.damage for r in rings) - boss_armour) 42 | 43 | 44 | def defence(armour, rings, boss_attack): 45 | return max(1, boss_attack - armour.armour - sum(r.armour for r in rings)) 46 | 47 | 48 | if __name__ == '__main__': 49 | import sys 50 | filename = sys.argv[-1] 51 | with open(filename) as f: 52 | boss_hp = int(next(f).rpartition(':')[-1]) 53 | boss_attack = int(next(f).rpartition(':')[-1]) 54 | boss_armour = int(next(f).rpartition(':')[-1]) 55 | 56 | player_hp = 100 57 | 58 | min_cost = min( 59 | weapon.cost + armour.cost + sum(r.cost for r in rings) 60 | for weapon, armour, rings in product( 61 | weapons, armour, combinations(rings, 2)) 62 | if (boss_hp // attack(weapon, rings, boss_armour) <= 63 | player_hp // defence(armour, rings, boss_attack))) 64 | print('Part 1:', min_cost) 65 | 66 | max_cost = max( 67 | weapon.cost + armour.cost + sum(r.cost for r in rings) 68 | for weapon, armour, rings in product( 69 | weapons, armour, combinations(rings, 2)) 70 | if (boss_hp // attack(weapon, rings, boss_armour) > 71 | player_hp // defence(armour, rings, boss_attack))) 72 | print('Part 2:', max_cost) 73 | -------------------------------------------------------------------------------- /2015/advent23.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | class CPU(object): 5 | def __init__(self, program, register_a=0): 6 | self.registers = {'a': register_a, 'b': 0} 7 | self.pos = 0 8 | self.program = program 9 | 10 | def run(self): 11 | while 0 <= self.pos < len(self.program): 12 | self.pos = self.execute() 13 | return self.registers['b'] 14 | 15 | def execute(self ): 16 | op, *args = self.program[self.pos] 17 | return getattr(self, 'op_' + op)(*args) 18 | 19 | def op_hlf(self, r): 20 | self.registers[r] >>= 1 21 | return self.pos + 1 22 | 23 | def op_tpl(self, r): 24 | self.registers[r] *= 3 25 | return self.pos + 1 26 | 27 | def op_inc(self, r): 28 | self.registers[r] += 1 29 | return self.pos + 1 30 | 31 | def op_jmp(self, offset): 32 | return self.pos + int(offset) 33 | 34 | def op_jie(self, r, offset): 35 | return self.pos + (int(offset) if not self.registers[r] % 2 else 1) 36 | 37 | def op_jio(self, r, offset): 38 | return self.pos + (int(offset) if self.registers[r] == 1 else 1) 39 | 40 | 41 | if __name__ == '__main__': 42 | import sys 43 | filename = sys.argv[-1] 44 | 45 | with open(filename) as f: 46 | instructions = [re.split(r'[ ,]+', line.strip()) for line in f] 47 | 48 | cpu = CPU(instructions) 49 | register_b = cpu.run() 50 | print('Part 1:', register_b) 51 | 52 | cpu = CPU(instructions, 1) 53 | register_b = cpu.run() 54 | print('Part 2:', register_b) 55 | -------------------------------------------------------------------------------- /2015/advent24.py: -------------------------------------------------------------------------------- 1 | from functools import reduce 2 | from itertools import combinations, groupby 3 | from operator import mul 4 | 5 | 6 | def third_groups(sizes, groups=3, target=None): 7 | if groups == 1: 8 | yield (tuple(sizes),) 9 | return 10 | if target is None: 11 | target = sum(sizes) // groups 12 | for l in range(1, len(sizes)): 13 | for combo in combinations(sizes, l): 14 | if sum(combo) != target: 15 | continue 16 | remaining = set(sizes).difference(combo) 17 | for res in third_groups(remaining, groups - 1, target): 18 | yield (combo,) + res 19 | 20 | 21 | def min_quantum_entanglement(sizes, groups=3): 22 | # the way I build the groups puts the smallest quantum entanglement first 23 | return reduce(mul, next(third_groups(sizes, groups))[0]) 24 | 25 | 26 | if __name__ == '__main__': 27 | import sys 28 | filename = sys.argv[-1] 29 | with open(filename) as f: 30 | sizes = [int(l) for l in f] 31 | 32 | print('Part 1:', min_quantum_entanglement(sizes)) 33 | print('Part 2:', min_quantum_entanglement(sizes, 4)) 34 | -------------------------------------------------------------------------------- /2015/advent25.py: -------------------------------------------------------------------------------- 1 | import re 2 | from functools import reduce 3 | 4 | 5 | def coord_to_count(row, col): 6 | # calculate the ordinal of the given coordinates, counting from 1 7 | return ((col + row - 2) * (col + row - 1) // 2) + col 8 | 9 | 10 | def calculate_code(row, col): 11 | count = coord_to_count(row, col) 12 | return reduce(lambda c, i: c * 252533 % 33554393, range(count - 1), 20151125) 13 | 14 | 15 | if __name__ == '__main__': 16 | import sys 17 | filename = sys.argv[-1] 18 | with open(filename) as f: 19 | line = next(f) 20 | row, col = map(int, re.search(r'row (\d+), column (\d+)', line).groups()) 21 | 22 | print('Part 1:', calculate_code(row, col)) 23 | -------------------------------------------------------------------------------- /2015/advent7.py: -------------------------------------------------------------------------------- 1 | import operator 2 | import re 3 | 4 | 5 | class Expression(object): 6 | _ops = { 7 | 'AND': operator.and_, 8 | 'OR': operator.or_, 9 | 'NOT': lambda x, y: ~y & 0xFFFF, # bitwise inversion, only one operand 10 | 'LSHIFT': lambda x, y: (x << y) & 0xFFFF, 11 | 'RSHIFT': operator.rshift, 12 | None: lambda x, y: x, # direct assignment, only one operand 13 | } 14 | 15 | _line = re.compile( 16 | r'(?:(?P[a-z]+|\d+) )?(?:(?P{}) (?P[a-z]+|\d+) )?' 17 | r'-> (?P[a-z]+)'.format( 18 | '|'.join([k for k in _ops if k is not None]))) 19 | 20 | def __init__(self, op1, operator, op2, target): 21 | self.op1 = int(op1) if op1 and op1.isdigit() else op1 22 | self.op2 = int(op2) if op2 and op2.isdigit() else op2 23 | self.op_name = operator 24 | self.operator = self._ops[operator] 25 | self.target = target 26 | 27 | @classmethod 28 | def from_line(cls, line): 29 | match = cls._line.search(line) 30 | if not match: 31 | raise ValueError('not a valid expression') 32 | return cls(**match.groupdict()) 33 | 34 | def __repr__(self): 35 | return ( 36 | '{}("{s.op1!r} {s.op_name!r} {s.op2!r} -> {s.target!r}")'.format( 37 | type(self).__name__, s=self)) 38 | 39 | def __call__(self, expressions, _cache): 40 | if self.target in _cache: 41 | return _cache[self.target] 42 | op1, op2 = ( 43 | expressions[op](expressions, _cache) if isinstance(op, str) else op 44 | for op in (self.op1, self.op2)) 45 | res = _cache[self.target] = self.operator(op1, op2) 46 | return res 47 | 48 | 49 | class Solver(object): 50 | def __init__(self): 51 | # target -> expression. All targets are unique 52 | self.expressions = {} 53 | 54 | def parse_instruction(self, line): 55 | expr = Expression.from_line(line) 56 | self.expressions[expr.target] = expr 57 | 58 | def solve(self, target='a'): 59 | return self.expressions[target](self.expressions, {}) 60 | 61 | def to_dot_graph(self, output): 62 | with open(output, 'w') as df: 63 | df.write('digraph advent_expression {\n') 64 | for node in self.expressions.values(): 65 | df.write( 66 | 'n{0} [shape=box,label="{0}"]\n' 67 | 'n{0}_expr [shape=plaintext,label="{1}"]\n' 68 | 'n{0} -> n{0}_expr\n'.format( 69 | node.target, node.op_name or '=')) 70 | for label, op in (('l', node.op1), ('r', node.op2)): 71 | if op is None: 72 | continue 73 | if isinstance(op, int): 74 | # Numbers are repeated 75 | df.write( 76 | 'n{0}{1} [shape=circle,label="{1}"]\n' 77 | 'n{0}_expr -> n{0}{1} [label="{2}"]\n'.format( 78 | node.target, op, label)) 79 | else: 80 | df.write('n{0}_expr -> n{1} [label="{2}"]\n'.format( 81 | node.target, op, label)) 82 | df.write('}\n') 83 | 84 | 85 | if __name__ == '__main__': 86 | import sys 87 | import os.path 88 | 89 | filename = sys.argv[-1] 90 | 91 | solver = Solver() 92 | with open(filename) as inputfile: 93 | for line in inputfile: 94 | solver.parse_instruction(line) 95 | result = solver.solve() 96 | print('Part 1:', result) 97 | 98 | if '--graph' in sys.argv: 99 | dirname, base = os.path.split(filename) 100 | output = os.path.join( 101 | dirname, os.path.splitext(base)[0] + '.dot') 102 | solver.to_dot_graph(output) 103 | print('Saved graph to', output) 104 | 105 | solver.expressions['b'].op1 = result 106 | print('Part 2:', solver.solve()) 107 | -------------------------------------------------------------------------------- /2015/advent8.py: -------------------------------------------------------------------------------- 1 | import os 2 | from ast import literal_eval 3 | 4 | here = os.path.dirname(os.path.abspath(__file__)) 5 | filename = os.path.join(here, 'inputs/input8.txt') 6 | 7 | # quick and dirty way; have Python do the parsing 8 | diff1 = diff2 = 0 9 | for l in open(filename): 10 | diff1 += len(l) - len(literal_eval(l)) 11 | diff2 += 2 + l.count('"') + l.count('\\') 12 | 13 | print('Part 1:', diff1) 14 | print('Part 2:', diff2) 15 | -------------------------------------------------------------------------------- /2015/advent9.py: -------------------------------------------------------------------------------- 1 | # Travelling salesm^WSanta. 2 | # Just bruteforce it; it's only 7! 3 | import re 4 | from itertools import permutations 5 | 6 | 7 | def shortest(g): 8 | return min( 9 | sum(g[a][b] for a, b in zip(path, path[1:])) 10 | for path in permutations(g)) 11 | 12 | 13 | def longest(g): 14 | return max( 15 | sum(g[a][b] for a, b in zip(path, path[1:])) 16 | for path in permutations(g)) 17 | 18 | 19 | def read_graph(fileobj): 20 | parse_line = re.compile(r'(\w+) to (\w+) = (\d+)') 21 | g = {} 22 | for line in fileobj: 23 | m = parse_line.search(line) 24 | if not m: 25 | continue 26 | from_, to, dist = m.groups() 27 | g.setdefault(from_, {})[to] = int(dist) 28 | g.setdefault(to, {})[from_] = int(dist) 29 | return g 30 | 31 | 32 | if __name__ == '__main__': 33 | import sys 34 | filename = sys.argv[1] 35 | with open(filename) as f: 36 | g = read_graph(f) 37 | print('Part 1:', shortest(g)) 38 | print('Part 2:', longest(g)) 39 | -------------------------------------------------------------------------------- /2016/2016-README.md: -------------------------------------------------------------------------------- 1 | # 2016 code 2 | 3 | These are my 2016 Python files, which at the time I didn't share beyond a few gists. These are provided as-is without the input files or validation if they still work. -------------------------------------------------------------------------------- /2016/puzzle01.py: -------------------------------------------------------------------------------- 1 | instructions = '''\ 2 | R5, L2, L1, R1, R3, R3, L3, R3, R4, L2, R4, L4, R4, R3, L2, L1, L1, R2, R4, 3 | R4, L4, R3, L2, R1, L4, R1, R3, L5, L4, L5, R3, L3, L1, L1, R4, R2, R2, L1, 4 | L4, R191, R5, L2, R46, R3, L1, R74, L2, R2, R187, R3, R4, R1, L4, L4, L2, R4, 5 | L5, R4, R3, L2, L1, R3, R3, R3, R1, R1, L4, R4, R1, R5, R2, R1, R3, L4, L2, 6 | L2, R1, L3, R1, R3, L5, L3, R5, R3, R4, L1, R3, R2, R1, R2, L4, L1, L1, R3, 7 | L3, R4, L2, L4, L5, L5, L4, R2, R5, L4, R4, L2, R3, L4, L3, L5, R5, L4, L2, 8 | R3, R5, R5, L1, L4, R3, L1, R2, L5, L1, R4, L1, R5, R1, L4, L4, L4, R4, R3, 9 | L5, R1, L3, R4, R3, L2, L1, R1, R2, R2, R2, L1, L1, L2, L5, L3, L1 10 | '''.split() 11 | 12 | 13 | class Position: 14 | hori_adjust = [1, 0, -1, 0] # for all 4 directions 15 | vert_adjust = [0, 1, 0, -1] # ditto 16 | 17 | def __init__(self): 18 | self.x, self.y = 0, 0 19 | self._direction = 0 # North 0, East 1, South 2, West 3 20 | self._visited = set() 21 | 22 | def do_step(self, instr): 23 | rotation = -1 if instr[0] == 'L' else 1 24 | steps = int(instr[1:].rstrip(',')) 25 | self._direction = (self._direction + rotation) % 4 26 | for _ in range(steps): 27 | self.x += self.hori_adjust[self._direction] 28 | self.y += self.vert_adjust[self._direction] 29 | if (self.x, self.y) in self._visited: 30 | return True 31 | self._visited.add((self.x, self.y)) 32 | 33 | def distance(self): 34 | return abs(self.x) + abs(self.y) 35 | 36 | 37 | pos = Position() 38 | for inst in instructions: 39 | if pos.do_step(inst): 40 | break 41 | print(pos.distance()) 42 | -------------------------------------------------------------------------------- /2016/puzzle02.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | HERE = os.path.dirname(os.path.abspath(__file__)) 3 | 4 | 5 | class PadSimple: 6 | keys = '123456789' 7 | move_deltas = {'U': -3, 'D': 3, 'L': -1, 'R': 1} 8 | illegal = {('U', 0, None), ('D', 2, None), ('L', None, 0), ('R', None, 2)} 9 | legal_moves = {} 10 | for direction, delta in move_deltas.items(): 11 | for row in range(3): 12 | for col in range(3): 13 | if illegal & {(direction, row, None), (direction, None, col)}: 14 | continue 15 | pos = row * 3 + col 16 | legal_moves[pos, direction] = pos + delta 17 | 18 | def __init__(self): 19 | self._pos = self.keys.index('5') 20 | 21 | def move(self, instructions): 22 | for instr in instructions: 23 | newpos = self.legal_moves.get((self._pos, instr)) 24 | if newpos is not None: 25 | self._pos = newpos 26 | return self.keys[self._pos] 27 | 28 | 29 | class PadConvoluted(PadSimple): 30 | keys = '123456789ABCD' 31 | legal_moves = { 32 | (0, 'D'): 2, 33 | (1, 'R'): 2, (1, 'D'): 5, 34 | (2, 'U'): 0, (2, 'R'): 3, (2, 'D'): 6, (2, 'L'): 1, 35 | (3, 'D'): 7, (3, 'L'): 2, 36 | (4, 'R'): 5, 37 | (5, 'U'): 1, (5, 'R'): 6, (5, 'D'): 9, (5, 'L'): 4, 38 | (6, 'U'): 2, (6, 'R'): 7, (6, 'D'): 10, (6, 'L'): 5, 39 | (7, 'U'): 3, (7, 'R'): 8, (7, 'D'): 11, (7, 'L'): 6, 40 | (8, 'L'): 7, 41 | (9, 'U'): 5, (9, 'R'): 10, 42 | (10, 'U'): 6, (10, 'R'): 11, (10, 'D'): 12, (10, 'L'): 9, 43 | (11, 'U'): 7, (11, 'L'): 10, 44 | (12, 'U'): 10, 45 | } 46 | 47 | def is_inbounds(self, newpos, samerow=False): 48 | if not 0 <= newpos < len(self.keys): 49 | # went off the pad 50 | return False 51 | if samerow: 52 | return (newpos // 3) == self._pos // 3 53 | return True 54 | 55 | 56 | def find_bathroom_code(pad, directions): 57 | code = [] 58 | for line in directions: 59 | num = pad.move(line.strip()) 60 | code.append(num) 61 | return ''.join(map(str, code)) 62 | 63 | 64 | sample = '''\ 65 | ULL 66 | RRDDD 67 | LURDL 68 | UUUUD 69 | ''' 70 | 71 | print('Star 1 sample code:', 72 | find_bathroom_code(PadSimple(), sample.splitlines())) 73 | 74 | with open(os.path.join(HERE, 'puzzle02_input.txt')) as input: 75 | print('Star 1 code:', 76 | find_bathroom_code(PadSimple(), input)) 77 | 78 | print('Star 2 sample code:', 79 | find_bathroom_code(PadConvoluted(), sample.splitlines())) 80 | 81 | with open(os.path.join(HERE, 'puzzle02_input.txt')) as input: 82 | print('Star 2 code:', 83 | find_bathroom_code(PadConvoluted(), input)) 84 | -------------------------------------------------------------------------------- /2016/puzzle03.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | from itertools import islice 3 | 4 | HERE = os.path.dirname(os.path.abspath(__file__)) 5 | 6 | 7 | def possible(*sides, pos=set(range(3))): 8 | return all(sum(sides[i] for i in (pos - {third})) > sides[third] 9 | for third in pos) 10 | 11 | count = 0 12 | with open(os.path.join(HERE, 'puzzle03_input.txt')) as triangles: 13 | for line in triangles: 14 | if not line.strip(): 15 | continue 16 | sides = map(int, line.split()) 17 | if possible(*sides): 18 | count += 1 19 | 20 | print('Star 1:', count) 21 | 22 | count = 0 23 | with open(os.path.join(HERE, 'puzzle03_input.txt')) as triangles: 24 | while True: 25 | cols = map(str.split, islice(triangles, 3)) 26 | triangle_sides = [map(int, row) for row in zip(*cols)] 27 | if not triangle_sides: 28 | break 29 | for sides in triangle_sides: 30 | if possible(*sides): 31 | count += 1 32 | 33 | print('Star 2:', count) 34 | -------------------------------------------------------------------------------- /2016/puzzle04.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | from collections import Counter 3 | from heapq import nsmallest 4 | from string import ascii_lowercase 5 | 6 | HERE = os.path.dirname(os.path.abspath(__file__)) 7 | 8 | 9 | def name_parts(roomname): 10 | name, _, checksum = roomname.partition('[') 11 | checksum = list(checksum.rstrip(']\n')) 12 | name, _, sector = name.rpartition('-') 13 | return name, int(sector), checksum 14 | 15 | 16 | def is_real(roomname): 17 | name, sector, checksum = name_parts(roomname) 18 | letter_counts = Counter(name.replace('-', '')) 19 | most_common = nsmallest( 20 | 5, letter_counts, key=lambda i: (-letter_counts[i], i)) 21 | return most_common == checksum 22 | 23 | 24 | def decrypt(roomname): 25 | name, sector, _ = name_parts(roomname) 26 | shift = sector % 26 27 | key = (ascii_lowercase * 2)[shift:shift + 26] 28 | return name.translate(str.maketrans(ascii_lowercase + '-', key + ' ')) 29 | 30 | 31 | with open(os.path.join(HERE, 'puzzle04_input.txt')) as rooms: 32 | total = sum(name_parts(room)[1] for room in rooms if is_real(room)) 33 | print('Star 1:', total) 34 | 35 | with open(os.path.join(HERE, 'puzzle04_input.txt')) as rooms: 36 | for room in rooms: 37 | if is_real(room): 38 | name = decrypt(room) 39 | if name == 'northpole object storage': 40 | print('Star 2:', name_parts(room)[1]) 41 | break 42 | -------------------------------------------------------------------------------- /2016/puzzle05.py: -------------------------------------------------------------------------------- 1 | from itertools import count 2 | from hashlib import md5 3 | 4 | 5 | def doorcharacters(doorid): 6 | doorid_templ = '{}%d'.format(doorid).encode('ascii') 7 | counter = count() 8 | while True: 9 | hashhex = md5(doorid_templ % next(counter)).hexdigest() 10 | if hashhex[:5] == '00000': 11 | yield hashhex[5], hashhex[6] 12 | 13 | 14 | doorid = 'ojvtpuvg' 15 | charpos = doorcharacters(doorid) 16 | 17 | star1result = [] 18 | star2result = [None] * 8 19 | star2found = 0 20 | while star2found < 8: 21 | pos, char = next(charpos) 22 | if len(star1result) < 8: 23 | star1result.append(pos) 24 | pos = int(pos, 16) 25 | if pos < 8 and star2result[pos] is None: 26 | star2result[pos] = char 27 | star2found += 1 28 | 29 | print('Star 1:', ''.join(star1result)) 30 | print('Star 2:', ''.join(star2result)) 31 | -------------------------------------------------------------------------------- /2016/puzzle06.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | from collections import Counter 3 | 4 | HERE = os.path.dirname(os.path.abspath(__file__)) 5 | 6 | 7 | def decode_by_freq(lines): 8 | counts = [] 9 | for line in lines: 10 | line = line.strip() 11 | if not counts: 12 | counts = [Counter() for _ in range(len(line))] 13 | for c, counter in zip(line, counts): 14 | counter[c] += 1 15 | most_common = ''.join([max(count, key=count.get) for count in counts]) 16 | least_common = ''.join([min(count, key=count.get) for count in counts]) 17 | return most_common, least_common 18 | 19 | 20 | testdata = '''\ 21 | eedadn 22 | drvtee 23 | eandsr 24 | raavrd 25 | atevrs 26 | tsrnev 27 | sdttsa 28 | rasrtv 29 | nssdts 30 | ntnada 31 | svetve 32 | tesnvt 33 | vntsnd 34 | vrdear 35 | dvrsen 36 | enarar 37 | '''.splitlines() 38 | 39 | testmost, testleast = decode_by_freq(testdata) 40 | print('Test:', testmost, testleast) 41 | 42 | with open(os.path.join(HERE, 'puzzle06_input.txt')) as messages: 43 | most, least = decode_by_freq(messages) 44 | print('Star 1:', most) 45 | print('Star 2:', least) 46 | -------------------------------------------------------------------------------- /2016/puzzle07.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import os.path 3 | import re 4 | import sys 5 | 6 | HERE = os.path.dirname(os.path.abspath(__file__)) 7 | 8 | Net = enum.Enum('Net', 'HYPERNET SUPERNET') 9 | 10 | 11 | class _Token: 12 | def __init__(self, value, net): 13 | self.value = value 14 | self.net = net 15 | 16 | def is_abba(self, _pat=re.compile(r'([a-z])([a-z])\2\1')): 17 | candidate = _pat.search(self.value) 18 | return bool(candidate and candidate.group(1) != candidate.group(2)) 19 | 20 | def abas(self, _pat=re.compile(r'([a-z])(?=([a-z])\1)')): 21 | for candidate in _pat.finditer(self.value): 22 | if candidate.group(1) != candidate.group(2): 23 | yield candidate.group(1, 2) 24 | 25 | 26 | class Hostname: 27 | def __init__(self, hostname): 28 | self.hostname = hostname 29 | 30 | def _tokens(self, _tokenize=re.compile(r'([\[\]])').split): 31 | inside_brackets = False 32 | for token in _tokenize(self.hostname): 33 | if token == '[': 34 | inside_brackets = True 35 | elif token == ']': 36 | inside_brackets = False 37 | else: 38 | yield _Token( 39 | token, Net.HYPERNET if inside_brackets else Net.SUPERNET) 40 | 41 | def supports_tls(self): 42 | found_abba = False 43 | for token in self._tokens(): 44 | if token.is_abba(): 45 | if token.net is Net.HYPERNET: 46 | return False 47 | found_abba = True 48 | return found_abba 49 | 50 | def supports_ssl(self): 51 | aba_candidates = [] 52 | hypernet_sequences = [] 53 | for token in self._tokens(): 54 | if token.net is Net.SUPERNET: 55 | aba_candidates.extend(token.abas()) 56 | else: 57 | hypernet_sequences.append(token) 58 | for aba in aba_candidates: 59 | bab = aba[::-1] 60 | if any(bab in token.abas() for token in hypernet_sequences): 61 | return True 62 | return False 63 | 64 | 65 | def test(hostname, expected, m): 66 | result = getattr(hostname, m)() 67 | if result != expected: 68 | print('{} FAIL: {.hostname} {} != {}'.format( 69 | m, hostname, result, expected)) 70 | else: 71 | print('{} PASS: {.hostname} {}'.format(m, hostname, result)) 72 | 73 | 74 | def test_tls(): 75 | testdata = ( 76 | ('abba[mnop]qrst', True), 77 | ('abcd[bddb]xyyx', False), 78 | ('aaaa[qwer]tyui', False), 79 | ('ioxxoj[asdfgh]zxcvbn', True), 80 | ) 81 | for hostname, expected in testdata: 82 | test(Hostname(hostname), expected, 'supports_tls') 83 | 84 | 85 | def test_ssl(): 86 | testdata = ( 87 | ('aba[bab]xyz', True), 88 | ('xyx[xyx]xyx', False), 89 | ('aaa[kek]eke', True), 90 | ('zazbz[bzb]cdb', True), 91 | ) 92 | for hostname, expected in testdata: 93 | test(Hostname(hostname), expected, 'supports_ssl') 94 | 95 | 96 | if __name__ == '__main__': 97 | if '-t' in sys.argv[1:]: 98 | test_tls() 99 | test_ssl() 100 | sys.exit(0) 101 | 102 | tls_count = 0 103 | ssl_count = 0 104 | with open(os.path.join(HERE, 'puzzle07_input.txt')) as hostnames: 105 | for line in hostnames: 106 | hostname = Hostname(line.strip()) 107 | tls_count += hostname.supports_tls() 108 | ssl_count += hostname.supports_ssl() 109 | 110 | print('Star 1:', tls_count) 111 | print('Star 2:', ssl_count) 112 | -------------------------------------------------------------------------------- /2016/puzzle08.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.6 2 | 3 | import os.path 4 | import re 5 | 6 | from itertools import chain, islice 7 | 8 | HERE = os.path.dirname(os.path.abspath(__file__)) 9 | 10 | 11 | class Display: 12 | def __init__(self, width=50, height=6): 13 | self.pixels = [['.'] * width for _ in range(height)] 14 | 15 | def voltage(self): 16 | return sum(1 for p in chain.from_iterable(self.pixels) if p == '#') 17 | 18 | def rect(self, a, b): 19 | for row in range(b): 20 | self.pixels[row][:a] = ['#'] * a 21 | 22 | def rotate_row(self, row, b): 23 | self.pixels[row] = self.pixels[row][-b:] + self.pixels[row][:-b] 24 | 25 | def rotate_column(self, col, b): 26 | column = next(islice(zip(*self.pixels), col, None)) 27 | for row, newval in zip(self.pixels, column[-b:] + column[:-b]): 28 | row[col] = newval 29 | 30 | def __str__(self): 31 | return '\n'.join([''.join(row) for row in self.pixels]) 32 | 33 | _commands = ( 34 | (re.compile(r'rect (\d+)x(\d+)'), rect), 35 | (re.compile(r'rotate column x=(\d+) by (\d+)'), rotate_column), 36 | (re.compile(r'rotate row y=(\d+) by (\d+)'), rotate_row), 37 | ) 38 | 39 | def apply_command(self, command): 40 | for pattern, method in self._commands: 41 | match = pattern.search(command) 42 | if match: 43 | method(self, *(map(int, match.groups()))) 44 | return 45 | 46 | 47 | def test(): 48 | d = Display(7, 3) 49 | 50 | d.apply_command('rect 3x2') 51 | assert str(d) == '###....\n###....\n.......' 52 | 53 | d.apply_command('rotate column x=1 by 1') 54 | assert str(d) == '#.#....\n###....\n.#.....' 55 | 56 | d.apply_command('rotate row y=0 by 4') 57 | assert str(d) == '....#.#\n###....\n.#.....' 58 | 59 | d.apply_command('rotate column x=1 by 1') 60 | assert str(d) == '.#..#.#\n#.#....\n.#.....' 61 | 62 | assert d.voltage() == 6 63 | 64 | 65 | if __name__ == '__main__': 66 | import sys 67 | import time 68 | if '-t' in sys.argv: 69 | test() 70 | sys.exit(0) 71 | 72 | animate = '-p' in sys.argv 73 | d = Display() 74 | with open(os.path.join(HERE, 'puzzle08_input.txt')) as commands: 75 | if animate: 76 | print('\n' * 5) 77 | for command in commands: 78 | d.apply_command(command) 79 | if animate: 80 | print('\u001b[50D\u001b[7A') 81 | print(str(d).translate({46: 32, 35: 0x2588})) 82 | time.sleep(0.05) 83 | if not animate: 84 | print('Star 1:', d.voltage()) 85 | print('Star 2:', d, sep='\n') 86 | -------------------------------------------------------------------------------- /2016/puzzle09.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | 3 | 4 | HERE = os.path.dirname(os.path.abspath(__file__)) 5 | 6 | 7 | def read_number(b, terminator): 8 | pos = 0 9 | while pos < len(b) and b[pos] != terminator: 10 | pos += 1 11 | return int(b[:pos].tobytes()), pos + 1 12 | 13 | 14 | def decompress_length(b, version=1): 15 | b = memoryview(b) 16 | pos = 0 17 | while pos < len(b): 18 | if b[pos] != 40: # ( 19 | yield 1 20 | pos += 1 21 | continue 22 | length, delta = read_number(b[pos + 1:], 120) # x 23 | pos += delta + 1 24 | count, delta = read_number(b[pos:], 41) # ) 25 | pos += delta 26 | if version == 1: 27 | yield count * length 28 | else: 29 | yield count * sum(decompress_length(b[pos:pos + length], version)) 30 | pos += length 31 | 32 | 33 | def test(): 34 | tests = { 35 | b'ADVENT': (1, len('ADVENT')), 36 | b'A(1x5)BC': (1, len('ABBBBBC')), 37 | b'(3x3)XYZ': (1, len('XYZXYZXYZ')), 38 | b'A(2x2)BCD(2x2)EFG': (1, len('ABCBCDEFEFG')), 39 | b'(6x1)(1x3)A': (1, len('(1x3)A')), 40 | b'X(8x2)(3x3)ABCY': (1, len('X(3x3)ABC(3x3)ABCY')), 41 | b'(3x3)XYZ': (2, len('XYZXYZXYZ')), 42 | b'X(8x2)(3x3)ABCY': (2, len('XABCABCABCABCABCABCY')), 43 | b'(27x12)(20x12)(13x14)(7x10)(1x12)A': (2, 241920), 44 | b'(25x3)(3x3)ABC(2x3)XY(5x2)PQRSTX(18x9)(3x2)TWO(5x7)SEVEN': (2, 445), 45 | } 46 | for test, (version, expected) in tests.items(): 47 | result = sum(decompress_length(test, version)) 48 | assert result == expected, (test, expected, result) 49 | 50 | 51 | if __name__ == '__main__': 52 | import sys 53 | if '-t' in sys.argv: 54 | test() 55 | sys.exit(0) 56 | 57 | with open(os.path.join(HERE, 'puzzle09_input.txt'), 'rb') as dataf: 58 | data = dataf.read().rstrip() 59 | 60 | length = sum(decompress_length(data)) 61 | print('Star 1:', length) 62 | 63 | length = sum(decompress_length(data, 2)) 64 | print('Star 2:', length) 65 | -------------------------------------------------------------------------------- /2016/puzzle10.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import re 3 | from collections import deque 4 | 5 | 6 | HERE = os.path.dirname(os.path.abspath(__file__)) 7 | 8 | 9 | def create_bot(source, low, high): 10 | def bot(namespace): 11 | chips = namespace.get(source) 12 | if chips is not None and len(chips) > 1: 13 | return {high: max(chips), low: min(chips)} 14 | bot.__qualname__ = source 15 | return bot 16 | 17 | 18 | _input = re.compile('value (\d+) goes to ((?:bot|output) \d+)') 19 | _bot = re.compile('(bot \d+) gives low to ((?:bot|output) \d+) ' 20 | 'and high to ((?:bot|output) \d+)') 21 | 22 | 23 | def solve(instructions): 24 | namespace = {} 25 | bots = deque() 26 | for instruction in instructions: 27 | input = _input.search(instruction) 28 | if input: 29 | value, target = input.groups() 30 | namespace.setdefault(target, []).append(int(value)) 31 | continue 32 | source, low, high = _bot.search(instruction).groups() 33 | bots.append(create_bot(source, low, high)) 34 | 35 | while bots: 36 | bot = bots.popleft() 37 | output = bot(namespace) 38 | if output: 39 | for target, value in output.items(): 40 | namespace.setdefault(target, []).append(value) 41 | else: 42 | bots.append(bot) 43 | 44 | return namespace 45 | 46 | 47 | def test(): 48 | test_instructions = '''\ 49 | value 5 goes to bot 2 50 | bot 2 gives low to bot 1 and high to bot 0 51 | value 3 goes to bot 1 52 | bot 1 gives low to output 1 and high to bot 0 53 | bot 0 gives low to output 2 and high to output 0 54 | value 2 goes to bot 2 55 | '''.splitlines(True) 56 | namespace = solve(test_instructions) 57 | assert namespace['output 0'] == [5] 58 | assert namespace['output 1'] == [2] 59 | assert namespace['output 2'] == [3] 60 | 61 | 62 | if __name__ == '__main__': 63 | import sys 64 | if '-t' in sys.argv: 65 | test() 66 | sys.exit(0) 67 | 68 | with open(os.path.join(HERE, 'puzzle10_input.txt'), 'r') as instructions: 69 | namespace = solve(instructions) 70 | 71 | botname = next(target for target, chips in namespace.items() 72 | if set(chips) == {17, 61}) 73 | print('Star 1:', botname) 74 | out1, out2, out3 = (namespace['output {}'.format(i)][0] for i in range(3)) 75 | print('Star 2:', out1 * out2 * out3) 76 | -------------------------------------------------------------------------------- /2016/puzzle11.py: -------------------------------------------------------------------------------- 1 | from itertools import accumulate 2 | 3 | 4 | def calc_moves(l): 5 | """ 6 | To move x items up one floor takes 2 * (x - 1) - 1 moves. 7 | 8 | So all we have to do is move those items up one floor, then calculate 9 | the number of moves needed to move those up together with what is already 10 | on that floor, etc. 11 | 12 | Input is a list of item counts per floor 13 | 14 | """ 15 | return sum(2 * (c - 1) - 1 for c in accumulate(l)) 16 | 17 | 18 | if __name__ == '__main__': 19 | # only the item count per floor matters. We don't care what's up on the top 20 | # floor either, that's already on the top floor. 21 | star1_input = [4, 2, 4] 22 | print('Star 1:', calc_moves(star1_input)) 23 | star2_input = [8, 2, 4] 24 | print('Star 2:', calc_moves(star2_input)) 25 | -------------------------------------------------------------------------------- /2016/puzzle12.py: -------------------------------------------------------------------------------- 1 | class BunnyProc: 2 | def __init__(self, print=False): 3 | self.registers = {'a': 0, 'b': 0, 'c': 0, 'd': 0} 4 | self.pos = 0 5 | self.print = print 6 | 7 | def execute(self, assembunny): 8 | self.assembunny = [instruction.split() for instruction in assembunny] 9 | self.pos = 0 10 | while self.pos < len(self.assembunny): 11 | op, *operands = self.assembunny[self.pos] 12 | getattr(self, 'op_' + op)(*operands) 13 | if self.print: 14 | print('\rpos: {0} a: {a} b: {b} c: {c} d: {d}'.format( 15 | self.pos, **self.registers), 16 | ' ' * 40, 17 | end='') 18 | 19 | def register_or_int(self, x): 20 | return self.registers[x] if x in self.registers else int(x) 21 | 22 | def op_cpy(self, x, y): 23 | try: 24 | self.registers[y] = self.register_or_int(x) 25 | except (KeyError, ValueError): 26 | pass 27 | self.pos += 1 28 | 29 | def op_inc(self, x): 30 | try: 31 | self.registers[x] += 1 32 | except KeyError: 33 | pass 34 | self.pos += 1 35 | 36 | def op_dec(self, x): 37 | try: 38 | self.registers[x] -= 1 39 | except KeyError: 40 | pass 41 | self.pos += 1 42 | 43 | def op_jnz(self, x, y): 44 | try: 45 | delta = self.register_or_int(y) if self.register_or_int(x) else 1 46 | self.pos += delta 47 | except (KeyError, ValueError): 48 | self.pos += 1 49 | 50 | 51 | def test(): 52 | proc = BunnyProc() 53 | proc.execute('''\ 54 | cpy 41 a 55 | inc a 56 | inc a 57 | dec a 58 | jnz a 2 59 | dec a 60 | '''.splitlines(True)) 61 | assert proc.registers['a'] == 42 62 | 63 | 64 | if __name__ == '__main__': 65 | import sys 66 | if '-t' in sys.argv: 67 | test() 68 | sys.exit(0) 69 | 70 | instructions = '''\ 71 | cpy 1 a 72 | cpy 1 b 73 | cpy 26 d 74 | jnz c 2 75 | jnz 1 5 76 | cpy 7 c 77 | inc d 78 | dec c 79 | jnz c -2 80 | cpy a c 81 | inc a 82 | dec b 83 | jnz b -2 84 | cpy c b 85 | dec d 86 | jnz d -6 87 | cpy 16 c 88 | cpy 17 d 89 | inc a 90 | dec d 91 | jnz d -2 92 | dec c 93 | jnz c -5 94 | '''.splitlines() 95 | proc = BunnyProc() 96 | proc.execute(instructions) 97 | print('Star 1:', proc.registers['a']) 98 | 99 | proc = BunnyProc() 100 | proc.execute(['cpy 1 c'] + instructions) 101 | print('Star 2:', proc.registers['a']) 102 | -------------------------------------------------------------------------------- /2016/puzzle13.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | from heapq import heapify, heappush, heappop 3 | from itertools import count 4 | 5 | DESIGNER_FAVOURITE = 1358 6 | 7 | 8 | class PriorityQueue: 9 | def __init__(self, *initial): 10 | self._queue = [] 11 | self._count = count() 12 | for pri, item in initial: 13 | self.put(pri, item) 14 | heapify(self._queue) 15 | 16 | def __len__(self): 17 | return len(self._queue) 18 | 19 | def put(self, pri, item): 20 | heappush(self._queue, (pri, next(self._count), item)) 21 | 22 | def get(self): 23 | if not self: 24 | raise ValueError('Queue is empty') 25 | return heappop(self._queue)[-1] 26 | 27 | 28 | def is_open(x, y): 29 | if x < 0 or y < 0: 30 | return False 31 | num = x * x + 3 * x + 2 * x * y + y + y * y 32 | num += DESIGNER_FAVOURITE 33 | return format(num, 'b').count('1') % 2 == 0 34 | 35 | 36 | class MazeState: 37 | def __init__(self, x, y, steps=0): 38 | self.x, self.y = x, y 39 | self.steps = steps 40 | 41 | def __eq__(self, other): 42 | return (self.x, self.y) == (other.x, other.y) 43 | 44 | def __hash__(self): 45 | return hash(self.x) ^ hash(self.y) 46 | 47 | def __repr__(self): 48 | return ''.format(self) 49 | 50 | def moves(self): 51 | for dx, dy in ((-1, 0), (1, 0), (0, -1), (0, 1)): 52 | x, y = self.x + dx, self.y + dy 53 | if is_open(x, y): 54 | yield MazeState(x, y, self.steps + 1) 55 | 56 | def heuristic(self, target): 57 | return abs(self.x - target.x) + abs(self.y - target.y) + self.steps 58 | 59 | 60 | def shortest_path(start, goal): 61 | queue = PriorityQueue((start.heuristic(goal), start)) 62 | open_ = {start: 0} 63 | closed = set() 64 | 65 | while open_: 66 | current = queue.get() 67 | if open_.get(current) != current.steps: 68 | # ignore items in the queue for which a shorter 69 | # path exists 70 | continue 71 | 72 | if current == goal: 73 | return current.steps 74 | 75 | del open_[current] 76 | closed.add(current) 77 | for neighbor in current.moves(): 78 | if neighbor in closed: 79 | continue 80 | if neighbor.steps >= open_.get(neighbor, float('inf')): 81 | # not a shorter path than we already have 82 | continue 83 | open_[neighbor] = neighbor.steps 84 | queue.put(neighbor.heuristic(goal), neighbor) 85 | 86 | 87 | def reachable(start, steps): 88 | visited = {start} 89 | queue = deque([start]) 90 | while queue: 91 | current = queue.popleft() 92 | for neighbor in current.moves(): 93 | if neighbor.steps <= 50 and neighbor not in visited: 94 | visited.add(neighbor) 95 | queue.append(neighbor) 96 | return len(visited) 97 | 98 | 99 | def test(): 100 | global DESIGNER_FAVOURITE 101 | DESIGNER_FAVOURITE = 10 102 | length = shortest_path(MazeState(1, 1), MazeState(7, 4)) 103 | assert length == 11 104 | 105 | 106 | if __name__ == '__main__': 107 | import sys 108 | 109 | if '-t' in sys.argv: 110 | test() 111 | sys.exit(0) 112 | 113 | length = shortest_path(MazeState(1, 1), MazeState(31, 39)) 114 | print('Star 1:', length) 115 | reachable_count = reachable(MazeState(1, 1), 50) 116 | print('Star 2:', reachable_count) 117 | -------------------------------------------------------------------------------- /2016/puzzle14.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from functools import reduce 4 | from hashlib import md5 5 | from itertools import count, repeat, islice 6 | 7 | 8 | def stretchkey(hash, count): 9 | return reduce(lambda h, i: md5(h.encode('ascii')).hexdigest(), 10 | repeat(None, count), hash) 11 | 12 | 13 | def keystream(salt, start, stretch=0, _cache={}): 14 | template = bytes(salt + '%d', 'ascii') 15 | counter = count(start) 16 | while True: 17 | index = next(counter) 18 | if (index, stretch) not in _cache: 19 | _cache[index, stretch] = stretchkey( 20 | md5(template % index).hexdigest(), stretch) 21 | yield index, _cache[index, stretch] 22 | 23 | 24 | def genkeys(salt, stretch=0, _t=re.compile(r'(.)\1\1')): 25 | for index, hash in keystream(salt, 0, stretch): 26 | match = _t.search(hash) 27 | if match: 28 | # candidate found, check for pentet in next 1000 29 | pentet = re.compile(r'({})\1\1\1\1'.format(match.group(1))) 30 | next1000 = islice(keystream(salt, index + 1, stretch), 1000) 31 | if any(pentet.search(k) for i, k in next1000): 32 | yield index, hash 33 | 34 | 35 | def test(): 36 | print('Star 1 test') 37 | keys = genkeys('abc') 38 | assert next(keys)[0] == 39 39 | assert next(keys)[0] == 92 40 | assert next(islice(keys, 61, None))[0] == 22728 41 | 42 | print('Star 2 test') 43 | keys = genkeys('abc', stretch=2016) 44 | assert next(keys)[0] == 10 45 | assert next(islice(keys, 62, None))[0] == 22551 46 | 47 | print('Tests passed') 48 | 49 | 50 | 51 | if __name__ == '__main__': 52 | import sys 53 | 54 | salt = 'cuanljph' 55 | 56 | if '-t' in sys.argv: 57 | test() 58 | sys.exit(0) 59 | 60 | keys = genkeys(salt) 61 | index, key64 = next(islice(keys, 63, None)) 62 | print('Star 1:', index) 63 | 64 | keys = genkeys(salt, stretch=2016) 65 | index, key64 = next(islice(keys, 63, None)) 66 | print('Star 2:', index) 67 | -------------------------------------------------------------------------------- /2016/puzzle15.py: -------------------------------------------------------------------------------- 1 | class Disc(object): 2 | def __init__(self, num, steps, start): 3 | # starting point of disk *relative to the starting time* 4 | # disc num is going to be num steps further by the time a ball released 5 | # at t0 reaches it. 6 | self.start = (start + num) % steps 7 | self.steps = steps 8 | 9 | def __iter__(self): 10 | pos = self.start 11 | while True: 12 | yield pos 13 | pos = (pos + 1) % self.steps 14 | 15 | 16 | def solve(discs): 17 | return next(i for i, dpos in enumerate(zip(*discs)) if not any(dpos)) 18 | 19 | 20 | def test(): 21 | print('Star 1 test') 22 | discs = [ 23 | Disc(1, 5, 4), 24 | Disc(2, 2, 1) 25 | ] 26 | assert solve(discs) == 5 27 | 28 | print('Tests passed') 29 | 30 | 31 | if __name__ == '__main__': 32 | import sys 33 | 34 | if '-t' in sys.argv: 35 | test() 36 | sys.exit(0) 37 | 38 | discs = [ 39 | Disc(1, 13, 1), 40 | Disc(2, 19, 10), 41 | Disc(3, 3, 2), 42 | Disc(4, 7, 1), 43 | Disc(5, 5, 3), 44 | Disc(6, 17, 5), 45 | ] 46 | print('Star 1:', solve(discs)) 47 | 48 | discs.append(Disc(7, 11, 0)) 49 | print('Star 2:', solve(discs)) 50 | -------------------------------------------------------------------------------- /2016/puzzle16.py: -------------------------------------------------------------------------------- 1 | def gendata(start): 2 | swap = {ord('0'): '1', ord('1'): '0'} 3 | a = start 4 | while True: 5 | a = '{}0{}'.format(a, a[::-1].translate(swap)) 6 | yield a 7 | 8 | 9 | def checksum(value): 10 | def combine(a, b): 11 | return '1' if a == b else '0' 12 | 13 | checksum = value 14 | while True: 15 | checksum = ''.join([combine(a, b) 16 | for a, b in zip(*([iter(checksum)] * 2))]) 17 | if len(checksum) % 2 == 1: 18 | return checksum 19 | 20 | 21 | def filldisk(start, length): 22 | data = next(filter(lambda d: len(d) >= length, gendata(start))) 23 | data = data[:length] 24 | return checksum(data) 25 | 26 | 27 | def test(): 28 | print('Star 1 test') 29 | assert next(gendata('1')) == '100' 30 | assert next(gendata('0')) == '001' 31 | assert next(gendata('11111')) == '11111000000' 32 | assert next(gendata('111100001010')) == '1111000010100101011110000' 33 | assert checksum('110010110100') == '100' 34 | assert filldisk('10000', 20) == '01100' 35 | 36 | print('Tests passed') 37 | 38 | 39 | if __name__ == '__main__': 40 | import sys 41 | 42 | if '-t' in sys.argv: 43 | test() 44 | sys.exit(0) 45 | 46 | start = '10001001100000001' 47 | 48 | print('Star 1:', filldisk(start, 272)) 49 | print('Star 2:', filldisk(start, 35651584)) 50 | -------------------------------------------------------------------------------- /2016/puzzle17.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | from hashlib import md5 3 | from heapq import heapify, heappush, heappop 4 | from itertools import count 5 | 6 | 7 | class PriorityQueue: 8 | def __init__(self, *initial): 9 | self._queue = [] 10 | self._count = count() 11 | for pri, item in initial: 12 | self.put(pri, item) 13 | heapify(self._queue) 14 | 15 | def __len__(self): 16 | return len(self._queue) 17 | 18 | def put(self, pri, item): 19 | heappush(self._queue, (pri, next(self._count), item)) 20 | 21 | def get(self): 22 | if not self: 23 | raise ValueError('Queue is empty') 24 | return heappop(self._queue)[-1] 25 | 26 | 27 | DIR = { 28 | 'U': (0, -1), 29 | 'D': (0, 1), 30 | 'L': (-1, 0), 31 | 'R': (1, 0), 32 | } 33 | 34 | 35 | class MazeState: 36 | def __init__(self, key, x=0, y=0, path=''): 37 | self.key = key 38 | self.x, self.y = x, y 39 | self.path = path 40 | doorhash = md5(bytes(key + path, 'ascii')).hexdigest() 41 | self.opendoors = {d: h in 'bcdef' for d, h in zip('UDLR', doorhash)} 42 | 43 | def __eq__(self, other): 44 | return (self.x, self.y, self.path) == (other.x, other.y, other.path) 45 | 46 | def __hash__(self): 47 | return hash(self.x) ^ hash(self.y) ^ hash(self.path) 48 | 49 | def __repr__(self): 50 | return ( 51 | ''.format( 52 | self)) 53 | 54 | def moves(self): 55 | for d, (dx, dy) in DIR.items(): 56 | x, y = self.x + dx, self.y + dy 57 | if self.opendoors[d] and 0 <= x < 4 and 0 <= y < 4: 58 | yield MazeState(self.key, x, y, self.path + d) 59 | 60 | def heuristic(self): 61 | return len(self.path) + (3 - self.x) + (3 - self.y) 62 | 63 | 64 | def shortest_path(start): 65 | queue = PriorityQueue((start.heuristic(), start)) 66 | closed = set() 67 | 68 | while queue: 69 | current = queue.get() 70 | 71 | if (current.x, current.y) == (3, 3): 72 | return current.path 73 | 74 | closed.add(current) 75 | for neighbor in current.moves(): 76 | if neighbor in closed: 77 | continue 78 | queue.put(neighbor.heuristic(), neighbor) 79 | 80 | 81 | def longest_path(start): 82 | queue = deque([start]) 83 | maxlength = 0 84 | 85 | while queue: 86 | current = queue.popleft() 87 | for neighbor in current.moves(): 88 | if (neighbor.x, neighbor.y) == (3, 3): 89 | maxlength = max(maxlength, len(neighbor.path)) 90 | continue 91 | queue.append(neighbor) 92 | 93 | return maxlength 94 | 95 | 96 | def test(): 97 | print('Star 1 test') 98 | assert shortest_path(MazeState('hijkl')) is None 99 | assert shortest_path(MazeState('ihgpwlah')) == 'DDRRRD' 100 | assert shortest_path(MazeState('kglvqrro')) == 'DDUDRLRRUDRD' 101 | assert shortest_path(MazeState('ulqzkmiv')) == ( 102 | 'DRURDRUDDLLDLUURRDULRLDUUDDDRR') 103 | 104 | print('Star 2 test') 105 | assert longest_path(MazeState('ihgpwlah')) == 370 106 | assert longest_path(MazeState('kglvqrro')) == 492 107 | assert longest_path(MazeState('ulqzkmiv')) == 830 108 | 109 | print('Tests passed') 110 | 111 | 112 | if __name__ == '__main__': 113 | import sys 114 | 115 | if '-t' in sys.argv: 116 | test() 117 | sys.exit(0) 118 | 119 | key = 'qljzarfv' 120 | start = MazeState(key) 121 | print('Star 1:', shortest_path(start)) 122 | print('Star 2:', longest_path(start)) 123 | -------------------------------------------------------------------------------- /2016/puzzle18.py: -------------------------------------------------------------------------------- 1 | from itertools import islice 2 | 3 | 4 | # set of preceding tile sequences that make a trap in the next row 5 | _is_trap = { 6 | (True, True, False), 7 | (False, True, True), 8 | (True, False, False), 9 | (False, False, True) 10 | } 11 | 12 | 13 | def gen_rows(start): 14 | cache = {} 15 | yield start 16 | row = start 17 | while True: 18 | if row not in cache: 19 | previous = (False,) + row + (False,) 20 | cache[row] = tuple( 21 | three in _is_trap 22 | for three in zip(previous, previous[1:], previous[2:])) 23 | row = cache[row] 24 | yield row 25 | 26 | 27 | def count_safe(start, rows): 28 | return sum(len(row) - sum(row) for row in islice(gen_rows(start), rows)) 29 | 30 | 31 | def row_from_str(row): 32 | return tuple(tile == '^' for tile in row) 33 | 34 | 35 | def test(): 36 | print('Star 1 test') 37 | start = row_from_str('..^^.') 38 | assert list(islice(gen_rows(start), 3)) == list(map( 39 | row_from_str, 40 | ('..^^.', '.^^^^', '^^..^') 41 | )) 42 | start = row_from_str('.^^.^.^^^^') 43 | assert count_safe(start, 10) == 38 44 | 45 | # print('Star 2 test') 46 | print('Tests passed') 47 | 48 | 49 | if __name__ == '__main__': 50 | import sys 51 | 52 | if '-t' in sys.argv: 53 | test() 54 | sys.exit(0) 55 | 56 | start = row_from_str( 57 | '^.....^.^^^^^.^..^^.^.......^^..^^^..^^^^..^.^^.^.' 58 | '^....^^...^^.^^.^...^^.^^^^..^^.....^.^...^.^.^^.^') 59 | print('Star 1:', count_safe(start, 40)) 60 | print('Star 2:', count_safe(start, 400000)) 61 | -------------------------------------------------------------------------------- /2016/puzzle19.py: -------------------------------------------------------------------------------- 1 | def josephus(elves): 2 | # Rotate binary value by 1 (MSB to LSB) for an O(logN) answer (basically 3 | # counting the bits in N, then shifting them in logN steps) 4 | size = elves.bit_length() 5 | mask = (1 << size) - 1 6 | return (elves << 1 & mask) + (elves >> size - 1) 7 | 8 | 9 | class ElfNode: 10 | # slots to avoid costly memory allocations, saves seconds of the 11 | # runtime. 12 | # We can save a little more time still by inlining __init__ and remove 13 | # but that makes the code also harder to follow. 14 | __slots__ = ('next_', 'num') 15 | 16 | def __init__(self, num): 17 | self.num = num 18 | 19 | def remove(self): 20 | # not really remove, remove the *next* node instead by 21 | # copying the state of the next to this 22 | self.num, self.next_ = self.next_.num, self.next_.next_ 23 | 24 | 25 | def elves_elimination(count): 26 | head = node = mid = ElfNode(1) 27 | midnum = count // 2 + 1 28 | for num in range(2, count + 1): 29 | node.next_ = node = ElfNode(num) 30 | if num == midnum: 31 | mid = node 32 | # close the circle 33 | node.next_ = head 34 | # clean up; only mid remains 35 | del head, node 36 | 37 | for i in range(count, 1, -1): 38 | mid.remove() 39 | # mid is automatically promoted to the next element 40 | # however, on an odd number of elves remaining, it's the elf to their 41 | # left that gets the chop instead. 42 | if i % 2: 43 | mid = mid.next_ 44 | 45 | return mid.num 46 | 47 | 48 | def test(): 49 | print('Star 1 test') 50 | assert josephus(5) == 3 51 | 52 | print('Star 2 test') 53 | assert elves_elimination(5) == 2 54 | print('Tests passed') 55 | 56 | 57 | if __name__ == '__main__': 58 | import sys 59 | 60 | if '-t' in sys.argv: 61 | test() 62 | sys.exit(0) 63 | 64 | count = 3014387 65 | print('Star 1:', josephus(count)) 66 | print('Star 2:', elves_elimination(count)) 67 | -------------------------------------------------------------------------------- /2016/puzzle20.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | 3 | from itertools import chain 4 | from functools import total_ordering 5 | 6 | 7 | HERE = os.path.dirname(os.path.abspath(__file__)) 8 | 9 | 10 | @total_ordering 11 | class IPRange: 12 | def __init__(self, start, stop): 13 | # make stop exclusive, saves on the +1s later on 14 | self.start, self.stop = start, stop + 1 15 | 16 | def __eq__(self, other): 17 | return (self.start, self.stop) == (other.start, other.stop) 18 | 19 | def __lt__(self, other): 20 | return (self.start, self.stop) < (other.start, other.stop) 21 | 22 | def __repr__(self): 23 | return ''.format(self) 24 | 25 | 26 | def gaps(ipranges, limit=2**32): 27 | ranges = iter(ipranges) 28 | first, second = next(ranges), next(ranges) 29 | try: 30 | while True: 31 | while second.stop <= first.stop: 32 | second = next(ranges) 33 | if first.stop < second.start: 34 | yield range(first.stop, second.start) 35 | first = second 36 | except StopIteration: 37 | if first.stop < limit: 38 | yield range(first.stop, limit) 39 | 40 | 41 | def test(): 42 | print('Star 1 test') 43 | ranges = sorted([IPRange(5, 8), IPRange(0, 2), IPRange(4, 7)]) 44 | assert list(chain.from_iterable(gaps(ranges, 10))) == [3, 9] 45 | 46 | print('Star 2 test') 47 | assert sum(map(len, gaps(ranges, 10))) == 2 48 | 49 | print('Tests passed') 50 | 51 | 52 | if __name__ == '__main__': 53 | import sys 54 | 55 | if '-t' in sys.argv: 56 | test() 57 | sys.exit(0) 58 | 59 | ipranges = [] 60 | with open(os.path.join(HERE, 'puzzle20_input.txt'), 'r') as ranges: 61 | for line in ranges: 62 | start, stop = map(int, line.split('-')) 63 | ipranges.append(IPRange(start, stop)) 64 | ipranges.sort() 65 | print('Star 1:', next(gaps(ipranges))[0]) 66 | print('Star 2:', sum(map(len, gaps(ipranges)))) 67 | -------------------------------------------------------------------------------- /2016/puzzle23.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import sys 3 | 4 | 5 | HERE = os.path.dirname(os.path.abspath(__file__)) 6 | sys.path.insert(0, HERE) 7 | import puzzle12 # noqa 8 | 9 | 10 | class ExtendedBunnyProc(puzzle12.BunnyProc): 11 | def execute(self, assembunny): 12 | self._patched_regions = {} 13 | super().execute(assembunny) 14 | 15 | def _clear_region(self, target): 16 | to_clear = [] 17 | for pos, orig in self._patched_regions.items(): 18 | if pos <= target < pos + len(orig): 19 | self.assembunny[pos:pos + len(orig)] = orig 20 | to_clear.append(pos) 21 | for pos in to_clear: 22 | del self._patched_regions[pos] 23 | 24 | def _test_patch(self): 25 | # peek ahead and see if we entered a multiplication or addition loop 26 | p = self.assembunny[self.pos:self.pos + 5] 27 | instr, op1, op2 = zip(*((i + [None])[:3] for i in p)) 28 | if (instr == ('inc', 'dec', 'jnz', 'dec', 'jnz') and 29 | (op2[2], op2[4]) == ('-2', '-5') and 30 | op1[0] not in op1[1:] and 31 | op1[1] == op1[2] and 32 | op1[3] == op1[4]): 33 | self._patched_regions[self.pos] = p 34 | self.assembunny[self.pos:self.pos + 1] = [ 35 | ['mul', op1[0], op1[1], op1[3]], 36 | ] 37 | return True 38 | elif (instr[:3] == ('dec', 'inc', 'jnz') and op2[2] == '-2' and 39 | (op1[0] == op1[2] != op1[1])): 40 | self._patched_regions[self.pos] = p[:3] 41 | self.assembunny[self.pos:self.pos + 1] = [ 42 | ['add', op1[1], op1[0]], 43 | ] 44 | return True 45 | 46 | def op_mul(self, x, y, z): 47 | # optimised operation, multitiply y and z, add to x. Resets y and z. 48 | self.registers[x] += self.register_or_int(y) * self.register_or_int(z) 49 | self.registers[y] = self.registers[z] = 0 50 | self.pos += 5 51 | 52 | def op_add(self, x, y): 53 | # optimised operation, add y to x. Resets y. 54 | self.registers[x] += self.register_or_int(y) 55 | self.registers[y] = 0 56 | self.pos += 3 57 | 58 | def op_inc(self, x): 59 | if self._test_patch(): 60 | return 61 | super().op_inc(x) 62 | 63 | def op_dec(self, x): 64 | if self._test_patch(): 65 | return 66 | super().op_dec(x) 67 | 68 | def op_tgl(self, x): 69 | map_ = { 70 | 'cpy': 'jnz', 71 | 'inc': 'dec', 72 | 'dec': 'inc', 73 | 'jnz': 'cpy', 74 | 'tgl': 'inc', 75 | } 76 | target = self.pos + self.register_or_int(x) 77 | self._clear_region(target) 78 | try: 79 | instr, *op = self.assembunny[target] 80 | except IndexError: 81 | pass 82 | else: 83 | self.assembunny[target] = [map_[instr], *op] 84 | self.pos += 1 85 | 86 | 87 | def test(): 88 | print('Star 1 test') 89 | proc = ExtendedBunnyProc() 90 | proc.execute('''\ 91 | cpy 2 a 92 | tgl a 93 | tgl a 94 | tgl a 95 | cpy 1 a 96 | dec a 97 | dec a 98 | '''.splitlines(True)) 99 | assert proc.registers['a'] == 3 100 | 101 | # print('Star 2 test') 102 | print('Tests passed') 103 | 104 | 105 | if __name__ == '__main__': 106 | if '-t' in sys.argv: 107 | test() 108 | sys.exit(0) 109 | 110 | instructions = '''\ 111 | cpy a b 112 | dec b 113 | cpy a d 114 | cpy 0 a 115 | cpy b c 116 | inc a 117 | dec c 118 | jnz c -2 119 | dec d 120 | jnz d -5 121 | dec b 122 | cpy b c 123 | cpy c d 124 | dec d 125 | inc c 126 | jnz d -2 127 | tgl c 128 | cpy -16 c 129 | jnz 1 c 130 | cpy 89 c 131 | jnz 84 d 132 | inc a 133 | inc d 134 | jnz d -2 135 | inc c 136 | jnz c -5 137 | '''.splitlines() 138 | 139 | proc = ExtendedBunnyProc() 140 | proc.execute(['cpy 7 a'] + instructions) 141 | print('Star 1:', proc.registers['a']) 142 | 143 | proc = ExtendedBunnyProc() 144 | proc.execute(['cpy 12 a'] + instructions) 145 | print('Star 2:', proc.registers['a']) 146 | -------------------------------------------------------------------------------- /2016/puzzle24.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | from heapq import heapify, heappush, heappop 3 | from itertools import combinations, count, permutations 4 | 5 | 6 | HERE = os.path.dirname(os.path.abspath(__file__)) 7 | 8 | 9 | class PriorityQueue: 10 | def __init__(self, *initial): 11 | self._queue = [] 12 | self._count = count() 13 | for pri, item in initial: 14 | self.put(pri, item) 15 | heapify(self._queue) 16 | 17 | def __len__(self): 18 | return len(self._queue) 19 | 20 | def put(self, pri, item): 21 | heappush(self._queue, (pri, next(self._count), item)) 22 | 23 | def get(self): 24 | if not self: 25 | raise ValueError('Queue is empty') 26 | return heappop(self._queue)[-1] 27 | 28 | 29 | class MazeState: 30 | def __init__(self, x, y, maze, steps=0): 31 | self.x, self.y = x, y 32 | self.maze = maze 33 | self.steps = steps 34 | 35 | def __eq__(self, other): 36 | return (self.x, self.y) == (other.x, other.y) 37 | 38 | def __hash__(self): 39 | return hash(self.x) ^ hash(self.y) 40 | 41 | def __repr__(self): 42 | return ''.format(self) 43 | 44 | def moves(self): 45 | m = self.maze 46 | for dx, dy in ((-1, 0), (1, 0), (0, -1), (0, 1)): 47 | x, y = self.x + dx, self.y + dy 48 | if 0 <= x < len(m) and 0 <= y < len(m[0]) and m[x][y] != '#': 49 | yield MazeState(x, y, m, self.steps + 1) 50 | 51 | def heuristic(self, target): 52 | return abs(self.x - target.x) + abs(self.y - target.y) + self.steps 53 | 54 | 55 | def shortest_path(start, goal): 56 | queue = PriorityQueue((start.heuristic(goal), start)) 57 | open_ = {start: 0} 58 | closed = set() 59 | 60 | while open_: 61 | current = queue.get() 62 | if open_.get(current) != current.steps: 63 | # ignore items in the queue for which a shorter 64 | # path exists 65 | continue 66 | 67 | if current == goal: 68 | return current.steps 69 | 70 | del open_[current] 71 | closed.add(current) 72 | for neighbor in current.moves(): 73 | if neighbor in closed: 74 | continue 75 | if neighbor.steps >= open_.get(neighbor, float('inf')): 76 | # not a shorter path than we already have 77 | continue 78 | open_[neighbor] = neighbor.steps 79 | queue.put(neighbor.heuristic(goal), neighbor) 80 | 81 | 82 | def quickest_path(maze, return_=False): 83 | points = {p: MazeState(x, y, maze) 84 | for x, row in enumerate(maze) 85 | for y, p in enumerate(row) if p.isdigit()} 86 | distances = {frozenset([start, goal]): shortest_path( 87 | points[start], points[goal]) 88 | for start, goal in combinations(points, 2)} 89 | 90 | def distance(path): 91 | return sum(distances[frozenset([a, b])] for a, b in zip(path, path[1:])) 92 | 93 | return min(distance(('0',) + p + (('0',) if return_ else ())) 94 | for p in permutations(points.keys() - {'0'})) 95 | 96 | 97 | def test(): 98 | print('Star 1 test') 99 | maze = '''\ 100 | ########### 101 | #0.1.....2# 102 | #.#######.# 103 | #4.......3# 104 | ########### 105 | '''.splitlines() 106 | assert quickest_path(maze) == 14 107 | 108 | # print('Star 2 test') 109 | print('Tests passed') 110 | 111 | 112 | if __name__ == '__main__': 113 | import sys 114 | 115 | if '-t' in sys.argv: 116 | test() 117 | sys.exit(0) 118 | 119 | with open(os.path.join(HERE, 'puzzle24_input.txt'), 'r') as mazedata: 120 | maze = [line.strip() for line in mazedata] 121 | 122 | print('Star 1:', quickest_path(maze)) 123 | print('Star 2:', quickest_path(maze, return_=True)) 124 | -------------------------------------------------------------------------------- /2016/puzzle25.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import sys 3 | 4 | 5 | HERE = os.path.dirname(os.path.abspath(__file__)) 6 | sys.path.insert(0, HERE) 7 | import puzzle23 # noqa 8 | 9 | 10 | class OscillatorBunnyProc(puzzle23.ExtendedBunnyProc): 11 | def _test_patch(self): 12 | # peek ahead and see if we entered a division or subtraction loop 13 | p = self.assembunny[self.pos:self.pos + 7] 14 | instr, op1, op2 = zip(*((i + [None])[:3] for i in p)) 15 | if (instr == ('jnz', 'jnz', 'dec', 'dec', 16 | 'jnz', 'inc', 'jnz') and 17 | op2 == ('2', '6', None, None, '-4', None, '-7') and 18 | op1[0] == op1[2] and op1[3] == op1[4]): 19 | self._patched_regions[self.pos] = p 20 | self.assembunny[self.pos:self.pos + 1] = [ 21 | ['div', op1[5], op1[0], op1[3]], 22 | ] 23 | return True 24 | 25 | return super()._test_patch() 26 | 27 | def op_div(self, a, b, c): 28 | # optimised operation, divide b by c, add result to a, 29 | # leave remainder c or set it to original value 30 | b_val, c_val = self.register_or_int(b), self.register_or_int(c) 31 | self.registers[a] += b_val // c_val 32 | self.registers[c] = (b_val % c_val) or c_val 33 | self.pos += 7 34 | 35 | def op_jnz(self, x, y): 36 | if self._test_patch(): 37 | return 38 | super().op_jnz(x, y) 39 | 40 | def op_out(self, x): 41 | print(self.register_or_int(x)) 42 | self.pos += 1 43 | 44 | 45 | if __name__ == '__main__': 46 | assembunny = '''\ 47 | cpy a d 48 | cpy 7 c 49 | cpy 362 b 50 | inc d 51 | dec b 52 | jnz b -2 53 | dec c 54 | jnz c -5 55 | cpy d a 56 | jnz 0 0 57 | cpy a b 58 | cpy 0 a 59 | cpy 2 c 60 | jnz b 2 61 | jnz 1 6 62 | dec b 63 | dec c 64 | jnz c -4 65 | inc a 66 | jnz 1 -7 67 | cpy 2 b 68 | jnz c 2 69 | jnz 1 4 70 | dec b 71 | dec c 72 | jnz 1 -4 73 | jnz 0 0 74 | out b 75 | jnz a -19 76 | jnz 1 -21 77 | '''.splitlines() 78 | 79 | start_value = int(assembunny[1].split()[1]) * int(assembunny[2].split()[1]) 80 | bit_count = start_value.bit_length() 81 | target = 1 82 | for _ in range((bit_count // 2) - 1): 83 | target = (target << 2) | 1 84 | 85 | if bit_count % 2 == 0: 86 | target = target << 1 87 | 88 | print('Star 1:', target - start_value) 89 | 90 | import sys 91 | if '-o' in sys.argv: 92 | proc = OscillatorBunnyProc() 93 | proc.registers['a'] = target - start_value 94 | proc.execute(assembunny) 95 | -------------------------------------------------------------------------------- /2017/Day 01.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import aocd\n", 10 | "\n", 11 | "data = aocd.get_data(year=2017, day=1)\n", 12 | "inputvalue = data.strip()" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": 2, 18 | "metadata": {}, 19 | "outputs": [ 20 | { 21 | "name": "stdout", 22 | "output_type": "stream", 23 | "text": [ 24 | "Part 1: 1175\n" 25 | ] 26 | } 27 | ], 28 | "source": [ 29 | "def captcha_next(inputvalue):\n", 30 | " same_as_next = (\n", 31 | " digit\n", 32 | " for digit, next in zip(inputvalue, inputvalue[1:] + inputvalue[0])\n", 33 | " if digit == next\n", 34 | " )\n", 35 | " return sum(map(int, same_as_next))\n", 36 | "\n", 37 | "\n", 38 | "tests = {\n", 39 | " \"1122\": 3,\n", 40 | " \"1111\": 4,\n", 41 | " \"1234\": 0,\n", 42 | " \"91212129\": 9,\n", 43 | "}\n", 44 | "for in_, out in tests.items():\n", 45 | " assert captcha_next(in_) == out\n", 46 | "\n", 47 | "print(\"Part 1:\", captcha_next(inputvalue))" 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": 3, 53 | "metadata": {}, 54 | "outputs": [ 55 | { 56 | "name": "stdout", 57 | "output_type": "stream", 58 | "text": [ 59 | "Part 2: 1166\n" 60 | ] 61 | } 62 | ], 63 | "source": [ 64 | "def captcha_halfway(inputvalue):\n", 65 | " halfway = len(inputvalue) // 2\n", 66 | " same_as_halfway = (\n", 67 | " digit\n", 68 | " for digit, other in zip(inputvalue, inputvalue[halfway:] + inputvalue[:halfway])\n", 69 | " if digit == other\n", 70 | " )\n", 71 | " return sum(map(int, same_as_halfway))\n", 72 | "\n", 73 | "\n", 74 | "tests = {\n", 75 | " \"1212\": 6,\n", 76 | " \"1221\": 0,\n", 77 | " \"123425\": 4,\n", 78 | " \"123123\": 12,\n", 79 | " \"12131415\": 4,\n", 80 | "}\n", 81 | "for in_, out in tests.items():\n", 82 | " assert captcha_halfway(in_) == out\n", 83 | "\n", 84 | "print(\"Part 2:\", captcha_halfway(inputvalue))" 85 | ] 86 | } 87 | ], 88 | "metadata": { 89 | "kernelspec": { 90 | "display_name": "Python 3", 91 | "language": "python", 92 | "name": "python3" 93 | }, 94 | "language_info": { 95 | "codemirror_mode": { 96 | "name": "ipython", 97 | "version": 3 98 | }, 99 | "file_extension": ".py", 100 | "mimetype": "text/x-python", 101 | "name": "python", 102 | "nbconvert_exporter": "python", 103 | "pygments_lexer": "ipython3", 104 | "version": "3.12.0" 105 | } 106 | }, 107 | "nbformat": 4, 108 | "nbformat_minor": 2 109 | } 110 | -------------------------------------------------------------------------------- /2017/Day 02.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import aocd\n", 10 | "\n", 11 | "data = aocd.get_data(day=2, year=2017)\n", 12 | "inputvalue = [[int(c) for c in line.split()] for line in data.splitlines()]" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": 2, 18 | "metadata": {}, 19 | "outputs": [ 20 | { 21 | "name": "stdout", 22 | "output_type": "stream", 23 | "text": [ 24 | "Part 1: 53460\n" 25 | ] 26 | } 27 | ], 28 | "source": [ 29 | "def checksum(sheet):\n", 30 | " return sum(max(row) - min(row) for row in sheet)\n", 31 | "\n", 32 | "\n", 33 | "assert checksum([[5, 1, 9, 5], [7, 5, 3], [2, 4, 6, 8]]) == 18\n", 34 | "print(\"Part 1:\", checksum(inputvalue))" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": 3, 40 | "metadata": {}, 41 | "outputs": [ 42 | { 43 | "data": { 44 | "text/plain": [ 45 | "282" 46 | ] 47 | }, 48 | "execution_count": 3, 49 | "metadata": {}, 50 | "output_type": "execute_result" 51 | } 52 | ], 53 | "source": [ 54 | "from itertools import combinations\n", 55 | "\n", 56 | "sum(\n", 57 | " int(div)\n", 58 | " for row in inputvalue\n", 59 | " for div in (max(a, b) / min(a, b) for a, b in combinations(row, 2))\n", 60 | " if div.is_integer()\n", 61 | ")" 62 | ] 63 | } 64 | ], 65 | "metadata": { 66 | "kernelspec": { 67 | "display_name": "Python 3", 68 | "language": "python", 69 | "name": "python3" 70 | }, 71 | "language_info": { 72 | "codemirror_mode": { 73 | "name": "ipython", 74 | "version": 3 75 | }, 76 | "file_extension": ".py", 77 | "mimetype": "text/x-python", 78 | "name": "python", 79 | "nbconvert_exporter": "python", 80 | "pygments_lexer": "ipython3", 81 | "version": "3.12.0" 82 | } 83 | }, 84 | "nbformat": 4, 85 | "nbformat_minor": 2 86 | } 87 | -------------------------------------------------------------------------------- /2017/Day 03.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import aocd\n", 10 | "\n", 11 | "inputvalue = int(aocd.get_data(day=3, year=2017))" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 2, 17 | "metadata": {}, 18 | "outputs": [ 19 | { 20 | "data": { 21 | "text/plain": [ 22 | "430" 23 | ] 24 | }, 25 | "execution_count": 2, 26 | "metadata": {}, 27 | "output_type": "execute_result" 28 | } 29 | ], 30 | "source": [ 31 | "import math\n", 32 | "\n", 33 | "\n", 34 | "def spiraldistance(pos):\n", 35 | " if pos == 1:\n", 36 | " return 0\n", 37 | " prev = int(math.sqrt(pos))\n", 38 | " if prev % 2 == 0:\n", 39 | " prev -= 1\n", 40 | " sidelength = prev + 1\n", 41 | " offset = (pos - (prev**2)) % sidelength\n", 42 | " middle = sidelength // 2\n", 43 | " return middle + abs(middle - offset)\n", 44 | "\n", 45 | "\n", 46 | "tests = {1: 0, 12: 3, 23: 2, 1024: 31}\n", 47 | "for pos, expected in tests.items():\n", 48 | " assert (\n", 49 | " spiraldistance(pos) == expected\n", 50 | " ), f\"spiraldistance({pos}) => {spiraldistance(pos)}, not {expected}\"\n", 51 | "\n", 52 | "spiraldistance(inputvalue)" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": 3, 58 | "metadata": {}, 59 | "outputs": [ 60 | { 61 | "name": "stdout", 62 | "output_type": "stream", 63 | "text": [ 64 | "[1, 1, 2, 4, 5, 10, 11, 23, 25, 26, 54, 57, 59, 122, 133, 142, 147, 304, 330, 351, 362, 747, 806, 880, 931]\n" 65 | ] 66 | }, 67 | { 68 | "data": { 69 | "text/plain": [ 70 | "312453" 71 | ] 72 | }, 73 | "execution_count": 3, 74 | "metadata": {}, 75 | "output_type": "execute_result" 76 | } 77 | ], 78 | "source": [ 79 | "from itertools import count, cycle, islice, product\n", 80 | "\n", 81 | "\n", 82 | "def summed_larger():\n", 83 | " grid = {(0, 0): 1}\n", 84 | " directions = tuple((x, y) for x, y in product((-1, 0, 1), repeat=2) if x or y)\n", 85 | " steps = cycle([(0, 1), (-1, 0), (0, -1), (1, 0)])\n", 86 | " step = next(steps)\n", 87 | " x, y = 1, 0\n", 88 | " size = 3\n", 89 | " yield 1\n", 90 | " for num in count(2):\n", 91 | " value = sum(grid.get((x + dx, y + dy), 0) for dx, dy in directions)\n", 92 | " yield value\n", 93 | " grid[(x, y)] = value\n", 94 | " if num == size**2: # next spiral ring\n", 95 | " size += 2\n", 96 | " x += 1\n", 97 | " step = next(steps)\n", 98 | " else:\n", 99 | " if (num - ((size - 2) ** 2)) % (size - 1) == 0: # change direction\n", 100 | " step = next(steps)\n", 101 | " x += step[0]\n", 102 | " y += step[1]\n", 103 | "\n", 104 | "\n", 105 | "print(list(islice(summed_larger(), 25)))\n", 106 | "next(filter(lambda v: v > inputvalue, summed_larger()))" 107 | ] 108 | } 109 | ], 110 | "metadata": { 111 | "kernelspec": { 112 | "display_name": "Python 3", 113 | "language": "python", 114 | "name": "python3" 115 | }, 116 | "language_info": { 117 | "codemirror_mode": { 118 | "name": "ipython", 119 | "version": 3 120 | }, 121 | "file_extension": ".py", 122 | "mimetype": "text/x-python", 123 | "name": "python", 124 | "nbconvert_exporter": "python", 125 | "pygments_lexer": "ipython3", 126 | "version": "3.12.0" 127 | } 128 | }, 129 | "nbformat": 4, 130 | "nbformat_minor": 2 131 | } 132 | -------------------------------------------------------------------------------- /2017/Day 04.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import aocd\n", 10 | "\n", 11 | "data = aocd.get_data(day=4, year=2017)\n", 12 | "phrases = list(map(str.strip, data.splitlines()))" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": 2, 18 | "metadata": {}, 19 | "outputs": [ 20 | { 21 | "name": "stdout", 22 | "output_type": "stream", 23 | "text": [ 24 | "Part 1: 383\n" 25 | ] 26 | } 27 | ], 28 | "source": [ 29 | "def is_valid(phrase):\n", 30 | " words = phrase.split()\n", 31 | " return len(words) == len(set(words))\n", 32 | "\n", 33 | "\n", 34 | "tests = {\n", 35 | " \"aa bb cc dd ee\": True,\n", 36 | " \"aa bb cc dd aa\": False,\n", 37 | " \"aa bb cc dd aaa\": True,\n", 38 | "}\n", 39 | "for phrase, expected in tests.items():\n", 40 | " assert is_valid(phrase) == expected\n", 41 | "\n", 42 | "print(\"Part 1:\", sum(map(is_valid, phrases)))" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 3, 48 | "metadata": {}, 49 | "outputs": [ 50 | { 51 | "name": "stdout", 52 | "output_type": "stream", 53 | "text": [ 54 | "Part 2: 265\n" 55 | ] 56 | } 57 | ], 58 | "source": [ 59 | "def is_valid_no_anagram(phrase):\n", 60 | " words = phrase.split()\n", 61 | " sorted_set = {\"\".join(sorted(w)) for w in words}\n", 62 | " return len(words) == len(sorted_set)\n", 63 | "\n", 64 | "\n", 65 | "tests = {\n", 66 | " \"abcde fghij\": True,\n", 67 | " \"abcde xyz ecdab\": False,\n", 68 | " \"a ab abc abd abf abj\": True,\n", 69 | " \"iiii oiii ooii oooi oooo\": True,\n", 70 | " \"oiii ioii iioi iiio\": False,\n", 71 | "}\n", 72 | "\n", 73 | "for phrase, expected in tests.items():\n", 74 | " assert is_valid_no_anagram(phrase) == expected\n", 75 | "\n", 76 | "print(\"Part 2:\", sum(map(is_valid_no_anagram, phrases)))" 77 | ] 78 | } 79 | ], 80 | "metadata": { 81 | "kernelspec": { 82 | "display_name": "Python 3", 83 | "language": "python", 84 | "name": "python3" 85 | }, 86 | "language_info": { 87 | "codemirror_mode": { 88 | "name": "ipython", 89 | "version": 3 90 | }, 91 | "file_extension": ".py", 92 | "mimetype": "text/x-python", 93 | "name": "python", 94 | "nbconvert_exporter": "python", 95 | "pygments_lexer": "ipython3", 96 | "version": "3.12.0" 97 | } 98 | }, 99 | "nbformat": 4, 100 | "nbformat_minor": 2 101 | } 102 | -------------------------------------------------------------------------------- /2017/Day 05.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import aocd\n", 10 | "\n", 11 | "data = aocd.get_data(day=5, year=2017)\n", 12 | "instructions = list(map(int, data.splitlines()))" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": 2, 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "def execute(instructions, decrement_at_3=False):\n", 22 | " pos = 0\n", 23 | " steps = 0\n", 24 | " while 0 <= pos < len(instructions):\n", 25 | " delta = instructions[pos]\n", 26 | " if (not decrement_at_3) or delta < 3:\n", 27 | " instructions[pos] += 1\n", 28 | " else:\n", 29 | " instructions[pos] -= 1\n", 30 | " pos += delta\n", 31 | " steps += 1\n", 32 | " return steps" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": 3, 38 | "metadata": {}, 39 | "outputs": [ 40 | { 41 | "name": "stdout", 42 | "output_type": "stream", 43 | "text": [ 44 | "Part 1: 372671\n" 45 | ] 46 | } 47 | ], 48 | "source": [ 49 | "assert execute([0, 3, 0, 1, -3]) == 5\n", 50 | "print(\"Part 1:\", execute(instructions[:]))" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": 4, 56 | "metadata": {}, 57 | "outputs": [ 58 | { 59 | "name": "stdout", 60 | "output_type": "stream", 61 | "text": [ 62 | "Part 2: 25608480\n" 63 | ] 64 | } 65 | ], 66 | "source": [ 67 | "assert execute([0, 3, 0, 1, -3], True) == 10\n", 68 | "print(\"Part 2:\", execute(instructions[:], True))" 69 | ] 70 | } 71 | ], 72 | "metadata": { 73 | "kernelspec": { 74 | "display_name": "Python 3", 75 | "language": "python", 76 | "name": "python3" 77 | }, 78 | "language_info": { 79 | "codemirror_mode": { 80 | "name": "ipython", 81 | "version": 3 82 | }, 83 | "file_extension": ".py", 84 | "mimetype": "text/x-python", 85 | "name": "python", 86 | "nbconvert_exporter": "python", 87 | "pygments_lexer": "ipython3", 88 | "version": "3.12.0" 89 | } 90 | }, 91 | "nbformat": 4, 92 | "nbformat_minor": 2 93 | } 94 | -------------------------------------------------------------------------------- /2017/Day 06.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import aocd\n", 10 | "\n", 11 | "data = aocd.get_data(day=6, year=2017)\n", 12 | "banks = [int(count) for count in data.split()]" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": 2, 18 | "metadata": {}, 19 | "outputs": [ 20 | { 21 | "name": "stdout", 22 | "output_type": "stream", 23 | "text": [ 24 | "Part 1: 14029\n" 25 | ] 26 | } 27 | ], 28 | "source": [ 29 | "def redistribute(banks):\n", 30 | " index, count = max(enumerate(banks), key=lambda iv: (iv[1], -iv[0]))\n", 31 | " banks[index] = 0\n", 32 | " for i in range(count):\n", 33 | " target = (i + index + 1) % len(banks)\n", 34 | " banks[target] += 1\n", 35 | "\n", 36 | "\n", 37 | "test = [0, 2, 7, 0]\n", 38 | "redistribute(test)\n", 39 | "assert test == [2, 4, 1, 2]\n", 40 | "redistribute(test)\n", 41 | "assert test == [3, 1, 2, 3]\n", 42 | "redistribute(test)\n", 43 | "assert test == [0, 2, 3, 4]\n", 44 | "redistribute(test)\n", 45 | "assert test == [1, 3, 4, 1]\n", 46 | "redistribute(test)\n", 47 | "assert test == [2, 4, 1, 2]\n", 48 | "\n", 49 | "\n", 50 | "def find_circle(banks):\n", 51 | " seen = set()\n", 52 | " while tuple(banks) not in seen:\n", 53 | " seen.add(tuple(banks))\n", 54 | " redistribute(banks)\n", 55 | " return len(seen)\n", 56 | "\n", 57 | "\n", 58 | "assert find_circle([0, 2, 7, 0]) == 5\n", 59 | "\n", 60 | "print(\"Part 1:\", find_circle(banks))" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": 3, 66 | "metadata": {}, 67 | "outputs": [ 68 | { 69 | "name": "stdout", 70 | "output_type": "stream", 71 | "text": [ 72 | "Part 2: 2765\n" 73 | ] 74 | } 75 | ], 76 | "source": [ 77 | "# starting from the last test banks setting, length of the loop\n", 78 | "assert find_circle(test) == 4\n", 79 | "\n", 80 | "# starting from the last banks state, length of the loop\n", 81 | "print(\"Part 2:\", find_circle(banks))" 82 | ] 83 | } 84 | ], 85 | "metadata": { 86 | "kernelspec": { 87 | "display_name": "Python 3", 88 | "language": "python", 89 | "name": "python3" 90 | }, 91 | "language_info": { 92 | "codemirror_mode": { 93 | "name": "ipython", 94 | "version": 3 95 | }, 96 | "file_extension": ".py", 97 | "mimetype": "text/x-python", 98 | "name": "python", 99 | "nbconvert_exporter": "python", 100 | "pygments_lexer": "ipython3", 101 | "version": "3.12.0" 102 | } 103 | }, 104 | "nbformat": 4, 105 | "nbformat_minor": 2 106 | } 107 | -------------------------------------------------------------------------------- /2017/Day 08.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import operator\n", 10 | "from collections import defaultdict\n", 11 | "from dataclasses import dataclass\n", 12 | "from typing import Callable\n", 13 | "\n", 14 | "operators = {\n", 15 | " \"inc\": operator.add,\n", 16 | " \"dec\": operator.sub,\n", 17 | "}\n", 18 | "\n", 19 | "comparisons = {\n", 20 | " \"<\": operator.lt,\n", 21 | " \"<=\": operator.le,\n", 22 | " \"==\": operator.eq,\n", 23 | " \"!=\": operator.ne,\n", 24 | " \">=\": operator.ge,\n", 25 | " \">\": operator.gt,\n", 26 | "}\n", 27 | "\n", 28 | "\n", 29 | "def parse_op(op):\n", 30 | " reg, op, value = op.split()\n", 31 | " value = int(value)\n", 32 | " op = operators[op]\n", 33 | " return (\n", 34 | " lambda registers: operator.setitem(registers, reg, op(registers[reg], value))\n", 35 | " or registers[reg]\n", 36 | " )\n", 37 | "\n", 38 | "\n", 39 | "def parse_cond(cond):\n", 40 | " reg, cond, target = cond.split()\n", 41 | " target = int(target)\n", 42 | " cond = comparisons[cond]\n", 43 | " return lambda registers: cond(registers[reg], target)\n", 44 | "\n", 45 | "\n", 46 | "@dataclass\n", 47 | "class Instruction(object):\n", 48 | " line: str\n", 49 | " operation: Callable[[dict], int]\n", 50 | " condition: Callable[[dict], bool]\n", 51 | "\n", 52 | " @classmethod\n", 53 | " def from_line(cls, line):\n", 54 | " line = line.strip()\n", 55 | " instr, cond = line.split(\"if\")\n", 56 | " return cls(line, parse_op(instr.strip()), parse_cond(cond.strip()))\n", 57 | "\n", 58 | " def execute(self, registers):\n", 59 | " if self.condition(registers):\n", 60 | " return self.operation(registers)\n", 61 | " return 0\n", 62 | "\n", 63 | "\n", 64 | "def read_program(lines):\n", 65 | " return [Instruction.from_line(line) for line in lines if line.strip()]\n", 66 | "\n", 67 | "\n", 68 | "def largest_register_value(program):\n", 69 | " registers = defaultdict(int)\n", 70 | " return (max(instr.execute(registers) for instr in program), max(registers.values()))\n", 71 | "\n", 72 | "\n", 73 | "test_program = read_program(\n", 74 | " \"\"\"\\\n", 75 | "b inc 5 if a > 1\n", 76 | "a inc 1 if b < 5\n", 77 | "c dec -10 if a >= 1\n", 78 | "c inc -20 if c == 10\n", 79 | "\"\"\".splitlines()\n", 80 | ")\n", 81 | "\n", 82 | "assert largest_register_value(test_program) == (10, 1)" 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": 2, 88 | "metadata": {}, 89 | "outputs": [], 90 | "source": [ 91 | "import aocd\n", 92 | "\n", 93 | "data = aocd.get_data(day=8, year=2017)\n", 94 | "program = read_program(data.splitlines())" 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": 3, 100 | "metadata": {}, 101 | "outputs": [ 102 | { 103 | "name": "stdout", 104 | "output_type": "stream", 105 | "text": [ 106 | "Part 1: 5946\n", 107 | "Part 2: 6026\n" 108 | ] 109 | } 110 | ], 111 | "source": [ 112 | "high_water_line, final_max = largest_register_value(program)\n", 113 | "print(\"Part 1:\", final_max)\n", 114 | "print(\"Part 2:\", high_water_line)" 115 | ] 116 | } 117 | ], 118 | "metadata": { 119 | "kernelspec": { 120 | "display_name": "Python 3", 121 | "language": "python", 122 | "name": "python3" 123 | }, 124 | "language_info": { 125 | "codemirror_mode": { 126 | "name": "ipython", 127 | "version": 3 128 | }, 129 | "file_extension": ".py", 130 | "mimetype": "text/x-python", 131 | "name": "python", 132 | "nbconvert_exporter": "python", 133 | "pygments_lexer": "ipython3", 134 | "version": "3.12.0" 135 | } 136 | }, 137 | "nbformat": 4, 138 | "nbformat_minor": 2 139 | } 140 | -------------------------------------------------------------------------------- /2017/Day 09.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "def parse_stream(stream):\n", 10 | " depth = 0\n", 11 | " garbage = escape = False\n", 12 | " for char in stream:\n", 13 | " if escape:\n", 14 | " escape = False\n", 15 | " elif char == \"!\":\n", 16 | " escape = True\n", 17 | " elif garbage:\n", 18 | " if char == \">\":\n", 19 | " garbage = False\n", 20 | " else:\n", 21 | " yield (\"G\", depth)\n", 22 | " else:\n", 23 | " if char == \"<\":\n", 24 | " garbage = True\n", 25 | " elif char == \"{\":\n", 26 | " depth += 1\n", 27 | " yield (\"O\", depth)\n", 28 | " elif char == \"}\":\n", 29 | " depth -= 1" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": 2, 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "tests = {\n", 39 | " \"{}\": 1,\n", 40 | " \"{{{}}}\": 3,\n", 41 | " \"{{},{}}\": 3,\n", 42 | " \"{{{},{},{{}}}}\": 6,\n", 43 | " \"{<{},{},{{}}>}\": 1,\n", 44 | " \"{,,,}\": 1,\n", 45 | " \"{{},{},{},{}}\": 5,\n", 46 | " \"{{},{},{},{}}\": 2,\n", 47 | "}\n", 48 | "for inp, expected in tests.items():\n", 49 | " result = sum(1 for op, depth in parse_stream(inp) if op == \"O\")\n", 50 | " assert result == expected, (inp, expected, result)\n", 51 | "\n", 52 | "\n", 53 | "tests = {\n", 54 | " \"{}\": 1,\n", 55 | " \"{{{}}}\": 6,\n", 56 | " \"{{},{}}\": 5,\n", 57 | " \"{{{},{},{{}}}}\": 16,\n", 58 | " \"{<{},{},{{}}>}\": 1,\n", 59 | " \"{,,,}\": 1,\n", 60 | " \"{{},{},{},{}}\": 9,\n", 61 | " \"{{},{},{},{}}\": 9,\n", 62 | " \"{{},{},{},{}}\": 3,\n", 63 | "}\n", 64 | "for inp, expected in tests.items():\n", 65 | " result = sum(depth for op, depth in parse_stream(inp) if op == \"O\")\n", 66 | " assert result == expected, (inp, expected, result)\n", 67 | "\n", 68 | "\n", 69 | "tests = {\n", 70 | " \"<>\": 0,\n", 71 | " \"\": 17,\n", 72 | " \"<<<<>\": 3,\n", 73 | " \"<{!>}>\": 2,\n", 74 | " \"\": 0,\n", 75 | " \">\": 0,\n", 76 | " '<{o\"i!a,<{i': 10,\n", 77 | "}\n", 78 | "for inp, expected in tests.items():\n", 79 | " result = sum(1 for op, depth in parse_stream(inp) if op == \"G\")\n", 80 | " assert result == expected, (inp, expected, result)" 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": 3, 86 | "metadata": {}, 87 | "outputs": [], 88 | "source": [ 89 | "import aocd\n", 90 | "\n", 91 | "stream = aocd.get_data(day=9, year=2017)" 92 | ] 93 | }, 94 | { 95 | "cell_type": "code", 96 | "execution_count": 4, 97 | "metadata": {}, 98 | "outputs": [ 99 | { 100 | "name": "stdout", 101 | "output_type": "stream", 102 | "text": [ 103 | "Part 1: 14421\n" 104 | ] 105 | } 106 | ], 107 | "source": [ 108 | "print(\"Part 1:\", sum(depth for op, depth in parse_stream(stream) if op == \"O\"))" 109 | ] 110 | }, 111 | { 112 | "cell_type": "code", 113 | "execution_count": 5, 114 | "metadata": {}, 115 | "outputs": [ 116 | { 117 | "name": "stdout", 118 | "output_type": "stream", 119 | "text": [ 120 | "Part 2: 6817\n" 121 | ] 122 | } 123 | ], 124 | "source": [ 125 | "print(\"Part 2:\", sum(1 for op, depth in parse_stream(stream) if op == \"G\"))" 126 | ] 127 | } 128 | ], 129 | "metadata": { 130 | "kernelspec": { 131 | "display_name": "Python 3", 132 | "language": "python", 133 | "name": "python3" 134 | }, 135 | "language_info": { 136 | "codemirror_mode": { 137 | "name": "ipython", 138 | "version": 3 139 | }, 140 | "file_extension": ".py", 141 | "mimetype": "text/x-python", 142 | "name": "python", 143 | "nbconvert_exporter": "python", 144 | "pygments_lexer": "ipython3", 145 | "version": "3.12.0" 146 | } 147 | }, 148 | "nbformat": 4, 149 | "nbformat_minor": 2 150 | } 151 | -------------------------------------------------------------------------------- /2017/Day 10.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from knothash import knot_hash_rounds, knot_hash\n", 10 | "\n", 11 | "assert knot_hash_rounds([3, 4, 1, 5], 5, 1)[:2] == [3, 4]\n", 12 | "\n", 13 | "tests = {\n", 14 | " b\"\": \"a2582a3a0e66e6e86e3812dcb672a272\",\n", 15 | " b\"AoC 2017\": \"33efeb34ea91902bb2f59c9920caa6cd\",\n", 16 | " b\"1,2,3\": \"3efbe78a8d82f29979031a4aa0b16a9d\",\n", 17 | " b\"1,2,4\": \"63960835bcdc130f0b66d7ff4f6a5a8e\",\n", 18 | "}\n", 19 | "for inp, expected in tests.items():\n", 20 | " assert knot_hash(inp) == expected" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 2, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "import aocd\n", 30 | "\n", 31 | "data = aocd.get_data(day=10, year=2017)\n", 32 | "lengths = [int(length) for length in data.split(\",\")]" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": 3, 38 | "metadata": { 39 | "scrolled": true 40 | }, 41 | "outputs": [ 42 | { 43 | "name": "stdout", 44 | "output_type": "stream", 45 | "text": [ 46 | "Part 1: 23874\n" 47 | ] 48 | } 49 | ], 50 | "source": [ 51 | "hashed = knot_hash_rounds(lengths, rounds=1)\n", 52 | "print(\"Part 1:\", hashed[0] * hashed[1])" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": 4, 58 | "metadata": {}, 59 | "outputs": [ 60 | { 61 | "name": "stdout", 62 | "output_type": "stream", 63 | "text": [ 64 | "Part 2: e1a65bfb5a5ce396025fab5528c25a87\n" 65 | ] 66 | } 67 | ], 68 | "source": [ 69 | "print(\"Part 2:\", knot_hash(data.encode(\"ascii\")))" 70 | ] 71 | } 72 | ], 73 | "metadata": { 74 | "kernelspec": { 75 | "display_name": "Python 3", 76 | "language": "python", 77 | "name": "python3" 78 | }, 79 | "language_info": { 80 | "codemirror_mode": { 81 | "name": "ipython", 82 | "version": 3 83 | }, 84 | "file_extension": ".py", 85 | "mimetype": "text/x-python", 86 | "name": "python", 87 | "nbconvert_exporter": "python", 88 | "pygments_lexer": "ipython3", 89 | "version": "3.12.0" 90 | } 91 | }, 92 | "nbformat": 4, 93 | "nbformat_minor": 2 94 | } 95 | -------------------------------------------------------------------------------- /2017/Day 11.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from dataclasses import dataclass, field\n", 10 | "\n", 11 | "directions = {\n", 12 | " \"n\": (0, -1),\n", 13 | " \"ne\": (1, -1),\n", 14 | " \"se\": (1, 0),\n", 15 | " \"s\": (0, 1),\n", 16 | " \"sw\": (-1, 1),\n", 17 | " \"nw\": (-1, 0),\n", 18 | "}\n", 19 | "\n", 20 | "\n", 21 | "@dataclass(frozen=True)\n", 22 | "class HexCoord(object):\n", 23 | " q: int = field(default=0)\n", 24 | " r: int = field(default=0)\n", 25 | "\n", 26 | " def hex_distance(self):\n", 27 | " s = -self.q - self.r\n", 28 | " return max(map(abs, (self.q, self.r, s)))\n", 29 | "\n", 30 | " def step(self, direction):\n", 31 | " dq, dr = directions[direction]\n", 32 | " return type(self)(self.q + dq, self.r + dr)\n", 33 | "\n", 34 | "\n", 35 | "def walk(directions):\n", 36 | " pos = HexCoord()\n", 37 | " for direction in directions:\n", 38 | " pos = pos.step(direction)\n", 39 | " yield pos\n", 40 | "\n", 41 | "\n", 42 | "def max_distance(directions):\n", 43 | " return max(pos.hex_distance() for pos in walk(directions))" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": 2, 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "from collections import deque\n", 53 | "\n", 54 | "tests = {\n", 55 | " \"ne,ne,ne\": 3,\n", 56 | " \"ne,ne,sw,sw\": 0,\n", 57 | " \"ne,ne,s,s\": 2,\n", 58 | " \"se,sw,se,sw,sw\": 3,\n", 59 | "}\n", 60 | "for inp, expected in tests.items():\n", 61 | " (pos,) = deque(walk(inp.split(\",\")), 1) # keep only the last value\n", 62 | " assert pos.hex_distance() == expected" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": 3, 68 | "metadata": {}, 69 | "outputs": [], 70 | "source": [ 71 | "import aocd\n", 72 | "\n", 73 | "data = aocd.get_data(day=11, year=2017)\n", 74 | "path = data.split(\",\")" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": 4, 80 | "metadata": {}, 81 | "outputs": [ 82 | { 83 | "name": "stdout", 84 | "output_type": "stream", 85 | "text": [ 86 | "Part 1: 796\n" 87 | ] 88 | } 89 | ], 90 | "source": [ 91 | "(pos,) = deque(walk(path), 1) # keep only the last value\n", 92 | "print(\"Part 1:\", pos.hex_distance())" 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": 5, 98 | "metadata": {}, 99 | "outputs": [ 100 | { 101 | "name": "stdout", 102 | "output_type": "stream", 103 | "text": [ 104 | "Part 2: 1585\n" 105 | ] 106 | } 107 | ], 108 | "source": [ 109 | "print(\"Part 2:\", max_distance(path))" 110 | ] 111 | } 112 | ], 113 | "metadata": { 114 | "kernelspec": { 115 | "display_name": "Python 3", 116 | "language": "python", 117 | "name": "python3" 118 | }, 119 | "language_info": { 120 | "codemirror_mode": { 121 | "name": "ipython", 122 | "version": 3 123 | }, 124 | "file_extension": ".py", 125 | "mimetype": "text/x-python", 126 | "name": "python", 127 | "nbconvert_exporter": "python", 128 | "pygments_lexer": "ipython3", 129 | "version": "3.12.0" 130 | } 131 | }, 132 | "nbformat": 4, 133 | "nbformat_minor": 2 134 | } 135 | -------------------------------------------------------------------------------- /2017/Day 12.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from collections import deque\n", 10 | "\n", 11 | "\n", 12 | "def visit_nodes(graph, start=0):\n", 13 | " queue = deque([start])\n", 14 | " seen = {start}\n", 15 | " yield start\n", 16 | " while queue:\n", 17 | " for next_ in graph[queue.popleft()]:\n", 18 | " if next_ in seen:\n", 19 | " continue\n", 20 | " yield next_\n", 21 | " seen.add(next_)\n", 22 | " queue.append(next_)\n", 23 | "\n", 24 | "\n", 25 | "def read_graph(lines):\n", 26 | " graph = {}\n", 27 | " for line in lines:\n", 28 | " if not line.strip():\n", 29 | " continue\n", 30 | " node, targets = line.split(\" <-> \")\n", 31 | " graph[int(node)] = [int(t) for t in targets.split(\",\")]\n", 32 | " return graph\n", 33 | "\n", 34 | "\n", 35 | "def graph_groups(graph):\n", 36 | " \"\"\"yield a node from each group\"\"\"\n", 37 | " seen = set()\n", 38 | " for node in graph:\n", 39 | " if node not in seen:\n", 40 | " yield node\n", 41 | " seen.update(visit_nodes(graph, node))" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": 2, 47 | "metadata": {}, 48 | "outputs": [], 49 | "source": [ 50 | "test_graph = read_graph(\n", 51 | " \"\"\"\\\n", 52 | "0 <-> 2\n", 53 | "1 <-> 1\n", 54 | "2 <-> 0, 3, 4\n", 55 | "3 <-> 2, 4\n", 56 | "4 <-> 2, 3, 6\n", 57 | "5 <-> 6\n", 58 | "6 <-> 4, 5\n", 59 | "\"\"\".splitlines()\n", 60 | ")\n", 61 | "assert sum(1 for _ in visit_nodes(test_graph)) == 6\n", 62 | "assert sum(1 for _ in graph_groups(test_graph)) == 2" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": 3, 68 | "metadata": {}, 69 | "outputs": [], 70 | "source": [ 71 | "import aocd\n", 72 | "\n", 73 | "data = aocd.get_data(day=12, year=2017)\n", 74 | "graph = read_graph(data.splitlines())" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": 4, 80 | "metadata": {}, 81 | "outputs": [ 82 | { 83 | "name": "stdout", 84 | "output_type": "stream", 85 | "text": [ 86 | "Part 1: 128\n" 87 | ] 88 | } 89 | ], 90 | "source": [ 91 | "print(\"Part 1:\", sum(1 for _ in visit_nodes(graph)))" 92 | ] 93 | }, 94 | { 95 | "cell_type": "code", 96 | "execution_count": 5, 97 | "metadata": {}, 98 | "outputs": [ 99 | { 100 | "name": "stdout", 101 | "output_type": "stream", 102 | "text": [ 103 | "Part 2: 209\n" 104 | ] 105 | } 106 | ], 107 | "source": [ 108 | "print(\"Part 2:\", sum(1 for _ in graph_groups(graph)))" 109 | ] 110 | } 111 | ], 112 | "metadata": { 113 | "kernelspec": { 114 | "display_name": "Python 3", 115 | "language": "python", 116 | "name": "python3" 117 | }, 118 | "language_info": { 119 | "codemirror_mode": { 120 | "name": "ipython", 121 | "version": 3 122 | }, 123 | "file_extension": ".py", 124 | "mimetype": "text/x-python", 125 | "name": "python", 126 | "nbconvert_exporter": "python", 127 | "pygments_lexer": "ipython3", 128 | "version": "3.12.0" 129 | } 130 | }, 131 | "nbformat": 4, 132 | "nbformat_minor": 2 133 | } 134 | -------------------------------------------------------------------------------- /2017/Day 13.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from dataclasses import dataclass\n", 10 | "from itertools import count\n", 11 | "\n", 12 | "\n", 13 | "@dataclass\n", 14 | "class Scanner(object):\n", 15 | " depth: int\n", 16 | " period: int\n", 17 | " severity: int\n", 18 | "\n", 19 | " @classmethod\n", 20 | " def from_line(cls, line):\n", 21 | " depth, range_ = map(int, line.split(\":\"))\n", 22 | " return cls(depth, (range_ - 1) * 2, depth * range_)\n", 23 | "\n", 24 | " def catches(self, delay=0):\n", 25 | " return (self.depth + delay) % self.period == 0\n", 26 | "\n", 27 | "\n", 28 | "def severity(firewall):\n", 29 | " return sum(s.severity for s in firewall if s.catches())\n", 30 | "\n", 31 | "\n", 32 | "def find_delay(firewall):\n", 33 | " return next(d for d in count() if not any(s.catches(d) for s in firewall))\n", 34 | "\n", 35 | "\n", 36 | "def read_firewall(lines):\n", 37 | " return [Scanner.from_line(line) for line in lines if line.strip()]" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 2, 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "test = read_firewall(\n", 47 | " \"\"\"\\\n", 48 | "0: 3\n", 49 | "1: 2\n", 50 | "4: 4\n", 51 | "6: 4\n", 52 | "\"\"\".splitlines()\n", 53 | ")\n", 54 | "\n", 55 | "assert severity(test) == 24\n", 56 | "assert find_delay(test) == 10" 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": 3, 62 | "metadata": {}, 63 | "outputs": [], 64 | "source": [ 65 | "import aocd\n", 66 | "\n", 67 | "data = aocd.get_data(day=13, year=2017)\n", 68 | "firewall = read_firewall(data.splitlines())" 69 | ] 70 | }, 71 | { 72 | "cell_type": "code", 73 | "execution_count": 4, 74 | "metadata": {}, 75 | "outputs": [ 76 | { 77 | "name": "stdout", 78 | "output_type": "stream", 79 | "text": [ 80 | "Part 1: 748\n" 81 | ] 82 | } 83 | ], 84 | "source": [ 85 | "print(\"Part 1:\", severity(firewall))" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": 5, 91 | "metadata": {}, 92 | "outputs": [ 93 | { 94 | "name": "stdout", 95 | "output_type": "stream", 96 | "text": [ 97 | "Part 2: 3873662\n" 98 | ] 99 | } 100 | ], 101 | "source": [ 102 | "print(\"Part 2:\", find_delay(firewall))" 103 | ] 104 | } 105 | ], 106 | "metadata": { 107 | "kernelspec": { 108 | "display_name": "Python 3", 109 | "language": "python", 110 | "name": "python3" 111 | }, 112 | "language_info": { 113 | "codemirror_mode": { 114 | "name": "ipython", 115 | "version": 3 116 | }, 117 | "file_extension": ".py", 118 | "mimetype": "text/x-python", 119 | "name": "python", 120 | "nbconvert_exporter": "python", 121 | "pygments_lexer": "ipython3", 122 | "version": "3.12.0" 123 | } 124 | }, 125 | "nbformat": 4, 126 | "nbformat_minor": 2 127 | } 128 | -------------------------------------------------------------------------------- /2017/Day 14.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from collections import deque\n", 10 | "\n", 11 | "import numpy as np\n", 12 | "\n", 13 | "from knothash import knot_hash\n", 14 | "\n", 15 | "hextobitcount = {\n", 16 | " \"0\": 0,\n", 17 | " \"1\": 1,\n", 18 | " \"2\": 1,\n", 19 | " \"3\": 2,\n", 20 | " \"4\": 1,\n", 21 | " \"5\": 2,\n", 22 | " \"6\": 2,\n", 23 | " \"7\": 3,\n", 24 | " \"8\": 1,\n", 25 | " \"9\": 2,\n", 26 | " \"a\": 2,\n", 27 | " \"b\": 3,\n", 28 | " \"c\": 2,\n", 29 | " \"d\": 3,\n", 30 | " \"e\": 3,\n", 31 | " \"f\": 4,\n", 32 | "}\n", 33 | "\n", 34 | "\n", 35 | "def gen_grid(prefix):\n", 36 | " return [knot_hash(b\"%s-%d\" % (prefix, i)) for i in range(128)]\n", 37 | "\n", 38 | "\n", 39 | "def count_squares(grid):\n", 40 | " return sum(hextobitcount[h] for row in grid for h in row)\n", 41 | "\n", 42 | "\n", 43 | "def bitset(row, bit):\n", 44 | " byte, bit = bit // 8, bit % 8\n", 45 | " return (bytes.fromhex(row)[byte] & (1 << (7 - bit))) != 0\n", 46 | "\n", 47 | "\n", 48 | "directions = ((-1, 0), (0, -1), (1, 0), (0, 1))\n", 49 | "\n", 50 | "\n", 51 | "def fill_region(r, c, grid, handled, directions=directions):\n", 52 | " queue = deque([(r, c)])\n", 53 | " handled[r, c] = True\n", 54 | " while queue:\n", 55 | " r, c = queue.popleft()\n", 56 | " for dr, dc in directions:\n", 57 | " nr, nc = r + dr, c + dc\n", 58 | " if not 0 <= nr < 128 or not 0 <= nc < 128:\n", 59 | " continue\n", 60 | " if not handled[nr, nc] and bitset(grid[nr], nc):\n", 61 | " handled[nr, nc] = True\n", 62 | " queue.append((nr, nc))\n", 63 | "\n", 64 | "\n", 65 | "def find_regions(grid):\n", 66 | " handled = np.zeros((128, 128), dtype=bool)\n", 67 | " regions = 0\n", 68 | " for r, row in enumerate(grid):\n", 69 | " for c in range(128):\n", 70 | " if not handled[r, c] and bitset(row, c):\n", 71 | " regions += 1\n", 72 | " fill_region(r, c, grid, handled)\n", 73 | " return regions" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": 2, 79 | "metadata": {}, 80 | "outputs": [], 81 | "source": [ 82 | "test = gen_grid(b\"flqrgnkx\")\n", 83 | "assert count_squares(test) == 8108\n", 84 | "assert find_regions(test) == 1242" 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": 3, 90 | "metadata": {}, 91 | "outputs": [], 92 | "source": [ 93 | "import aocd\n", 94 | "\n", 95 | "data = aocd.get_data(day=14, year=2017)\n", 96 | "grid = gen_grid(data.encode(\"ascii\"))" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": 4, 102 | "metadata": {}, 103 | "outputs": [ 104 | { 105 | "name": "stdout", 106 | "output_type": "stream", 107 | "text": [ 108 | "Part 1: 8216\n" 109 | ] 110 | } 111 | ], 112 | "source": [ 113 | "print(\"Part 1:\", count_squares(grid))" 114 | ] 115 | }, 116 | { 117 | "cell_type": "code", 118 | "execution_count": 5, 119 | "metadata": {}, 120 | "outputs": [ 121 | { 122 | "name": "stdout", 123 | "output_type": "stream", 124 | "text": [ 125 | "Part 2: 1139\n" 126 | ] 127 | } 128 | ], 129 | "source": [ 130 | "print(\"Part 2:\", find_regions(grid))" 131 | ] 132 | } 133 | ], 134 | "metadata": { 135 | "kernelspec": { 136 | "display_name": "Python 3", 137 | "language": "python", 138 | "name": "python3" 139 | }, 140 | "language_info": { 141 | "codemirror_mode": { 142 | "name": "ipython", 143 | "version": 3 144 | }, 145 | "file_extension": ".py", 146 | "mimetype": "text/x-python", 147 | "name": "python", 148 | "nbconvert_exporter": "python", 149 | "pygments_lexer": "ipython3", 150 | "version": "3.12.0" 151 | } 152 | }, 153 | "nbformat": 4, 154 | "nbformat_minor": 2 155 | } 156 | -------------------------------------------------------------------------------- /2017/Day 15.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from itertools import islice\n", 10 | "\n", 11 | "factor_a, factor_b = 16807, 48271\n", 12 | "\n", 13 | "\n", 14 | "def generator(start, factor, mod=1):\n", 15 | " value = start\n", 16 | " while True:\n", 17 | " value *= factor\n", 18 | " value %= 2147483647\n", 19 | " if value % mod == 0:\n", 20 | " yield value\n", 21 | "\n", 22 | "\n", 23 | "def count_matches(gen_a, gen_b, limit=4 * 10**7):\n", 24 | " return sum(\n", 25 | " 1 for a, b in islice(zip(gen_a, gen_b), limit) if a & 0xFFFF == b & 0xFFFF\n", 26 | " )" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": 2, 32 | "metadata": {}, 33 | "outputs": [], 34 | "source": [ 35 | "test_gen_a, test_gen_b = generator(65, factor_a), generator(8921, factor_b)\n", 36 | "tests = [\n", 37 | " (1092455, 430625591),\n", 38 | " (1181022009, 1233683848),\n", 39 | " (245556042, 1431495498),\n", 40 | " (1744312007, 137874439),\n", 41 | " (1352636452, 285222916),\n", 42 | "]\n", 43 | "for expected_a, expected_b in tests:\n", 44 | " assert (next(test_gen_a), next(test_gen_b)) == (expected_a, expected_b)\n", 45 | "\n", 46 | "tests = [\n", 47 | " (1352636452, 1233683848),\n", 48 | " (1992081072, 862516352),\n", 49 | " (530830436, 1159784568),\n", 50 | " (1980017072, 1616057672),\n", 51 | " (740335192, 412269392),\n", 52 | "]\n", 53 | "test_gen_a, test_gen_b = generator(65, factor_a, 4), generator(8921, factor_b, 8)\n", 54 | "for expected_a, expected_b in tests:\n", 55 | " assert (next(test_gen_a), next(test_gen_b)) == (expected_a, expected_b)" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": 3, 61 | "metadata": {}, 62 | "outputs": [], 63 | "source": [ 64 | "test_gen_a, test_gen_b = generator(65, factor_a), generator(8921, factor_b)\n", 65 | "assert count_matches(test_gen_a, test_gen_b) == 588" 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "execution_count": 4, 71 | "metadata": {}, 72 | "outputs": [], 73 | "source": [ 74 | "test_gen_a, test_gen_b = generator(65, factor_a, 4), generator(8921, factor_b, 8)\n", 75 | "assert count_matches(test_gen_a, test_gen_b, 5 * 10**6) == 309" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": 5, 81 | "metadata": {}, 82 | "outputs": [], 83 | "source": [ 84 | "import aocd\n", 85 | "\n", 86 | "data = aocd.get_data(day=15, year=2017)\n", 87 | "start_a, start_b = (int(line.rpartition(\" \")[-1]) for line in data.splitlines())" 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": 6, 93 | "metadata": {}, 94 | "outputs": [ 95 | { 96 | "name": "stdout", 97 | "output_type": "stream", 98 | "text": [ 99 | "Part 1: 573\n" 100 | ] 101 | } 102 | ], 103 | "source": [ 104 | "print(\n", 105 | " \"Part 1:\", count_matches(generator(start_a, factor_a), generator(start_b, factor_b))\n", 106 | ")" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": 7, 112 | "metadata": {}, 113 | "outputs": [ 114 | { 115 | "name": "stdout", 116 | "output_type": "stream", 117 | "text": [ 118 | "Part 2: 294\n" 119 | ] 120 | } 121 | ], 122 | "source": [ 123 | "print(\n", 124 | " \"Part 2:\",\n", 125 | " count_matches(\n", 126 | " generator(start_a, factor_a, 4), generator(start_b, factor_b, 8), 5 * 10**6\n", 127 | " ),\n", 128 | ")" 129 | ] 130 | } 131 | ], 132 | "metadata": { 133 | "kernelspec": { 134 | "display_name": "Python 3", 135 | "language": "python", 136 | "name": "python3" 137 | }, 138 | "language_info": { 139 | "codemirror_mode": { 140 | "name": "ipython", 141 | "version": 3 142 | }, 143 | "file_extension": ".py", 144 | "mimetype": "text/x-python", 145 | "name": "python", 146 | "nbconvert_exporter": "python", 147 | "pygments_lexer": "ipython3", 148 | "version": "3.12.0" 149 | } 150 | }, 151 | "nbformat": 4, 152 | "nbformat_minor": 2 153 | } 154 | -------------------------------------------------------------------------------- /2017/Day 16.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "def spin(line, x):\n", 10 | " line[:] = line[-x:] + line[:-x]\n", 11 | "\n", 12 | "\n", 13 | "def exchange(line, a, b):\n", 14 | " line[a], line[b] = line[b], line[a]\n", 15 | "\n", 16 | "\n", 17 | "def partner(line, a, b):\n", 18 | " exchange(line, line.index(a), line.index(b))\n", 19 | "\n", 20 | "\n", 21 | "dancemoves = {\n", 22 | " \"s\": spin,\n", 23 | " \"x\": exchange,\n", 24 | " \"p\": partner,\n", 25 | "}\n", 26 | "\n", 27 | "\n", 28 | "def dance(moves, dancers=\"abcdefghijklmnop\", repeats=1):\n", 29 | " line = list(dancers)\n", 30 | "\n", 31 | " seen = []\n", 32 | " for _ in range(repeats):\n", 33 | " for move in moves:\n", 34 | " move, instr = move[0], move[1:].split(\"/\")\n", 35 | " if move != \"p\":\n", 36 | " instr = map(int, instr)\n", 37 | " dancemoves[move](line, *instr)\n", 38 | " key = \"\".join(line)\n", 39 | " if key in seen:\n", 40 | " return seen[(repeats - 1) % len(seen)]\n", 41 | " seen.append(key)\n", 42 | "\n", 43 | " return key" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": 2, 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "assert dance([\"s1\", \"x3/4\", \"pe/b\"], \"abcde\") == \"baedc\"" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": 3, 58 | "metadata": {}, 59 | "outputs": [], 60 | "source": [ 61 | "import aocd\n", 62 | "\n", 63 | "data = aocd.get_data(day=16, year=2017)\n", 64 | "moves = data.split(\",\")" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": 4, 70 | "metadata": {}, 71 | "outputs": [ 72 | { 73 | "name": "stdout", 74 | "output_type": "stream", 75 | "text": [ 76 | "Part 1: ceijbfoamgkdnlph\n" 77 | ] 78 | } 79 | ], 80 | "source": [ 81 | "print(\"Part 1:\", dance(moves))" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": 5, 87 | "metadata": {}, 88 | "outputs": [ 89 | { 90 | "name": "stdout", 91 | "output_type": "stream", 92 | "text": [ 93 | "Part 2: pnhajoekigcbflmd\n" 94 | ] 95 | } 96 | ], 97 | "source": [ 98 | "print(\"Part 2:\", dance(moves, repeats=10**9))" 99 | ] 100 | } 101 | ], 102 | "metadata": { 103 | "kernelspec": { 104 | "display_name": "Python 3", 105 | "language": "python", 106 | "name": "python3" 107 | }, 108 | "language_info": { 109 | "codemirror_mode": { 110 | "name": "ipython", 111 | "version": 3 112 | }, 113 | "file_extension": ".py", 114 | "mimetype": "text/x-python", 115 | "name": "python", 116 | "nbconvert_exporter": "python", 117 | "pygments_lexer": "ipython3", 118 | "version": "3.12.0" 119 | } 120 | }, 121 | "nbformat": 4, 122 | "nbformat_minor": 2 123 | } 124 | -------------------------------------------------------------------------------- /2017/Day 17.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from collections import deque\n", 10 | "\n", 11 | "\n", 12 | "def spinlock(step):\n", 13 | " buffer = deque([0])\n", 14 | " for i in range(1, 2018):\n", 15 | " buffer.rotate(-step)\n", 16 | " buffer.append(i)\n", 17 | " return buffer\n", 18 | "\n", 19 | "\n", 20 | "def spinlock_after_zero(step):\n", 21 | " zero_pos = 0\n", 22 | " for i in range(1, 50000001):\n", 23 | " zero_pos = (zero_pos - step) % i\n", 24 | " if zero_pos == i - 1:\n", 25 | " last_after_zero = i\n", 26 | " return last_after_zero" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": 2, 32 | "metadata": {}, 33 | "outputs": [], 34 | "source": [ 35 | "test = spinlock(3)\n", 36 | "assert list(test)[-4:] + list(test)[:3] == [1512, 1134, 151, 2017, 638, 1513, 851]" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": 3, 42 | "metadata": {}, 43 | "outputs": [], 44 | "source": [ 45 | "import aocd\n", 46 | "\n", 47 | "data = aocd.get_data(day=17, year=2017)\n", 48 | "steps = int(data)" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": 4, 54 | "metadata": {}, 55 | "outputs": [ 56 | { 57 | "name": "stdout", 58 | "output_type": "stream", 59 | "text": [ 60 | "Part 1: 926\n" 61 | ] 62 | } 63 | ], 64 | "source": [ 65 | "buffer = spinlock(steps)\n", 66 | "print(\"Part 1:\", buffer[0])" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": 5, 72 | "metadata": {}, 73 | "outputs": [ 74 | { 75 | "name": "stdout", 76 | "output_type": "stream", 77 | "text": [ 78 | "Part 2: 10150888\n" 79 | ] 80 | } 81 | ], 82 | "source": [ 83 | "last_after_zero = spinlock_after_zero(steps)\n", 84 | "print(\"Part 2:\", last_after_zero)" 85 | ] 86 | } 87 | ], 88 | "metadata": { 89 | "kernelspec": { 90 | "display_name": "Python 3", 91 | "language": "python", 92 | "name": "python3" 93 | }, 94 | "language_info": { 95 | "codemirror_mode": { 96 | "name": "ipython", 97 | "version": 3 98 | }, 99 | "file_extension": ".py", 100 | "mimetype": "text/x-python", 101 | "name": "python", 102 | "nbconvert_exporter": "python", 103 | "pygments_lexer": "ipython3", 104 | "version": "3.12.0" 105 | } 106 | }, 107 | "nbformat": 4, 108 | "nbformat_minor": 2 109 | } 110 | -------------------------------------------------------------------------------- /2017/Day 19.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 3, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "def follow_path(map_):\n", 10 | " x, y = map_[0].index(\"|\"), 0\n", 11 | " dx, dy = 0, 1\n", 12 | " steps = 0\n", 13 | " found = []\n", 14 | " while True:\n", 15 | " steps += 1\n", 16 | " x, y = x + dx, y + dy\n", 17 | " mapchar = map_[y][x]\n", 18 | " if mapchar == \" \" or not 0 <= x < len(map_[0]) or not 0 <= y < len(map_):\n", 19 | " # map end found\n", 20 | " return \"\".join(found), steps\n", 21 | " if mapchar.isalpha():\n", 22 | " found.append(mapchar)\n", 23 | " elif mapchar == \"+\":\n", 24 | " # direction change, find new\n", 25 | " if dx:\n", 26 | " dx, dy = 0, 1 if map_[y - 1][x] == \" \" else -1\n", 27 | " else:\n", 28 | " dx, dy = 1 if map_[y][x - 1] == \" \" else -1, 0" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": 4, 34 | "metadata": {}, 35 | "outputs": [], 36 | "source": [ 37 | "word, steps = follow_path(\n", 38 | " [\n", 39 | " \" | \",\n", 40 | " \" | +--+ \",\n", 41 | " \" A | C \",\n", 42 | " \" F---|----E|--+ \",\n", 43 | " \" | | | D \",\n", 44 | " \" +B-+ +--+ \",\n", 45 | " ]\n", 46 | ")\n", 47 | "assert word == \"ABCDEF\"\n", 48 | "assert steps == 38" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": 5, 54 | "metadata": {}, 55 | "outputs": [], 56 | "source": [ 57 | "import aocd\n", 58 | "\n", 59 | "data = aocd.get_data(day=19, year=2017)\n", 60 | "map_ = data.splitlines()" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": 6, 66 | "metadata": {}, 67 | "outputs": [ 68 | { 69 | "name": "stdout", 70 | "output_type": "stream", 71 | "text": [ 72 | "Part 1: AYRPVMEGQ\n", 73 | "Part 2: 16408\n" 74 | ] 75 | } 76 | ], 77 | "source": [ 78 | "word, steps = list(follow_path(map_))\n", 79 | "print(\"Part 1:\", word)\n", 80 | "print(\"Part 2:\", steps)" 81 | ] 82 | } 83 | ], 84 | "metadata": { 85 | "kernelspec": { 86 | "display_name": "Python 3", 87 | "language": "python", 88 | "name": "python3" 89 | }, 90 | "language_info": { 91 | "codemirror_mode": { 92 | "name": "ipython", 93 | "version": 3 94 | }, 95 | "file_extension": ".py", 96 | "mimetype": "text/x-python", 97 | "name": "python", 98 | "nbconvert_exporter": "python", 99 | "pygments_lexer": "ipython3", 100 | "version": "3.12.0" 101 | } 102 | }, 103 | "nbformat": 4, 104 | "nbformat_minor": 2 105 | } 106 | -------------------------------------------------------------------------------- /2017/Day 24.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "def enumerate_paths(graph, node=0, path=()):\n", 10 | " # yields path as [(node_a, node_b)]\n", 11 | " seen = {frozenset(edge) for edge in path}\n", 12 | " for next_ in graph[node]:\n", 13 | " edge = (node, next_)\n", 14 | " if frozenset(edge) not in seen:\n", 15 | " subpath = path + (edge,)\n", 16 | " yield subpath\n", 17 | " yield from enumerate_paths(graph, next_, subpath)\n", 18 | "\n", 19 | "\n", 20 | "def strongest_bridge(graph):\n", 21 | " return max(sum(a + b for a, b in path) for path in enumerate_paths(graph))\n", 22 | "\n", 23 | "\n", 24 | "def longest_strongest_bridge(graph):\n", 25 | " max_length = 0\n", 26 | " max_weight = 0\n", 27 | " for path in enumerate_paths(graph):\n", 28 | " if len(path) < max_length:\n", 29 | " continue\n", 30 | " weight = sum(a + b for a, b in path)\n", 31 | " if len(path) == max_length:\n", 32 | " max_weight = max(max_weight, weight)\n", 33 | " else:\n", 34 | " max_length, max_weight = len(path), weight\n", 35 | " return max_weight\n", 36 | "\n", 37 | "\n", 38 | "def read_graph(lines):\n", 39 | " graph = {}\n", 40 | " for line in lines:\n", 41 | " node_a, node_b = map(int, line.strip().split(\"/\"))\n", 42 | " graph.setdefault(node_a, set()).add(node_b)\n", 43 | " graph.setdefault(node_b, set()).add(node_a)\n", 44 | " return graph" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": 2, 50 | "metadata": {}, 51 | "outputs": [], 52 | "source": [ 53 | "test_graph = read_graph(\n", 54 | " \"\"\"\\\n", 55 | "0/2\n", 56 | "2/2\n", 57 | "2/3\n", 58 | "3/4\n", 59 | "3/5\n", 60 | "0/1\n", 61 | "10/1\n", 62 | "9/10\n", 63 | "\"\"\".splitlines()\n", 64 | ")\n", 65 | "assert strongest_bridge(test_graph) == 31\n", 66 | "assert longest_strongest_bridge(test_graph) == 19" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": 3, 72 | "metadata": {}, 73 | "outputs": [], 74 | "source": [ 75 | "import aocd\n", 76 | "\n", 77 | "data = aocd.get_data(day=24, year=2017)\n", 78 | "graph = read_graph(data.splitlines())" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": 4, 84 | "metadata": {}, 85 | "outputs": [ 86 | { 87 | "name": "stdout", 88 | "output_type": "stream", 89 | "text": [ 90 | "Part 1: 1868\n" 91 | ] 92 | } 93 | ], 94 | "source": [ 95 | "print(\"Part 1:\", strongest_bridge(graph))" 96 | ] 97 | }, 98 | { 99 | "cell_type": "code", 100 | "execution_count": 5, 101 | "metadata": {}, 102 | "outputs": [ 103 | { 104 | "name": "stdout", 105 | "output_type": "stream", 106 | "text": [ 107 | "Part 2: 1841\n" 108 | ] 109 | } 110 | ], 111 | "source": [ 112 | "print(\"Part 2:\", longest_strongest_bridge(graph))" 113 | ] 114 | } 115 | ], 116 | "metadata": { 117 | "kernelspec": { 118 | "display_name": "Python 3", 119 | "language": "python", 120 | "name": "python3" 121 | }, 122 | "language_info": { 123 | "codemirror_mode": { 124 | "name": "ipython", 125 | "version": 3 126 | }, 127 | "file_extension": ".py", 128 | "mimetype": "text/x-python", 129 | "name": "python", 130 | "nbconvert_exporter": "python", 131 | "pygments_lexer": "ipython3", 132 | "version": "3.12.0" 133 | } 134 | }, 135 | "nbformat": 4, 136 | "nbformat_minor": 2 137 | } 138 | -------------------------------------------------------------------------------- /2017/knothash.py: -------------------------------------------------------------------------------- 1 | from collections import deque 2 | from functools import reduce 3 | from itertools import chain, repeat 4 | from operator import xor 5 | 6 | 7 | def knot_hash_rounds(lenghts, size=256, rounds=64): 8 | string = deque(range(size), size) 9 | skip = pos = 0 10 | for length in chain.from_iterable(repeat(lenghts, rounds)): 11 | string.extend(reversed([string.popleft() for _ in range(length)])) 12 | string.rotate(-skip) 13 | pos, skip = (pos + length + skip) % size, skip + 1 14 | string.rotate(pos) 15 | return list(string) 16 | 17 | 18 | def knot_hash(value, _suffix=bytes([17, 31, 73, 47, 23])): 19 | sparse_hash = knot_hash_rounds(value + _suffix) 20 | dense_hash = bytes( 21 | reduce(xor, (sparse_hash[b] for b in range(i, i + 16))) 22 | for i in range(0, 256, 16) 23 | ) 24 | return dense_hash.hex() 25 | -------------------------------------------------------------------------------- /2017/ruff.toml: -------------------------------------------------------------------------------- 1 | extend = "../pyproject.toml" 2 | 3 | [lint.isort] 4 | known-first-party = ["knothash"] 5 | -------------------------------------------------------------------------------- /2018/Day 01.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Day 1 - warm-up excercise\n", 8 | "\n", 9 | "- [Day 1](https://adventofcode.com/2018/day/1)\n", 10 | "\n", 11 | "Day 1 is here started; the challenge is to parse text into numbers and sum them.\n" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 1, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "import aocd\n", 21 | "\n", 22 | "data = aocd.get_data(day=1, year=2018)\n", 23 | "changes = list(map(int, data.splitlines()))" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": 2, 29 | "metadata": {}, 30 | "outputs": [ 31 | { 32 | "name": "stdout", 33 | "output_type": "stream", 34 | "text": [ 35 | "Part 1: 536\n" 36 | ] 37 | } 38 | ], 39 | "source": [ 40 | "print(\"Part 1:\", sum(changes))" 41 | ] 42 | }, 43 | { 44 | "cell_type": "markdown", 45 | "metadata": {}, 46 | "source": [ 47 | "## Part 2 - find a cycle\n", 48 | "\n", 49 | "We can use a set to see when a frequency recurs, and [`itertools.cycle()`](https://docs.python.org/3/library/itertools.html#itertools.cycle) to loop over the input numbers repeatedly.\n" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": 3, 55 | "metadata": {}, 56 | "outputs": [], 57 | "source": [ 58 | "from itertools import cycle\n", 59 | "\n", 60 | "seen = set()\n", 61 | "freq = 0\n", 62 | "repeated_changes = cycle(changes)\n", 63 | "for change in cycle(changes):\n", 64 | " freq += change\n", 65 | " if freq in seen:\n", 66 | " break\n", 67 | " seen.add(freq)" 68 | ] 69 | }, 70 | { 71 | "cell_type": "code", 72 | "execution_count": 4, 73 | "metadata": {}, 74 | "outputs": [ 75 | { 76 | "name": "stdout", 77 | "output_type": "stream", 78 | "text": [ 79 | "Part 2: 75108\n" 80 | ] 81 | } 82 | ], 83 | "source": [ 84 | "print(\"Part 2:\", freq)" 85 | ] 86 | } 87 | ], 88 | "metadata": { 89 | "kernelspec": { 90 | "display_name": "Python 3", 91 | "language": "python", 92 | "name": "python3" 93 | }, 94 | "language_info": { 95 | "codemirror_mode": { 96 | "name": "ipython", 97 | "version": 3 98 | }, 99 | "file_extension": ".py", 100 | "mimetype": "text/x-python", 101 | "name": "python", 102 | "nbconvert_exporter": "python", 103 | "pygments_lexer": "ipython3", 104 | "version": "3.12.0" 105 | } 106 | }, 107 | "nbformat": 4, 108 | "nbformat_minor": 2 109 | } 110 | -------------------------------------------------------------------------------- /2019/Day 01.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Day 1 - Another year, another first day\n", 8 | "\n", 9 | "- https://adventofcode.com/2019/day/1\n", 10 | "\n", 11 | "As always, a warm-up excersice to start with. For me, it's the time of finding my aoc repository and upgrading all my packages first. :-)\n" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 1, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "import aocd\n", 21 | "\n", 22 | "data = aocd.get_data(day=1, year=2019)\n", 23 | "modules = list(map(int, data.splitlines()))" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": 2, 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "def fuel_requirement(mass: int) -> int:\n", 33 | " return mass // 3 - 2\n", 34 | "\n", 35 | "\n", 36 | "tests = {\n", 37 | " 12: 2,\n", 38 | " 14: 2,\n", 39 | " 1969: 654,\n", 40 | " 100756: 33583,\n", 41 | "}\n", 42 | "for mass, expected in tests.items():\n", 43 | " assert (\n", 44 | " fuel_requirement(mass) == expected\n", 45 | " ), f\"fuel_requirement({mass}) = {fuel_requirement(mass)} vs. {expected}\"" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": 3, 51 | "metadata": {}, 52 | "outputs": [ 53 | { 54 | "name": "stdout", 55 | "output_type": "stream", 56 | "text": [ 57 | "Part 1: 3346639\n" 58 | ] 59 | } 60 | ], 61 | "source": [ 62 | "print(\"Part 1:\", sum(map(fuel_requirement, modules)))" 63 | ] 64 | }, 65 | { 66 | "cell_type": "markdown", 67 | "metadata": {}, 68 | "source": [ 69 | "Part two adds a little iteration; make sure to add to the total only if the new requirement is larger than 0!\n" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": 4, 75 | "metadata": {}, 76 | "outputs": [], 77 | "source": [ 78 | "def iterative_fuel_calc(mass: int) -> int:\n", 79 | " total = 0\n", 80 | " requirement = fuel_requirement(mass)\n", 81 | " while requirement > 0:\n", 82 | " total += requirement\n", 83 | " requirement = fuel_requirement(requirement)\n", 84 | " return total\n", 85 | "\n", 86 | "\n", 87 | "tests = {\n", 88 | " 14: 2,\n", 89 | " 1969: 966,\n", 90 | " 100756: 50346,\n", 91 | "}\n", 92 | "for mass, expected in tests.items():\n", 93 | " assert (\n", 94 | " iterative_fuel_calc(mass) == expected\n", 95 | " ), f\"iterative_fuel_calc({mass}) = {iterative_fuel_calc(mass)} vs. {expected}\"" 96 | ] 97 | }, 98 | { 99 | "cell_type": "code", 100 | "execution_count": 5, 101 | "metadata": {}, 102 | "outputs": [ 103 | { 104 | "name": "stdout", 105 | "output_type": "stream", 106 | "text": [ 107 | "Part 2: 5017110\n" 108 | ] 109 | } 110 | ], 111 | "source": [ 112 | "print(\"Part 2:\", sum(map(iterative_fuel_calc, modules)))" 113 | ] 114 | } 115 | ], 116 | "metadata": { 117 | "kernelspec": { 118 | "display_name": "Python 3", 119 | "language": "python", 120 | "name": "python3" 121 | }, 122 | "language_info": { 123 | "codemirror_mode": { 124 | "name": "ipython", 125 | "version": 3 126 | }, 127 | "file_extension": ".py", 128 | "mimetype": "text/x-python", 129 | "name": "python", 130 | "nbconvert_exporter": "python", 131 | "pygments_lexer": "ipython3", 132 | "version": "3.12.0" 133 | } 134 | }, 135 | "nbformat": 4, 136 | "nbformat_minor": 2 137 | } 138 | -------------------------------------------------------------------------------- /2019/Day 09.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Day 9 - Relative mode and unlimited memory\n", 8 | "\n", 9 | "- https://adventofcode.com/2019/day/9\n", 10 | "\n", 11 | "Today's puzzle requires updates to the [`intcode` module](./incode.py):\n", 12 | "\n", 13 | "- Added a `ParameterMode.set` hook; so far I had hard-coded mode 0 (positional) setting in `BoundInstruction`.\n", 14 | "- Support relative mode and the `9` opcode:\n", 15 | " - I've added support for _registers_, in general, containing a single register named _relative base_\n", 16 | " - I've updated the `ParameterMode.get` hooks to accept a `registers` keyword argument, so we can base parameters off of registers.\n", 17 | " - I've added a `relative` mode to `PamaterMode`, which returns the memory value relative to the _relative base_ register, or sets memory values using the same rules.\n", 18 | " - I've added a `registers` keyword argument to the `Instruction.__call__()` method so instructions can update\n", 19 | " register values.\n", 20 | "- Support unlimited memory addressing\n", 21 | " - I've added a new Memory class that handles this. For now I'm going to assume that memory addressing is not\n", 22 | " going to address wildly large values and so just `.extend()` the list with zeros to grow the memory.\n", 23 | " We could use a sparse implementation instead if that ever proves to be incorrect. I've put in an assertion\n", 24 | " to catch memory addresses greater than 16 bits.\n", 25 | "\n", 26 | "See the [diff on GitHub](https://github.com/mjpieters/adventofcode/commit/3d69f28) for details on the opcode and memory code changes.\n", 27 | "\n", 28 | "With these changes, parts 1 and 2 where a breeze to run (with part 2 taking about 3 seconds).\n" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": 1, 34 | "metadata": {}, 35 | "outputs": [], 36 | "source": [ 37 | "import aocd\n", 38 | "\n", 39 | "data = aocd.get_data(day=9, year=2019)\n", 40 | "memory = list(map(int, data.split(\",\")))" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": 2, 46 | "metadata": {}, 47 | "outputs": [ 48 | { 49 | "name": "stdout", 50 | "output_type": "stream", 51 | "text": [ 52 | "Part 1: 3989758265\n" 53 | ] 54 | } 55 | ], 56 | "source": [ 57 | "from intcode import CPU, ioset\n", 58 | "\n", 59 | "output, intset = ioset(1)\n", 60 | "CPU(intset).reset(memory).execute()\n", 61 | "print(\"Part 1:\", output[0])" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": 3, 67 | "metadata": {}, 68 | "outputs": [ 69 | { 70 | "name": "stdout", 71 | "output_type": "stream", 72 | "text": [ 73 | "Part 2: 76791\n" 74 | ] 75 | } 76 | ], 77 | "source": [ 78 | "output, intset = ioset(2)\n", 79 | "CPU(intset).reset(memory).execute()\n", 80 | "print(\"Part 2:\", output[0])" 81 | ] 82 | } 83 | ], 84 | "metadata": { 85 | "kernelspec": { 86 | "display_name": "Python 3", 87 | "language": "python", 88 | "name": "python3" 89 | }, 90 | "language_info": { 91 | "codemirror_mode": { 92 | "name": "ipython", 93 | "version": 3 94 | }, 95 | "file_extension": ".py", 96 | "mimetype": "text/x-python", 97 | "name": "python", 98 | "nbconvert_exporter": "python", 99 | "pygments_lexer": "ipython3", 100 | "version": "3.12.0" 101 | } 102 | }, 103 | "nbformat": 4, 104 | "nbformat_minor": 2 105 | } 106 | -------------------------------------------------------------------------------- /2019/Day 25.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Day 25 - Adventure!\n", 8 | "\n", 9 | "This is a text adventure. Just enjoy playing it :-D\n", 10 | "\n", 11 | "(Yes, I could automate exploration, item collection and somehow detecting the items you shouldn't want to take, but where's the fun in that).\n" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 1, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "from __future__ import annotations\n", 21 | "\n", 22 | "import sys\n", 23 | "from typing import Callable, List\n", 24 | "\n", 25 | "from intcode import CPU, Instruction, InstructionSet, base_opcodes\n", 26 | "\n", 27 | "\n", 28 | "def ascii_input() -> Callable[[], int]:\n", 29 | " buffer: List[int] = []\n", 30 | "\n", 31 | " def input_one() -> int:\n", 32 | " nonlocal buffer\n", 33 | " if not buffer:\n", 34 | " buffer += input().encode(\"ASCII\")\n", 35 | " buffer += (10,)\n", 36 | " buffer.reverse()\n", 37 | " return buffer.pop()\n", 38 | "\n", 39 | " return input_one\n", 40 | "\n", 41 | "\n", 42 | "def print_ascii(value: int) -> None:\n", 43 | " sys.stdout.write(chr(value))\n", 44 | "\n", 45 | "\n", 46 | "def run_ascii(memory: List[int]) -> None:\n", 47 | " opcodes: InstructionSet = {\n", 48 | " **base_opcodes,\n", 49 | " 3: Instruction(ascii_input(), output=True),\n", 50 | " 4: Instruction(print_ascii, 1),\n", 51 | " }\n", 52 | " CPU(opcodes).reset(memory).execute()" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": 2, 58 | "metadata": {}, 59 | "outputs": [], 60 | "source": [ 61 | "import aocd\n", 62 | "\n", 63 | "data = aocd.get_data(day=25, year=2019)\n", 64 | "memory = list(map(int, data.split(\",\")))" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": 3, 70 | "metadata": {}, 71 | "outputs": [ 72 | { 73 | "name": "stdout", 74 | "output_type": "stream", 75 | "text": [ 76 | "\n", 77 | "\n", 78 | "\n", 79 | "== Hull Breach ==\n", 80 | "You got in through a hole in the floor here. To keep your ship from also freezing, the hole has been sealed.\n", 81 | "\n", 82 | "Doors here lead:\n", 83 | "- north\n", 84 | "- east\n", 85 | "- south\n", 86 | "\n", 87 | "Command?\n" 88 | ] 89 | } 90 | ], 91 | "source": [ 92 | "run_ascii(memory)" 93 | ] 94 | } 95 | ], 96 | "metadata": { 97 | "kernelspec": { 98 | "display_name": "Python 3", 99 | "language": "python", 100 | "name": "python3" 101 | }, 102 | "language_info": { 103 | "codemirror_mode": { 104 | "name": "ipython", 105 | "version": 3 106 | }, 107 | "file_extension": ".py", 108 | "mimetype": "text/x-python", 109 | "name": "python", 110 | "nbconvert_exporter": "python", 111 | "pygments_lexer": "ipython3", 112 | "version": "3.12.0" 113 | } 114 | }, 115 | "nbformat": 4, 116 | "nbformat_minor": 2 117 | } 118 | -------------------------------------------------------------------------------- /2019/aoc201913-breakout.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjpieters/adventofcode/784024370bf1f39f814d83b2413fd3729fac7bed/2019/aoc201913-breakout.gif -------------------------------------------------------------------------------- /2019/ruff.toml: -------------------------------------------------------------------------------- 1 | extend = "../pyproject.toml" 2 | 3 | [lint.isort] 4 | known-first-party = ["intcode"] 5 | -------------------------------------------------------------------------------- /2020/Day 01.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Welcome to AoC 2020, time for a holiday!\n", 8 | "\n", 9 | "- https://adventofcode.com/2020/day/1\n", 10 | "\n", 11 | "Another year, another set of puzzles! This time, we are 'not doing christmas'; Eric Wastl has torn up the [AoC Bingo card](https://www.reddit.com/r/adventofcode/comments/k3q7tr/my_advent_of_code_2020_bingo_card_fun_little_side/) and decided to send us to a tropical island instead. :-P\n", 12 | "\n", 13 | "As always, we start with a warm-up. Rather than iterate over all combinations ($O(n^2)$), put the numbers in a set, loop over the set for the first number, and see if `2020 - first` is a member of the set. That gives us a $O(n)$ runtime!\n" 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": 1, 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "import aocd\n", 23 | "\n", 24 | "coins = set(map(int, aocd.get_data(day=1, year=2020).splitlines()))" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 2, 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "from operator import mul\n", 34 | "\n", 35 | "\n", 36 | "def find_sum(coins, target=2020):\n", 37 | " for first in coins:\n", 38 | " second = target - first\n", 39 | " if second in coins:\n", 40 | " return first, second\n", 41 | "\n", 42 | " raise ValueError(\"Not solvable\")\n", 43 | "\n", 44 | "\n", 45 | "def sum_coins(coins):\n", 46 | " return mul(*find_sum(coins))\n", 47 | "\n", 48 | "\n", 49 | "test = set(\n", 50 | " map(\n", 51 | " int,\n", 52 | " \"\"\"\\\n", 53 | "1721\n", 54 | "979\n", 55 | "366\n", 56 | "299\n", 57 | "675\n", 58 | "1456\n", 59 | "\"\"\".splitlines(),\n", 60 | " )\n", 61 | ")\n", 62 | "assert sum_coins(test) == 514579" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": 3, 68 | "metadata": {}, 69 | "outputs": [ 70 | { 71 | "name": "stdout", 72 | "output_type": "stream", 73 | "text": [ 74 | "Part 1: 73371\n" 75 | ] 76 | } 77 | ], 78 | "source": [ 79 | "print(\"Part 1:\", sum_coins(coins))" 80 | ] 81 | }, 82 | { 83 | "cell_type": "markdown", 84 | "metadata": {}, 85 | "source": [ 86 | "# Part 2\n", 87 | "\n", 88 | "To find the third coin, just pop one of the values of the set of coins, run the first puzzle solution to see if there is a combination that sums to `2020 - selected`, and continue until we found a combination. That's an $O(N^2)$ solution:\n" 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": 4, 94 | "metadata": {}, 95 | "outputs": [], 96 | "source": [ 97 | "def sum_three(coins):\n", 98 | " while coins:\n", 99 | " selected = coins.pop()\n", 100 | " remainder = 2020 - selected\n", 101 | " try:\n", 102 | " return selected * mul(*find_sum(coins, target=remainder))\n", 103 | " except ValueError:\n", 104 | " continue\n", 105 | "\n", 106 | "\n", 107 | "assert sum_three(test) == 241861950" 108 | ] 109 | }, 110 | { 111 | "cell_type": "code", 112 | "execution_count": 5, 113 | "metadata": {}, 114 | "outputs": [ 115 | { 116 | "name": "stdout", 117 | "output_type": "stream", 118 | "text": [ 119 | "Part 2: 127642310\n" 120 | ] 121 | } 122 | ], 123 | "source": [ 124 | "print(\"Part 2:\", sum_three(coins))" 125 | ] 126 | } 127 | ], 128 | "metadata": { 129 | "kernelspec": { 130 | "display_name": "Python 3", 131 | "name": "python3" 132 | }, 133 | "language_info": { 134 | "codemirror_mode": { 135 | "name": "ipython", 136 | "version": 3 137 | }, 138 | "file_extension": ".py", 139 | "mimetype": "text/x-python", 140 | "name": "python", 141 | "nbconvert_exporter": "python", 142 | "pygments_lexer": "ipython3", 143 | "version": "3.12.0" 144 | }, 145 | "orig_nbformat": 2 146 | }, 147 | "nbformat": 4, 148 | "nbformat_minor": 2 149 | } 150 | -------------------------------------------------------------------------------- /2020/Day 02.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Counting letters\n", 8 | "\n", 9 | "- https://adventofcode.com/2020/day/2\n", 10 | "\n", 11 | "I like to use a dataclass for parsing tasks like these. A single regex to read out each line, and methods on the class to implement the password rule checks.\n" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 1, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "import re\n", 21 | "from dataclasses import dataclass\n", 22 | "\n", 23 | "_line = re.compile(\n", 24 | " r\"^(?P\\d+)-(?P\\d+) (?P[a-z]):\\s*(?P[a-z]+)$\"\n", 25 | ")\n", 26 | "\n", 27 | "\n", 28 | "@dataclass\n", 29 | "class PWRule:\n", 30 | " min_: int\n", 31 | " max_: int\n", 32 | " letter: str\n", 33 | " password: str\n", 34 | "\n", 35 | " @classmethod\n", 36 | " def from_line(cls, line: str) -> \"PWRule\":\n", 37 | " match = _line.search(line)\n", 38 | " min_, max_ = int(match[\"min_\"]), int(match[\"max_\"])\n", 39 | " return cls(min_, max_, match[\"letter\"], match[\"password\"])\n", 40 | "\n", 41 | " def is_valid(self) -> bool:\n", 42 | " return self.min_ <= self.password.count(self.letter) <= self.max_\n", 43 | "\n", 44 | " def is_valid_toboggan_policy(self) -> bool:\n", 45 | " return (self.password[self.min_ - 1], self.password[self.max_ - 1]).count(\n", 46 | " self.letter\n", 47 | " ) == 1\n", 48 | "\n", 49 | "\n", 50 | "def read_passwords(lines):\n", 51 | " return [PWRule.from_line(line) for line in lines]\n", 52 | "\n", 53 | "\n", 54 | "test = read_passwords(\n", 55 | " \"\"\"\\\n", 56 | "1-3 a: abcde\n", 57 | "1-3 b: cdefg\n", 58 | "2-9 c: ccccccccc\n", 59 | "\"\"\".splitlines()\n", 60 | ")\n", 61 | "\n", 62 | "assert sum(pwr.is_valid() for pwr in test) == 2\n", 63 | "assert sum(pwr.is_valid_toboggan_policy() for pwr in test) == 1" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": 2, 69 | "metadata": {}, 70 | "outputs": [], 71 | "source": [ 72 | "import aocd\n", 73 | "\n", 74 | "rules = read_passwords(aocd.get_data(day=2, year=2020).splitlines())" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": 3, 80 | "metadata": {}, 81 | "outputs": [ 82 | { 83 | "name": "stdout", 84 | "output_type": "stream", 85 | "text": [ 86 | "Part 1: 591\n" 87 | ] 88 | } 89 | ], 90 | "source": [ 91 | "print(\"Part 1:\", sum(pwr.is_valid() for pwr in rules))" 92 | ] 93 | }, 94 | { 95 | "cell_type": "code", 96 | "execution_count": 4, 97 | "metadata": {}, 98 | "outputs": [ 99 | { 100 | "name": "stdout", 101 | "output_type": "stream", 102 | "text": [ 103 | "Part 2: 335\n" 104 | ] 105 | } 106 | ], 107 | "source": [ 108 | "print(\"Part 2:\", sum(pwr.is_valid_toboggan_policy() for pwr in rules))" 109 | ] 110 | } 111 | ], 112 | "metadata": { 113 | "kernelspec": { 114 | "display_name": "Python 3", 115 | "name": "python3" 116 | }, 117 | "language_info": { 118 | "codemirror_mode": { 119 | "name": "ipython", 120 | "version": 3 121 | }, 122 | "file_extension": ".py", 123 | "mimetype": "text/x-python", 124 | "name": "python", 125 | "nbconvert_exporter": "python", 126 | "pygments_lexer": "ipython3", 127 | "version": "3.12.0" 128 | }, 129 | "orig_nbformat": 2 130 | }, 131 | "nbformat": 4, 132 | "nbformat_minor": 2 133 | } 134 | -------------------------------------------------------------------------------- /2020/Day 03.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Modulo tree hopping\n", 8 | "\n", 9 | "- https://adventofcode.com/2020/day/3\n", 10 | "\n", 11 | "How could we not have a modulo-based challenge in Advent of Code? This one is simple, to wrap our map to the right, just keep increasing the column number by 3 and use that number module the width of the map.\n", 12 | "\n", 13 | "I used a functional style implementation for both parts; the column count is handled by [`itertools.count()`](https://docs.python.org/3/library/itertools.html#itertools.count) with a `step` size (part two makes that step size variable), and [`itertools.islice()`](https://docs.python.org/3/library/itertools.html#itertools.islice) makes it trivial to take every Nth row.\n", 14 | "\n", 15 | "Multiplying all the tree counts for each slope (part 2) is done using [`functools.reduce()`](https://docs.python.org/3/library/functools.html#functools.reduce), passing the numbers to [`operator.mul()`](https://docs.python.org/3/library/operator.html#operator.mul) as the functional equivalent of the `*` multiplication operator.\n" 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "execution_count": 1, 21 | "metadata": {}, 22 | "outputs": [], 23 | "source": [ 24 | "from functools import reduce\n", 25 | "from itertools import count, islice\n", 26 | "from operator import mul\n", 27 | "\n", 28 | "\n", 29 | "def count_trees(map, right=3, down=1):\n", 30 | " rows = islice(map.splitlines(), None, None, down)\n", 31 | " cols = count(step=right)\n", 32 | " return sum(row[col % len(row)] == \"#\" for (row, col) in zip(rows, cols))\n", 33 | "\n", 34 | "\n", 35 | "def test_slopes(map):\n", 36 | " dirs = ((1, 1), (3, 1), (5, 1), (7, 1), (1, 2))\n", 37 | " counts = (count_trees(map, r, d) for r, d in dirs)\n", 38 | " return reduce(mul, counts)\n", 39 | "\n", 40 | "\n", 41 | "testmap = \"\"\"\\\n", 42 | "..##.......\n", 43 | "#...#...#..\n", 44 | ".#....#..#.\n", 45 | "..#.#...#.#\n", 46 | ".#...##..#.\n", 47 | "..#.##.....\n", 48 | ".#.#.#....#\n", 49 | ".#........#\n", 50 | "#.##...#...\n", 51 | "#...##....#\n", 52 | ".#..#...#.#\n", 53 | "\"\"\"\n", 54 | "assert count_trees(testmap) == 7\n", 55 | "assert test_slopes(testmap) == 336" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": 2, 61 | "metadata": {}, 62 | "outputs": [], 63 | "source": [ 64 | "import aocd\n", 65 | "\n", 66 | "map_ = aocd.get_data(day=3, year=2020)" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": 3, 72 | "metadata": {}, 73 | "outputs": [ 74 | { 75 | "name": "stdout", 76 | "output_type": "stream", 77 | "text": [ 78 | "Part 1: 292\n" 79 | ] 80 | } 81 | ], 82 | "source": [ 83 | "print(\"Part 1:\", count_trees(map_))" 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": 4, 89 | "metadata": {}, 90 | "outputs": [ 91 | { 92 | "name": "stdout", 93 | "output_type": "stream", 94 | "text": [ 95 | "Part 2: 9354744432\n" 96 | ] 97 | } 98 | ], 99 | "source": [ 100 | "print(\"Part 2:\", test_slopes(map_))" 101 | ] 102 | } 103 | ], 104 | "metadata": { 105 | "kernelspec": { 106 | "display_name": "Python 3", 107 | "name": "python3" 108 | }, 109 | "language_info": { 110 | "codemirror_mode": { 111 | "name": "ipython", 112 | "version": 3 113 | }, 114 | "file_extension": ".py", 115 | "mimetype": "text/x-python", 116 | "name": "python", 117 | "nbconvert_exporter": "python", 118 | "pygments_lexer": "ipython3", 119 | "version": "3.12.0" 120 | }, 121 | "orig_nbformat": 2 122 | }, 123 | "nbformat": 4, 124 | "nbformat_minor": 2 125 | } 126 | -------------------------------------------------------------------------------- /2020/Day 06.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Grouped sets\n", 8 | "\n", 9 | "- https://adventofcode.com/2020/day/6\n", 10 | "\n", 11 | "We are asked, basically, to create sets for each group of answers. Like on day 4, we can split the input on double newlines, then just get the number of unique letters by removing the newlines (and so only keep the letters):\n" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 1, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "from typing import Sequence\n", 21 | "\n", 22 | "\n", 23 | "def unique_answers(groups: Sequence[str]) -> int:\n", 24 | " return sum(len(set(group.replace(\"\\n\", \"\"))) for group in groups)\n", 25 | "\n", 26 | "\n", 27 | "test = \"\"\"\\\n", 28 | "abc\n", 29 | "\n", 30 | "a\n", 31 | "b\n", 32 | "c\n", 33 | "\n", 34 | "ab\n", 35 | "ac\n", 36 | "\n", 37 | "a\n", 38 | "a\n", 39 | "a\n", 40 | "a\n", 41 | "\n", 42 | "b\n", 43 | "\"\"\".split(\"\\n\\n\")\n", 44 | "\n", 45 | "assert unique_answers(test) == 11" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": 2, 51 | "metadata": {}, 52 | "outputs": [], 53 | "source": [ 54 | "import aocd\n", 55 | "\n", 56 | "groups = aocd.get_data(day=6, year=2020).split(\"\\n\\n\")" 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": 3, 62 | "metadata": {}, 63 | "outputs": [ 64 | { 65 | "name": "stdout", 66 | "output_type": "stream", 67 | "text": [ 68 | "Part 1: 6726\n" 69 | ] 70 | } 71 | ], 72 | "source": [ 73 | "print(\"Part 1:\", unique_answers(groups))" 74 | ] 75 | }, 76 | { 77 | "cell_type": "markdown", 78 | "metadata": {}, 79 | "source": [ 80 | "## Part 2, set intersections\n", 81 | "\n", 82 | "Part two turns it into a set intersection excercise. Splitting each group by newlines, turn each line into a set, then intersect these and get the length:\n" 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": 4, 88 | "metadata": {}, 89 | "outputs": [], 90 | "source": [ 91 | "from typing import Set\n", 92 | "\n", 93 | "\n", 94 | "def group_shared_answers(group: str) -> Set[str]:\n", 95 | " return set.intersection(*map(set, group.splitlines()))\n", 96 | "\n", 97 | "\n", 98 | "def shared_answers(groups: Sequence[str]) -> int:\n", 99 | " return sum(len(group_shared_answers(group)) for group in groups)\n", 100 | "\n", 101 | "\n", 102 | "assert shared_answers(test) == 6" 103 | ] 104 | }, 105 | { 106 | "cell_type": "code", 107 | "execution_count": 5, 108 | "metadata": {}, 109 | "outputs": [ 110 | { 111 | "name": "stdout", 112 | "output_type": "stream", 113 | "text": [ 114 | "Part 2: 3316\n" 115 | ] 116 | } 117 | ], 118 | "source": [ 119 | "print(\"Part 2:\", shared_answers(groups))" 120 | ] 121 | } 122 | ], 123 | "metadata": { 124 | "kernelspec": { 125 | "display_name": "Python 3", 126 | "name": "python3" 127 | }, 128 | "language_info": { 129 | "codemirror_mode": { 130 | "name": "ipython", 131 | "version": 3 132 | }, 133 | "file_extension": ".py", 134 | "mimetype": "text/x-python", 135 | "name": "python", 136 | "nbconvert_exporter": "python", 137 | "pygments_lexer": "ipython3", 138 | "version": "3.12.0" 139 | }, 140 | "orig_nbformat": 2 141 | }, 142 | "nbformat": 4, 143 | "nbformat_minor": 2 144 | } 145 | -------------------------------------------------------------------------------- /2020/Day 15.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Day 15 - Generate a number series\n", 8 | "\n", 9 | "- https://adventofcode.com/2020/day/15\n", 10 | "\n", 11 | "To generate the game numbers, map numbers to the round they were last spoken in and track the most recent generated number. The next number is `0` if the most recent number is in the rounds mapping, otherwise it's `round_number - last_round_number`:\n" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 1, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "def play_memory_game(start_numbers: list[int], numrounds: int = 2020) -> int:\n", 21 | " seen = [0] * numrounds\n", 22 | " for i, n in enumerate(start_numbers[:-1], 1):\n", 23 | " seen[n] = i\n", 24 | " most_recent = start_numbers[-1]\n", 25 | " for round_ in range(len(start_numbers), numrounds):\n", 26 | " last_seen = seen[most_recent]\n", 27 | " seen[most_recent] = round_\n", 28 | " most_recent = 0 if not last_seen else round_ - last_seen\n", 29 | " return most_recent\n", 30 | "\n", 31 | "\n", 32 | "tests = {\n", 33 | " (1, 3, 2): 1,\n", 34 | " (2, 1, 3): 10,\n", 35 | " (1, 2, 3): 27,\n", 36 | " (2, 3, 1): 78,\n", 37 | " (3, 2, 1): 438,\n", 38 | " (3, 1, 2): 1836,\n", 39 | "}\n", 40 | "for test_start, expected in tests.items():\n", 41 | " assert play_memory_game(test_start) == expected" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": 2, 47 | "metadata": {}, 48 | "outputs": [], 49 | "source": [ 50 | "import aocd\n", 51 | "\n", 52 | "start_numbers = [int(n) for n in aocd.get_data(day=15, year=2020).split(\",\")]" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": 3, 58 | "metadata": {}, 59 | "outputs": [ 60 | { 61 | "name": "stdout", 62 | "output_type": "stream", 63 | "text": [ 64 | "Part 1: 620\n" 65 | ] 66 | } 67 | ], 68 | "source": [ 69 | "print(\"Part 1:\", play_memory_game(start_numbers))" 70 | ] 71 | }, 72 | { 73 | "cell_type": "markdown", 74 | "metadata": {}, 75 | "source": [ 76 | "## Part 2, upping the ante\n", 77 | "\n", 78 | "We are now asked to keep playing the game for 30 million rounds, rather than just 2020. Clearly, there must be a short-cut here. However, I found that brute-forcing this wasn't _that_ bad, after a bit of optimising, each test input takes ~7-8 seconds to process.\n", 79 | "\n", 80 | "The best I could think of was to use a pre-allocated list rather than a dictionary to map numbers to their round value, to avoid the cost of dynamically growing the dictionary and hashing.\n" 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": 4, 86 | "metadata": {}, 87 | "outputs": [], 88 | "source": [ 89 | "tests_30m = {\n", 90 | " (0, 3, 6): 175594,\n", 91 | " (1, 3, 2): 2578,\n", 92 | " (2, 1, 3): 3544142,\n", 93 | " (1, 2, 3): 261214,\n", 94 | " (2, 3, 1): 6895259,\n", 95 | " (3, 2, 1): 18,\n", 96 | " (3, 1, 2): 362,\n", 97 | "}\n", 98 | "\n", 99 | "for test_start, expected in tests_30m.items():\n", 100 | " assert play_memory_game(test_start, 30_000_000) == expected" 101 | ] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "execution_count": 5, 106 | "metadata": {}, 107 | "outputs": [ 108 | { 109 | "name": "stdout", 110 | "output_type": "stream", 111 | "text": [ 112 | "Part 2: 110871\n", 113 | "CPU times: user 3.33 s, sys: 39.2 ms, total: 3.37 s\n", 114 | "Wall time: 3.39 s\n" 115 | ] 116 | } 117 | ], 118 | "source": [ 119 | "%time print(\"Part 2:\", play_memory_game(start_numbers, 30_000_000))" 120 | ] 121 | } 122 | ], 123 | "metadata": { 124 | "kernelspec": { 125 | "display_name": "Python 3", 126 | "language": "python", 127 | "name": "python3" 128 | }, 129 | "language_info": { 130 | "codemirror_mode": { 131 | "name": "ipython", 132 | "version": 3 133 | }, 134 | "file_extension": ".py", 135 | "mimetype": "text/x-python", 136 | "name": "python", 137 | "nbconvert_exporter": "python", 138 | "pygments_lexer": "ipython3", 139 | "version": "3.12.0" 140 | }, 141 | "orig_nbformat": 2 142 | }, 143 | "nbformat": 4, 144 | "nbformat_minor": 2 145 | } 146 | -------------------------------------------------------------------------------- /2020/Day 25.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Day 25 - Breaking the door code\n", 8 | "\n", 9 | "- https://adventofcode.com/2020/day/25\n", 10 | "\n", 11 | "We are given an introduction into [public key encryption](https://en.wikipedia.org/wiki/Public-key_cryptography); the private keys are the _loop size_ the card and door use, and the _transform subject number_ function described is basically a [memory efficient modular exponentiation](https://en.wikipedia.org/wiki/Modular_exponentiation#Memory-efficient_method), which is a fundamental building block for [RSA public key encryption]().\n", 12 | "\n", 13 | "The inputs to the `transform_subject_number`, 7 and 20201227, are both prime numbers as expected, but they are pretty small. We can brute-force finding the loop sizes for either the card or door, it is guaranteed to be in the range [1, 20201227). Even Python code can iterate through 20 million numbers pretty fast. _Real_ public key encryption involves much, much larger numbers.\n", 14 | "\n", 15 | "Note that the Python [`pow()` function](https://docs.python.org/3/library/functions.html#pow) supports modular exponentiation out of the box; we can implement the `transform_subject_number(subject_number, loop_size)` function as `pow(subject_number, loop_size, 20201227)`.\n", 16 | "\n", 17 | "As is the case every year, part 2 is given to us for free. Merry Christmas!\n" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": 1, 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "from functools import partial\n", 27 | "from itertools import count\n", 28 | "\n", 29 | "MOD = 20201227\n", 30 | "KEY_SEED = 7\n", 31 | "\n", 32 | "transform_subject_number = partial(pow, mod=MOD)\n", 33 | "\n", 34 | "\n", 35 | "def crack_encryption_key(key: int) -> int:\n", 36 | " for loop_size in count(1):\n", 37 | " if transform_subject_number(KEY_SEED, loop_size) == key:\n", 38 | " return loop_size\n", 39 | "\n", 40 | "\n", 41 | "assert transform_subject_number(KEY_SEED, 8) == 5764801\n", 42 | "assert crack_encryption_key(5764801) == 8\n", 43 | "assert transform_subject_number(KEY_SEED, 11) == 17807724\n", 44 | "assert crack_encryption_key(17807724) == 11\n", 45 | "assert (\n", 46 | " transform_subject_number(17807724, 8)\n", 47 | " == 14897079\n", 48 | " == transform_subject_number(5764801, 11)\n", 49 | ")" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": 2, 55 | "metadata": {}, 56 | "outputs": [], 57 | "source": [ 58 | "import aocd\n", 59 | "\n", 60 | "card_key, door_key = map(int, aocd.get_data(day=25, year=2020).split())" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": 3, 66 | "metadata": {}, 67 | "outputs": [ 68 | { 69 | "name": "stdout", 70 | "output_type": "stream", 71 | "text": [ 72 | "Part 1: 7936032\n" 73 | ] 74 | } 75 | ], 76 | "source": [ 77 | "card_loop_size = crack_encryption_key(card_key)\n", 78 | "encryption_key = transform_subject_number(door_key, card_loop_size)\n", 79 | "print(\"Part 1:\", encryption_key)" 80 | ] 81 | } 82 | ], 83 | "metadata": { 84 | "kernelspec": { 85 | "display_name": "Python 3", 86 | "language": "python", 87 | "name": "python3" 88 | }, 89 | "language_info": { 90 | "codemirror_mode": { 91 | "name": "ipython", 92 | "version": 3 93 | }, 94 | "file_extension": ".py", 95 | "mimetype": "text/x-python", 96 | "name": "python", 97 | "nbconvert_exporter": "python", 98 | "pygments_lexer": "ipython3", 99 | "version": "3.9.0-final" 100 | }, 101 | "orig_nbformat": 2 102 | }, 103 | "nbformat": 4, 104 | "nbformat_minor": 2 105 | } 106 | -------------------------------------------------------------------------------- /2021/Day 01.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Sonar Sweep\n", 8 | "\n", 9 | "- https://adventofcode.com/2021/day/1\n", 10 | "\n", 11 | "And we are off for 2021! (I, on the other hand, started late this year). The sleigh keys went overboard and our job this year is to collect enough stars to power the submarine experimental key-finding antenna. Any excuse for an advent calendar :-D\n", 12 | "\n", 13 | "To kick off, we are asked to analyze sonar data. Part one is about counting increments. Easy peasy, use a paired window iterator to compare the previous with the current value, and sum the booleans (Python's boolean type is a subclass of its integer type, and `False` has the value 0, `True` has the value 1, so counting a series of boolean tests is as easy as passing the series to `sum()`).\n", 14 | "\n", 15 | "The[`itertools` module documentation page has a [recipes section](https://docs.python.org/3/library/itertools.html#itertools-recipes), which includes a more generic `sliding_window()` function definition for arbitrary size windows, which I've used here.\n" 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "execution_count": 1, 21 | "metadata": {}, 22 | "outputs": [], 23 | "source": [ 24 | "import aocd\n", 25 | "\n", 26 | "scans = [int(depth) for depth in aocd.get_data(day=1, year=2021).splitlines()]" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": 2, 32 | "metadata": {}, 33 | "outputs": [ 34 | { 35 | "name": "stdout", 36 | "output_type": "stream", 37 | "text": [ 38 | "Part 1: 1791\n" 39 | ] 40 | } 41 | ], 42 | "source": [ 43 | "from collections import deque\n", 44 | "from itertools import islice\n", 45 | "\n", 46 | "\n", 47 | "def sliding_window(iterable, n):\n", 48 | " # sliding_window('ABCDEFG', 4) -> ABCD BCDE CDEF DEFG\n", 49 | " it = iter(iterable)\n", 50 | " window = deque(islice(it, n), maxlen=n)\n", 51 | " if len(window) == n:\n", 52 | " yield tuple(window)\n", 53 | " for x in it:\n", 54 | " window.append(x)\n", 55 | " yield tuple(window)\n", 56 | "\n", 57 | "\n", 58 | "def count_increases(scans):\n", 59 | " return sum(b > a for a, b in sliding_window(scans, 2))\n", 60 | "\n", 61 | "\n", 62 | "print(\"Part 1:\", count_increases(scans))" 63 | ] 64 | }, 65 | { 66 | "cell_type": "markdown", 67 | "metadata": {}, 68 | "source": [ 69 | "## Part 2: increase the sliding window size\n", 70 | "\n", 71 | "My decision to use the generic sliding window function proved very handy for part 2, as this part is specifically about increasing the size of the sliding window!\n", 72 | "\n", 73 | "Of course, Eric Wastl makes it slightly more complicated by asking you to combine sliding windows. We need a pair-wise sliding window over a size-3 sliding window that sums the 3 scans in that window. Still, Python itertools makes this super easy.\n" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": 3, 79 | "metadata": {}, 80 | "outputs": [ 81 | { 82 | "name": "stdout", 83 | "output_type": "stream", 84 | "text": [ 85 | "Part 2: 1822\n" 86 | ] 87 | } 88 | ], 89 | "source": [ 90 | "def count_3windowsum_increases(scans):\n", 91 | " three_window_sum = (sum(window) for window in sliding_window(scans, 3))\n", 92 | " return count_increases(three_window_sum)\n", 93 | "\n", 94 | "\n", 95 | "print(\"Part 2:\", count_3windowsum_increases(scans))" 96 | ] 97 | } 98 | ], 99 | "metadata": { 100 | "interpreter": { 101 | "hash": "b1b6870d1e0a983b1943c858d70ac8a7c80477f9f3ca364eb8daa198319a8a87" 102 | }, 103 | "kernelspec": { 104 | "display_name": "Python 3.9.6 64-bit ('adventofcode-mOkh6lsX': pipenv)", 105 | "language": "python", 106 | "name": "python3" 107 | }, 108 | "language_info": { 109 | "codemirror_mode": { 110 | "name": "ipython", 111 | "version": 3 112 | }, 113 | "file_extension": ".py", 114 | "mimetype": "text/x-python", 115 | "name": "python", 116 | "nbconvert_exporter": "python", 117 | "pygments_lexer": "ipython3", 118 | "version": "3.12.0" 119 | }, 120 | "orig_nbformat": 4 121 | }, 122 | "nbformat": 4, 123 | "nbformat_minor": 2 124 | } 125 | -------------------------------------------------------------------------------- /2022/Day 01.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Calorie Counting\n", 8 | "\n", 9 | "- https://adventofcode.com/2022/day/1\n", 10 | "\n", 11 | "Welcome back for another round of Advent of Code :-) As always, the first day is just a warm-up. We need to read the AoC puzzle data, split it into blocks per Elf, convert the lines to integers, sum them and find the highest value. Basic stuff.\n" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 1, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "import aocd\n", 21 | "\n", 22 | "calories = [\n", 23 | " [int(line) for line in block.splitlines()]\n", 24 | " for block in aocd.get_data(day=1, year=2022).split(\"\\n\\n\")\n", 25 | "]" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": 2, 31 | "metadata": {}, 32 | "outputs": [ 33 | { 34 | "name": "stdout", 35 | "output_type": "stream", 36 | "text": [ 37 | "Part 1: 64929\n" 38 | ] 39 | } 40 | ], 41 | "source": [ 42 | "print(\"Part 1:\", max(sum(per_elf) for per_elf in calories))" 43 | ] 44 | }, 45 | { 46 | "cell_type": "markdown", 47 | "metadata": {}, 48 | "source": [ 49 | "## Part 2 - a heap of calories\n", 50 | "\n", 51 | "Part two asks for the sum of the top three calorie sums. You could sum and sort, but that'd be inefficient, even if only in theory. You don't need to know the exacty ordering of all the calorie sums, only which 3 are bigger than all other calorie sums!\n", 52 | "\n", 53 | "What you want to use instead is a [_heap queue_](https://en.wikipedia.org/wiki/Binary_heap) to track the top 3 sums. The Python standard library includes the [`heapq` module](https://docs.python.org/3/library/heapq.html) that lets us do just that. In fact, picking the top N items from a sequence is common enough that the module has a dedicated function for it, [`heapq.nlargest()`](https://docs.python.org/3/library/heapq.html#heapq.nlargest), so we use that here.\n" 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": 3, 59 | "metadata": {}, 60 | "outputs": [ 61 | { 62 | "name": "stdout", 63 | "output_type": "stream", 64 | "text": [ 65 | "Part 2: 193697\n" 66 | ] 67 | } 68 | ], 69 | "source": [ 70 | "import heapq\n", 71 | "\n", 72 | "print(\"Part 2:\", sum(heapq.nlargest(3, (sum(per_elf) for per_elf in calories))))" 73 | ] 74 | } 75 | ], 76 | "metadata": { 77 | "kernelspec": { 78 | "display_name": "Python 3.11.0 ('adventofcode-mOkh6lsX')", 79 | "language": "python", 80 | "name": "python3" 81 | }, 82 | "language_info": { 83 | "codemirror_mode": { 84 | "name": "ipython", 85 | "version": 3 86 | }, 87 | "file_extension": ".py", 88 | "mimetype": "text/x-python", 89 | "name": "python", 90 | "nbconvert_exporter": "python", 91 | "pygments_lexer": "ipython3", 92 | "version": "3.12.0" 93 | }, 94 | "orig_nbformat": 4, 95 | "vscode": { 96 | "interpreter": { 97 | "hash": "8bb5fd587ebf4d90f905285c44a569046664a8863ee065ff2dd968491b671e06" 98 | } 99 | } 100 | }, 101 | "nbformat": 4, 102 | "nbformat_minor": 2 103 | } 104 | -------------------------------------------------------------------------------- /2022/Day 03.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# The set of all misplaced items\n", 8 | "\n", 9 | "- https://adventofcode.com/2022/day/3\n", 10 | "\n", 11 | "We are asked to find the single common element between two compartments in a rucksack. This is what _sets_ are really great for, the intersection of the two sets of letters.\n" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 1, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "import string\n", 21 | "\n", 22 | "priorities = {letter: i for i, letter in enumerate(string.ascii_letters, 1)}\n", 23 | "\n", 24 | "\n", 25 | "def misplaced_item(rucksack: str) -> str:\n", 26 | " half = len(rucksack) // 2\n", 27 | " [letter] = set(rucksack[:half]) & set(rucksack[half:])\n", 28 | " return letter\n", 29 | "\n", 30 | "\n", 31 | "example = \"\"\"\\\n", 32 | "vJrwpWtwJgWrhcsFMMfFFhFp\n", 33 | "jqHRNqRjqzjGDLGLrsFMfFZSrLrFZsSL\n", 34 | "PmmdzqPrVvPwwTWBwg\n", 35 | "wMqvLMZHhHMvwLHjbvcjnnSBnvTQFn\n", 36 | "ttgJtRGJQctTZtZT\n", 37 | "CrZsJsPPZsGzwwsLwLmpwMDw\n", 38 | "\"\"\".splitlines()\n", 39 | "\n", 40 | "\n", 41 | "assert sum(priorities[misplaced_item(r)] for r in example) == 157" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": 2, 47 | "metadata": {}, 48 | "outputs": [ 49 | { 50 | "name": "stdout", 51 | "output_type": "stream", 52 | "text": [ 53 | "Part 1: 7763\n" 54 | ] 55 | } 56 | ], 57 | "source": [ 58 | "import aocd\n", 59 | "\n", 60 | "rucksacks = aocd.get_data(day=3, year=2022).splitlines()\n", 61 | "print(\"Part 1:\", sum(priorities[misplaced_item(r)] for r in rucksacks))" 62 | ] 63 | }, 64 | { 65 | "cell_type": "markdown", 66 | "metadata": {}, 67 | "source": [ 68 | "## Part 2, intersecting more than 2\n", 69 | "\n", 70 | "The problem hasn't really changed, we now have to intersect 3 rucksacks to find the single item. Since the [`set().intersection()` method](https://docs.python.org/3/library/stdtypes.html#frozenset.intersection) can take multiple inputs, this is trivial to achieve.\n" 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": 3, 76 | "metadata": {}, 77 | "outputs": [], 78 | "source": [ 79 | "from typing import Iterator\n", 80 | "\n", 81 | "\n", 82 | "def badge(rucksack: str, *rucksacks: str) -> str:\n", 83 | " [letter] = set(rucksack).intersection(*rucksacks)\n", 84 | " return letter\n", 85 | "\n", 86 | "\n", 87 | "def badges(*rucksacks: str) -> Iterator[str]:\n", 88 | " it = iter(rucksacks)\n", 89 | " for group in zip(it, it, it):\n", 90 | " yield badge(*group)\n", 91 | "\n", 92 | "\n", 93 | "assert sum(priorities[badge] for badge in badges(*example)) == 70" 94 | ] 95 | }, 96 | { 97 | "cell_type": "code", 98 | "execution_count": 4, 99 | "metadata": {}, 100 | "outputs": [ 101 | { 102 | "name": "stdout", 103 | "output_type": "stream", 104 | "text": [ 105 | "Part 2: 2569\n" 106 | ] 107 | } 108 | ], 109 | "source": [ 110 | "print(\"Part 2:\", sum(priorities[badge] for badge in badges(*rucksacks)))" 111 | ] 112 | } 113 | ], 114 | "metadata": { 115 | "kernelspec": { 116 | "display_name": "Python 3.11.0 ('adventofcode-mOkh6lsX')", 117 | "language": "python", 118 | "name": "python3" 119 | }, 120 | "language_info": { 121 | "codemirror_mode": { 122 | "name": "ipython", 123 | "version": 3 124 | }, 125 | "file_extension": ".py", 126 | "mimetype": "text/x-python", 127 | "name": "python", 128 | "nbconvert_exporter": "python", 129 | "pygments_lexer": "ipython3", 130 | "version": "3.12.0" 131 | }, 132 | "orig_nbformat": 4, 133 | "vscode": { 134 | "interpreter": { 135 | "hash": "8bb5fd587ebf4d90f905285c44a569046664a8863ee065ff2dd968491b671e06" 136 | } 137 | } 138 | }, 139 | "nbformat": 4, 140 | "nbformat_minor": 2 141 | } 142 | -------------------------------------------------------------------------------- /2023/Day 09.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# A derivative puzzle\n", 8 | "\n", 9 | "- https://adventofcode.com/2023/day/9\n", 10 | "\n", 11 | "The puzzle example describes the process of finding the [derivative](https://en.wikipedia.org/wiki/Derivative) of the sensor value function. Given that we don't actually know the functions of the sensor values, this is a very good explanation of how you would go about finding the next output value.\n", 12 | "\n", 13 | "We can use numpy to do all the work for us in a single call; the [`numpy.diff()` function](https://numpy.org/doc/stable/reference/generated/numpy.diff.html#numpy.diff) lets us calculate the final difference between each value across multiple steps, plus it'll handle the appended 0 for us. The final value is the total change for the formula, but it could be positive or negative, so all we have to do is to take the absolute value; e.g. for the input `1 3 6 10 15 21` the `numpy.diff(arr, n=arr.shape[1], append=0)` function spits out `-28`. And it'll do so across all the rows of input values, so a simple `numpy.abs(....sum())` expression later and we have our answer.\n" 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": 1, 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "import numpy as np\n", 23 | "from numpy.typing import NDArray\n", 24 | "\n", 25 | "\n", 26 | "def sensor_inputs(text: str) -> NDArray[np.int_]:\n", 27 | " return np.loadtxt(text.splitlines(), dtype=int)\n", 28 | "\n", 29 | "\n", 30 | "def predict_next_sensor_sum(sensor_inputs: NDArray[np.int_]) -> int:\n", 31 | " return np.abs(np.diff(sensor_inputs, n=sensor_inputs.shape[1], append=0).sum())\n", 32 | "\n", 33 | "\n", 34 | "test_inputs = sensor_inputs(\n", 35 | " \"\"\"\\\n", 36 | "0 3 6 9 12 15\n", 37 | "1 3 6 10 15 21\n", 38 | "10 13 16 21 30 45\n", 39 | "\"\"\"\n", 40 | ")\n", 41 | "assert predict_next_sensor_sum(test_inputs) == 114" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": 2, 47 | "metadata": {}, 48 | "outputs": [ 49 | { 50 | "name": "stdout", 51 | "output_type": "stream", 52 | "text": [ 53 | "Part 1: 1762065988\n" 54 | ] 55 | } 56 | ], 57 | "source": [ 58 | "import aocd\n", 59 | "\n", 60 | "inputs = sensor_inputs(aocd.get_data(day=9, year=2023))\n", 61 | "print(\"Part 1:\", predict_next_sensor_sum(inputs))" 62 | ] 63 | }, 64 | { 65 | "cell_type": "markdown", 66 | "metadata": {}, 67 | "source": [ 68 | "# Going backwards in time\n", 69 | "\n", 70 | "For part two, all we need to do is use `prepend` instead of `append` for our calculations. Simple!\n", 71 | "\n", 72 | "Or, even simpler, just reverse the input array across the rows and use our original function.\n" 73 | ] 74 | }, 75 | { 76 | "cell_type": "code", 77 | "execution_count": 3, 78 | "metadata": {}, 79 | "outputs": [], 80 | "source": [ 81 | "def predict_prev_sensor_sum(sensor_inputs: NDArray[np.int_]) -> int:\n", 82 | " return predict_next_sensor_sum(sensor_inputs[:, ::-1])\n", 83 | "\n", 84 | "\n", 85 | "assert predict_prev_sensor_sum(test_inputs) == 2" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": 4, 91 | "metadata": {}, 92 | "outputs": [ 93 | { 94 | "name": "stdout", 95 | "output_type": "stream", 96 | "text": [ 97 | "Part 2: 1066\n" 98 | ] 99 | } 100 | ], 101 | "source": [ 102 | "print(\"Part 2:\", predict_prev_sensor_sum(inputs))" 103 | ] 104 | } 105 | ], 106 | "metadata": { 107 | "kernelspec": { 108 | "display_name": "adventofcode-bRnAxXn--py3.12", 109 | "language": "python", 110 | "name": "python3" 111 | }, 112 | "language_info": { 113 | "codemirror_mode": { 114 | "name": "ipython", 115 | "version": 3 116 | }, 117 | "file_extension": ".py", 118 | "mimetype": "text/x-python", 119 | "name": "python", 120 | "nbconvert_exporter": "python", 121 | "pygments_lexer": "ipython3", 122 | "version": "3.12.0" 123 | } 124 | }, 125 | "nbformat": 4, 126 | "nbformat_minor": 2 127 | } 128 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Martijn Pieters 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 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "adventofcode" 3 | version = "2023" 4 | description = "Advent of Code puzzle solutions" 5 | authors = ["Martijn Pieters "] 6 | license = "MIT" 7 | readme = "README.md" 8 | repository = "https://github.com/mjpieters/adventofcode" 9 | keywords = ["advent-of-code", "aoc", "puzzles", "solutions"] 10 | 11 | 12 | [tool.poetry.dependencies] 13 | python = "^3.12" 14 | jupyter = "*" 15 | graphviz = "*" 16 | numpy = "*" 17 | pillow = "*" 18 | marshmallow = "*" 19 | matplotlib = "*" 20 | scipy = "*" 21 | astor = "*" 22 | sympy = "*" 23 | regex = "*" 24 | requests = "*" 25 | easing-functions = "*" 26 | 27 | [tool.poetry.dependencies.advent-of-code-data] 28 | # Commit that provides type hints for the library, use until 29 | # new release is made available. 30 | "git"="https://github.com/wimglenn/advent-of-code-data.git" 31 | "rev"="9a0a72f1eb7178a6f6901e1879ebbad82689b93c" 32 | 33 | [tool.poetry.group.dev.dependencies] 34 | ruff = "^0.1.6" 35 | pyright = "^1.1.339" 36 | 37 | 38 | [tool.ruff] 39 | extend-include = ["*.ipynb"] 40 | extend-exclude = ["jupyter_notebook_config.py", "2015/", "2016/"] 41 | ignore = ["E402", "E501"] 42 | unfixable = ["B"] 43 | line-length = 88 44 | select = ["B", "E", "F", "W", "B9"] 45 | 46 | 47 | [tool.pyright] 48 | include = ["."] 49 | exclude = ["2015", "2016"] 50 | ignore = ["jupyter_notebook_config.py"] 51 | strict = ["2023"] 52 | # pyright 1.1.339 "standard" mode, manually applied until pylance includes that version 53 | reportFunctionMemberAccess = "error" 54 | reportIncompatibleMethodOverride = "error" 55 | reportIncompatibleVariableOverride = "error" 56 | reportOverlappingOverload = "error" 57 | 58 | 59 | [build-system] 60 | requires = ["poetry-core"] 61 | build-backend = "poetry.core.masonry.api" 62 | -------------------------------------------------------------------------------- /rust/2018/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aoc_rust_2018" 3 | version = "0.1.0" 4 | authors = ["Martijn Pieters "] 5 | edition = "2018" 6 | 7 | [lib] 8 | bench = false 9 | 10 | [dependencies] 11 | aoc-runner = "0.3.0" 12 | aoc-runner-derive = "0.3.0" 13 | counter = "0.5.2" 14 | lazy_static = "1.4.0" 15 | ranges = "0.3.1" 16 | regex = "1.4.2" 17 | sequence_trie = "0.3.6" 18 | -------------------------------------------------------------------------------- /rust/2018/src/day01.rs: -------------------------------------------------------------------------------- 1 | use aoc_runner_derive::{aoc, aoc_generator}; 2 | use std::collections::BTreeSet; 3 | use std::num::ParseIntError; 4 | 5 | #[aoc_generator(day1)] 6 | fn read_changes(input: &str) -> Result, ParseIntError> { 7 | input.lines().map(|l| l.parse()).collect() 8 | } 9 | 10 | #[aoc(day1, part1)] 11 | fn part1(changes: &Vec) -> i32 { 12 | changes.iter().sum() 13 | } 14 | 15 | #[aoc(day1, part2)] 16 | fn part2(changes: &Vec) -> i32 { 17 | let mut seen = BTreeSet::new(); 18 | let mut freq = 0; 19 | 20 | seen.insert(freq); 21 | 22 | changes 23 | .iter() 24 | .cycle() 25 | .take_while(|&&change| { 26 | freq += change; 27 | seen.insert(freq) 28 | }) 29 | .count(); 30 | 31 | freq 32 | } 33 | 34 | #[cfg(test)] 35 | mod tests { 36 | use super::*; 37 | 38 | static EXAMPLES1: &[&str] = &[ 39 | "+1\n-2\n+3\n+1\n", 40 | "+1\n+1\n+1\n", 41 | "+1\n+1\n-2\n", 42 | "-1\n-2\n-3\n", 43 | ]; 44 | 45 | static EXAMPLES2: &[&str] = &[ 46 | "+1\n-2\n+3\n+1\n", 47 | "+1\n-1", 48 | "+3\n+3\n+4\n-2\n-4\n", 49 | "-6\n+3\n+8\n+5\n-6\n", 50 | "+7\n+7\n-2\n-7\n-4\n", 51 | ]; 52 | 53 | #[test] 54 | fn part1_example() -> Result<(), ParseIntError> { 55 | assert_eq!(part1(&read_changes(&EXAMPLES1[0])?), 3); 56 | assert_eq!(part1(&read_changes(&EXAMPLES1[1])?), 3); 57 | assert_eq!(part1(&read_changes(&EXAMPLES1[2])?), 0); 58 | assert_eq!(part1(&read_changes(&EXAMPLES1[3])?), -6); 59 | Ok(()) 60 | } 61 | 62 | #[test] 63 | fn part2_example() -> Result<(), ParseIntError> { 64 | assert_eq!(part2(&read_changes(&EXAMPLES2[0])?), 2); 65 | assert_eq!(part2(&read_changes(&EXAMPLES2[1])?), 0); 66 | assert_eq!(part2(&read_changes(&EXAMPLES2[2])?), 10); 67 | assert_eq!(part2(&read_changes(&EXAMPLES2[3])?), 5); 68 | assert_eq!(part2(&read_changes(&EXAMPLES2[4])?), 14); 69 | Ok(()) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /rust/2018/src/day02.rs: -------------------------------------------------------------------------------- 1 | use aoc_runner_derive::aoc; 2 | use counter::Counter; 3 | use sequence_trie::SequenceTrie; 4 | 5 | #[aoc(day2, part1)] 6 | fn checksum(input: &str) -> u32 { 7 | let (twos, threes) = input.lines().fold((0_u32, 0_u32), |(twos, threes), id| { 8 | let (count2, count3) = id.chars().collect::>().values().fold( 9 | (0_u32, 0_u32), 10 | |(count2, count3), c| match c { 11 | 2 => (count2 + 1, count3), 12 | 3 => (count2, count3 + 1), 13 | _ => (count2, count3), 14 | }, 15 | ); 16 | match (count2, count3) { 17 | (0, 0) => (twos, threes), 18 | (1..=u32::MAX, 0) => (twos + 1, count3), 19 | (0, 1..=u32::MAX) => (twos, count3 + 1), 20 | (1..=u32::MAX, 1..=u32::MAX) => (twos + 1, count3 + 1), 21 | } 22 | }); 23 | 24 | twos * threes 25 | } 26 | 27 | #[aoc(day2, part2)] 28 | fn matching_ids(input: &str) -> String { 29 | let mut trie: SequenceTrie = SequenceTrie::new(); 30 | for id in input.lines() { 31 | let mut stack: Vec<_> = vec![&trie]; 32 | for (i, byte) in id.as_bytes().iter().enumerate() { 33 | if i == stack.len() { 34 | // insert remainder as a new trie entry 35 | trie.insert(id.as_bytes().iter(), ()); 36 | break; 37 | } 38 | let current = stack[i]; 39 | let mut found = None; 40 | for (tchar, sub) in current.children_with_keys() { 41 | if *tchar == *byte { 42 | // save subnode key for when we can safely mutate the stack. 43 | found = Some(tchar); 44 | } else { 45 | // check if there is a full postfix match 46 | if sub.get(id.as_bytes().iter().skip(i + 1)).is_some() { 47 | return String::from([&id[..i], &id[i + 1..]].concat()); 48 | }; 49 | }; 50 | } 51 | if let Some(tchar) = found { 52 | stack.push(¤t.get_node([*tchar].iter()).unwrap()); 53 | }; 54 | } 55 | } 56 | unreachable!("No solution!") 57 | } 58 | 59 | #[cfg(test)] 60 | mod tests { 61 | use super::*; 62 | 63 | static EXAMPLE1: &str = "abcdef\nbababc\nabbcde\nabcccd\naabcdd\nabcdee\nababab"; 64 | static EXAMPLE2: &str = "abcde\nfghij\nklmno\npqrst\nfguij\naxcye\nwvxyz"; 65 | 66 | #[test] 67 | fn part1_example() { 68 | assert_eq!(checksum(&EXAMPLE1), 12); 69 | } 70 | 71 | #[test] 72 | fn part2_example() { 73 | assert_eq!(matching_ids(&EXAMPLE2), "fgij"); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /rust/2018/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate lazy_static; 3 | 4 | use aoc_runner_derive::aoc_lib; 5 | 6 | pub mod day01; 7 | pub mod day02; 8 | pub mod day03; 9 | 10 | aoc_lib! { year = 2018 } 11 | -------------------------------------------------------------------------------- /rust/2019/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aoc2019" 3 | version = "0.1.0" 4 | authors = ["Martijn Pieters "] 5 | edition = "2018" 6 | 7 | [lib] 8 | bench = false 9 | 10 | [dependencies] 11 | aoc-runner = "0.3.0" 12 | aoc-runner-derive = "0.3.0" 13 | -------------------------------------------------------------------------------- /rust/2019/src/day01.rs: -------------------------------------------------------------------------------- 1 | use aoc_runner_derive::{aoc, aoc_generator}; 2 | use std::iter::successors; 3 | use std::num::ParseIntError; 4 | 5 | #[aoc_generator(day1)] 6 | fn parse_modules(input: &str) -> Result, ParseIntError> { 7 | input.lines().map(|l| l.parse()).collect() 8 | } 9 | 10 | #[inline] 11 | fn fuel_requirement(mass: u32) -> u32 { 12 | (mass / 3).saturating_sub(2) 13 | } 14 | 15 | #[inline] 16 | fn iterative_fuel_calc(mass: u32) -> u32 { 17 | successors(Some(mass), |&m| { 18 | let requirement = fuel_requirement(m); 19 | match requirement { 20 | 0 => None, 21 | _ => Some(requirement), 22 | } 23 | }) 24 | .skip(1) 25 | .sum() 26 | } 27 | 28 | #[aoc(day1, part1)] 29 | fn total_fuel(modules: &Vec) -> u32 { 30 | modules.iter().map(|&m| fuel_requirement(m)).sum() 31 | } 32 | 33 | #[aoc(day1, part2)] 34 | fn total_fuel_iterative(modules: &Vec) -> u32 { 35 | modules.iter().map(|&m| iterative_fuel_calc(m)).sum() 36 | } 37 | 38 | #[cfg(test)] 39 | mod tests { 40 | use super::*; 41 | 42 | static EXAMPLES1: [(u32, u32); 4] = [(12, 2), (14, 2), (1969, 654), (100756, 33583)]; 43 | static EXAMPLES2: [(u32, u32); 3] = [(14, 2), (1969, 966), (100756, 50346)]; 44 | 45 | #[test] 46 | fn test_fuel_requirement() { 47 | for (mass, expected) in EXAMPLES1.iter() { 48 | assert_eq!(fuel_requirement(*mass), *expected); 49 | } 50 | } 51 | #[test] 52 | fn test_iterative_fuel_calc() { 53 | for (mass, expected) in EXAMPLES2.iter() { 54 | assert_eq!(iterative_fuel_calc(*mass), *expected); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /rust/2019/src/lib.rs: -------------------------------------------------------------------------------- 1 | use aoc_runner_derive::aoc_lib; 2 | 3 | pub mod day01; 4 | 5 | aoc_lib! { year = 2019 } 6 | -------------------------------------------------------------------------------- /rust/2020/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aoc2020" 3 | version = "0.1.0" 4 | authors = ["Martijn Pieters "] 5 | edition = "2018" 6 | 7 | [lib] 8 | bench = false 9 | 10 | [dependencies] 11 | aoc-runner = "0.3.0" 12 | aoc-runner-derive = "0.3.0" 13 | bit-set = "0.5.2" 14 | itertools = "0.9.0" 15 | lazy_static = "1.4.0" 16 | regex = "1.4.2" 17 | -------------------------------------------------------------------------------- /rust/2020/src/day01.rs: -------------------------------------------------------------------------------- 1 | use aoc_runner_derive::{aoc, aoc_generator}; 2 | use bit_set::BitSet; 3 | use std::collections::BTreeSet; 4 | use std::num::ParseIntError; 5 | 6 | #[aoc_generator(day1)] 7 | fn read_coins(input: &str) -> Result, ParseIntError> { 8 | input.lines().map(|l| l.parse()).collect() 9 | } 10 | 11 | fn find_sum(coins: &BTreeSet, target: u32) -> Option<(u32, u32)> { 12 | for first in coins { 13 | let second = target.wrapping_sub(*first); 14 | if coins.contains(&second) { 15 | return Some((*first, second)); 16 | } 17 | } 18 | None 19 | } 20 | 21 | #[aoc(day1, part1)] 22 | fn part1(coins: &BTreeSet) -> u32 { 23 | if let Some((first, second)) = find_sum(coins, 2020) { 24 | return first * second; 25 | } 26 | unreachable!("No solution!") 27 | } 28 | 29 | #[aoc(day1, part2)] 30 | fn part2(coins: &BTreeSet) -> u32 { 31 | for selected in coins { 32 | if let Some((first, second)) = find_sum(coins, 2020_u32.wrapping_sub(*selected)) { 33 | return selected * first * second; 34 | } 35 | } 36 | unreachable!("No solution!"); 37 | } 38 | 39 | // BitSet versions 40 | 41 | #[aoc_generator(day1, None, BitSet)] 42 | fn read_bitarray(input: &str) -> Result, ParseIntError> { 43 | input.lines().map(|l| l.parse()).collect() 44 | } 45 | 46 | fn find_sum_bitset(coins: &BitSet, target: usize) -> Option<(usize, usize)> { 47 | for first in coins { 48 | let second = target.wrapping_sub(first); 49 | if coins.contains(second) { 50 | return Some((first, second)); 51 | } 52 | } 53 | None 54 | } 55 | 56 | #[aoc(day1, part1, BitSet)] 57 | fn part1_bitset(coins: &BitSet) -> usize { 58 | if let Some((first, second)) = find_sum_bitset(&coins, 2020) { 59 | return first * second; 60 | }; 61 | unreachable!("No solution!"); 62 | } 63 | 64 | #[aoc(day1, part2, BitSet)] 65 | fn part2_bitset(coins: &BitSet) -> usize { 66 | for selected in coins { 67 | if let Some((first, second)) = find_sum_bitset(&coins, 2020_usize.wrapping_sub(selected)) { 68 | return selected * first * second; 69 | } 70 | } 71 | unreachable!("No solution!"); 72 | } 73 | 74 | #[cfg(test)] 75 | mod tests { 76 | use super::*; 77 | 78 | static EXAMPLE: &str = "1721\n979\n366\n299\n675\n1456"; 79 | 80 | #[test] 81 | fn test_part2() -> Result<(), ParseIntError> { 82 | let coins = read_coins(EXAMPLE)?; 83 | assert_eq!(part1(&coins), 514579); 84 | Ok(()) 85 | } 86 | 87 | #[test] 88 | fn test_part1() -> Result<(), ParseIntError> { 89 | let coins = read_coins(EXAMPLE)?; 90 | assert_eq!(part2(&coins), 241861950); 91 | Ok(()) 92 | } 93 | 94 | #[test] 95 | fn test_part1_bitset() -> Result<(), ParseIntError> { 96 | let coins = read_bitarray(EXAMPLE)?; 97 | assert_eq!(part1_bitset(&coins), 514579); 98 | Ok(()) 99 | } 100 | 101 | #[test] 102 | fn test_part2_bitset() -> Result<(), ParseIntError> { 103 | let coins = read_bitarray(EXAMPLE)?; 104 | assert_eq!(part2_bitset(&coins), 241861950); 105 | Ok(()) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /rust/2020/src/day02.rs: -------------------------------------------------------------------------------- 1 | use aoc_runner_derive::{aoc, aoc_generator}; 2 | use regex::Regex; 3 | use std::error::Error; 4 | use std::fmt::Display; 5 | use std::str::FromStr; 6 | 7 | lazy_static! { 8 | static ref LINERE: Regex = 9 | Regex::new(r"^(?P\d+)-(?P\d+)\s*(?P[a-z]):\s*(?P[a-z]+)$") 10 | .unwrap(); 11 | } 12 | 13 | #[derive(Debug)] 14 | struct PWRule { 15 | min: usize, 16 | max: usize, 17 | letter: char, 18 | password: String, 19 | } 20 | 21 | impl PWRule { 22 | fn is_valid(&self) -> bool { 23 | let count = self.password.chars().filter(|c| *c == self.letter).count(); 24 | self.min <= count && count <= self.max 25 | } 26 | 27 | fn is_valid_toboggan_policy(&self) -> bool { 28 | self.password 29 | .chars() 30 | .enumerate() 31 | .filter(|(i, c)| (*i == self.min - 1 || *i == self.max - 1) && *c == self.letter) 32 | .count() 33 | == 1 34 | } 35 | } 36 | 37 | #[derive(Debug)] 38 | struct PWRuleError; 39 | 40 | impl Error for PWRuleError {} 41 | impl Display for PWRuleError { 42 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 43 | write!(f, "Could not parse password rule") 44 | } 45 | } 46 | 47 | impl FromStr for PWRule { 48 | type Err = PWRuleError; 49 | fn from_str(line: &str) -> Result { 50 | LINERE 51 | .captures(line) 52 | .and_then(|cap| { 53 | Some(PWRule { 54 | min: cap.name("min")?.as_str().parse().ok()?, 55 | max: cap.name("max")?.as_str().parse().ok()?, 56 | letter: cap.name("letter")?.as_str().chars().next()?, 57 | password: cap.name("password")?.as_str().into(), 58 | }) 59 | }) 60 | .ok_or(PWRuleError) 61 | } 62 | } 63 | 64 | #[aoc_generator(day2)] 65 | fn read_pwrules(input: &str) -> Result, PWRuleError> { 66 | input.lines().map(|l| l.parse()).collect() 67 | } 68 | 69 | #[aoc(day2, part1)] 70 | fn count_valid(pwrules: &Vec) -> usize { 71 | pwrules.iter().filter(|pwrule| pwrule.is_valid()).count() 72 | } 73 | 74 | #[aoc(day2, part2)] 75 | fn count_valid_toboggan(pwrules: &Vec) -> usize { 76 | pwrules 77 | .iter() 78 | .filter(|pwrule| pwrule.is_valid_toboggan_policy()) 79 | .count() 80 | } 81 | 82 | #[cfg(test)] 83 | mod tests { 84 | use super::*; 85 | 86 | static EXAMPLE: &str = "1-3 a: abcde\n1-3 b: cdefg\n2-9 c: ccccccccc"; 87 | 88 | #[test] 89 | fn test_part1() -> Result<(), PWRuleError> { 90 | let pwrules = read_pwrules(EXAMPLE)?; 91 | assert_eq!(count_valid(&pwrules), 2); 92 | Ok(()) 93 | } 94 | 95 | #[test] 96 | fn test_part2() -> Result<(), PWRuleError> { 97 | let pwrules = read_pwrules(EXAMPLE)?; 98 | assert_eq!(count_valid_toboggan(&pwrules), 1); 99 | Ok(()) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /rust/2020/src/day03.rs: -------------------------------------------------------------------------------- 1 | use aoc_runner_derive::aoc; 2 | 3 | fn count_trees(map: &str, right: usize, down: usize) -> u32 { 4 | let cols = (0..).step_by(right); 5 | map.lines() 6 | .step_by(down) 7 | .zip(cols) 8 | .map(|(row, c)| { 9 | let col = c % row.len(); 10 | if row[col..=col] == *"#" { 11 | 1_u32 12 | } else { 13 | 0_u32 14 | } 15 | }) 16 | .sum() 17 | } 18 | 19 | fn test_slopes(map: &str) -> u32 { 20 | let slopes = [(1, 1), (3, 1), (5, 1), (7, 1), (1, 2)]; 21 | slopes 22 | .iter() 23 | .map(|(right, down)| count_trees(map, *right, *down)) 24 | .product() 25 | } 26 | 27 | #[aoc(day3, part1)] 28 | fn part1(map: &str) -> u32 { 29 | count_trees(map, 3, 1) 30 | } 31 | 32 | #[aoc(day3, part2)] 33 | fn part2(map: &str) -> u32 { 34 | test_slopes(map) 35 | } 36 | 37 | #[cfg(test)] 38 | mod tests { 39 | use super::*; 40 | 41 | static EXAMPLE: &str = "\ 42 | ..##.......\n\ 43 | #...#...#..\n\ 44 | .#....#..#.\n\ 45 | ..#.#...#.#\n\ 46 | .#...##..#.\n\ 47 | ..#.##.....\n\ 48 | .#.#.#....#\n\ 49 | .#........#\n\ 50 | #.##...#...\n\ 51 | #...##....#\n\ 52 | .#..#...#.#\n\ 53 | "; 54 | 55 | #[test] 56 | fn test_part1() { 57 | assert_eq!(part1(&EXAMPLE), 7); 58 | } 59 | 60 | #[test] 61 | fn test_part2() { 62 | assert_eq!(part2(&EXAMPLE), 336); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /rust/2020/src/day05.rs: -------------------------------------------------------------------------------- 1 | use aoc_runner_derive::{aoc, aoc_generator}; 2 | use itertools::Itertools; 3 | use std::collections::BTreeSet; 4 | use std::error::Error; 5 | use std::fmt::{Debug, Display}; 6 | use std::str::FromStr; 7 | 8 | #[derive(PartialEq, Eq, PartialOrd, Ord)] 9 | struct SeatId { 10 | pub id: u16, 11 | pub row: u8, 12 | pub col: u8, 13 | } 14 | 15 | impl SeatId { 16 | fn new(id: u16) -> Self { 17 | Self { 18 | id: id & 1023, 19 | row: ((id >> 3) & 127) as u8, 20 | col: (id & 7) as u8, 21 | } 22 | } 23 | } 24 | 25 | impl Debug for SeatId { 26 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 27 | let rowid = format!("{:07b}", self.row) 28 | .replace("0", "F") 29 | .replace("1", "B"); 30 | let colid = format!("{:03b}", self.col) 31 | .replace("0", "L") 32 | .replace("1", "R"); 33 | f.write_fmt(format_args!("", self.id, rowid, colid)) 34 | } 35 | } 36 | 37 | #[derive(Debug)] 38 | struct BoardingPassParseError; 39 | 40 | impl Display for BoardingPassParseError { 41 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 42 | f.write_str("Not a valid boarding pass") 43 | } 44 | } 45 | 46 | impl Error for BoardingPassParseError {} 47 | 48 | impl FromStr for SeatId { 49 | type Err = BoardingPassParseError; 50 | 51 | fn from_str(s: &str) -> Result { 52 | match s.len() { 53 | 10 => s.char_indices().fold(Ok(0), |acc, (i, c)| match acc { 54 | Ok(id) => match (i, c) { 55 | (0..=6, 'F') | (7..=9, 'L') => Ok(id << 1), 56 | (0..=6, 'B') | (7..=9, 'R') => Ok((id << 1) | 1), 57 | _ => Err(BoardingPassParseError), 58 | }, 59 | _ => acc, 60 | }), 61 | _ => Err(BoardingPassParseError), 62 | } 63 | .and_then(|id| Ok(SeatId::new(id))) 64 | } 65 | } 66 | 67 | #[aoc_generator(day5)] 68 | fn parse_boardingpasses(input: &str) -> Result, BoardingPassParseError> { 69 | input.lines().map(|l| l.parse()).collect() 70 | } 71 | 72 | #[aoc(day5, part1)] 73 | fn max_seatid(seatids: &Vec) -> u16 { 74 | seatids.iter().max().unwrap().id 75 | } 76 | 77 | #[aoc(day5, part2)] 78 | fn find_empty(seatids: &Vec) -> u16 { 79 | let minid: u16 = (u16::from(seatids.iter().map(|s| s.row).min().unwrap()) << 3) & 7; 80 | let maxid: u16 = u16::from(seatids.iter().map(|s| s.row).max().unwrap()) << 3; 81 | let occupied: BTreeSet<_> = seatids.iter().map(|s| s.id).collect(); 82 | 83 | (minid..maxid) 84 | .tuple_windows() 85 | .filter_map(|(b, s, a)| { 86 | match ( 87 | occupied.contains(&b), 88 | occupied.contains(&s), 89 | occupied.contains(&a), 90 | ) { 91 | (true, false, true) => Some(s), 92 | _ => None, 93 | } 94 | }) 95 | .next() 96 | .unwrap() 97 | } 98 | 99 | #[cfg(test)] 100 | mod tests { 101 | use super::*; 102 | 103 | static EXAMPLES: [(&str, u8, u8, u16); 4] = [ 104 | ("FBFBBFFRLR", 44, 5, 357), 105 | ("BFFFBBFRRR", 70, 7, 567), 106 | ("FFFBBBFRRR", 14, 7, 119), 107 | ("BBFFBBFRLL", 102, 4, 820), 108 | ]; 109 | 110 | #[test] 111 | fn test_seatids() -> Result<(), BoardingPassParseError> { 112 | let mut seatids: Vec = vec![]; 113 | for (pass, row, col, id) in EXAMPLES.iter() { 114 | let seatid: SeatId = pass.parse()?; 115 | assert_eq!(seatid.row, *row); 116 | assert_eq!(seatid.col, *col); 117 | assert_eq!(seatid.id, *id); 118 | seatids.push(seatid); 119 | } 120 | println!("Seat IDs: {:?}", seatids); 121 | assert_eq!( 122 | seatids.iter().max().unwrap().id, 123 | *EXAMPLES.iter().map(|(.., id)| id).max().unwrap() 124 | ); 125 | Ok(()) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /rust/2020/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate lazy_static; 3 | 4 | use aoc_runner_derive::aoc_lib; 5 | 6 | pub mod day01; 7 | pub mod day02; 8 | pub mod day03; 9 | // pub mod day04; 10 | pub mod day05; 11 | 12 | aoc_lib! { year = 2020 } 13 | -------------------------------------------------------------------------------- /rust/2021/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "aoc2021" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | aoc-runner = "0.3.0" 10 | aoc-runner-derive = "0.3.0" 11 | itertools = "0.10.3" 12 | -------------------------------------------------------------------------------- /rust/2021/src/lib.rs: -------------------------------------------------------------------------------- 1 | use aoc_runner_derive::aoc_lib; 2 | 3 | pub mod day18; 4 | 5 | aoc_lib! { year = 2021 } 6 | -------------------------------------------------------------------------------- /start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" 3 | LOG="$DIR/jupyter_notebook.log" 4 | CONFIG="$DIR/jupyter_notebook_config.py" 5 | poetry run nohup jupyter notebook --config="$CONFIG" 1>/dev/null 2>> "$LOG" & 6 | -------------------------------------------------------------------------------- /stop: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | poetry run jupyter notebook stop 3 | --------------------------------------------------------------------------------