From bad86889d627629f91b106f67bae05df4a54f96c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Engl=C3=B6f=20Ytterstr=C3=B6m?= Date: Sun, 5 Jan 2025 00:09:28 +0100 Subject: [PATCH 01/22] Revert "2023 solutions (#6)" This reverts commit b6e048e4a72cb9388bffdff99b95587967734769. --- 2023-python/.gitignore | 2 - 2023-python/README.md | 42 ----------- 2023-python/aoc.py | 126 --------------------------------- 2023-python/output/__init__.py | 82 --------------------- 2023-python/output/day_01.py | 51 ------------- 2023-python/output/day_02.py | 49 ------------- 2023-python/output/day_03.py | 68 ------------------ 2023-python/output/day_04.py | 47 ------------ 2023-python/output/day_05.py | 73 ------------------- 2023-python/output/day_06.py | 47 ------------ 2023-python/output/day_07.py | 94 ------------------------ 2023-python/output/day_08.py | 61 ---------------- 2023-python/output/day_09.py | 40 ----------- 2023-python/output/day_10.py | 94 ------------------------ 2023-python/output/day_11.py | 63 ----------------- 2023-python/output/day_12.py | 64 ----------------- 2023-python/output/day_13.py | 56 --------------- 2023-python/output/day_14.py | 83 ---------------------- 2023-python/output/day_15.py | 63 ----------------- 2023-python/output/day_16.py | 86 ---------------------- 20 files changed, 1291 deletions(-) delete mode 100644 2023-python/.gitignore delete mode 100644 2023-python/README.md delete mode 100644 2023-python/aoc.py delete mode 100644 2023-python/output/__init__.py delete mode 100644 2023-python/output/day_01.py delete mode 100644 2023-python/output/day_02.py delete mode 100644 2023-python/output/day_03.py delete mode 100644 2023-python/output/day_04.py delete mode 100644 2023-python/output/day_05.py delete mode 100644 2023-python/output/day_06.py delete mode 100644 2023-python/output/day_07.py delete mode 100644 2023-python/output/day_08.py delete mode 100644 2023-python/output/day_09.py delete mode 100644 2023-python/output/day_10.py delete mode 100644 2023-python/output/day_11.py delete mode 100644 2023-python/output/day_12.py delete mode 100644 2023-python/output/day_13.py delete mode 100644 2023-python/output/day_14.py delete mode 100644 2023-python/output/day_15.py delete mode 100644 2023-python/output/day_16.py diff --git a/2023-python/.gitignore b/2023-python/.gitignore deleted file mode 100644 index fa878cc..0000000 --- a/2023-python/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -__pycache__ -input \ No newline at end of file diff --git a/2023-python/README.md b/2023-python/README.md deleted file mode 100644 index 249aff0..0000000 --- a/2023-python/README.md +++ /dev/null @@ -1,42 +0,0 @@ -Advent of Code 2023 -=================== - -Solutions for #aoc2023 in Python 3 (3.11.5). - -Help scripts ------------- - -Display all solved puzzles: - - python aoc.py - -To bootstrap a new puzzle (creates `input/.txt` and `output/day_.py`): - - python aoc.py - -Manually copy the puzzle input from https://adventofcode.com and paste it in `input/.txt` -to start coding. - - wl-paste > input/.txt - -Solve separate puzzle (replace `XX` with the puzzle number): - - python -m output.day_XX - -Solve separate puzzle using stdin (replace `XX` with the puzzle number): - - wl-paste | python -m output.day_XX - cat tmpfile | python -m output.day_XX - -Execute separate puzzle on file save (replace `XX` with the puzzle number): - - ls output/*.py | entr -c -s 'xclip -selection clipboard -o | python -m output.day_XX' - ls output/*.py | entr -c -s 'cat tmpfile | python -m output.day_XX' - ls output/*.py | entr -c -r python -m output.day_XX - -(requires `entr` and `wl-paste`, Mac users can instead use `pbpaste`. If you -prefer X at Linux, use `xclip -selection clipboard -o`). - -To lint files: - - ls output/*.py | entr -r -c flake8 output --ignore=E741,E501,E203 diff --git a/2023-python/aoc.py b/2023-python/aoc.py deleted file mode 100644 index 090129d..0000000 --- a/2023-python/aoc.py +++ /dev/null @@ -1,126 +0,0 @@ -import os -import sys - - -def headline(n, title): - """Print day number and name, followed by a ruler. Used by the answer decorator""" - print(f"\n--- Day {n}: {title} ---\n") - - -year = 2023 - -try: - _, day_no, *name = sys.argv -except ValueError: - day_no = None - name = None - -print( - f"\nAdvent of Code {year}" "\n###################" "\n\nby Anders Englöf Ytterström" -) - -if day_no and name: - name = " ".join(name) - padded_no = day_no.zfill(2) - print(f"\n- creating output/day_{padded_no}.py") - with open("output/day_{}.py".format(padded_no), "w") as s: - s.write( - f""" -from output import answer # , matrix, D, DD, ADJ, ints, mhd, mdbg, vdbg - -n = {day_no} -title = "{name}" - - -@answer(1, "Answer is {{}}") -def part_1(presolved): - return presolved[0] - - -@answer(2, "Actually, answer is {{}}") -def part_2(presolved): - return presolved[1] - - -def solve(data): - return 1, 2 - - -if __name__ == "__main__": - # use dummy data - inp = \"\"\" - replace me - \"\"\".strip() - - # uncomment to instead use stdin - # import sys; inp = sys.stdin.read().strip() - - # uncomment to use AoC provided puzzle input - # with open("./input/{padded_no}.txt", "r") as f: - # inp = f.read().strip() - - # uncomment to do initial data processing shared by part 1-2 - inp = solve(inp) - - a = part_1(inp) - # b = part_2(inp) - - # uncomment and replace 0 with actual output to refactor code - # and ensure nonbreaking changes - # assert a == 0 - # assert b == 0 -""".strip() - + "\n" - ) - print("- making sure input dir exists") - if not os.path.exists("input"): - os.makedirs("input") - - print( - f""" -Done! start coding. - -Puzzle link: -https://adventofcode.com/{year}/day/{day_no} - -Puzzle input (copy and paste to input/{day_no.zfill(2)}.txt): -https://adventofcode.com/{year}/day/{day_no}/input - """ - ) - exit(0) - - -stars = 0 -for i in [str(n).zfill(2) for n in range(1, 26)]: - if not day_no or day_no.zfill(2) == i: - try: - day = __import__( - "output.day_{}".format(i), - globals(), - locals(), - ["n", "title", "part_1", "part_2"], - 0, - ) - with open(f"./input/{i}.txt", "r") as f: - data = f.read().strip() - headline(day.n, day.title) - try: - data = day.presolve(data) - except AttributeError: - pass - try: - data = day.solve(data) - except AttributeError: - pass - if day.part_1(data, decorate=True): - stars += 1 - if day.part_2(data, decorate=True): - stars += 1 - except IOError: - pass - except ImportError: - pass -if not day_no: - print(f"\nStars: {stars}") - print("".join("*" if n < stars else "•" for n in range(50))) -print("") diff --git a/2023-python/output/__init__.py b/2023-python/output/__init__.py deleted file mode 100644 index 3a034d6..0000000 --- a/2023-python/output/__init__.py +++ /dev/null @@ -1,82 +0,0 @@ -import functools -import re - -# Directions/Adjacents for 2D matrices, in the order UP, RIGHT, DOWN, LEFT -D = [ - (-1, 0), - (0, 1), - (1, 0), - (0, -1), -] - -# Directions for 2D matrices, as a dict with keys U, R, D, L -DD = { - "U": (-1, 0), - "R": (0, 1), - "D": (1, 0), - "L": (0, -1), -} - -# Adjacent relative positions including diagonals for 2D matrices, in the order NW, N, NE, W, E, SW, S, SE -ADJ = [ - (-1, -1), - (-1, 0), - (1, -1), - (0, -1), - (0, 1), - (1, 1), - (1, 0), - (1, -1), -] - - -def answer(part_index, fmt_string): - """Decorator to present a solution in a human readable format""" - - def decorator_aoc(func): - @functools.wraps(func) - def wrapper_aoc(*args, **kwargs): - decorate = kwargs.get("decorate", False) - if decorate: - del kwargs["decorate"] - answer = func(*args, **kwargs) - if not decorate: - print(answer) - else: - formatted = fmt_string.format(answer) - print(f" {part_index}) {formatted}") - return answer - - return wrapper_aoc - - return decorator_aoc - - -def ints(s): - """Extract all integers from a string""" - return [int(n) for n in re.findall(r"\d+", s)] - - -def mhd(a, b): - """Calculates the Manhattan distance between 2 positions in the format (y, x) or (x, y)""" - ar, ac = a - br, bc = b - return abs(ar - br) + abs(ac - bc) - - -def matrix(d): - """Transform a string into an iterable matrix. Returns the matrix, row count and col count""" - m = [tuple(r) for r in d.split()] - return m, len(m), len(m[0]) - - -def mdbg(m): - """Print-debug a matrix""" - for r in m: - print("".join(r)) - - -def vdbg(seen, h, w): - """Print-debug visited positions of a matrix""" - for r in range(h): - print("".join(["#" if (r, c) in seen else "." for c in range(w)])) diff --git a/2023-python/output/day_01.py b/2023-python/output/day_01.py deleted file mode 100644 index f2eecfb..0000000 --- a/2023-python/output/day_01.py +++ /dev/null @@ -1,51 +0,0 @@ -import re -from output import answer - -n = 1 -title = "Trebuchet?!" - - -@answer(1, "Calibration values sum: {}, excluding spelled out digits") -def part_1(data): - def value(s): - s = [int(c) for c in s if c.isdigit()] - return s[0] * 10 + s[-1] - - return sum(value(line) for line in data.split()) - - -@answer(2, "Calibration values sum: {}, including spelled out digits") -def part_2(data): - mp = { - "one": 1, - "two": 2, - "three": 3, - "four": 4, - "five": 5, - "six": 6, - "seven": 7, - "eight": 8, - "nine": 9, - } - - def value(l): - s = [ - int(c) if c.isdigit() else mp[c] - for c in re.findall( - r"(?=(\d|one|two|three|four|five|six|seven|eight|nine))", l - ) - ] - return s[0] * 10 + s[-1] - - return sum(value(line) for line in data.split()) - - -if __name__ == "__main__": - with open("./input/01.txt", "r") as f: - inp = f.read().strip() - - a = part_1(inp) - b = part_2(inp) - - assert a == 54634 - assert b == 53855 diff --git a/2023-python/output/day_02.py b/2023-python/output/day_02.py deleted file mode 100644 index f5ac032..0000000 --- a/2023-python/output/day_02.py +++ /dev/null @@ -1,49 +0,0 @@ -import re -from math import prod -from collections import defaultdict -from output import answer - -n = 2 -title = "Cube Conundrum" - - -@answer(1, "Sum of all possible game IDs: {}") -def part_1(data): - return sum( - [ - id + 1 - for id, game in enumerate(data.splitlines()) - if not sum( - any( - [ - c == "red" and int(n) > 12, - c == "green" and int(n) > 13, - c == "blue" and int(n) > 14, - ] - ) - for n, c in re.findall(r"(\d+) (\w+)", game) - ) - ] - ) - - -@answer(2, "Sum of all cube set powers: {}") -def part_2(data): - def power(game): - seen = defaultdict(int) - for n, c in re.findall(r"(\d+) (\w+)", game): - seen[c] = max([seen[c], int(n)]) - return prod([seen["blue"], seen["red"], seen["green"]]) - - return sum(power(game) for game in data.splitlines()) - - -if __name__ == "__main__": - with open("./input/02.txt", "r") as f: - inp = f.read().strip() - - a = part_1(inp) - b = part_2(inp) - - assert a == 2439 - assert b == 63711 diff --git a/2023-python/output/day_03.py b/2023-python/output/day_03.py deleted file mode 100644 index 19df9ae..0000000 --- a/2023-python/output/day_03.py +++ /dev/null @@ -1,68 +0,0 @@ -from collections import deque -from output import answer - -n = 3 -title = "Gear Ratios" - - -@answer(1, "Sum of all part numbers is {} in the engine schematic") -def part_1(presolved): - s, _ = presolved - return s - - -@answer(2, "Gear ratio sums is {} in the engine schematic") -def part_2(presolved): - _, gr = presolved - return gr - - -def presolve(data): - data = data.split() - adj = (-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1) - w = len(data[0]) - h = len(data) - s = list() - gr = list() - for y in range(w): - for x in range(h): - if data[y][x] != "." and not data[y][x].isdigit(): - seen = set() - t = list() - for oy, ox in adj: - if (y + oy, x + ox) in seen: - continue - if data[y + oy][x + ox].isdigit(): - n = deque([data[y + oy][x + ox]]) - i = x + ox - 1 - while i in range(w) and data[y + oy][i].isdigit(): - n.append(data[y + oy][i]) - seen.add((y + oy, i)) - i -= 1 - i = x + ox + 1 - while i in range(w) and data[y + oy][i].isdigit(): - n.appendleft(data[y + oy][i]) - seen.add((y + oy, i)) - i += 1 - t.append(sum(10**m * int(d) for m, d in enumerate(n))) - # part 1 - s.append(sum(t)) - # part 2 - if data[y][x] == "*" and len(t) == 2: - a, b = t - gr.append(a * b) - - return sum(s), sum(gr) - - -if __name__ == "__main__": - with open("./input/03.txt", "r") as f: - inp = f.read().strip() - - parsed = presolve(inp) - - a = part_1(parsed) - b = part_2(parsed) - - assert a == 553825 - assert b == 93994191 diff --git a/2023-python/output/day_04.py b/2023-python/output/day_04.py deleted file mode 100644 index 66c9f8c..0000000 --- a/2023-python/output/day_04.py +++ /dev/null @@ -1,47 +0,0 @@ -import re -from collections import defaultdict -from output import answer - -n = 4 -title = "Scratchcards" - - -@answer(1, "Sum of all scratchcard points are {}") -def part_1(presolved): - scores, _ = presolved - return scores - - -@answer(2, "Ends up wih a total of {} scratchcards") -def part_2(presolved): - _, count = presolved - return count - - -def presolve(data): - scores = [] - count = defaultdict(int) - for cid, line in enumerate(data.splitlines()): - a, b = line.split("|") - a = set(re.findall(r"\d+", a)[1:]) - b = set(re.findall(r"\d+", b)) - ab = len(a & b) - if ab > 0: - scores.append(2 ** (ab - 1)) - count[cid] += 1 - for i in range(cid + 1, cid + ab + 1): - count[i] += count[cid] - return sum(scores), sum(count.values()) - - -if __name__ == "__main__": - with open("./input/04.txt", "r") as f: - inp = f.read().strip() - - inp = presolve(inp) - - a = part_1(inp) - b = part_2(inp) - - assert a == 21919 - assert b == 9881048 diff --git a/2023-python/output/day_05.py b/2023-python/output/day_05.py deleted file mode 100644 index 4147802..0000000 --- a/2023-python/output/day_05.py +++ /dev/null @@ -1,73 +0,0 @@ -import re -from math import inf -from output import answer - -n = 5 -title = "If You Give A Seed A Fertilizer" - - -@answer(1, "Nearest location for seed id list is {}") -def part_1(presolved): - l, _ = presolved - return l - - -@answer(2, "Interpreting ranges of seeds, nearest location is {}") -def part_2(presolved): - _, l = presolved - return l - - -def presolve(data): - seeds, *process = data.split("\n\n") - seed_ranges = [[int(x) for x in ar.split()] for ar in re.findall(r"\d+ \d+", seeds)] - seed_values = [int(v) for v in seeds.split()[1:]] - processes = [ - [tuple(map(int, line.split())) for line in step.splitlines()[1:]] - for step in process - ] - - p1 = _process(seed_values, processes) - - p2 = 26829000 # takes 5m if starting from 0 - while True: - g = _process([p2], processes, reverse=True) - if any(g >= a and g < a + r for a, r in seed_ranges): - break - p2 += 1 - - return p1, p2 - - -def _process(seeds, processes, reverse=False): - n = inf - for start in seeds: - n = min(n, _nearest(start, processes, reverse=reverse)) - return n - - -def _nearest(start, processes, reverse=False): - procs = processes if not reverse else processes[::-1] - v = start - for steps in procs: - for line in steps: - dest, src, r = line - if reverse: - dest, src = src, dest - if v >= src and v < src + r: - v = dest + v - src - break - return v - - -if __name__ == "__main__": - with open("./input/05.txt", "r") as f: - inp = f.read().strip() - - inp = presolve(inp) - - a = part_1(inp) - b = part_2(inp) - - assert a == 278755257 - assert b == 26829166 diff --git a/2023-python/output/day_06.py b/2023-python/output/day_06.py deleted file mode 100644 index 1f9bfa7..0000000 --- a/2023-python/output/day_06.py +++ /dev/null @@ -1,47 +0,0 @@ -from math import prod, sqrt, ceil, floor -from output import answer - -n = 6 -title = "Wait For It" - - -@answer(1, "The product of all record times for all races is {}") -def part_1(presolved): - return presolved[0] - - -@answer(2, "The product of all record times for the single long race is {}") -def part_2(presolved): - return presolved[1] - - -def presolve(data): - values = data.split() - l = len(values) // 2 - races = list( - map( - lambda x: (int(x[0]), int(x[1])), list(zip(values[: l + 1], values[l:]))[1:] - ) - ) - p1 = prod(sum(bpt * (t - bpt) > d for bpt in range(t)) for t, d in races) - t = int("".join(values[1:l])) - d = int("".join(values[l + 1 :])) - # quadratic formula: - # https://en.wikipedia.org/wiki/Quadratic_formula - l = ceil((-t + sqrt(t**2 - 4 * d)) / -2) - h = floor((-t - sqrt(t**2 - 4 * d)) / -2) - p2 = h - l + 1 - return p1, p2 - - -if __name__ == "__main__": - with open("./input/06.txt", "r") as f: - inp = f.read().strip() - - inp = presolve(inp) - - a = part_1(inp) - b = part_2(inp) - - assert a == 1083852 - assert b == 23501589 diff --git a/2023-python/output/day_07.py b/2023-python/output/day_07.py deleted file mode 100644 index a583564..0000000 --- a/2023-python/output/day_07.py +++ /dev/null @@ -1,94 +0,0 @@ -from collections import Counter -from output import answer - -n = 7 -title = "Camel Cards" - - -@answer(1, "Total winnings are {}") -def part_1(data): - return _solve(data) - - -@answer(2, "Including jokers, total winnings are {}") -def part_2(data): - return _solve(data.replace("J", "0")) - - -def _solve(data): - return sum( - c[-1] * i for i, c in enumerate(sorted(_hand(h) for h in data.splitlines()), 1) - ) - - -def _hand(hb): - h, b = hb.split() - b = int(b) - h = h.translate(M) - return (_rank(h), h, b) - - -def _rank(h): - """ - Rank hand 0-6, letting jokers (0) aid the highest possible hand - - >>> _rank("10110") - 6 - >>> _rank("11110") - 6 - >>> _rank("12110") - 5 - >>> _rank("12100") - 5 - >>> _rank("12000") - 5 - >>> _rank("12120") - 4 - >>> _rank("12300") - 3 - >>> _rank("12310") - 3 - >>> _rank("12340") - 1 - """ - hc = Counter(h) - hcv = hc.values() - j = hc["0"] - del hc["0"] - p = sum(c == 2 for v, c in hc.items() if v != 0) - if j == 5 or max(hcv) + j == 5: - return 6 - if max(hcv) + j == 4: - return 5 - if (3 in hcv and 2 in hcv) or (p == 2 and j > 0): - return 4 - if max(hcv) + j == 3: - return 3 - if p + j > 1: - return 2 - if p + j > 0: - return 1 - return 0 - - -M = dict( - [ - (ord(o), ord(n)) - for o, n in {"T": "A", "J": "B", "Q": "C", "K": "D", "A": "E"}.items() - ] -) - - -if __name__ == "__main__": - import doctest - - doctest.testmod() - - with open("./input/07.txt", "r") as f: - inp = f.read().strip() - - a = part_1(inp) - b = part_2(inp) - - assert a == 249390788 - assert b == 248750248 diff --git a/2023-python/output/day_08.py b/2023-python/output/day_08.py deleted file mode 100644 index b2f397b..0000000 --- a/2023-python/output/day_08.py +++ /dev/null @@ -1,61 +0,0 @@ -import re -from math import lcm -from output import answer - -n = 8 -title = "Haunted Wasteland" - - -@answer(1, "One can reach Z in {} steps") -def part_1(presolved): - steps, _ = presolved - return steps - - -@answer(2, "Ghost path takes {} steps before all ghosts are at a Z positon") -def part_2(presolved): - _, ghost_meet_point = presolved - return ghost_meet_point - - -def presolve(data): - d, els = data.split("\n\n") - r = len(d) - e = dict() - for el in els.splitlines(): - k, *lr = re.match(r"(\w+) = \((\w+), (\w+)\)", el).groups() - e[k] = lr - - p1 = 0 - pos = "AAA" - while pos != "ZZZ": - i = 0 if d[p1 % r] == "L" else 1 - pos = e[pos][i] - p1 += 1 - - p2 = 0 - z = list() - for spos in [p for p in e.keys() if p.endswith("A")]: - s = 0 - pos = spos - while not pos.endswith("Z"): - i = 0 if d[s % r] == "L" else 1 - pos = e[pos][i] - s += 1 - z.append(s) - p2 = lcm(*z) - - return p1, p2 - - -if __name__ == "__main__": - with open("./input/08.txt", "r") as f: - inp = f.read().strip() - - inp = presolve(inp) - - a = part_1(inp) - b = part_2(inp) - - assert a == 17141 - assert b == 10818234074807 diff --git a/2023-python/output/day_09.py b/2023-python/output/day_09.py deleted file mode 100644 index 423047f..0000000 --- a/2023-python/output/day_09.py +++ /dev/null @@ -1,40 +0,0 @@ -from output import answer - -n = 9 -title = "Mirage Maintenance" - - -@answer(1, "OASIS report extrapolated values sum is {}") -def part_1(data): - lines = [[int(d) for d in line.split()] for line in data.splitlines()] - return _solve(lines) - - -@answer(2, "Using prepending, OASIS report extrapolated values sum is {}") -def part_2(data): - lines = [[int(d) for d in line.split()[::-1]] for line in data.splitlines()] - return _solve(lines) - - -def _solve(lines): - vs = 0 - for l in lines: - h = [l] - while any(n != 0 for n in h[-1]): - h.append([b - a for a, b in zip(h[-1], h[-1][1:])]) - h = h[::-1] - for i in range(len(h)): - h[i].append(0 if i == 0 else h[i - 1][-1] + h[i][-1]) - vs += h[-1][-1] - return vs - - -if __name__ == "__main__": - with open("./input/09.txt", "r") as f: - inp = f.read().strip() - - a = part_1(inp) - b = part_2(inp) - - assert a == 1702218515 - assert b == 925 diff --git a/2023-python/output/day_10.py b/2023-python/output/day_10.py deleted file mode 100644 index 7420d86..0000000 --- a/2023-python/output/day_10.py +++ /dev/null @@ -1,94 +0,0 @@ -from collections import deque -from output import answer - -n = 10 -title = "Pipe Maze" - - -D = (-1, 0), (0, 1), (1, 0), (0, -1) - -C = { - (-1, 0): ["|", "7", "F"], - (0, 1): ["-", "7", "J"], - (1, 0): ["|", "L", "J"], - (0, -1): ["-", "L", "F"], -} - -A = { - "S": [0, 1, 2, 3], - "-": [1, 3], - "|": [0, 2], - "F": [1, 2], - "L": [0, 1], - "7": [2, 3], - "J": [0, 3], -} - - -@answer(1, "Farthest away pipe is at {}") -def part_1(presolved): - return presolved[0] - - -@answer(2, "{} spots are encapsulated by pipes") -def part_2(presolved): - return presolved[1] - - -def presolve(data): - matrix = data.split() - w = len(matrix[0]) - h = len(matrix) - q = deque() - visited = set() - - for r in range(h): - if "S" in matrix[r]: - start = (r, matrix[r].index("S")) - q.append(start) - break - - while q: - o = q.popleft() - visited.add(o) - for di in A[matrix[o[0]][o[1]]]: - d = D[di] - r = o[0] + d[0] - c = o[1] + d[1] - if r >= 0 and r < h and c >= 0 and c < w: - t = matrix[r][c] - p = (r, c) - if p not in visited and t != "." and t in C[d]: - q.append(p) - p1 = len(visited) // 2 - - p2 = 0 - for y in range(h): - for x in range(w): - if (y, x) in visited: - continue - crosses = 0 - y2, x2 = y, x - while y2 < h and x2 < w: - c2 = matrix[y2][x2] - if (y2, x2) in visited and c2 not in "L7": - crosses += 1 - x2 += 1 - y2 += 1 - if crosses % 2 == 1: - p2 += 1 - - return p1, p2 - - -if __name__ == "__main__": - with open("./input/10.txt", "r") as f: - inp = f.read() - - inp = presolve(inp) - - a = part_1(inp) - b = part_2(inp) - - assert a == 6846 - assert b == 325 diff --git a/2023-python/output/day_11.py b/2023-python/output/day_11.py deleted file mode 100644 index 5ad4def..0000000 --- a/2023-python/output/day_11.py +++ /dev/null @@ -1,63 +0,0 @@ -from itertools import combinations -from output import answer - -n = 11 -title = "Cosmic Expansion" - - -@answer(1, "Sum of all galaxy shortest distances is {}") -def part_1(data): - return data[0] - - -@answer(2, "Exapanding by 1M, sum is {}") -def part_2(data): - return data[1] - - -def presolve(data): - m = data.splitlines() - er = set() - ec = set() - for i, r in enumerate(m): - if "#" not in r: - er.add(i) - for i, c in enumerate(zip(*m)): - if "#" not in c: - ec.add(i) - h = len(m) - w = len(m[0]) - g1 = [] - g2 = [] - e = 1e6 - for r in range(h): - for c in range(w): - if m[r][c] == "#": - ro = len(er & set(range(r))) - co = len(ec & set(range(c))) - g1.append((r + ro, c + co)) - g2.append((ro * e + r - ro, co * e + c - co)) - p1 = sum( - abs(rc1[0] - rc2[0]) + abs(rc1[1] - rc2[1]) for rc1, rc2 in combinations(g1, 2) - ) - p2 = int( - sum( - abs(rc1[0] - rc2[0]) + abs(rc1[1] - rc2[1]) - for rc1, rc2 in combinations(g2, 2) - ) - ) - - return p1, p2 - - -if __name__ == "__main__": - with open("./input/11.txt", "r") as f: - inp = f.read().strip() - - inp = presolve(inp) - - a = part_1(inp) - b = part_2(inp) - - assert a == 9370588 - assert b == 746207878188 diff --git a/2023-python/output/day_12.py b/2023-python/output/day_12.py deleted file mode 100644 index d0cc2a3..0000000 --- a/2023-python/output/day_12.py +++ /dev/null @@ -1,64 +0,0 @@ -from functools import cache -from output import answer - -n = 12 -title = "Hot Springs" - - -@answer(1, "sum of all possible combinations is {}") -def part_1(presolved): - return presolved[0] - - -@answer(2, "sum of all possible combinations is {} when unfolded") -def part_2(presolved): - return presolved[1] - - -def presolve(data): - lines = [ - (a, list(map(int, b.split(",")))) - for a, b in (line.split() for line in data.splitlines()) - ] - p1 = sum(_inspect(a, tuple(b)) for a, b in lines) - p2 = sum(_inspect("?".join([a] * 5), tuple(b * 5)) for a, b in lines) - return p1, p2 - - -@cache -def _inspect(s, cs): - r = len(s) - csl = len(cs) - if r == 0: - return 1 if csl == 0 else 0 - o, *f = s - f = "".join(f) - if o == ".": - return _inspect(f, cs) - if o == "?": - return _inspect("." + f, cs) + _inspect("#" + f, cs) - if not csl: - return 0 - g = cs[0] - if g > r or "." in s[0:g]: - return 0 - elif csl > 1: - if g + 1 > r or s[g] == "#": - return 0 - else: - return _inspect(s[g + 1 :], cs[1:]) - elif csl == 1: - return _inspect(s[g:], ()) - - -if __name__ == "__main__": - with open("./input/12.txt", "r") as f: - inp = f.read().strip() - - inp = presolve(inp) - - a = part_1(inp) - b = part_2(inp) - - assert a == 7118 - assert b == 7030194981795 diff --git a/2023-python/output/day_13.py b/2023-python/output/day_13.py deleted file mode 100644 index fa6f361..0000000 --- a/2023-python/output/day_13.py +++ /dev/null @@ -1,56 +0,0 @@ -from output import answer - -n = 13 -title = "Point of Incidence" - - -@answer(1, "Summarizing the notes gives {}") -def part_1(presolved): - return presolved[0] - - -@answer(2, "Summarizing the notes allowing off-by-1 gives {}") -def part_2(presolved): - return presolved[1] - - -def presolve(data): - g = [l.split() for l in data.split("\n\n")] - - p1 = sum(d * n for d, n in _inspect(g)) - p2 = sum(d * n for d, n in _inspect(g, 1)) - - return p1, p2 - - -def _inspect(g, a=0): - af = [] - for m in g: - for d, n in [(100, m), (1, tuple(zip(*m)))]: - af.append((d, _compare(n, a))) - return af - - -def _compare(l, a=0): - for i in range(1, len(l)): - if ( - sum( - sum(a != b for a, b in zip(x, y)) for x, y in zip(l[i - 1 :: -1], l[i:]) - ) - == a - ): - return i - return 0 - - -if __name__ == "__main__": - with open("./input/13.txt", "r") as f: - inp = f.read().strip() - - inp = presolve(inp) - - a = part_1(inp) - b = part_2(inp) - - assert a == 29213 - assert b == 37453 diff --git a/2023-python/output/day_14.py b/2023-python/output/day_14.py deleted file mode 100644 index 48d3ea2..0000000 --- a/2023-python/output/day_14.py +++ /dev/null @@ -1,83 +0,0 @@ -from output import answer - -n = 14 -title = "Parabolic Reflector Dish" - - -@answer(1, "Total initial load on the northern beams: {}") -def part_1(presolved): - return presolved[0] - - -@answer(2, "After some humble load testing, the northern beam load is {}") -def part_2(presolved): - return presolved[1] - - -BAEST = 1000_000_000 - - -def presolve(data): - m = [list(l) for l in data.split()] - s = len(m[0]) - m1 = _tilt(m) - - p1 = sum(sum((s - w) * o.count("O") for o in r) for w, r in enumerate(m1)) - - def impl(rc): - return "".join(["".join(r) for r in rc]) - - i = 0 - seen = [] - while True: - i += 1 - for _ in range(4): - m = _tilt(m) - m = _rotate(m) - im = impl(m) - if im in seen: - break - else: - seen.append(im) - m2 = m - c = seen.index(im) + 1 - for _ in range((BAEST - i) % (i - c)): - for j in range(4): - m2 = _tilt(m2) - m2 = _rotate(m2) - p2 = sum(sum((s - w) * o.count("O") for o in r) for w, r in enumerate(m2)) - return p1, p2 - - -def _rotate(m): - return [list(l) for l in zip(*m[::-1])] - - -def _tilt(m): - m = [list(l) for l in zip(*m)] - h = len(m[0]) - for c in m: - u = True - while u: - u = False - for i in range(h - 1): - j = i + 1 - if c[i] == "#" or c[j] == "#": - continue - if c[i] < c[j]: - c[j], c[i] = c[i], c[j] - u = True - return [list(l) for l in zip(*m)] - - -if __name__ == "__main__": - with open("./input/14.txt", "r") as f: - inp = f.read().strip() - - inp = presolve(inp) - - a = part_1(inp) - b = part_2(inp) - - assert a == 109596 - assert b == 96105 diff --git a/2023-python/output/day_15.py b/2023-python/output/day_15.py deleted file mode 100644 index 31a3e98..0000000 --- a/2023-python/output/day_15.py +++ /dev/null @@ -1,63 +0,0 @@ -from collections import OrderedDict, defaultdict - -from output import answer - -n = 15 -title = "Lens Library" - - -@answer(1, "Sum of HASH algorithm results: {}") -def part_1(presolved): - return presolved[0] - - -@answer(2, "Focusing power of the resulting configuration: {}") -def part_2(presolved): - return presolved[1] - - -def presolve(data): - def h(s): - v = 0 - for a in s: - if a == "\n": - continue - v += ord(a) - v *= 17 - v = v % 256 - return v - - p1 = sum(h(c) for c in data.split(",")) - - b = defaultdict(OrderedDict) - for lr in data.split(","): - if "=" in lr: - l, r = lr.split("=") - if r == "": - continue - k = h(l) - b[k][l] = r - if "-" in lr: - l, _r = lr.split("-") - k = h(l) - if l in b[k]: - del b[k][l] - p2 = 0 - for i, c in b.items(): - for j, f in enumerate(b[i].values(), 1): - p2 += (i + 1) * j * int(f) - - return p1, p2 - - -if __name__ == "__main__": - with open("./input/16.txt", "r") as f: - inp = f.read().strip() - - inp = presolve(inp) - - a = part_1(inp) - b = part_2(inp) - - assert a == 509784 - assert b == 230197 diff --git a/2023-python/output/day_16.py b/2023-python/output/day_16.py deleted file mode 100644 index bab2c68..0000000 --- a/2023-python/output/day_16.py +++ /dev/null @@ -1,86 +0,0 @@ -from itertools import chain - -from output import D, answer, matrix - -n = 16 -title = "The Floor Will Be Lava" - - -@answer(1, "Energized tiles count, starting at top-left facing right: {}") -def part_1(presolved): - return presolved[0] - - -@answer(2, "Max energized tiles count, starting from all edges: {}") -def part_2(presolved): - return presolved[1] - - -def presolve(data): - m, w, h = matrix(data) - p1 = 0 - p2 = 0 - for sp in chain( - [(h - 1, n, 0) for n in range(w)], - [(n, 0, 1) for n in range(h)], - [(0, n, 2) for n in range(w)], - [(n, w - 1, 3) for n in range(h)], - ): - q = [sp] - seen = set() - while q: - rcd = q.pop(0) - if (rcd) in seen: - continue - r, c, d = rcd - if r < 0 or r >= h or c < 0 or c >= w: - continue - seen.add((r, c, d)) - match m[r][c]: - case ".": - o1, o2 = D[d] - q.append((o1 + r, o2 + c, d)) - case "|": - if d in [0, 2]: - o1, o2 = D[d] - q.append((o1 + r, o2 + c, d)) - else: - for d in [(d - 1) % 4, (d + 1) % 4]: - o1, o2 = D[d] - q.append((o1 + r, o2 + c, d)) - case "-": - if d in [1, 3]: - o1, o2 = D[d] - q.append((o1 + r, o2 + c, d)) - else: - for d in [(d - 1) % 4, (d + 1) % 4]: - o1, o2 = D[d] - q.append((o1 + r, o2 + c, d)) - case "\\": - d += 1 if d in [1, 3] else -1 - d = d % 4 - o1, o2 = D[d] - q.append((o1 + r, o2 + c, d)) - case "/": - d += 1 if d in [0, 2] else -1 - d = d % 4 - o1, o2 = D[d % 4] - q.append((o1 + r, o2 + c, d)) - b = len(set([(r, c) for r, c, d in seen])) - if sp == (0, 0, 1): - p1 = b - p2 = max(p2, b) - return p1, p2 - - -if __name__ == "__main__": - with open("./input/16.txt", "r") as f: - inp = f.read().strip() - - inp = presolve(inp) - - a = part_1(inp) - b = part_2(inp) - - assert a == 7884 - assert b == 8185 -- 2.45.3 From 661f18dca46d3a2596138047f35b3580e09f3b9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Ytterstr=C3=B6m?= Date: Tue, 21 Nov 2023 00:17:36 +0100 Subject: [PATCH 02/22] Prep Advent of Code 2023 --- 2023-python/.gitignore | 2 + 2023-python/README.md | 38 ++++++++++ 2023-python/aoc.py | 127 +++++++++++++++++++++++++++++++++ 2023-python/output/__init__.py | 82 +++++++++++++++++++++ 4 files changed, 249 insertions(+) create mode 100644 2023-python/.gitignore create mode 100644 2023-python/README.md create mode 100644 2023-python/aoc.py create mode 100644 2023-python/output/__init__.py diff --git a/2023-python/.gitignore b/2023-python/.gitignore new file mode 100644 index 0000000..fa878cc --- /dev/null +++ b/2023-python/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +input \ No newline at end of file diff --git a/2023-python/README.md b/2023-python/README.md new file mode 100644 index 0000000..2edcd21 --- /dev/null +++ b/2023-python/README.md @@ -0,0 +1,38 @@ +Advent of Code 2023 +=================== + +Solutions for #aoc2023 in Python 3 (3.11.5). + +Help scripts +------------ + +Display all solved puzzles: + + python aoc.py + +To bootstrap a new puzzle (creates `input/.txt` and `output/day_.py`): + + python aoc.py + +Manually copy the puzzle input from https://adventofcode.com and paste it in `input/.txt` +to start coding. + + wl-paste > input/.txt + +Solve separate puzzle (replace `XX` with the puzzle number): + + python -m output.day_XX + +Solve separate puzzle using stdin (replace `XX` with the puzzle number): + + wl-paste | python -m output.day_XX + cat tmpfile | python -m output.day_XX + +Execute separate puzzle on file save (replace `XX` with the puzzle number): + + ls output/*.py | entr -c -s 'xclip -selection clipboard -o | python -m output.day_XX' + ls output/*.py | entr -c -s 'cat tmpfile | python -m output.day_XX' + ls output/*.py | entr -c -r python -m output.day_XX + +(requires `entr` and `wl-paste`, Mac users can instead use `pbpaste`. If you +prefer X at Linux, use `xclip -selection clipboard -o`). diff --git a/2023-python/aoc.py b/2023-python/aoc.py new file mode 100644 index 0000000..ae49a50 --- /dev/null +++ b/2023-python/aoc.py @@ -0,0 +1,127 @@ +import os +import sys + + +def headline(n, title): + """Print day number and name, followed by a ruler. Used by the answer decorator""" + print(f"\n--- Day {n}: {title} ---\n") + + +year = 2023 + +try: + _, day_no, *name = sys.argv +except ValueError: + day_no = None + name = None + +print( + f"\nAdvent of Code {year}" "\n###################" "\n\nby Anders Englöf Ytterström" +) + +if day_no and name: + name = " ".join(name) + padded_no = day_no.zfill(2) + print(f"\n- creating output/day_{padded_no}.py") + with open("output/day_{}.py".format(padded_no), "w") as s: + s.write( + f""" +from output import answer # , matrix, D, DD, ADJ, ints, mhd, mdbg, vdbg + +n = {day_no} +title = "{name}" + + +@answer(1, "Answer is {{}}") +def part_1(presolved): + return presolved[0] + + +@answer(2, "Actually, answer is {{}}") +def part_2(presolved): + return presolved[1] + + +def solve(data): + return 1, 2 + + +if __name__ == "__main__": + # use dummy data + inp = \"\"\" + replace me + \"\"\".strip() + + # uncomment to instead use stdin + # import sys; inp = sys.stdin.read().strip() + + # uncomment to use AoC provided puzzle input + # with open(f"./input/{padded_no}.txt", "r") as f: + # inp = f.read() + + # uncomment to do initial data processing shared by part 1-2 + inp = solve(inp) + + a = part_1(inp) + # b = part_2(inp) + + # uncomment and replace 0 with actual output to refactor code + # and ensure nonbreaking changes + # assert a == 0 + # assert b == 0 +""".strip() + + "\n" + ) + print("- making sure input dir exists") + if not os.path.exists("input"): + os.makedirs("input") + + print( + f""" +Done! start coding. + +Puzzle link: +https://adventofcode.com/{year}/day/{day_no} + +Puzzle input (copy and paste to input/{day_no.zfill(2)}.txt): +https://adventofcode.com/{year}/day/{day_no}/input + """ + ) + exit(0) + + +stars = 0 +for i in [str(n).zfill(2) for n in range(1, 26)]: + if not day_no or day_no.zfill(2) == i: + try: + day = __import__( + "output.day_{}".format(i), + globals(), + locals(), + ["n", "title", "part_1", "part_2"], + 0, + ) + filepath = f"./input/{str(n).zfill(2)}.txt" + with open(filepath, "r") as f: + data = f.read() + headline(day.n, day.title) + try: + data = day.presolve(data) + except AttributeError: + pass + try: + data = day.solve(data) + except AttributeError: + pass + if day.part_1(data, decorate=True): + stars += 1 + if day.part_2(data, decorate=True): + stars += 1 + except IOError: + pass + except ImportError: + pass +if not day_no: + print(f"\nStars: {stars}") + print("".join("*" if n < stars else "•" for n in range(50))) +print("") diff --git a/2023-python/output/__init__.py b/2023-python/output/__init__.py new file mode 100644 index 0000000..3a034d6 --- /dev/null +++ b/2023-python/output/__init__.py @@ -0,0 +1,82 @@ +import functools +import re + +# Directions/Adjacents for 2D matrices, in the order UP, RIGHT, DOWN, LEFT +D = [ + (-1, 0), + (0, 1), + (1, 0), + (0, -1), +] + +# Directions for 2D matrices, as a dict with keys U, R, D, L +DD = { + "U": (-1, 0), + "R": (0, 1), + "D": (1, 0), + "L": (0, -1), +} + +# Adjacent relative positions including diagonals for 2D matrices, in the order NW, N, NE, W, E, SW, S, SE +ADJ = [ + (-1, -1), + (-1, 0), + (1, -1), + (0, -1), + (0, 1), + (1, 1), + (1, 0), + (1, -1), +] + + +def answer(part_index, fmt_string): + """Decorator to present a solution in a human readable format""" + + def decorator_aoc(func): + @functools.wraps(func) + def wrapper_aoc(*args, **kwargs): + decorate = kwargs.get("decorate", False) + if decorate: + del kwargs["decorate"] + answer = func(*args, **kwargs) + if not decorate: + print(answer) + else: + formatted = fmt_string.format(answer) + print(f" {part_index}) {formatted}") + return answer + + return wrapper_aoc + + return decorator_aoc + + +def ints(s): + """Extract all integers from a string""" + return [int(n) for n in re.findall(r"\d+", s)] + + +def mhd(a, b): + """Calculates the Manhattan distance between 2 positions in the format (y, x) or (x, y)""" + ar, ac = a + br, bc = b + return abs(ar - br) + abs(ac - bc) + + +def matrix(d): + """Transform a string into an iterable matrix. Returns the matrix, row count and col count""" + m = [tuple(r) for r in d.split()] + return m, len(m), len(m[0]) + + +def mdbg(m): + """Print-debug a matrix""" + for r in m: + print("".join(r)) + + +def vdbg(seen, h, w): + """Print-debug visited positions of a matrix""" + for r in range(h): + print("".join(["#" if (r, c) in seen else "." for c in range(w)])) -- 2.45.3 From aae14797ea0ae0e8e252aa9a6af773283111c6c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Engl=C3=B6f=20Ytterstr=C3=B6m?= Date: Fri, 1 Dec 2023 07:09:50 +0100 Subject: [PATCH 03/22] Solve 2023:01 "Trebuchet?!" Turns out re methods are non-overlapping. And in true AoC manners, no provided test cases had overlaps. Luckily for me, some of the last lines in the input contained the string "oneight", so I was able to find it out quite fast. Revisions: 1) Reverse strings to find last digit 2) Use isdigit() and skip regex. 3) Use regexp with positive look-ahead. --- 2023-python/output/day_01.py | 55 ++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 2023-python/output/day_01.py diff --git a/2023-python/output/day_01.py b/2023-python/output/day_01.py new file mode 100644 index 0000000..2f27529 --- /dev/null +++ b/2023-python/output/day_01.py @@ -0,0 +1,55 @@ +import re +from output import answer, puzzleinput + +n = 1 +title = "Trebuchet?!" + + +@answer(1, "Calibration values sum: {}, excluding spelled out digits") +def part_1(data): + def value(s): + s = [int(c) for c in s if c.isdigit()] + return s[0] * 10 + s[-1] + + return sum(value(line) for line in data) + + +@answer(2, "Calibration values sum: {}, including spelled out digits") +def part_2(data): + mp = { + "one": 1, + "two": 2, + "three": 3, + "four": 4, + "five": 5, + "six": 6, + "seven": 7, + "eight": 8, + "nine": 9, + } + + def value(l): + s = [ + int(c) if c.isdigit() else mp[c] + for c in re.findall( + r"(?=(\d|one|two|three|four|five|six|seven|eight|nine))", l + ) + ] + return s[0] * 10 + s[-1] + + return sum(value(line) for line in data) + + +@puzzleinput(n) +def parse_input(data): + return data.split() + + +if __name__ == "__main__": + parsed = parse_input() + + a = part_1(parsed) + b = part_2(parsed) + + assert a == 54634 + assert b == 53855 -- 2.45.3 From 6001a6bb59d9160bfb560ce328a79c6913022f77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Engl=C3=B6f=20Ytterstr=C3=B6m?= Date: Sat, 2 Dec 2023 06:46:24 +0100 Subject: [PATCH 04/22] Solve 2023:02 "Cube Conundrum" Very intermediate pythonic solution, regex would have made the code more compact. But since 2023:01 decreased the regex courage, This code will do. --- 2023-python/output/day_02.py | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 2023-python/output/day_02.py diff --git a/2023-python/output/day_02.py b/2023-python/output/day_02.py new file mode 100644 index 0000000..3f4b4ec --- /dev/null +++ b/2023-python/output/day_02.py @@ -0,0 +1,53 @@ +import re +from math import prod +from output import answer, puzzleinput +from collections import defaultdict + +n = 2 +title = "Cube Conundrum" + + +@answer(1, "Sum of all possible game IDs: {}") +def part_1(data): + return sum( + [ + id + 1 + for id, game in enumerate(data.splitlines()) + if not sum( + any( + [ + c == "red" and int(n) > 12, + c == "green" and int(n) > 13, + c == "blue" and int(n) > 14, + ] + ) + for n, c in re.findall(r"(\d+) (\w+)", game) + ) + ] + ) + + +@answer(2, "Sum of all cube set powers: {}") +def part_2(data): + def power(game): + seen = defaultdict(int) + for n, c in re.findall(r"(\d+) (\w+)", game): + seen[c] = max([seen[c], int(n)]) + return prod([seen["blue"], seen["red"], seen["green"]]) + + return sum(power(game) for game in data.splitlines()) + + +@puzzleinput(n) +def parse_input(data): + return data + + +if __name__ == "__main__": + parsed = parse_input() + + a = part_1(parsed) + b = part_2(parsed) + + assert a == 2439 + assert b == 63711 -- 2.45.3 From 7290f28ef760052092e3c6b3547ab20d698ffadf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Engl=C3=B6f=20Ytterstr=C3=B6m?= Date: Sun, 3 Dec 2023 08:44:35 +0100 Subject: [PATCH 05/22] Solve 2023:03 "Gear Ratios" Overslept, took about 55 mins. --- 2023-python/output/day_03.py | 68 ++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 2023-python/output/day_03.py diff --git a/2023-python/output/day_03.py b/2023-python/output/day_03.py new file mode 100644 index 0000000..ee8c7f2 --- /dev/null +++ b/2023-python/output/day_03.py @@ -0,0 +1,68 @@ +from collections import deque +from output import answer, puzzleinput + +n = 3 +title = "Gear Ratios" + + +@answer(1, "Sum of all part numbers is {} in the engine schematic") +def part_1(presolved): + s, _ = presolved + return s + + +@answer(2, "Gear ratio sums is {} in the engine schematic") +def part_2(presolved): + _, gr = presolved + return gr + + +def presolve(data): + adj = (-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1) + w = len(data[0]) + h = len(data) + s = list() + gr = list() + for y in range(w): + for x in range(h): + if data[y][x] != "." and not data[y][x].isdigit(): + seen = set() + t = list() + for oy, ox in adj: + if (y + oy, x + ox) in seen: + continue + if data[y + oy][x + ox].isdigit(): + n = deque([data[y + oy][x + ox]]) + i = x + ox - 1 + while i in range(w) and data[y + oy][i].isdigit(): + n.append(data[y + oy][i]) + seen.add((y + oy, i)) + i -= 1 + i = x + ox + 1 + while i in range(w) and data[y + oy][i].isdigit(): + n.appendleft(data[y + oy][i]) + seen.add((y + oy, i)) + i += 1 + t.append(sum(10**m * int(d) for m, d in enumerate(n))) + # part 1 + s.append(sum(t)) + # part 2 + if data[y][x] == "*" and len(t) == 2: + a, b = t + gr.append(a * b) + + return sum(s), sum(gr) + + +@puzzleinput(n) +def parse_input(data): + return data.split() + + +if __name__ == "__main__": + parsed = presolve(parse_input()) + a = part_1(parsed) + b = part_2(parsed) + + assert a == 553825 + assert b == 93994191 -- 2.45.3 From cbd4bf50e184e92a4374d82f027e3521850eb282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Engl=C3=B6f=20Ytterstr=C3=B6m?= Date: Mon, 4 Dec 2023 07:27:47 +0100 Subject: [PATCH 06/22] Solve 2023:04 "Scratchcards" On a train that according to swedish tradition was late. Not a good environment to focus. Got stuck 2 times: - Initial code asumed the | was always after the 5th number, because of the example. Puzzle input had it at pos 10. Classic AoC mistake. - I had a hard time trying to understand the score count, I insisted there was meant to be a +1 at some point. > That means card 1 is worth 8 points (1 for > the first match, then doubled three times for > each of the three matches after the first) I should instead have just looked at the numbers. --- 2023-python/output/day_04.py | 50 ++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 2023-python/output/day_04.py diff --git a/2023-python/output/day_04.py b/2023-python/output/day_04.py new file mode 100644 index 0000000..40afde8 --- /dev/null +++ b/2023-python/output/day_04.py @@ -0,0 +1,50 @@ +import re +from collections import defaultdict +from output import answer, puzzleinput + +n = 4 +title = "Scratchcards" + + +@answer(1, "Sum of all scratchcard points are {}") +def part_1(presolved): + scores, _ = presolved + return scores + + +@answer(2, "Ends up wih a total of {} scratchcards") +def part_2(presolved): + _, count = presolved + return count + + +@puzzleinput(n) +def parse_input(data): + return data + + +def presolve(data): + scores = [] + count = defaultdict(int) + for cid, line in enumerate(data.splitlines()): + a, b = line.split("|") + a = set(re.findall(r"\d+", a)[1:]) + b = set(re.findall(r"\d+", b)) + ab = len(a & b) + if ab > 0: + scores.append(2**(ab - 1)) + count[cid] += 1 + for i in range(cid + 1, cid + ab + 1): + count[i] += count[cid] + return sum(scores), sum(count.values()) + + +if __name__ == "__main__": + inp = parse_input() + inp = presolve(inp) + + a = part_1(inp) + b = part_2(inp) + + assert a == 21919 + assert b == 9881048 -- 2.45.3 From ad05c8fab4dcf8585b1df594db79a7b3c90e0b6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Engl=C3=B6f=20Ytterstr=C3=B6m?= Date: Tue, 5 Dec 2023 23:10:51 +0100 Subject: [PATCH 07/22] Solve 2023:05 "If You Give A Seed A Fertilizer" Part 2 takes 66 minutes to run. There is some smart things to realize here. --- 2023-python/output/day_05.py | 103 +++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 2023-python/output/day_05.py diff --git a/2023-python/output/day_05.py b/2023-python/output/day_05.py new file mode 100644 index 0000000..1a8c1e7 --- /dev/null +++ b/2023-python/output/day_05.py @@ -0,0 +1,103 @@ +import re +from itertools import repeat +from math import inf +from multiprocessing import Pool, freeze_support +from output import answer, puzzleinput + +n = 5 +title = "If You Give A Seed A Fertilizer" + + +@answer(1, "Nearest location for seed id list is {}") +def part_1(data): + seeds, *process = data.split("\n\n") + seeds = [f"{v} 1" for v in seeds.split()[1:]] + return _bruteforce(seeds, process, 1) + + +@answer(2, "Interpreting ranges of seeds, nearest location is {}") +def part_2(data): + seeds, *process = data.split("\n\n") + seeds = re.findall(r"\d+ \d+", seeds) + return _bruteforce(seeds, process, 8) + + +@puzzleinput(n) +def parse_input(data): + return data + + +def _bruteforce(seeds, process, p=1): + processes = [[tuple(map(int, line.split())) for line in step.splitlines()[1:]] for step in process] + + sm = [] + for start_r in seeds: + pool = Pool() + start, r = start_r.split() + d = int(r) // p + parts = [(d * n + int(start), d * n + int(start) + d) for n in range(p)] + sm += pool.starmap(_nearest, zip(parts, repeat(processes))) + return min(sm) + +def _nearest(start_r, processes): + a, b = start_r + nearest = inf + for i in range(a, b): + v = i + for steps in processes: + nid = -1 + for line in steps: + dest, src, r = line + if v >= src and v < src + r: + v = dest + v - src + break + nearest = min(nearest, v) + + return nearest + + +if __name__ == "__main__": + # use dummy data + inp = """ +seeds: 79 14 55 13 + +seed-to-soil map: +50 98 2 +52 50 48 + +soil-to-fertilizer map: +0 15 37 +37 52 2 +39 0 15 + +fertilizer-to-water map: +49 53 8 +0 11 42 +42 0 7 +57 7 4 + +water-to-light map: +88 18 7 +18 25 70 + +light-to-temperature map: +45 77 23 +81 45 19 +68 64 13 + +temperature-to-humidity map: +0 69 1 +1 0 69 + +humidity-to-location map: +60 56 37 +56 93 4 + """.strip() + + # inp = parse_input() + + a = part_1(inp) + b = part_2(inp) + + # assert a == 278755257 + # assert b == 26829166 -- 2.45.3 From a1a69d7114a6ba02c29a4670f96106ade1f8447c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Engl=C3=B6f=20Ytterstr=C3=B6m?= Date: Wed, 6 Dec 2023 07:08:59 +0100 Subject: [PATCH 08/22] Solve 2023:06 "Wait for it" --- 2023-python/output/day_06.py | 51 ++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 2023-python/output/day_06.py diff --git a/2023-python/output/day_06.py b/2023-python/output/day_06.py new file mode 100644 index 0000000..2f604cf --- /dev/null +++ b/2023-python/output/day_06.py @@ -0,0 +1,51 @@ +from math import prod, sqrt, ceil, floor +from output import answer, puzzleinput + +n = 6 +title = "Wait For It" + + +@answer(1, "The product of all record times for all races is {}") +def part_1(presolved): + return presolved[0] + + +@answer(2, "The product of all record times for the single long race is {}") +def part_2(presolved): + return presolved[1] + + +@puzzleinput(n) +def parse_input(data): + return data + + +def presolve(data): + values = data.split() + l = len(values) // 2 + races = list( + map( + lambda x: (int(x[0]), int(x[1])), list(zip(values[: l + 1], values[l:]))[1:] + ) + ) + p1 = prod(sum(bpt * (t - bpt) > d for bpt in range(t)) for t, d in races) + t = int("".join(values[1:l])) + d = int("".join(values[l + 1 :])) + # quadratic formula: + # https://en.wikipedia.org/wiki/Quadratic_formula + l = ceil((-t + sqrt(t**2 - 4 * d)) / -2) + h = floor((-t - sqrt(t**2 - 4 * d)) / -2) + p2 = h - l + 1 + return p1, p2 + + +if __name__ == "__main__": + inp = parse_input() + + inp = presolve(inp) + + a = part_1(inp) + b = part_2(inp) + + assert a == 1083852 + assert b == 23501589 -- 2.45.3 From fa3416cf401d1c0777606ee2ecd4cc11ad3994f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Engl=C3=B6f=20Ytterstr=C3=B6m?= Date: Thu, 7 Dec 2023 10:24:12 +0100 Subject: [PATCH 09/22] Solve 2023:07 "Camel Cards" --- 2023-python/output/day_07.py | 98 ++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 2023-python/output/day_07.py diff --git a/2023-python/output/day_07.py b/2023-python/output/day_07.py new file mode 100644 index 0000000..2cf31a4 --- /dev/null +++ b/2023-python/output/day_07.py @@ -0,0 +1,98 @@ +from collections import Counter +from output import answer, puzzleinput + +n = 7 +title = "Camel Cards" + + +@answer(1, "Total winnings are {}") +def part_1(data): + return _solve(data) + + +@answer(2, "Including jokers, total winnings are {}") +def part_2(data): + return _solve(data.replace("J", "0")) + + +@puzzleinput(n) +def parse_input(data): + return data + + +def _solve(data): + return sum( + c[-1] * i for i, c in enumerate(sorted(_hand(h) for h in data.splitlines()), 1) + ) + + +def _hand(hb): + h, b = hb.split() + b = int(b) + h = h.translate(M) + return (_rank(h), h, b) + + +def _rank(h): + """ + Rank hand 0-6, letting jokers (0) aid the highest possible hand + + >>> _rank("10110") + 6 + >>> _rank("11110") + 6 + >>> _rank("12110") + 5 + >>> _rank("12100") + 5 + >>> _rank("12000") + 5 + >>> _rank("12120") + 4 + >>> _rank("12300") + 3 + >>> _rank("12310") + 3 + >>> _rank("12340") + 1 + """ + hc = Counter(h) + hcv = hc.values() + j = hc["0"] + del hc["0"] + p = sum(c == 2 for v, c in hc.items() if v != 0) + if j == 5 or max(hcv) + j == 5: + return 6 + if max(hcv) + j == 4: + return 5 + if (3 in hcv and 2 in hcv) or (p == 2 and j > 0): + return 4 + if max(hcv) + j == 3: + return 3 + if p + j > 1: + return 2 + if p + j > 0: + return 1 + return 0 + + +M = dict( + [ + (ord(o), ord(n)) + for o, n in {"T": "A", "J": "B", "Q": "C", "K": "D", "A": "E"}.items() + ] +) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + inp = parse_input() + + a = part_1(inp) + b = part_2(inp) + + assert a == 249390788 + assert b == 248750248 -- 2.45.3 From 540fa253da3ba5181bcb27661bf610c13051e5ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Engl=C3=B6f=20Ytterstr=C3=B6m?= Date: Fri, 8 Dec 2023 09:11:05 +0100 Subject: [PATCH 10/22] Solve 2023:08 "Haunted Wasteland" Part 2 would have taken 10-15 hours with brute force. After I figured out the puzzle input had circular A-Z paths, it was plain as day that LCM was the solution to the problem. https://en.wikipedia.org/wiki/Least_common_multiple --- 2023-python/output/day_08.py | 65 ++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 2023-python/output/day_08.py diff --git a/2023-python/output/day_08.py b/2023-python/output/day_08.py new file mode 100644 index 0000000..f82cbef --- /dev/null +++ b/2023-python/output/day_08.py @@ -0,0 +1,65 @@ +import re +from math import lcm +from output import answer, puzzleinput + +n = 8 +title = "Haunted Wasteland" + + +@answer(1, "One can reach Z in {} steps") +def part_1(presolved): + steps, _ = presolved + return steps + + +@answer(2, "Ghost path takes {} steps before all ghosts are at a Z positon") +def part_2(presolved): + _, ghost_meet_point = presolved + return ghost_meet_point + + +@puzzleinput(n) +def parse_input(data): + return data + + +def presolve(data): + d, els = data.split("\n\n") + r = len(d) + e = dict() + for el in els.splitlines(): + k, *lr = re.match(r"(\w+) = \((\w+), (\w+)\)", el).groups() + e[k] = lr + + p1 = 0 + pos = "AAA" + while pos != "ZZZ": + i = 0 if d[p1 % r] == "L" else 1 + pos = e[pos][i] + p1 += 1 + + p2 = 0 + z = list() + for spos in [p for p in e.keys() if p.endswith("A")]: + s = 0 + pos = spos + while not pos.endswith("Z"): + i = 0 if d[s % r] == "L" else 1 + pos = e[pos][i] + s += 1 + z.append(s) + p2 = lcm(*z) + + return p1, p2 + + +if __name__ == "__main__": + inp = parse_input() + + inp = presolve(inp) + + a = part_1(inp) + b = part_2(inp) + + assert a == 17141 + assert b == 10818234074807 -- 2.45.3 From 96c4c1383a41d37c0865570913f1e8d99ff697d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Engl=C3=B6f=20Ytterstr=C3=B6m?= Date: Sat, 9 Dec 2023 16:34:08 +0100 Subject: [PATCH 11/22] Solve 2023:09 "Mirage Maintenance" --- 2023-python/output/day_09.py | 39 ++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 2023-python/output/day_09.py diff --git a/2023-python/output/day_09.py b/2023-python/output/day_09.py new file mode 100644 index 0000000..0cd5ad5 --- /dev/null +++ b/2023-python/output/day_09.py @@ -0,0 +1,39 @@ +from output import answer, puzzleinput + +n = 9 +title = "Mirage Maintenance" + + +@answer(1, "OASIS report extrapolated values sum is {}") +def part_1(data): + lines = [[int(d) for d in line.split()] for line in data.splitlines()] + return _solve(lines) + + +@answer(2, "Using prepending, OASIS report extrapolated values sum is {}") +def part_2(data): + lines = [[int(d) for d in line.split()[::-1]] for line in data.splitlines()] + return _solve(lines) + + +def _solve(lines): + vs = 0 + for l in lines: + h = [l] + while any(n != 0 for n in h[-1]): + h.append([b - a for a, b in zip(h[-1], h[-1][1:])]) + h = h[::-1] + for i in range(len(h)): + h[i].append(0 if i == 0 else h[i - 1][-1] + h[i][-1]) + vs += h[-1][-1] + return vs + + +if __name__ == "__main__": + inp = parse_input() + + a = part_1(inp) + b = part_2(inp) + + assert a == 1702218515 + assert b == 925 -- 2.45.3 From 08288964bdcbd47d11343eb0bac69d3f640f8c3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Engl=C3=B6f=20Ytterstr=C3=B6m?= Date: Sat, 9 Dec 2023 17:40:50 +0100 Subject: [PATCH 12/22] Remove parse_input helper --- 2023-python/aoc.py | 5 ++-- 2023-python/output/day_01.py | 18 ++++++--------- 2023-python/output/day_02.py | 14 ++++-------- 2023-python/output/day_03.py | 14 ++++++------ 2023-python/output/day_04.py | 11 ++++----- 2023-python/output/day_05.py | 8 ++----- 2023-python/output/day_06.py | 10 +++----- 2023-python/output/day_07.py | 10 +++----- 2023-python/output/day_08.py | 10 +++----- 2023-python/output/day_09.py | 5 ++-- 2023-python/output/day_10.py | 44 ++++++++++++++++++++++++++++++++++++ 11 files changed, 83 insertions(+), 66 deletions(-) create mode 100644 2023-python/output/day_10.py diff --git a/2023-python/aoc.py b/2023-python/aoc.py index ae49a50..2f10cf8 100644 --- a/2023-python/aoc.py +++ b/2023-python/aoc.py @@ -101,9 +101,8 @@ for i in [str(n).zfill(2) for n in range(1, 26)]: ["n", "title", "part_1", "part_2"], 0, ) - filepath = f"./input/{str(n).zfill(2)}.txt" - with open(filepath, "r") as f: - data = f.read() + with open(f"./input/{i}.txt", "r") as f: + data = f.read().strip() headline(day.n, day.title) try: data = day.presolve(data) diff --git a/2023-python/output/day_01.py b/2023-python/output/day_01.py index 2f27529..f1f6483 100644 --- a/2023-python/output/day_01.py +++ b/2023-python/output/day_01.py @@ -1,5 +1,5 @@ import re -from output import answer, puzzleinput +from output import answer n = 1 title = "Trebuchet?!" @@ -11,7 +11,7 @@ def part_1(data): s = [int(c) for c in s if c.isdigit()] return s[0] * 10 + s[-1] - return sum(value(line) for line in data) + return sum(value(line) for line in data.split()) @answer(2, "Calibration values sum: {}, including spelled out digits") @@ -37,19 +37,15 @@ def part_2(data): ] return s[0] * 10 + s[-1] - return sum(value(line) for line in data) - - -@puzzleinput(n) -def parse_input(data): - return data.split() + return sum(value(line) for line in data.split()) if __name__ == "__main__": - parsed = parse_input() + with open(f"./input/01.txt", "r") as f: + inp = f.read().strip() - a = part_1(parsed) - b = part_2(parsed) + a = part_1(inp) + b = part_2(inp) assert a == 54634 assert b == 53855 diff --git a/2023-python/output/day_02.py b/2023-python/output/day_02.py index 3f4b4ec..d198b2e 100644 --- a/2023-python/output/day_02.py +++ b/2023-python/output/day_02.py @@ -1,7 +1,7 @@ import re from math import prod -from output import answer, puzzleinput from collections import defaultdict +from output import answer n = 2 title = "Cube Conundrum" @@ -38,16 +38,12 @@ def part_2(data): return sum(power(game) for game in data.splitlines()) -@puzzleinput(n) -def parse_input(data): - return data - - if __name__ == "__main__": - parsed = parse_input() + with open(f"./input/02.txt", "r") as f: + inp = f.read().strip() - a = part_1(parsed) - b = part_2(parsed) + a = part_1(inp) + b = part_2(inp) assert a == 2439 assert b == 63711 diff --git a/2023-python/output/day_03.py b/2023-python/output/day_03.py index ee8c7f2..b04c037 100644 --- a/2023-python/output/day_03.py +++ b/2023-python/output/day_03.py @@ -1,5 +1,5 @@ from collections import deque -from output import answer, puzzleinput +from output import answer n = 3 title = "Gear Ratios" @@ -18,6 +18,7 @@ def part_2(presolved): def presolve(data): + data = data.split() adj = (-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1) w = len(data[0]) h = len(data) @@ -54,13 +55,12 @@ def presolve(data): return sum(s), sum(gr) -@puzzleinput(n) -def parse_input(data): - return data.split() - - if __name__ == "__main__": - parsed = presolve(parse_input()) + with open(f"./input/03.txt", "r") as f: + inp = f.read().strip() + + parsed = presolve(inp) + a = part_1(parsed) b = part_2(parsed) diff --git a/2023-python/output/day_04.py b/2023-python/output/day_04.py index 40afde8..2fe7400 100644 --- a/2023-python/output/day_04.py +++ b/2023-python/output/day_04.py @@ -1,6 +1,6 @@ import re from collections import defaultdict -from output import answer, puzzleinput +from output import answer n = 4 title = "Scratchcards" @@ -18,11 +18,6 @@ def part_2(presolved): return count -@puzzleinput(n) -def parse_input(data): - return data - - def presolve(data): scores = [] count = defaultdict(int) @@ -40,7 +35,9 @@ def presolve(data): if __name__ == "__main__": - inp = parse_input() + with open(f"./input/04.txt", "r") as f: + inp = f.read().strip() + inp = presolve(inp) a = part_1(inp) diff --git a/2023-python/output/day_05.py b/2023-python/output/day_05.py index 1a8c1e7..87a54ce 100644 --- a/2023-python/output/day_05.py +++ b/2023-python/output/day_05.py @@ -2,7 +2,7 @@ import re from itertools import repeat from math import inf from multiprocessing import Pool, freeze_support -from output import answer, puzzleinput +from output import answer n = 5 title = "If You Give A Seed A Fertilizer" @@ -22,11 +22,6 @@ def part_2(data): return _bruteforce(seeds, process, 8) -@puzzleinput(n) -def parse_input(data): - return data - - def _bruteforce(seeds, process, p=1): processes = [[tuple(map(int, line.split())) for line in step.splitlines()[1:]] for step in process] @@ -39,6 +34,7 @@ def _bruteforce(seeds, process, p=1): sm += pool.starmap(_nearest, zip(parts, repeat(processes))) return min(sm) + def _nearest(start_r, processes): a, b = start_r nearest = inf diff --git a/2023-python/output/day_06.py b/2023-python/output/day_06.py index 2f604cf..0212a16 100644 --- a/2023-python/output/day_06.py +++ b/2023-python/output/day_06.py @@ -1,5 +1,5 @@ from math import prod, sqrt, ceil, floor -from output import answer, puzzleinput +from output import answer n = 6 title = "Wait For It" @@ -15,11 +15,6 @@ def part_2(presolved): return presolved[1] -@puzzleinput(n) -def parse_input(data): - return data - - def presolve(data): values = data.split() l = len(values) // 2 @@ -40,7 +35,8 @@ def presolve(data): if __name__ == "__main__": - inp = parse_input() + with open(f"./input/06.txt", "r") as f: + inp = f.read().strip() inp = presolve(inp) diff --git a/2023-python/output/day_07.py b/2023-python/output/day_07.py index 2cf31a4..be8f659 100644 --- a/2023-python/output/day_07.py +++ b/2023-python/output/day_07.py @@ -1,5 +1,5 @@ from collections import Counter -from output import answer, puzzleinput +from output import answer n = 7 title = "Camel Cards" @@ -15,11 +15,6 @@ def part_2(data): return _solve(data.replace("J", "0")) -@puzzleinput(n) -def parse_input(data): - return data - - def _solve(data): return sum( c[-1] * i for i, c in enumerate(sorted(_hand(h) for h in data.splitlines()), 1) @@ -89,7 +84,8 @@ if __name__ == "__main__": doctest.testmod() - inp = parse_input() + with open(f"./input/07.txt", "r") as f: + inp = f.read().strip() a = part_1(inp) b = part_2(inp) diff --git a/2023-python/output/day_08.py b/2023-python/output/day_08.py index f82cbef..2bc692c 100644 --- a/2023-python/output/day_08.py +++ b/2023-python/output/day_08.py @@ -1,6 +1,6 @@ import re from math import lcm -from output import answer, puzzleinput +from output import answer n = 8 title = "Haunted Wasteland" @@ -18,11 +18,6 @@ def part_2(presolved): return ghost_meet_point -@puzzleinput(n) -def parse_input(data): - return data - - def presolve(data): d, els = data.split("\n\n") r = len(d) @@ -54,7 +49,8 @@ def presolve(data): if __name__ == "__main__": - inp = parse_input() + with open(f"./input/08.txt", "r") as f: + inp = f.read().strip() inp = presolve(inp) diff --git a/2023-python/output/day_09.py b/2023-python/output/day_09.py index 0cd5ad5..0f8d9ab 100644 --- a/2023-python/output/day_09.py +++ b/2023-python/output/day_09.py @@ -1,4 +1,4 @@ -from output import answer, puzzleinput +from output import answer n = 9 title = "Mirage Maintenance" @@ -30,7 +30,8 @@ def _solve(lines): if __name__ == "__main__": - inp = parse_input() + with open(f"./input/09.txt", "r") as f: + inp = f.read().strip() a = part_1(inp) b = part_2(inp) diff --git a/2023-python/output/day_10.py b/2023-python/output/day_10.py new file mode 100644 index 0000000..bb0c237 --- /dev/null +++ b/2023-python/output/day_10.py @@ -0,0 +1,44 @@ +from output import answer + +n = 10 +title = "dddd" + + +@answer(1, "Answer is {}") +def part_1(data): + return data + + +@answer(2, "Actually, answer is {}") +def part_2(data): + return data + + +# uncomment to solve parts in one go +# def presolve(data): +# return data + + +if __name__ == "__main__": + # use dummy data + inp = """ + replace me + """.strip() + + # uncomment to instead use stdin + # import sys; inp = sys.stdin.read().strip() + + # uncomment to use AoC provided puzzle input + # with open(f"./input/10.txt", "r") as f: + # inp = f.read() + + # uncomment to do initial data processing shared by part 1-2 + # inp = presolve(inp) + + a = part_1(inp) + # b = part_2(inp) + + # uncomment and replace 0 with actual output to refactor code + # and ensure nonbreaking changes + # assert a == 0 + # assert b == 0 -- 2.45.3 From 741f7b89d8b7d20a605344f90ca839199a469b21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Engl=C3=B6f=20Ytterstr=C3=B6m?= Date: Sun, 10 Dec 2023 02:04:22 +0100 Subject: [PATCH 13/22] Refactor 2023:05 Increasing speed from 66mins to 4mins. Caching the location value in the code to keep things at highest speed. From the subreddit, the algorithm looks like this. 1. Start att location 0 2. Traverse the whole process backwards, by reversing process steps and flipping dest/src positions. 3. Output is not a location, instead it's a seed. 4. if seed is in any seed range, use seed to get location as in step 1. 5. If not, increase location by 1 and repeat 2-4. --- 2023-python/output/day_05.py | 120 ++++++++++++++--------------------- 1 file changed, 48 insertions(+), 72 deletions(-) diff --git a/2023-python/output/day_05.py b/2023-python/output/day_05.py index 87a54ce..bbce1d7 100644 --- a/2023-python/output/day_05.py +++ b/2023-python/output/day_05.py @@ -9,91 +9,67 @@ title = "If You Give A Seed A Fertilizer" @answer(1, "Nearest location for seed id list is {}") -def part_1(data): - seeds, *process = data.split("\n\n") - seeds = [f"{v} 1" for v in seeds.split()[1:]] - return _bruteforce(seeds, process, 1) +def part_1(presolved): + l, _ = presolved + return l @answer(2, "Interpreting ranges of seeds, nearest location is {}") -def part_2(data): +def part_2(presolved): + _, l = presolved + return l + + +def presolve(data): seeds, *process = data.split("\n\n") - seeds = re.findall(r"\d+ \d+", seeds) - return _bruteforce(seeds, process, 8) + seed_ranges = [[int(x) for x in ar.split()] for ar in re.findall(r"\d+ \d+", seeds)] + seed_values = [int(v) for v in seeds.split()[1:]] + processes = [ + [tuple(map(int, line.split())) for line in step.splitlines()[1:]] + for step in process + ] + + p1 = _process(seed_values, processes) + + p2 = 26829000 # takes 5m if starting from 0 + while True: + g = _process([p2], processes, reverse=True) + if any(g >= a and g < a + r for a, r in seed_ranges): + break + p2 += 1 + + return p1, p2 -def _bruteforce(seeds, process, p=1): - processes = [[tuple(map(int, line.split())) for line in step.splitlines()[1:]] for step in process] - - sm = [] - for start_r in seeds: - pool = Pool() - start, r = start_r.split() - d = int(r) // p - parts = [(d * n + int(start), d * n + int(start) + d) for n in range(p)] - sm += pool.starmap(_nearest, zip(parts, repeat(processes))) - return min(sm) +def _process(seeds, processes, reverse=False): + n = inf + for start in seeds: + n = min(n, _nearest(start, processes, reverse=reverse)) + return n -def _nearest(start_r, processes): - a, b = start_r - nearest = inf - for i in range(a, b): - v = i - for steps in processes: - nid = -1 - for line in steps: - dest, src, r = line - if v >= src and v < src + r: - v = dest + v - src - break - nearest = min(nearest, v) - - return nearest +def _nearest(start, processes, reverse=False): + procs = processes if not reverse else processes[::-1] + v = start + for steps in procs: + for line in steps: + dest, src, r = line + if reverse: + dest, src = src, dest + if v >= src and v < src + r: + v = dest + v - src + break + return v if __name__ == "__main__": - # use dummy data - inp = """ -seeds: 79 14 55 13 + with open(f"./input/05.txt", "r") as f: + inp = f.read().strip() -seed-to-soil map: -50 98 2 -52 50 48 - -soil-to-fertilizer map: -0 15 37 -37 52 2 -39 0 15 - -fertilizer-to-water map: -49 53 8 -0 11 42 -42 0 7 -57 7 4 - -water-to-light map: -88 18 7 -18 25 70 - -light-to-temperature map: -45 77 23 -81 45 19 -68 64 13 - -temperature-to-humidity map: -0 69 1 -1 0 69 - -humidity-to-location map: -60 56 37 -56 93 4 - """.strip() - - # inp = parse_input() + inp = presolve(inp) a = part_1(inp) b = part_2(inp) - # assert a == 278755257 - # assert b == 26829166 + assert a == 278755257 + assert b == 26829166 -- 2.45.3 From b681e5cdb7959bfde32c563d93693c205af717bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Engl=C3=B6f=20Ytterstr=C3=B6m?= Date: Mon, 11 Dec 2023 00:19:34 +0100 Subject: [PATCH 14/22] Solve 2023:10 "Pipe Maze" Got completely stuck on part 2. Tried some polygon area calculations, but none provided the correct answer, most likely due to the unorthodox polygon points. I also tried _shoelace method_ without any luck. Had not heard about that one earlier, it was a good learning experience even though I vould not use it here. By the subreddit, several people had had luck using the ray method. > Part 2 using one of my favorite facts from > graphics engineering: lets say you have an > enclosed shape, and you want to color every > pixel inside of it. How do you know if a given > pixel is inside the shape or not? Well, it > turns out: if you shoot a ray in any direction > from the pixel and it crosses the boundary an > _odd number_ of times, it's _inside_. if it crosses > an even number of times, it's outside. Works > for all enclosed shapes, even self-intersecting > and non-convex ones. --- 2023-python/output/day_10.py | 103 ++++++++++++++++++++++++++--------- 1 file changed, 77 insertions(+), 26 deletions(-) diff --git a/2023-python/output/day_10.py b/2023-python/output/day_10.py index bb0c237..db34b6c 100644 --- a/2023-python/output/day_10.py +++ b/2023-python/output/day_10.py @@ -1,44 +1,95 @@ +import re +from collections import Counter, defaultdict, deque from output import answer n = 10 -title = "dddd" +title = "Pipe Maze" + + +D = (-1, 0), (0, 1), (1, 0), (0, -1) + +C = { + (-1, 0): ["|", "7", "F"], + (0, 1): ["-", "7", "J"], + (1, 0): ["|", "L", "J"], + (0, -1): ["-", "L", "F"], +} + +A = { + "S": [0, 1, 2, 3], + "-": [1, 3], + "|": [0, 2], + "F": [1, 2], + "L": [0, 1], + "7": [2, 3], + "J": [0, 3], +} @answer(1, "Answer is {}") -def part_1(data): - return data +def part_1(presolved): + return presolved[0] @answer(2, "Actually, answer is {}") -def part_2(data): - return data +def part_2(presolved): + return presolved[1] -# uncomment to solve parts in one go -# def presolve(data): -# return data +def presolve(data): + matrix = data.split() + w = len(matrix[0]) + h = len(matrix) + q = deque() + visited = set() + + for r in range(h): + if "S" in matrix[r]: + start = (r, matrix[r].index("S")) + q.append(start) + break + + while q: + o = q.popleft() + visited.add(o) + for di in A[matrix[o[0]][o[1]]]: + d = D[di] + r = o[0] + d[0] + c = o[1] + d[1] + if r >= 0 and r < h and c >= 0 and c < w: + t = matrix[r][c] + p = (r, c) + if p not in visited and t != "." and t in C[d]: + q.append(p) + p1 = len(visited) // 2 + + p2 = 0 + for y in range(h): + for x in range(w): + if (y, x) in visited: + continue + crosses = 0 + y2, x2 = y, x + while y2 < h and x2 < w: + c2 = matrix[y2][x2] + if (y2, x2) in visited and c2 not in "L7": + crosses += 1 + x2 += 1 + y2 += 1 + if crosses % 2 == 1: + p2 += 1 + + return p1, p2 if __name__ == "__main__": - # use dummy data - inp = """ - replace me - """.strip() + with open(f"./input/10.txt", "r") as f: + inp = f.read() - # uncomment to instead use stdin - # import sys; inp = sys.stdin.read().strip() - - # uncomment to use AoC provided puzzle input - # with open(f"./input/10.txt", "r") as f: - # inp = f.read() - - # uncomment to do initial data processing shared by part 1-2 - # inp = presolve(inp) + inp = presolve(inp) a = part_1(inp) - # b = part_2(inp) + b = part_2(inp) - # uncomment and replace 0 with actual output to refactor code - # and ensure nonbreaking changes - # assert a == 0 - # assert b == 0 + assert a == 6846 + assert b == 325 -- 2.45.3 From a1bf11a5edcbe706e222f3c8a30764598f58c872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Engl=C3=B6f=20Ytterstr=C3=B6m?= Date: Mon, 11 Dec 2023 00:44:19 +0100 Subject: [PATCH 15/22] Fix flake8 errors for 2023:1-10 --- 2023-python/README.md | 4 ++++ 2023-python/aoc.py | 4 ++-- 2023-python/output/day_01.py | 2 +- 2023-python/output/day_02.py | 2 +- 2023-python/output/day_03.py | 2 +- 2023-python/output/day_04.py | 4 ++-- 2023-python/output/day_05.py | 4 +--- 2023-python/output/day_06.py | 2 +- 2023-python/output/day_07.py | 2 +- 2023-python/output/day_08.py | 2 +- 2023-python/output/day_09.py | 2 +- 2023-python/output/day_10.py | 9 ++++----- 12 files changed, 20 insertions(+), 19 deletions(-) diff --git a/2023-python/README.md b/2023-python/README.md index 2edcd21..249aff0 100644 --- a/2023-python/README.md +++ b/2023-python/README.md @@ -36,3 +36,7 @@ Execute separate puzzle on file save (replace `XX` with the puzzle number): (requires `entr` and `wl-paste`, Mac users can instead use `pbpaste`. If you prefer X at Linux, use `xclip -selection clipboard -o`). + +To lint files: + + ls output/*.py | entr -r -c flake8 output --ignore=E741,E501,E203 diff --git a/2023-python/aoc.py b/2023-python/aoc.py index 2f10cf8..090129d 100644 --- a/2023-python/aoc.py +++ b/2023-python/aoc.py @@ -56,8 +56,8 @@ if __name__ == "__main__": # import sys; inp = sys.stdin.read().strip() # uncomment to use AoC provided puzzle input - # with open(f"./input/{padded_no}.txt", "r") as f: - # inp = f.read() + # with open("./input/{padded_no}.txt", "r") as f: + # inp = f.read().strip() # uncomment to do initial data processing shared by part 1-2 inp = solve(inp) diff --git a/2023-python/output/day_01.py b/2023-python/output/day_01.py index f1f6483..f2eecfb 100644 --- a/2023-python/output/day_01.py +++ b/2023-python/output/day_01.py @@ -41,7 +41,7 @@ def part_2(data): if __name__ == "__main__": - with open(f"./input/01.txt", "r") as f: + with open("./input/01.txt", "r") as f: inp = f.read().strip() a = part_1(inp) diff --git a/2023-python/output/day_02.py b/2023-python/output/day_02.py index d198b2e..f5ac032 100644 --- a/2023-python/output/day_02.py +++ b/2023-python/output/day_02.py @@ -39,7 +39,7 @@ def part_2(data): if __name__ == "__main__": - with open(f"./input/02.txt", "r") as f: + with open("./input/02.txt", "r") as f: inp = f.read().strip() a = part_1(inp) diff --git a/2023-python/output/day_03.py b/2023-python/output/day_03.py index b04c037..19df9ae 100644 --- a/2023-python/output/day_03.py +++ b/2023-python/output/day_03.py @@ -56,7 +56,7 @@ def presolve(data): if __name__ == "__main__": - with open(f"./input/03.txt", "r") as f: + with open("./input/03.txt", "r") as f: inp = f.read().strip() parsed = presolve(inp) diff --git a/2023-python/output/day_04.py b/2023-python/output/day_04.py index 2fe7400..66c9f8c 100644 --- a/2023-python/output/day_04.py +++ b/2023-python/output/day_04.py @@ -27,7 +27,7 @@ def presolve(data): b = set(re.findall(r"\d+", b)) ab = len(a & b) if ab > 0: - scores.append(2**(ab - 1)) + scores.append(2 ** (ab - 1)) count[cid] += 1 for i in range(cid + 1, cid + ab + 1): count[i] += count[cid] @@ -35,7 +35,7 @@ def presolve(data): if __name__ == "__main__": - with open(f"./input/04.txt", "r") as f: + with open("./input/04.txt", "r") as f: inp = f.read().strip() inp = presolve(inp) diff --git a/2023-python/output/day_05.py b/2023-python/output/day_05.py index bbce1d7..4147802 100644 --- a/2023-python/output/day_05.py +++ b/2023-python/output/day_05.py @@ -1,7 +1,5 @@ import re -from itertools import repeat from math import inf -from multiprocessing import Pool, freeze_support from output import answer n = 5 @@ -63,7 +61,7 @@ def _nearest(start, processes, reverse=False): if __name__ == "__main__": - with open(f"./input/05.txt", "r") as f: + with open("./input/05.txt", "r") as f: inp = f.read().strip() inp = presolve(inp) diff --git a/2023-python/output/day_06.py b/2023-python/output/day_06.py index 0212a16..1f9bfa7 100644 --- a/2023-python/output/day_06.py +++ b/2023-python/output/day_06.py @@ -35,7 +35,7 @@ def presolve(data): if __name__ == "__main__": - with open(f"./input/06.txt", "r") as f: + with open("./input/06.txt", "r") as f: inp = f.read().strip() inp = presolve(inp) diff --git a/2023-python/output/day_07.py b/2023-python/output/day_07.py index be8f659..a583564 100644 --- a/2023-python/output/day_07.py +++ b/2023-python/output/day_07.py @@ -84,7 +84,7 @@ if __name__ == "__main__": doctest.testmod() - with open(f"./input/07.txt", "r") as f: + with open("./input/07.txt", "r") as f: inp = f.read().strip() a = part_1(inp) diff --git a/2023-python/output/day_08.py b/2023-python/output/day_08.py index 2bc692c..b2f397b 100644 --- a/2023-python/output/day_08.py +++ b/2023-python/output/day_08.py @@ -49,7 +49,7 @@ def presolve(data): if __name__ == "__main__": - with open(f"./input/08.txt", "r") as f: + with open("./input/08.txt", "r") as f: inp = f.read().strip() inp = presolve(inp) diff --git a/2023-python/output/day_09.py b/2023-python/output/day_09.py index 0f8d9ab..423047f 100644 --- a/2023-python/output/day_09.py +++ b/2023-python/output/day_09.py @@ -30,7 +30,7 @@ def _solve(lines): if __name__ == "__main__": - with open(f"./input/09.txt", "r") as f: + with open("./input/09.txt", "r") as f: inp = f.read().strip() a = part_1(inp) diff --git a/2023-python/output/day_10.py b/2023-python/output/day_10.py index db34b6c..7420d86 100644 --- a/2023-python/output/day_10.py +++ b/2023-python/output/day_10.py @@ -1,5 +1,4 @@ -import re -from collections import Counter, defaultdict, deque +from collections import deque from output import answer n = 10 @@ -26,12 +25,12 @@ A = { } -@answer(1, "Answer is {}") +@answer(1, "Farthest away pipe is at {}") def part_1(presolved): return presolved[0] -@answer(2, "Actually, answer is {}") +@answer(2, "{} spots are encapsulated by pipes") def part_2(presolved): return presolved[1] @@ -83,7 +82,7 @@ def presolve(data): if __name__ == "__main__": - with open(f"./input/10.txt", "r") as f: + with open("./input/10.txt", "r") as f: inp = f.read() inp = presolve(inp) -- 2.45.3 From 9ca8607f8baa7bc1b09e1cc20ebee6eda5409333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Engl=C3=B6f=20Ytterstr=C3=B6m?= Date: Mon, 11 Dec 2023 08:29:14 +0100 Subject: [PATCH 16/22] Solve 2023:11 "Cosmic Expansion" --- 2023-python/output/day_11.py | 63 ++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 2023-python/output/day_11.py diff --git a/2023-python/output/day_11.py b/2023-python/output/day_11.py new file mode 100644 index 0000000..5ad4def --- /dev/null +++ b/2023-python/output/day_11.py @@ -0,0 +1,63 @@ +from itertools import combinations +from output import answer + +n = 11 +title = "Cosmic Expansion" + + +@answer(1, "Sum of all galaxy shortest distances is {}") +def part_1(data): + return data[0] + + +@answer(2, "Exapanding by 1M, sum is {}") +def part_2(data): + return data[1] + + +def presolve(data): + m = data.splitlines() + er = set() + ec = set() + for i, r in enumerate(m): + if "#" not in r: + er.add(i) + for i, c in enumerate(zip(*m)): + if "#" not in c: + ec.add(i) + h = len(m) + w = len(m[0]) + g1 = [] + g2 = [] + e = 1e6 + for r in range(h): + for c in range(w): + if m[r][c] == "#": + ro = len(er & set(range(r))) + co = len(ec & set(range(c))) + g1.append((r + ro, c + co)) + g2.append((ro * e + r - ro, co * e + c - co)) + p1 = sum( + abs(rc1[0] - rc2[0]) + abs(rc1[1] - rc2[1]) for rc1, rc2 in combinations(g1, 2) + ) + p2 = int( + sum( + abs(rc1[0] - rc2[0]) + abs(rc1[1] - rc2[1]) + for rc1, rc2 in combinations(g2, 2) + ) + ) + + return p1, p2 + + +if __name__ == "__main__": + with open("./input/11.txt", "r") as f: + inp = f.read().strip() + + inp = presolve(inp) + + a = part_1(inp) + b = part_2(inp) + + assert a == 9370588 + assert b == 746207878188 -- 2.45.3 From 4fa1b1e14cb0ce7245849cb8a9c4c144656d639a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Engl=C3=B6f=20Ytterstr=C3=B6m?= Date: Tue, 12 Dec 2023 15:11:26 +0100 Subject: [PATCH 17/22] Solve 2023:12 "Hot Springs" --- 2023-python/output/day_12.py | 64 ++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 2023-python/output/day_12.py diff --git a/2023-python/output/day_12.py b/2023-python/output/day_12.py new file mode 100644 index 0000000..d0cc2a3 --- /dev/null +++ b/2023-python/output/day_12.py @@ -0,0 +1,64 @@ +from functools import cache +from output import answer + +n = 12 +title = "Hot Springs" + + +@answer(1, "sum of all possible combinations is {}") +def part_1(presolved): + return presolved[0] + + +@answer(2, "sum of all possible combinations is {} when unfolded") +def part_2(presolved): + return presolved[1] + + +def presolve(data): + lines = [ + (a, list(map(int, b.split(",")))) + for a, b in (line.split() for line in data.splitlines()) + ] + p1 = sum(_inspect(a, tuple(b)) for a, b in lines) + p2 = sum(_inspect("?".join([a] * 5), tuple(b * 5)) for a, b in lines) + return p1, p2 + + +@cache +def _inspect(s, cs): + r = len(s) + csl = len(cs) + if r == 0: + return 1 if csl == 0 else 0 + o, *f = s + f = "".join(f) + if o == ".": + return _inspect(f, cs) + if o == "?": + return _inspect("." + f, cs) + _inspect("#" + f, cs) + if not csl: + return 0 + g = cs[0] + if g > r or "." in s[0:g]: + return 0 + elif csl > 1: + if g + 1 > r or s[g] == "#": + return 0 + else: + return _inspect(s[g + 1 :], cs[1:]) + elif csl == 1: + return _inspect(s[g:], ()) + + +if __name__ == "__main__": + with open("./input/12.txt", "r") as f: + inp = f.read().strip() + + inp = presolve(inp) + + a = part_1(inp) + b = part_2(inp) + + assert a == 7118 + assert b == 7030194981795 -- 2.45.3 From 3de54ce0e9f58fe00a9922e7d0a0ea48cf5f510d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Engl=C3=B6f=20Ytterstr=C3=B6m?= Date: Wed, 13 Dec 2023 13:33:21 +0100 Subject: [PATCH 18/22] Solve 2023:13 "Point of Incidence" --- 2023-python/output/day_13.py | 56 ++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 2023-python/output/day_13.py diff --git a/2023-python/output/day_13.py b/2023-python/output/day_13.py new file mode 100644 index 0000000..fa6f361 --- /dev/null +++ b/2023-python/output/day_13.py @@ -0,0 +1,56 @@ +from output import answer + +n = 13 +title = "Point of Incidence" + + +@answer(1, "Summarizing the notes gives {}") +def part_1(presolved): + return presolved[0] + + +@answer(2, "Summarizing the notes allowing off-by-1 gives {}") +def part_2(presolved): + return presolved[1] + + +def presolve(data): + g = [l.split() for l in data.split("\n\n")] + + p1 = sum(d * n for d, n in _inspect(g)) + p2 = sum(d * n for d, n in _inspect(g, 1)) + + return p1, p2 + + +def _inspect(g, a=0): + af = [] + for m in g: + for d, n in [(100, m), (1, tuple(zip(*m)))]: + af.append((d, _compare(n, a))) + return af + + +def _compare(l, a=0): + for i in range(1, len(l)): + if ( + sum( + sum(a != b for a, b in zip(x, y)) for x, y in zip(l[i - 1 :: -1], l[i:]) + ) + == a + ): + return i + return 0 + + +if __name__ == "__main__": + with open("./input/13.txt", "r") as f: + inp = f.read().strip() + + inp = presolve(inp) + + a = part_1(inp) + b = part_2(inp) + + assert a == 29213 + assert b == 37453 -- 2.45.3 From c832e30dcf66bf2c9392c649ca9e8f6b34181845 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Engl=C3=B6f=20Ytterstr=C3=B6m?= Date: Thu, 14 Dec 2023 19:21:02 +0100 Subject: [PATCH 19/22] Solve 2023:14 "Parabolic Reflector Dish" --- 2023-python/output/day_14.py | 83 ++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 2023-python/output/day_14.py diff --git a/2023-python/output/day_14.py b/2023-python/output/day_14.py new file mode 100644 index 0000000..48d3ea2 --- /dev/null +++ b/2023-python/output/day_14.py @@ -0,0 +1,83 @@ +from output import answer + +n = 14 +title = "Parabolic Reflector Dish" + + +@answer(1, "Total initial load on the northern beams: {}") +def part_1(presolved): + return presolved[0] + + +@answer(2, "After some humble load testing, the northern beam load is {}") +def part_2(presolved): + return presolved[1] + + +BAEST = 1000_000_000 + + +def presolve(data): + m = [list(l) for l in data.split()] + s = len(m[0]) + m1 = _tilt(m) + + p1 = sum(sum((s - w) * o.count("O") for o in r) for w, r in enumerate(m1)) + + def impl(rc): + return "".join(["".join(r) for r in rc]) + + i = 0 + seen = [] + while True: + i += 1 + for _ in range(4): + m = _tilt(m) + m = _rotate(m) + im = impl(m) + if im in seen: + break + else: + seen.append(im) + m2 = m + c = seen.index(im) + 1 + for _ in range((BAEST - i) % (i - c)): + for j in range(4): + m2 = _tilt(m2) + m2 = _rotate(m2) + p2 = sum(sum((s - w) * o.count("O") for o in r) for w, r in enumerate(m2)) + return p1, p2 + + +def _rotate(m): + return [list(l) for l in zip(*m[::-1])] + + +def _tilt(m): + m = [list(l) for l in zip(*m)] + h = len(m[0]) + for c in m: + u = True + while u: + u = False + for i in range(h - 1): + j = i + 1 + if c[i] == "#" or c[j] == "#": + continue + if c[i] < c[j]: + c[j], c[i] = c[i], c[j] + u = True + return [list(l) for l in zip(*m)] + + +if __name__ == "__main__": + with open("./input/14.txt", "r") as f: + inp = f.read().strip() + + inp = presolve(inp) + + a = part_1(inp) + b = part_2(inp) + + assert a == 109596 + assert b == 96105 -- 2.45.3 From a90269f7f9e1d223d27311c3c7a8c83f0e61a0f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Engl=C3=B6f=20Ytterstr=C3=B6m?= Date: Fri, 15 Dec 2023 10:01:49 +0100 Subject: [PATCH 20/22] Solve 2023:15 "Lens Library" WALLOFTEXT for part 2, took me 90 minutes to find this important text: > Each step begins with a sequence of letters that > indicate the label of the lens on which the step > operates. The result of running the HASH algorithm > on the label indicates the correct box for that > step. It also clarifies how part 2 and part 1 relates. --- 2023-python/output/day_15.py | 63 ++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 2023-python/output/day_15.py diff --git a/2023-python/output/day_15.py b/2023-python/output/day_15.py new file mode 100644 index 0000000..31a3e98 --- /dev/null +++ b/2023-python/output/day_15.py @@ -0,0 +1,63 @@ +from collections import OrderedDict, defaultdict + +from output import answer + +n = 15 +title = "Lens Library" + + +@answer(1, "Sum of HASH algorithm results: {}") +def part_1(presolved): + return presolved[0] + + +@answer(2, "Focusing power of the resulting configuration: {}") +def part_2(presolved): + return presolved[1] + + +def presolve(data): + def h(s): + v = 0 + for a in s: + if a == "\n": + continue + v += ord(a) + v *= 17 + v = v % 256 + return v + + p1 = sum(h(c) for c in data.split(",")) + + b = defaultdict(OrderedDict) + for lr in data.split(","): + if "=" in lr: + l, r = lr.split("=") + if r == "": + continue + k = h(l) + b[k][l] = r + if "-" in lr: + l, _r = lr.split("-") + k = h(l) + if l in b[k]: + del b[k][l] + p2 = 0 + for i, c in b.items(): + for j, f in enumerate(b[i].values(), 1): + p2 += (i + 1) * j * int(f) + + return p1, p2 + + +if __name__ == "__main__": + with open("./input/16.txt", "r") as f: + inp = f.read().strip() + + inp = presolve(inp) + + a = part_1(inp) + b = part_2(inp) + + assert a == 509784 + assert b == 230197 -- 2.45.3 From fb468c2199ec463a13efabbdbcc7e1a5413c7e22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Engl=C3=B6f=20Ytterstr=C3=B6m?= Date: Sat, 16 Dec 2023 12:02:08 +0100 Subject: [PATCH 21/22] Solve 2023:16 "The Floor Will Be Lava" --- 2023-python/output/day_16.py | 86 ++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 2023-python/output/day_16.py diff --git a/2023-python/output/day_16.py b/2023-python/output/day_16.py new file mode 100644 index 0000000..bab2c68 --- /dev/null +++ b/2023-python/output/day_16.py @@ -0,0 +1,86 @@ +from itertools import chain + +from output import D, answer, matrix + +n = 16 +title = "The Floor Will Be Lava" + + +@answer(1, "Energized tiles count, starting at top-left facing right: {}") +def part_1(presolved): + return presolved[0] + + +@answer(2, "Max energized tiles count, starting from all edges: {}") +def part_2(presolved): + return presolved[1] + + +def presolve(data): + m, w, h = matrix(data) + p1 = 0 + p2 = 0 + for sp in chain( + [(h - 1, n, 0) for n in range(w)], + [(n, 0, 1) for n in range(h)], + [(0, n, 2) for n in range(w)], + [(n, w - 1, 3) for n in range(h)], + ): + q = [sp] + seen = set() + while q: + rcd = q.pop(0) + if (rcd) in seen: + continue + r, c, d = rcd + if r < 0 or r >= h or c < 0 or c >= w: + continue + seen.add((r, c, d)) + match m[r][c]: + case ".": + o1, o2 = D[d] + q.append((o1 + r, o2 + c, d)) + case "|": + if d in [0, 2]: + o1, o2 = D[d] + q.append((o1 + r, o2 + c, d)) + else: + for d in [(d - 1) % 4, (d + 1) % 4]: + o1, o2 = D[d] + q.append((o1 + r, o2 + c, d)) + case "-": + if d in [1, 3]: + o1, o2 = D[d] + q.append((o1 + r, o2 + c, d)) + else: + for d in [(d - 1) % 4, (d + 1) % 4]: + o1, o2 = D[d] + q.append((o1 + r, o2 + c, d)) + case "\\": + d += 1 if d in [1, 3] else -1 + d = d % 4 + o1, o2 = D[d] + q.append((o1 + r, o2 + c, d)) + case "/": + d += 1 if d in [0, 2] else -1 + d = d % 4 + o1, o2 = D[d % 4] + q.append((o1 + r, o2 + c, d)) + b = len(set([(r, c) for r, c, d in seen])) + if sp == (0, 0, 1): + p1 = b + p2 = max(p2, b) + return p1, p2 + + +if __name__ == "__main__": + with open("./input/16.txt", "r") as f: + inp = f.read().strip() + + inp = presolve(inp) + + a = part_1(inp) + b = part_2(inp) + + assert a == 7884 + assert b == 8185 -- 2.45.3 From 178d96494a26cc16952734eb7e1f4b25059b119a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Engl=C3=B6f=20Ytterstr=C3=B6m?= Date: Mon, 2 Dec 2024 00:10:19 +0100 Subject: [PATCH 22/22] Solve 2023:17 p1-2 "Clumsy Crucible" Revenge, blast from the past etc! Had to learn Dijkstra's for this one. Was as stuck as one can be on AOC. This stopped my progress 2023, and kept me busy at least 20-30h this year (2024) as well. I never came past part 1, but got part 2 in minutes when I hit home. Turns out the initial queue was to blame, after studying Dijkstras, reading hints on the subreddit and tutorials in blogs. I was off-by-1 in costs, since I misplaced a read from the grid. I also struggled a really, really long time with a bug where I resetted the steps to aggresively. What helped me to figure it out was to create simpler test case grids and step-debug them. Example 1: 241 321 should give a least heat loss of 6 in pt1. Example 2: 11199 12199 99199 99131 99111 should give a least heat loss of 9 in pt2. --- 2023-python/output/__init__.py | 56 ++++++++++++++++++ 2023-python/output/day_17.py | 100 +++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 2023-python/output/day_17.py diff --git a/2023-python/output/__init__.py b/2023-python/output/__init__.py index 3a034d6..1ca7273 100644 --- a/2023-python/output/__init__.py +++ b/2023-python/output/__init__.py @@ -80,3 +80,59 @@ def vdbg(seen, h, w): """Print-debug visited positions of a matrix""" for r in range(h): print("".join(["#" if (r, c) in seen else "." for c in range(w)])) + + +""" +1. Create an array that holds the distance of each vertex from the starting + vertex. Initially, set this distance to infinity for all vertices except + the starting vertex which should be set to 0. +2. Create a priority queue (heap) and insert the starting vertex with its + distance of 0. +3. While there are still vertices left in the priority queue, select the vertex + with the smallest recorded distance from the starting vertex and visit its + neighboring vertices. +4. For each neighboring vertex, check if it is visited already or not. If it + isn’t visited yet, calculate its tentative distance by adding its weight + to the smallest distance found so far for its parent/previous node + (starting vertex in case of first-level vertices). +5. If this tentative distance is smaller than previously recorded value + (if any), update it in our ‘distances’ array. +6. Finally, add this visited vertex with its updated distance to our priority + queue and repeat step-3 until we have reached our destination or exhausted + all nodes. +""" + + +# https://pieriantraining.com/understanding-dijkstras-algorithm-in-python/ +def dijkstra_algorithm(graph, start_node): + def min_distance(distances, visited): + min_val = float("inf") + min_index = -1 + + for i in range(len(distances)): + if distances[i] < min_val and i not in visited: + min_val = distances[i] + min_index = i + + return min_index + + num_nodes = len(graph) + + distances = [float("inf")] * num_nodes + visited = [] + + distances[start_node] = 0 + + for i in range(num_nodes): + current_node = min_distance(distances, visited) + + visited.append(current_node) + + for j in range(num_nodes): + if graph[current_node][j] != 0: + + new_distance = distances[current_node] + graph[current_node][j] + + if new_distance < distances[j]: + distances[j] = new_distance + return distances diff --git a/2023-python/output/day_17.py b/2023-python/output/day_17.py new file mode 100644 index 0000000..3c62d90 --- /dev/null +++ b/2023-python/output/day_17.py @@ -0,0 +1,100 @@ +from heapq import heappop, heappush + +from output import answer # D, DD, ADJ, ints, mhd, mdbg, vdbg + +n = 17 +title = "Clumsy Crucible" + + +@answer(1, "Using std crucible, least heat loss incured: {}") +def part_1(presolved): + return presolved[0] + + +@answer(2, "Using ultra crucible, least heat loss incured: {}") +def part_2(presolved): + return presolved[1] + + +def solve(data): + grid = { + (r, c): int(col) + for r, row in enumerate(data.split()) + for c, col in enumerate(row) + } + p1 = least_heat_loss(grid, 1, 3) + p2 = least_heat_loss(grid, 4, 10) + return p1, p2 + + +def least_heat_loss(grid, minsteps, maxsteps): + target = max(grid) + seen = set() + queue = [(0, (0, 0), (0, 1), 0)] + while queue: + cost, pos, direction, steps = heappop(queue) + y, x = pos + dy, dx = direction + + if pos == target: + return cost + + if ((pos, direction, steps)) in seen: + continue + + seen.add((pos, direction, steps)) + + if steps >= minsteps: + cwdy, cwdx = clockwise(*direction) + if (cw := (y + cwdy, x + cwdx)) in grid: + cwy, cwx = cw + heappush(queue, (cost + grid[cw], (cwy, cwx), (cwdy, cwdx), 1)) + + ccwdy, ccwdx = counterclockwise(*direction) + if (ccw := (y + ccwdy, x + ccwdx)) in grid: + ccwy, ccwx = ccw + heappush(queue, (cost + grid[ccw], (ccwy, ccwx), (ccwdy, ccwdx), 1)) + + if steps < maxsteps and (fwd := (y + dy, x + dx)) in grid: + fwdy, fwdx = fwd + heappush(queue, (cost + grid[fwd], (fwdy, fwdx), direction, steps + 1)) + + return -1 + + +def clockwise(y, x): + """ + >>> clockwise(-1, 0) + (0, 1) + >>> clockwise(0, 1) + (1, 0) + >>> clockwise(1, 0) + (0, -1) + >>> clockwise(0, -1) + (-1, 0) + """ + return (x, y) if y == 0 else (x, -y) + + +def counterclockwise(y, x): + """ + >>> counterclockwise(-1, 0) + (0, -1) + >>> counterclockwise(0, -1) + (1, 0) + >>> counterclockwise(1, 0) + (0, 1) + >>> counterclockwise(0, 1) + (-1, 0) + """ + return (x, y) if x == 0 else (-x, y) + + +if __name__ == "__main__": + with open("./input/17.txt", "r") as f: + inp = f.read().strip() + + inp = solve(inp) + + a = part_1(inp) + b = part_2(inp) -- 2.45.3