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..249aff0 --- /dev/null +++ b/2023-python/README.md @@ -0,0 +1,42 @@ +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 new file mode 100644 index 0000000..090129d --- /dev/null +++ b/2023-python/aoc.py @@ -0,0 +1,126 @@ +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 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)])) diff --git a/2023-python/output/day_01.py b/2023-python/output/day_01.py new file mode 100644 index 0000000..f2eecfb --- /dev/null +++ b/2023-python/output/day_01.py @@ -0,0 +1,51 @@ +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 new file mode 100644 index 0000000..f5ac032 --- /dev/null +++ b/2023-python/output/day_02.py @@ -0,0 +1,49 @@ +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 new file mode 100644 index 0000000..19df9ae --- /dev/null +++ b/2023-python/output/day_03.py @@ -0,0 +1,68 @@ +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 new file mode 100644 index 0000000..66c9f8c --- /dev/null +++ b/2023-python/output/day_04.py @@ -0,0 +1,47 @@ +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 new file mode 100644 index 0000000..4147802 --- /dev/null +++ b/2023-python/output/day_05.py @@ -0,0 +1,73 @@ +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 new file mode 100644 index 0000000..1f9bfa7 --- /dev/null +++ b/2023-python/output/day_06.py @@ -0,0 +1,47 @@ +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 new file mode 100644 index 0000000..a583564 --- /dev/null +++ b/2023-python/output/day_07.py @@ -0,0 +1,94 @@ +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 new file mode 100644 index 0000000..b2f397b --- /dev/null +++ b/2023-python/output/day_08.py @@ -0,0 +1,61 @@ +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 new file mode 100644 index 0000000..423047f --- /dev/null +++ b/2023-python/output/day_09.py @@ -0,0 +1,40 @@ +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 new file mode 100644 index 0000000..7420d86 --- /dev/null +++ b/2023-python/output/day_10.py @@ -0,0 +1,94 @@ +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 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 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 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 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 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 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