diff --git a/src/holt59/aoc/2022/day1.py b/src/holt59/aoc/2022/day1.py index 4dc115c..17776a8 100644 --- a/src/holt59/aoc/2022/day1.py +++ b/src/holt59/aoc/2022/day1.py @@ -1,7 +1,12 @@ -import sys +from typing import Any, Iterator -blocks = sys.stdin.read().split("\n\n") -values = sorted(sum(map(int, block.split())) for block in blocks) +from ..base import BaseSolver -print(f"answer 1 is {values[-1]}") -print(f"answer 2 is {sum(values[-3:])}") + +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + blocks = input.split("\n\n") + values = sorted(sum(map(int, block.split())) for block in blocks) + + yield values[-1] + yield sum(values[-3:]) diff --git a/src/holt59/aoc/2022/day10.py b/src/holt59/aoc/2022/day10.py index 73470c6..f06442e 100644 --- a/src/holt59/aoc/2022/day10.py +++ b/src/holt59/aoc/2022/day10.py @@ -1,38 +1,43 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() - -cycle = 1 -x = 1 - -values = {cycle: x} - -for line in lines: - cycle += 1 - - if line == "noop": - pass - else: - r = int(line.split()[1]) - - values[cycle] = x - - cycle += 1 - x += r - - values[cycle] = x - -answer_1 = sum(c * values[c] for c in range(20, max(values.keys()) + 1, 40)) -print(f"answer 1 is {answer_1}") +from ..base import BaseSolver -for i in range(6): - for j in range(40): - v = values[1 + i * 40 + j] +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = [line.strip() for line in input.splitlines()] - if j >= v - 1 and j <= v + 1: - print("#", end="") - else: - print(".", end="") + cycle, x = 1, 1 + values = {cycle: x} - print() + for line in lines: + cycle += 1 + + if line == "noop": + pass + else: + r = int(line.split()[1]) + + values[cycle] = x + + cycle += 1 + x += r + + values[cycle] = x + + answer_1 = sum(c * values[c] for c in range(20, max(values.keys()) + 1, 40)) + yield answer_1 + + yield ( + "\n" + + "\n".join( + "".join( + "#" + if j >= (v := values[1 + i * 40 + j]) - 1 and j <= v + 1 + else "." + for j in range(40) + ) + for i in range(6) + ) + + "\n" + ) diff --git a/src/holt59/aoc/2022/day11.py b/src/holt59/aoc/2022/day11.py index 5c2efd9..a505d71 100644 --- a/src/holt59/aoc/2022/day11.py +++ b/src/holt59/aoc/2022/day11.py @@ -1,7 +1,8 @@ import copy -import sys from functools import reduce -from typing import Callable, Final, Mapping, Sequence +from typing import Any, Callable, Final, Iterator, Mapping, Sequence + +from ..base import BaseSolver class Monkey: @@ -119,24 +120,28 @@ def monkey_business(inspects: dict[Monkey, int]) -> int: return sorted_levels[-2] * sorted_levels[-1] -monkeys = [parse_monkey(block.splitlines()) for block in sys.stdin.read().split("\n\n")] +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + monkeys = [parse_monkey(block.splitlines()) for block in input.split("\n\n")] -# case 1: we simply divide the worry by 3 after applying the monkey worry operation -answer_1 = monkey_business( - run(copy.deepcopy(monkeys), 20, me_worry_fn=lambda w: w // 3) -) -print(f"answer 1 is {answer_1}") + # case 1: we simply divide the worry by 3 after applying the monkey worry operation + yield monkey_business( + run(copy.deepcopy(monkeys), 20, me_worry_fn=lambda w: w // 3) + ) -# case 2: to keep reasonable level values, we can use a modulo operation, we need to -# use the product of all "divisible by" test so that the test remains valid -# -# (a + b) % c == ((a % c) + (b % c)) % c --- this would work for a single test value -# -# (a + b) % c == ((a % d) + (b % d)) % c --- if d is a multiple of c, which is why here -# we use the product of all test value -# -total_test_value = reduce(lambda w, m: w * m.test_value, monkeys, 1) -answer_2 = monkey_business( - run(copy.deepcopy(monkeys), 10_000, me_worry_fn=lambda w: w % total_test_value) -) -print(f"answer 2 is {answer_2}") + # case 2: to keep reasonable level values, we can use a modulo operation, we need to + # use the product of all "divisible by" test so that the test remains valid + # + # (a + b) % c == ((a % c) + (b % c)) % c --- this would work for a single test value + # + # (a + b) % c == ((a % d) + (b % d)) % c --- if d is a multiple of c, which is why here + # we use the product of all test value + # + total_test_value = reduce(lambda w, m: w * m.test_value, monkeys, 1) + yield monkey_business( + run( + copy.deepcopy(monkeys), + 10_000, + me_worry_fn=lambda w: w % total_test_value, + ) + ) diff --git a/src/holt59/aoc/2022/day12.py b/src/holt59/aoc/2022/day12.py index 35a7116..f4811a0 100644 --- a/src/holt59/aoc/2022/day12.py +++ b/src/holt59/aoc/2022/day12.py @@ -1,6 +1,7 @@ import heapq -import sys -from typing import Callable, Iterator, TypeVar +from typing import Any, Callable, Iterator, TypeVar + +from ..base import BaseSolver Node = TypeVar("Node") @@ -68,30 +69,6 @@ def make_path(parents: dict[Node, Node], start: Node, end: Node) -> list[Node] | return list(reversed(path)) -def print_path(path: list[tuple[int, int]], n_rows: int, n_cols: int) -> None: - end = path[-1] - - graph = [["." for _c in range(n_cols)] for _r in range(n_rows)] - graph[end[0]][end[1]] = "E" - - for i in range(0, len(path) - 1): - cr, cc = path[i] - nr, nc = path[i + 1] - - if cr == nr and nc == cc - 1: - graph[cr][cc] = "<" - elif cr == nr and nc == cc + 1: - graph[cr][cc] = ">" - elif cr == nr - 1 and nc == cc: - graph[cr][cc] = "v" - elif cr == nr + 1 and nc == cc: - graph[cr][cc] = "^" - else: - assert False, "{} -> {} infeasible".format(path[i], path[i + 1]) - - print("\n".join("".join(row) for row in graph)) - - def neighbors( grid: list[list[int]], node: tuple[int, int], up: bool ) -> Iterator[tuple[int, int]]: @@ -118,46 +95,74 @@ def neighbors( # === main code === -lines = sys.stdin.read().splitlines() -grid = [[ord(cell) - ord("a") for cell in line] for line in lines] +class Solver(BaseSolver): + def print_path(self, path: list[tuple[int, int]], n_rows: int, n_cols: int) -> None: + end = path[-1] -start: tuple[int, int] | None = None -end: tuple[int, int] | None = None + graph = [["." for _c in range(n_cols)] for _r in range(n_rows)] + graph[end[0]][end[1]] = "E" -# for part 2 -start_s: list[tuple[int, int]] = [] + for i in range(0, len(path) - 1): + cr, cc = path[i] + nr, nc = path[i + 1] -for i_row, row in enumerate(grid): - for i_col, col in enumerate(row): - if chr(col + ord("a")) == "S": - start = (i_row, i_col) - start_s.append(start) - elif chr(col + ord("a")) == "E": - end = (i_row, i_col) - elif col == 0: - start_s.append((i_row, i_col)) + if cr == nr and nc == cc - 1: + graph[cr][cc] = "<" + elif cr == nr and nc == cc + 1: + graph[cr][cc] = ">" + elif cr == nr - 1 and nc == cc: + graph[cr][cc] = "v" + elif cr == nr + 1 and nc == cc: + graph[cr][cc] = "^" + else: + assert False, "{} -> {} infeasible".format(path[i], path[i + 1]) -assert start is not None -assert end is not None + for row in graph: + self.logger.info("".join(row)) -# fix values -grid[start[0]][start[1]] = 0 -grid[end[0]][end[1]] = ord("z") - ord("a") + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() + grid = [[ord(cell) - ord("a") for cell in line] for line in lines] -lengths_1, parents_1 = dijkstra( - start=start, neighbors=lambda n: neighbors(grid, n, True), cost=lambda lhs, rhs: 1 -) -path_1 = make_path(parents_1, start, end) -assert path_1 is not None + start: tuple[int, int] | None = None + end: tuple[int, int] | None = None -print_path(path_1, n_rows=len(grid), n_cols=len(grid[0])) + # for part 2 + start_s: list[tuple[int, int]] = [] -print(f"answer 1 is {lengths_1[end] - 1}") + for i_row, row in enumerate(grid): + for i_col, col in enumerate(row): + if chr(col + ord("a")) == "S": + start = (i_row, i_col) + start_s.append(start) + elif chr(col + ord("a")) == "E": + end = (i_row, i_col) + elif col == 0: + start_s.append((i_row, i_col)) -lengths_2, parents_2 = dijkstra( - start=end, neighbors=lambda n: neighbors(grid, n, False), cost=lambda lhs, rhs: 1 -) -answer_2 = min(lengths_2.get(start, float("inf")) for start in start_s) -print(f"answer 2 is {answer_2}") + assert start is not None + assert end is not None + + # fix values + grid[start[0]][start[1]] = 0 + grid[end[0]][end[1]] = ord("z") - ord("a") + + lengths_1, parents_1 = dijkstra( + start=start, + neighbors=lambda n: neighbors(grid, n, True), + cost=lambda lhs, rhs: 1, + ) + path_1 = make_path(parents_1, start, end) + assert path_1 is not None + + self.print_path(path_1, n_rows=len(grid), n_cols=len(grid[0])) + yield lengths_1[end] - 1 + + lengths_2, _ = dijkstra( + start=end, + neighbors=lambda n: neighbors(grid, n, False), + cost=lambda lhs, rhs: 1, + ) + yield min(lengths_2.get(start, float("inf")) for start in start_s) diff --git a/src/holt59/aoc/2022/day13.py b/src/holt59/aoc/2022/day13.py index c9d2361..e2c5d72 100644 --- a/src/holt59/aoc/2022/day13.py +++ b/src/holt59/aoc/2022/day13.py @@ -1,11 +1,8 @@ import json -import sys from functools import cmp_to_key -from typing import TypeAlias, cast +from typing import Any, Iterator, TypeAlias, cast -blocks = sys.stdin.read().strip().split("\n\n") - -pairs = [tuple(json.loads(p) for p in block.split("\n")) for block in blocks] +from ..base import BaseSolver Packet: TypeAlias = list[int | list["Packet"]] @@ -28,14 +25,18 @@ def compare(lhs: Packet, rhs: Packet) -> int: return len(rhs) - len(lhs) -answer_1 = sum(i + 1 for i, (lhs, rhs) in enumerate(pairs) if compare(lhs, rhs) > 0) -print(f"answer_1 is {answer_1}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + blocks = input.split("\n\n") + pairs = [tuple(json.loads(p) for p in block.split("\n")) for block in blocks] -dividers = [[[2]], [[6]]] + yield sum(i + 1 for i, (lhs, rhs) in enumerate(pairs) if compare(lhs, rhs) > 0) -packets = [packet for packets in pairs for packet in packets] -packets.extend(dividers) -packets = list(reversed(sorted(packets, key=cmp_to_key(compare)))) + dividers = [[[2]], [[6]]] -d_index = [packets.index(d) + 1 for d in dividers] -print(f"answer 2 is {d_index[0] * d_index[1]}") + packets = [packet for packets in pairs for packet in packets] + packets.extend(dividers) + packets = list(reversed(sorted(packets, key=cmp_to_key(compare)))) + + d_index = [packets.index(d) + 1 for d in dividers] + yield d_index[0] * d_index[1] diff --git a/src/holt59/aoc/2022/day14.py b/src/holt59/aoc/2022/day14.py index 17d937c..4603157 100644 --- a/src/holt59/aoc/2022/day14.py +++ b/src/holt59/aoc/2022/day14.py @@ -1,6 +1,7 @@ -import sys from enum import Enum, auto -from typing import Callable, cast +from typing import Any, Callable, Iterator, cast + +from ..base import BaseSolver class Cell(Enum): @@ -12,26 +13,6 @@ class Cell(Enum): return {Cell.AIR: ".", Cell.ROCK: "#", Cell.SAND: "O"}[self] -def print_blocks(blocks: dict[tuple[int, int], Cell]): - """ - Print the given set of blocks on a grid. - - Args: - blocks: Set of blocks to print. - """ - x_min, y_min, x_max, y_max = ( - min(x for x, _ in blocks), - 0, - max(x for x, _ in blocks), - max(y for _, y in blocks), - ) - - for y in range(y_min, y_max + 1): - print( - "".join(str(blocks.get((x, y), Cell.AIR)) for x in range(x_min, x_max + 1)) - ) - - def flow( blocks: dict[tuple[int, int], Cell], stop_fn: Callable[[int, int], bool], @@ -84,57 +65,75 @@ def flow( # === inputs === -lines = sys.stdin.read().splitlines() -paths: list[list[tuple[int, int]]] = [] -for line in lines: - parts = line.split(" -> ") - paths.append( - [ - cast(tuple[int, int], tuple(int(c.strip()) for c in part.split(","))) - for part in parts - ] - ) +class Solver(BaseSolver): + def print_blocks(self, blocks: dict[tuple[int, int], Cell]): + """ + Print the given set of blocks on a grid. + Args: + blocks: Set of blocks to print. + """ + x_min, y_min, x_max, y_max = ( + min(x for x, _ in blocks), + 0, + max(x for x, _ in blocks), + max(y for _, y in blocks), + ) -blocks: dict[tuple[int, int], Cell] = {} -for path in paths: - for start, end in zip(path[:-1], path[1:]): - x_start = min(start[0], end[0]) - x_end = max(start[0], end[0]) + 1 - y_start = min(start[1], end[1]) - y_end = max(start[1], end[1]) + 1 + for y in range(y_min, y_max + 1): + self.logger.info( + "".join( + str(blocks.get((x, y), Cell.AIR)) for x in range(x_min, x_max + 1) + ) + ) - for x in range(x_start, x_end): - for y in range(y_start, y_end): - blocks[x, y] = Cell.ROCK + def solve(self, input: str) -> Iterator[Any]: + lines = [line.strip() for line in input.splitlines()] -print_blocks(blocks) -print() + paths: list[list[tuple[int, int]]] = [] + for line in lines: + parts = line.split(" -> ") + paths.append( + [ + cast( + tuple[int, int], tuple(int(c.strip()) for c in part.split(",")) + ) + for part in parts + ] + ) -x_min, y_min, x_max, y_max = ( - min(x for x, _ in blocks), - 0, - max(x for x, _ in blocks), - max(y for _, y in blocks), -) + blocks: dict[tuple[int, int], Cell] = {} + for path in paths: + for start, end in zip(path[:-1], path[1:]): + x_start = min(start[0], end[0]) + x_end = max(start[0], end[0]) + 1 + y_start = min(start[1], end[1]) + y_end = max(start[1], end[1]) + 1 -# === part 1 === + for x in range(x_start, x_end): + for y in range(y_start, y_end): + blocks[x, y] = Cell.ROCK -blocks_1 = flow( - blocks.copy(), stop_fn=lambda x, y: y > y_max, fill_fn=lambda x, y: Cell.AIR -) -print_blocks(blocks_1) -print(f"answer 1 is {sum(v == Cell.SAND for v in blocks_1.values())}") -print() + self.print_blocks(blocks) -# === part 2 === + y_max = max(y for _, y in blocks) -blocks_2 = flow( - blocks.copy(), - stop_fn=lambda x, y: x == 500 and y == 0, - fill_fn=lambda x, y: Cell.AIR if y < y_max + 2 else Cell.ROCK, -) -blocks_2[500, 0] = Cell.SAND -print_blocks(blocks_2) -print(f"answer 2 is {sum(v == Cell.SAND for v in blocks_2.values())}") + # === part 1 === + + blocks_1 = flow( + blocks.copy(), stop_fn=lambda x, y: y > y_max, fill_fn=lambda x, y: Cell.AIR + ) + self.print_blocks(blocks_1) + yield sum(v == Cell.SAND for v in blocks_1.values()) + + # === part 2 === + + blocks_2 = flow( + blocks.copy(), + stop_fn=lambda x, y: x == 500 and y == 0, + fill_fn=lambda x, y: Cell.AIR if y < y_max + 2 else Cell.ROCK, + ) + blocks_2[500, 0] = Cell.SAND + self.print_blocks(blocks_2) + yield sum(v == Cell.SAND for v in blocks_2.values()) diff --git a/src/holt59/aoc/2022/day15.py b/src/holt59/aoc/2022/day15.py index fb13272..ac9bba8 100644 --- a/src/holt59/aoc/2022/day15.py +++ b/src/holt59/aoc/2022/day15.py @@ -1,4 +1,4 @@ -import sys +import itertools as it from typing import Any, Iterator import numpy as np @@ -21,9 +21,7 @@ class Solver(BaseSolver): no_beacons_row_l.append(sx + np.arange(0, d - abs(sy - row) + 1)) # type: ignore beacons_at_row = set(bx for (bx, by) in sensor_to_beacon.values() if by == row) - no_beacons_row = set(np.concatenate(no_beacons_row_l)).difference( - beacons_at_row - ) # type: ignore + no_beacons_row = set(it.chain(*no_beacons_row_l)).difference(beacons_at_row) # type: ignore return len(no_beacons_row) @@ -62,8 +60,9 @@ class Solver(BaseSolver): for (sx, sy), (bx, by) in sensor_to_beacon.items(): d = abs(sx - bx) + abs(sy - by) m.add_constraint( - m.abs(x - sx) + m.abs(y - sy) >= d + 1, ctname=f"ct_{sx}_{sy}" - ) # type: ignore + m.abs(x - sx) + m.abs(y - sy) >= d + 1, # type: ignore + ctname=f"ct_{sx}_{sy}", + ) m.set_objective("min", x + y) @@ -92,5 +91,5 @@ class Solver(BaseSolver): # x, y, a2 = part2_cplex(sensor_to_beacon, xy_max) x, y, a2 = self.part2_intervals(sensor_to_beacon, xy_max) - self.logger.info("answer 2 is {at} (x={x}, y={y})") + self.logger.info(f"answer 2 is {a2} (x={x}, y={y})") yield a2 diff --git a/src/holt59/aoc/2022/day16.py b/src/holt59/aoc/2022/day16.py index 055ed25..f9068a5 100644 --- a/src/holt59/aoc/2022/day16.py +++ b/src/holt59/aoc/2022/day16.py @@ -3,12 +3,13 @@ from __future__ import annotations import heapq import itertools import re -import sys from collections import defaultdict -from typing import FrozenSet, NamedTuple +from typing import Any, FrozenSet, Iterator, NamedTuple from tqdm import tqdm +from ..base import BaseSolver + class Pipe(NamedTuple): name: str @@ -36,8 +37,8 @@ def breadth_first_search(pipes: dict[str, Pipe], pipe: Pipe) -> dict[Pipe, int]: Runs a BFS from the given pipe and return the shortest distance (in term of hops) to all other pipes. """ - queue = [(0, pipe_1)] - visited = set() + queue = [(0, pipe)] + visited: set[Pipe] = set() distances: dict[Pipe, int] = {} while len(distances) < len(pipes): @@ -122,37 +123,37 @@ def part_2( # === MAIN === -lines = sys.stdin.read().splitlines() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = [line.strip() for line in input.splitlines()] + pipes: dict[str, Pipe] = {} + for line in lines: + r = re.match( + R"Valve ([A-Z]+) has flow rate=([0-9]+); tunnels? leads? to valves? (.+)", + line, + ) + assert r -pipes: dict[str, Pipe] = {} -for line in lines: - r = re.match( - R"Valve ([A-Z]+) has flow rate=([0-9]+); tunnels? leads? to valves? (.+)", - line, - ) - assert r + g = r.groups() - g = r.groups() + pipes[g[0]] = Pipe(g[0], int(g[1]), g[2].split(", ")) - pipes[g[0]] = Pipe(g[0], int(g[1]), g[2].split(", ")) + # compute distances from one valve to any other + distances: dict[tuple[Pipe, Pipe], int] = {} + for pipe_1 in pipes.values(): + distances.update( + { + (pipe_1, pipe_2): distance + for pipe_2, distance in breadth_first_search(pipes, pipe_1).items() + } + ) -# compute distances from one valve to any other -distances: dict[tuple[Pipe, Pipe], int] = {} -for pipe_1 in pipes.values(): - distances.update( - { - (pipe_1, pipe_2): distance - for pipe_2, distance in breadth_first_search(pipes, pipe_1).items() - } - ) + # valves with flow + relevant_pipes = frozenset(pipe for pipe in pipes.values() if pipe.flow > 0) -# valves with flow -relevant_pipes = frozenset(pipe for pipe in pipes.values() if pipe.flow > 0) + # 1651, 1653 + yield part_1(pipes["AA"], 30, distances, relevant_pipes) - -# 1651, 1653 -print(part_1(pipes["AA"], 30, distances, relevant_pipes)) - -# 1707, 2223 -print(part_2(pipes["AA"], 26, distances, relevant_pipes)) + # 1707, 2223 + yield part_2(pipes["AA"], 26, distances, relevant_pipes) diff --git a/src/holt59/aoc/2022/day17.py b/src/holt59/aoc/2022/day17.py index 6f02c26..4d22d78 100644 --- a/src/holt59/aoc/2022/day17.py +++ b/src/holt59/aoc/2022/day17.py @@ -1,12 +1,16 @@ -import sys -from typing import Sequence, TypeVar +from typing import Any, Iterator, Sequence, TypeAlias, TypeVar import numpy as np +from numpy.typing import NDArray + +from ..base import BaseSolver T = TypeVar("T") +Tower: TypeAlias = NDArray[np.bool] -def print_tower(tower: np.ndarray, out: str = "#"): + +def print_tower(tower: Tower, out: str = "#"): print("-" * (tower.shape[1] + 2)) non_empty = False for row in reversed(range(1, tower.shape[0])): @@ -17,7 +21,7 @@ def print_tower(tower: np.ndarray, out: str = "#"): print("+" + "-" * tower.shape[1] + "+") -def tower_height(tower: np.ndarray) -> int: +def tower_height(tower: Tower) -> int: return int(tower.shape[0] - tower[::-1, :].argmax(axis=0).min() - 1) @@ -45,8 +49,8 @@ def build_tower( n_rocks: int, jets: str, early_stop: bool = False, - init: np.ndarray = np.ones(WIDTH, dtype=bool), -) -> tuple[np.ndarray, int, int, dict[int, int]]: + init: Tower = np.ones(WIDTH, dtype=bool), +) -> tuple[Tower, int, int, dict[int, int]]: tower = EMPTY_BLOCKS.copy() tower[0, :] = init @@ -95,26 +99,24 @@ def build_tower( return tower, rock_count, done_at.get((i_rock, i_jet), -1), heights -line = sys.stdin.read().strip() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + tower, *_ = build_tower(2022, input) + yield tower_height(tower) -tower, *_ = build_tower(2022, line) -answer_1 = tower_height(tower) -print(f"answer 1 is {answer_1}") + TOTAL_ROCKS = 1_000_000_000_000 + _tower_1, n_rocks_1, prev_1, heights_1 = build_tower(TOTAL_ROCKS, input, True) + assert prev_1 > 0 -TOTAL_ROCKS = 1_000_000_000_000 -tower_1, n_rocks_1, prev_1, heights_1 = build_tower(TOTAL_ROCKS, line, True) -assert prev_1 > 0 + # 2767 1513 + remaining_rocks = TOTAL_ROCKS - n_rocks_1 + n_repeat_rocks = n_rocks_1 - prev_1 + n_repeat_towers = remaining_rocks // n_repeat_rocks -# 2767 1513 -remaining_rocks = TOTAL_ROCKS - n_rocks_1 -n_repeat_rocks = n_rocks_1 - prev_1 -n_repeat_towers = remaining_rocks // n_repeat_rocks + base_height = heights_1[prev_1] + repeat_height = heights_1[prev_1 + n_repeat_rocks - 1] - heights_1[prev_1] + remaining_height = ( + heights_1[prev_1 + remaining_rocks % n_repeat_rocks] - heights_1[prev_1] + ) -base_height = heights_1[prev_1] -repeat_height = heights_1[prev_1 + n_repeat_rocks - 1] - heights_1[prev_1] -remaining_height = ( - heights_1[prev_1 + remaining_rocks % n_repeat_rocks] - heights_1[prev_1] -) - -answer_2 = base_height + (n_repeat_towers + 1) * repeat_height + remaining_height -print(f"answer 2 is {answer_2}") + yield base_height + (n_repeat_towers + 1) * repeat_height + remaining_height diff --git a/src/holt59/aoc/2022/day18.py b/src/holt59/aoc/2022/day18.py index c8d0f9a..4a93b3b 100644 --- a/src/holt59/aoc/2022/day18.py +++ b/src/holt59/aoc/2022/day18.py @@ -1,50 +1,58 @@ -import sys +from typing import Any, Iterator import numpy as np -xyz = np.asarray( - [ - tuple(int(x) for x in row.split(",")) # type: ignore - for row in sys.stdin.read().splitlines() - ] -) +from ..base import BaseSolver -xyz = xyz - xyz.min(axis=0) + 1 -cubes = np.zeros(xyz.max(axis=0) + 3, dtype=bool) -cubes[xyz[:, 0], xyz[:, 1], xyz[:, 2]] = True +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + xyz = np.asarray( + [ + tuple(int(x) for x in row.split(",")) # type: ignore + for row in input.splitlines() + ] + ) -n_dims = len(cubes.shape) + xyz = xyz - xyz.min(axis=0) + 1 -faces = [(-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, 1, 0), (0, 0, -1), (0, 0, 1)] + cubes = np.zeros(xyz.max(axis=0) + 3, dtype=bool) + cubes[xyz[:, 0], xyz[:, 1], xyz[:, 2]] = True -answer_1 = sum( - 1 for x, y, z in xyz for dx, dy, dz in faces if not cubes[x + dx, y + dy, z + dz] -) -print(f"answer 1 is {answer_1}") + faces = [(-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, 1, 0), (0, 0, -1), (0, 0, 1)] -visited = np.zeros_like(cubes, dtype=bool) -queue = [(0, 0, 0)] + yield sum( + 1 + for x, y, z in xyz + for dx, dy, dz in faces + if not cubes[x + dx, y + dy, z + dz] + ) -n_faces = 0 -while queue: - x, y, z = queue.pop(0) + visited = np.zeros_like(cubes, dtype=bool) + queue = [(0, 0, 0)] - if visited[x, y, z]: - continue + n_faces = 0 + while queue: + x, y, z = queue.pop(0) - visited[x, y, z] = True + if visited[x, y, z]: + continue - for dx, dy, dz in faces: - nx, ny, nz = x + dx, y + dy, z + dz - if not all(n >= 0 and n < cubes.shape[i] for i, n in enumerate((nx, ny, nz))): - continue + visited[x, y, z] = True - if visited[nx, ny, nz]: - continue + for dx, dy, dz in faces: + nx, ny, nz = x + dx, y + dy, z + dz + if not all( + n >= 0 and n < cubes.shape[i] for i, n in enumerate((nx, ny, nz)) + ): + continue - if cubes[nx, ny, nz]: - n_faces += 1 - else: - queue.append((nx, ny, nz)) -print(f"answer 2 is {n_faces}") + if visited[nx, ny, nz]: + continue + + if cubes[nx, ny, nz]: + n_faces += 1 + else: + queue.append((nx, ny, nz)) + + yield n_faces diff --git a/src/holt59/aoc/2022/day19.py b/src/holt59/aoc/2022/day19.py index 7d888fa..ef4d6bc 100644 --- a/src/holt59/aoc/2022/day19.py +++ b/src/holt59/aoc/2022/day19.py @@ -1,10 +1,11 @@ -import sys -from typing import Any, Literal +from typing import Any, Iterator, Literal import numpy as np import parse # pyright: ignore[reportMissingTypeStubs] from numpy.typing import NDArray +from ..base import BaseSolver + Reagent = Literal["ore", "clay", "obsidian", "geode"] REAGENTS: tuple[Reagent, ...] = ( "ore", @@ -62,29 +63,6 @@ def dominates(lhs: State, rhs: State): ) -lines = sys.stdin.read().splitlines() - -blueprints: list[dict[Reagent, IntOfReagent]] = [] -for line in lines: - r: list[int] = parse.parse( # type: ignore - "Blueprint {}: " - "Each ore robot costs {:d} ore. " - "Each clay robot costs {:d} ore. " - "Each obsidian robot costs {:d} ore and {:d} clay. " - "Each geode robot costs {:d} ore and {:d} obsidian.", - line, - ) - - blueprints.append( - { - "ore": {"ore": r[1]}, - "clay": {"ore": r[2]}, - "obsidian": {"ore": r[3], "clay": r[4]}, - "geode": {"ore": r[5], "obsidian": r[6]}, - } - ) - - def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int: # since we can only build one robot per time, we do not need more than X robots # of type K where X is the maximum number of K required among all robots, e.g., @@ -173,11 +151,31 @@ def run(blueprint: dict[Reagent, dict[Reagent, int]], max_time: int) -> int: return max(state.reagents["geode"] for state in state_after_t[max_time]) -answer_1 = sum( - (i_blueprint + 1) * run(blueprint, 24) - for i_blueprint, blueprint in enumerate(blueprints) -) -print(f"answer 1 is {answer_1}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + blueprints: list[dict[Reagent, IntOfReagent]] = [] + for line in input.splitlines(): + r: list[int] = parse.parse( # type: ignore + "Blueprint {}: " + "Each ore robot costs {:d} ore. " + "Each clay robot costs {:d} ore. " + "Each obsidian robot costs {:d} ore and {:d} clay. " + "Each geode robot costs {:d} ore and {:d} obsidian.", + line, + ) -answer_2 = run(blueprints[0], 32) * run(blueprints[1], 32) * run(blueprints[2], 32) -print(f"answer 2 is {answer_2}") + blueprints.append( + { + "ore": {"ore": r[1]}, + "clay": {"ore": r[2]}, + "obsidian": {"ore": r[3], "clay": r[4]}, + "geode": {"ore": r[5], "obsidian": r[6]}, + } + ) + + yield sum( + (i_blueprint + 1) * run(blueprint, 24) + for i_blueprint, blueprint in enumerate(blueprints) + ) + + yield (run(blueprints[0], 32) * run(blueprints[1], 32) * run(blueprints[2], 32)) diff --git a/src/holt59/aoc/2022/day2.py b/src/holt59/aoc/2022/day2.py index 4d23c7c..c78b283 100644 --- a/src/holt59/aoc/2022/day2.py +++ b/src/holt59/aoc/2022/day2.py @@ -1,4 +1,6 @@ -import sys +from typing import Any, Iterator + +from ..base import BaseSolver def score_1(ux: int, vx: int) -> int: @@ -33,21 +35,23 @@ def score_2(ux: int, vx: int) -> int: return (ux + vx - 1) % 3 + 1 + vx * 3 -lines = sys.stdin.readlines() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = input.splitlines() -# the solution relies on replacing rock / paper / scissor by values 0 / 1 / 2 and using -# modulo-3 arithmetic -# -# in modulo-3 arithmetic, the winning move is 1 + the opponent move (e.g., winning move -# if opponent plays 0 is 1, or 0 if opponent plays 2 (0 = (2 + 1 % 3))) -# + # the solution relies on replacing rock / paper / scissor by values 0 / 1 / 2 and using + # modulo-3 arithmetic + # + # in modulo-3 arithmetic, the winning move is 1 + the opponent move (e.g., winning move + # if opponent plays 0 is 1, or 0 if opponent plays 2 (0 = (2 + 1 % 3))) + # -# we read the lines in a Nx2 in array with value 0/1/2 instead of A/B/C or X/Y/Z for -# easier manipulation -values = [(ord(row[0]) - ord("A"), ord(row[2]) - ord("X")) for row in lines] + # we read the lines in a Nx2 in array with value 0/1/2 instead of A/B/C or X/Y/Z for + # easier manipulation + values = [(ord(row[0]) - ord("A"), ord(row[2]) - ord("X")) for row in lines] -# part 1 - 13526 -print(f"answer 1 is {sum(score_1(*v) for v in values)}") + # part 1 - 13526 + yield sum(score_1(*v) for v in values) -# part 2 - 14204 -print(f"answer 2 is {sum(score_2(*v) for v in values)}") + # part 2 - 14204 + yield sum(score_2(*v) for v in values) diff --git a/src/holt59/aoc/2022/day20.py b/src/holt59/aoc/2022/day20.py index 5448d0a..8c74c35 100644 --- a/src/holt59/aoc/2022/day20.py +++ b/src/holt59/aoc/2022/day20.py @@ -1,6 +1,8 @@ from __future__ import annotations -import sys +from typing import Any, Iterator + +from ..base import BaseSolver class Number: @@ -65,10 +67,9 @@ def decrypt(numbers: list[Number], key: int, rounds: int) -> int: ) -numbers = [Number(int(x)) for i, x in enumerate(sys.stdin.readlines())] +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + numbers = [Number(int(x)) for x in input.splitlines()] -answer_1 = decrypt(numbers, 1, 1) -print(f"answer 1 is {answer_1}") - -answer_2 = decrypt(numbers, 811589153, 10) -print(f"answer 2 is {answer_2}") + yield decrypt(numbers, 1, 1) + yield decrypt(numbers, 811589153, 10) diff --git a/src/holt59/aoc/2022/day21.py b/src/holt59/aoc/2022/day21.py index c5b3736..db9dd47 100644 --- a/src/holt59/aoc/2022/day21.py +++ b/src/holt59/aoc/2022/day21.py @@ -1,6 +1,7 @@ import operator -import sys -from typing import Callable +from typing import Any, Callable, Iterator + +from ..base import BaseSolver def compute(monkeys: dict[str, int | tuple[str, str, str]], monkey: str) -> int: @@ -77,31 +78,31 @@ def invert( return monkeys -lines = sys.stdin.read().splitlines() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = [line.strip() for line in input.splitlines()] -monkeys: dict[str, int | tuple[str, str, str]] = {} + monkeys: dict[str, int | tuple[str, str, str]] = {} -op_monkeys: set[str] = set() + op_monkeys: set[str] = set() -for line in lines: - parts = line.split(":") - name = parts[0].strip() + for line in lines: + parts = line.split(":") + name = parts[0].strip() - try: - value = int(parts[1].strip()) - monkeys[name] = value - except ValueError: - op1, ope, op2 = parts[1].strip().split() - monkeys[name] = (op1, ope, op2) + try: + value = int(parts[1].strip()) + monkeys[name] = value + except ValueError: + op1, ope, op2 = parts[1].strip().split() + monkeys[name] = (op1, ope, op2) - op_monkeys.add(name) + op_monkeys.add(name) + yield compute(monkeys.copy(), "root") -answer_1 = compute(monkeys.copy(), "root") -print(f"answer 1 is {answer_1}") - -# assume the second operand of 'root' can be computed, and the first one depends on -# humn, which is the case is my input and the test input -p1, _, p2 = monkeys["root"] # type: ignore -answer_2 = compute(invert(monkeys, "humn", compute(monkeys.copy(), p2)), "humn") -print(f"answer 2 is {answer_2}") + # assume the second operand of 'root' can be computed, and the first one depends on + # humn, which is the case is my input and the test input + assert isinstance(monkeys["root"], tuple) + p1, _, p2 = monkeys["root"] # type: ignore + yield compute(invert(monkeys, "humn", compute(monkeys.copy(), p2)), "humn") diff --git a/src/holt59/aoc/2022/day22.py b/src/holt59/aoc/2022/day22.py index 571f314..510791b 100644 --- a/src/holt59/aoc/2022/day22.py +++ b/src/holt59/aoc/2022/day22.py @@ -1,223 +1,243 @@ import re -import sys -from typing import Callable +from typing import Any, Callable, Iterator import numpy as np +from ..base import BaseSolver + VOID, EMPTY, WALL = 0, 1, 2 TILE_FROM_CHAR = {" ": VOID, ".": EMPTY, "#": WALL} SCORES = {"E": 0, "S": 1, "W": 2, "N": 3} -board_map_s, direction_s = sys.stdin.read().split("\n\n") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + board_map_s, direction_s = input.split("\n\n") -# board -board_lines = board_map_s.splitlines() -max_line = max(len(line) for line in board_lines) -board = np.array( - [ - [TILE_FROM_CHAR[c] for c in row] + [VOID] * (max_line - len(row)) - for row in board_map_s.splitlines() - ] -) + # board + board_lines = board_map_s.splitlines() + max_line = max(len(line) for line in board_lines) + board = np.array( + [ + [TILE_FROM_CHAR[c] for c in row] + [VOID] * (max_line - len(row)) + for row in board_map_s.splitlines() + ] + ) -directions = [ - int(p1) if p2 else p1 for p1, p2 in re.findall(R"(([0-9])+|L|R)", direction_s) -] + directions = [ + int(p1) if p2 else p1 + for p1, p2 in re.findall(R"(([0-9])+|L|R)", direction_s) + ] + # find on each row and column the first and last non-void + row_first_non_void = np.argmax(board != VOID, axis=1) + row_last_non_void = ( + board.shape[1] - np.argmax(board[:, ::-1] != VOID, axis=1) - 1 + ) + col_first_non_void = np.argmax(board != VOID, axis=0) + col_last_non_void = ( + board.shape[0] - np.argmax(board[::-1, :] != VOID, axis=0) - 1 + ) -# find on each row and column the first and last non-void -row_first_non_void = np.argmax(board != VOID, axis=1) -row_last_non_void = board.shape[1] - np.argmax(board[:, ::-1] != VOID, axis=1) - 1 -col_first_non_void = np.argmax(board != VOID, axis=0) -col_last_non_void = board.shape[0] - np.argmax(board[::-1, :] != VOID, axis=0) - 1 + faces = np.zeros_like(board) + size = np.gcd(board.shape[0], board.shape[1]) + for row in range(0, board.shape[0], size): + for col in range(row_first_non_void[row], row_last_non_void[row], size): + faces[row : row + size, col : col + size] = faces.max() + 1 + SIZE = np.gcd(*board.shape) -faces = np.zeros_like(board) -size = np.gcd(board.shape[0], board.shape[1]) -for row in range(0, board.shape[0], size): - for col in range(row_first_non_void[row], row_last_non_void[row], size): - faces[row : row + size, col : col + size] = faces.max() + 1 + # TODO: deduce this from the actual cube... + faces_wrap: dict[int, dict[str, Callable[[int, int], tuple[int, int, str]]]] -SIZE = np.gcd(*board.shape) + if board.shape == (12, 16): # example + faces_wrap = { + 1: { + "W": lambda y, x: (4, 4 + y, "S"), # 3N + "N": lambda y, x: (4, 11 - x, "S"), # 2N + "E": lambda y, x: (11 - y, 15, "W"), # 6E + }, + 2: { + "W": lambda y, x: (11, 19 - y, "N"), # 6S + "N": lambda y, x: (0, 11 - y, "S"), # 1N + "S": lambda y, x: (11, 11 - x, "N"), # 5S + }, + 3: { + "N": lambda y, x: (x - 4, 8, "E"), # 1W + "S": lambda y, x: (15 - x, 8, "E"), # 5W + }, + 4: {"E": lambda y, x: (8, 19 - y, "S")}, # 6N + 5: { + "W": lambda y, x: (7, 15 - y, "N"), # 3S + "S": lambda y, x: (7, 11 - x, "N"), # 2S + }, + 6: { + "N": lambda y, x: (19 - x, 11, "W"), # 4E + "E": lambda y, x: (11 - y, 11, "W"), # 1E + "S": lambda y, x: (19 - x, 0, "E"), # 2W + }, + } -# TODO: deduce this from the actual cube... -faces_wrap: dict[int, dict[str, Callable[[int, int], tuple[int, int, str]]]] - -if board.shape == (12, 16): # example - faces_wrap = { - 1: { - "W": lambda y, x: (4, 4 + y, "S"), # 3N - "N": lambda y, x: (4, 11 - x, "S"), # 2N - "E": lambda y, x: (11 - y, 15, "W"), # 6E - }, - 2: { - "W": lambda y, x: (11, 19 - y, "N"), # 6S - "N": lambda y, x: (0, 11 - y, "S"), # 1N - "S": lambda y, x: (11, 11 - x, "N"), # 5S - }, - 3: { - "N": lambda y, x: (x - 4, 8, "E"), # 1W - "S": lambda y, x: (15 - x, 8, "E"), # 5W - }, - 4: {"E": lambda y, x: (8, 19 - y, "S")}, # 6N - 5: { - "W": lambda y, x: (7, 15 - y, "N"), # 3S - "S": lambda y, x: (7, 11 - x, "N"), # 2S - }, - 6: { - "N": lambda y, x: (19 - x, 11, "W"), # 4E - "E": lambda y, x: (11 - y, 11, "W"), # 1E - "S": lambda y, x: (19 - x, 0, "E"), # 2W - }, - } - -else: - faces_wrap = { - 1: { - "W": lambda y, x: (3 * SIZE - y - 1, 0, "E"), # 4W - "N": lambda y, x: (2 * SIZE + x, 0, "E"), # 6W - }, - 2: { - "N": lambda y, x: (4 * SIZE - 1, x - 2 * SIZE, "N"), # 6S - "E": lambda y, x: (3 * SIZE - y - 1, 2 * SIZE - 1, "W"), # 5E - "S": lambda y, x: (x - SIZE, 2 * SIZE - 1, "W"), # 3E - }, - 3: { - "W": lambda y, x: (2 * SIZE, y - SIZE, "S"), # 4N - "E": lambda y, x: (SIZE - 1, SIZE + y, "N"), # 2S - }, - 4: { - "W": lambda y, x: (3 * SIZE - y - 1, SIZE, "E"), # 1W - "N": lambda y, x: (SIZE + x, SIZE, "E"), # 3W - }, - 5: { - "E": lambda y, x: (3 * SIZE - y - 1, 3 * SIZE - 1, "W"), # 2E - "S": lambda y, x: (2 * SIZE + x, SIZE - 1, "W"), # 6E - }, - 6: { - "W": lambda y, x: (0, y - 2 * SIZE, "S"), # 1N - "E": lambda y, x: (3 * SIZE - 1, y - 2 * SIZE, "N"), # 5S - "S": lambda y, x: (0, x + 2 * SIZE, "S"), # 2N - }, - } - - -def wrap_part_1(y0: int, x0: int, r0: str) -> tuple[int, int, str]: - if r0 == "E": - return y0, row_first_non_void[y0], r0 - elif r0 == "S": - return col_first_non_void[x0], x0, r0 - elif r0 == "W": - return y0, row_last_non_void[y0], r0 - elif r0 == "N": - return col_last_non_void[x0], x0, r0 - - assert False - - -def wrap_part_2(y0: int, x0: int, r0: str) -> tuple[int, int, str]: - cube = faces[y0, x0] - assert r0 in faces_wrap[cube] - return faces_wrap[cube][r0](y0, x0) - - -def run(wrap: Callable[[int, int, str], tuple[int, int, str]]) -> tuple[int, int, str]: - y0 = 0 - x0 = np.where(board[0] == EMPTY)[0][0] - r0 = "E" - - for direction in directions: - if isinstance(direction, int): - while direction > 0: - if r0 == "E": - xi = np.where(board[y0, x0 + 1 : x0 + direction + 1] == WALL)[0] - if len(xi): - x0 = x0 + xi[0] - direction = 0 - elif ( - x0 + direction < board.shape[1] - and board[y0, x0 + direction] == EMPTY - ): - x0 = x0 + direction - direction = 0 - else: - y0_t, x0_t, r0_t = wrap(y0, x0, r0) - if board[y0_t, x0_t] == WALL: - x0 = row_last_non_void[y0] - direction = 0 - else: - direction = direction - (row_last_non_void[y0] - x0) - 1 - y0, x0, r0 = y0_t, x0_t, r0_t - elif r0 == "S": - yi = np.where(board[y0 + 1 : y0 + direction + 1, x0] == WALL)[0] - if len(yi): - y0 = y0 + yi[0] - direction = 0 - elif ( - y0 + direction < board.shape[0] - and board[y0 + direction, x0] == EMPTY - ): - y0 = y0 + direction - direction = 0 - else: - y0_t, x0_t, r0_t = wrap(y0, x0, r0) - if board[y0_t, x0_t] == WALL: - y0 = col_last_non_void[x0] - direction = 0 - else: - direction = direction - (col_last_non_void[x0] - y0) - 1 - y0, x0, r0 = y0_t, x0_t, r0_t - elif r0 == "W": - left = max(x0 - direction - 1, 0) - xi = np.where(board[y0, left:x0] == WALL)[0] - if len(xi): - x0 = left + xi[-1] + 1 - direction = 0 - elif x0 - direction >= 0 and board[y0, x0 - direction] == EMPTY: - x0 = x0 - direction - direction = 0 - else: - y0_t, x0_t, r0_t = wrap(y0, x0, r0) - if board[y0_t, x0_t] == WALL: - x0 = row_first_non_void[y0] - direction = 0 - else: - direction = direction - (x0 - row_first_non_void[y0]) - 1 - y0, x0, r0 = y0_t, x0_t, r0_t - elif r0 == "N": - top = max(y0 - direction - 1, 0) - yi = np.where(board[top:y0, x0] == WALL)[0] - if len(yi): - y0 = top + yi[-1] + 1 - direction = 0 - elif y0 - direction >= 0 and board[y0 - direction, x0] == EMPTY: - y0 = y0 - direction - direction = 0 - else: - y0_t, x0_t, r0_t = wrap(y0, x0, r0) - if board[y0_t, x0_t] == WALL: - y0 = col_first_non_void[x0] - direction = 0 - else: - direction = direction - (y0 - col_first_non_void[x0]) - 1 - y0, x0, r0 = y0_t, x0_t, r0_t else: - r0 = { - "E": {"L": "N", "R": "S"}, - "N": {"L": "W", "R": "E"}, - "W": {"L": "S", "R": "N"}, - "S": {"L": "E", "R": "W"}, - }[r0][direction] + faces_wrap = { + 1: { + "W": lambda y, x: (3 * SIZE - y - 1, 0, "E"), # 4W + "N": lambda y, x: (2 * SIZE + x, 0, "E"), # 6W + }, + 2: { + "N": lambda y, x: (4 * SIZE - 1, x - 2 * SIZE, "N"), # 6S + "E": lambda y, x: (3 * SIZE - y - 1, 2 * SIZE - 1, "W"), # 5E + "S": lambda y, x: (x - SIZE, 2 * SIZE - 1, "W"), # 3E + }, + 3: { + "W": lambda y, x: (2 * SIZE, y - SIZE, "S"), # 4N + "E": lambda y, x: (SIZE - 1, SIZE + y, "N"), # 2S + }, + 4: { + "W": lambda y, x: (3 * SIZE - y - 1, SIZE, "E"), # 1W + "N": lambda y, x: (SIZE + x, SIZE, "E"), # 3W + }, + 5: { + "E": lambda y, x: (3 * SIZE - y - 1, 3 * SIZE - 1, "W"), # 2E + "S": lambda y, x: (2 * SIZE + x, SIZE - 1, "W"), # 6E + }, + 6: { + "W": lambda y, x: (0, y - 2 * SIZE, "S"), # 1N + "E": lambda y, x: (3 * SIZE - 1, y - 2 * SIZE, "N"), # 5S + "S": lambda y, x: (0, x + 2 * SIZE, "S"), # 2N + }, + } - return y0, x0, r0 + def wrap_part_1(y0: int, x0: int, r0: str) -> tuple[int, int, str]: + if r0 == "E": + return y0, row_first_non_void[y0], r0 + elif r0 == "S": + return col_first_non_void[x0], x0, r0 + elif r0 == "W": + return y0, row_last_non_void[y0], r0 + elif r0 == "N": + return col_last_non_void[x0], x0, r0 + assert False -y1, x1, r1 = run(wrap_part_1) -answer_1 = 1000 * (1 + y1) + 4 * (1 + x1) + SCORES[r1] -print(f"answer 1 is {answer_1}") + def wrap_part_2(y0: int, x0: int, r0: str) -> tuple[int, int, str]: + cube = faces[y0, x0] + assert r0 in faces_wrap[cube] + return faces_wrap[cube][r0](y0, x0) -y2, x2, r2 = run(wrap_part_2) -answer_2 = 1000 * (1 + y2) + 4 * (1 + x2) + SCORES[r2] -print(f"answer 2 is {answer_2}") + def run( + wrap: Callable[[int, int, str], tuple[int, int, str]], + ) -> tuple[int, int, str]: + y0 = 0 + x0 = np.where(board[0] == EMPTY)[0][0] + r0 = "E" + + for direction in directions: + if isinstance(direction, int): + while direction > 0: + if r0 == "E": + xi = np.where( + board[y0, x0 + 1 : x0 + direction + 1] == WALL + )[0] + if len(xi): + x0 = x0 + xi[0] + direction = 0 + elif ( + x0 + direction < board.shape[1] + and board[y0, x0 + direction] == EMPTY + ): + x0 = x0 + direction + direction = 0 + else: + y0_t, x0_t, r0_t = wrap(y0, x0, r0) + if board[y0_t, x0_t] == WALL: + x0 = row_last_non_void[y0] + direction = 0 + else: + direction = ( + direction - (row_last_non_void[y0] - x0) - 1 + ) + y0, x0, r0 = y0_t, x0_t, r0_t + elif r0 == "S": + yi = np.where( + board[y0 + 1 : y0 + direction + 1, x0] == WALL + )[0] + if len(yi): + y0 = y0 + yi[0] + direction = 0 + elif ( + y0 + direction < board.shape[0] + and board[y0 + direction, x0] == EMPTY + ): + y0 = y0 + direction + direction = 0 + else: + y0_t, x0_t, r0_t = wrap(y0, x0, r0) + if board[y0_t, x0_t] == WALL: + y0 = col_last_non_void[x0] + direction = 0 + else: + direction = ( + direction - (col_last_non_void[x0] - y0) - 1 + ) + y0, x0, r0 = y0_t, x0_t, r0_t + elif r0 == "W": + left = max(x0 - direction - 1, 0) + xi = np.where(board[y0, left:x0] == WALL)[0] + if len(xi): + x0 = left + xi[-1] + 1 + direction = 0 + elif ( + x0 - direction >= 0 + and board[y0, x0 - direction] == EMPTY + ): + x0 = x0 - direction + direction = 0 + else: + y0_t, x0_t, r0_t = wrap(y0, x0, r0) + if board[y0_t, x0_t] == WALL: + x0 = row_first_non_void[y0] + direction = 0 + else: + direction = ( + direction - (x0 - row_first_non_void[y0]) - 1 + ) + y0, x0, r0 = y0_t, x0_t, r0_t + elif r0 == "N": + top = max(y0 - direction - 1, 0) + yi = np.where(board[top:y0, x0] == WALL)[0] + if len(yi): + y0 = top + yi[-1] + 1 + direction = 0 + elif ( + y0 - direction >= 0 + and board[y0 - direction, x0] == EMPTY + ): + y0 = y0 - direction + direction = 0 + else: + y0_t, x0_t, r0_t = wrap(y0, x0, r0) + if board[y0_t, x0_t] == WALL: + y0 = col_first_non_void[x0] + direction = 0 + else: + direction = ( + direction - (y0 - col_first_non_void[x0]) - 1 + ) + y0, x0, r0 = y0_t, x0_t, r0_t + else: + r0 = { + "E": {"L": "N", "R": "S"}, + "N": {"L": "W", "R": "E"}, + "W": {"L": "S", "R": "N"}, + "S": {"L": "E", "R": "W"}, + }[r0][direction] + + return y0, x0, r0 + + y1, x1, r1 = run(wrap_part_1) + yield 1000 * (1 + y1) + 4 * (1 + x1) + SCORES[r1] + + y2, x2, r2 = run(wrap_part_2) + yield 1000 * (1 + y2) + 4 * (1 + x2) + SCORES[r2] diff --git a/src/holt59/aoc/2022/day23.py b/src/holt59/aoc/2022/day23.py index 902c20b..0695dd9 100644 --- a/src/holt59/aoc/2022/day23.py +++ b/src/holt59/aoc/2022/day23.py @@ -1,6 +1,8 @@ import itertools -import sys from collections import defaultdict +from typing import Any, Iterator + +from ..base import BaseSolver Directions = list[ tuple[ @@ -18,7 +20,7 @@ DIRECTIONS: Directions = [ def min_max_yx(positions: set[tuple[int, int]]) -> tuple[int, int, int, int]: - ys, xs = {y for y, x in positions}, {x for y, x in positions} + ys, xs = {y for y, _x in positions}, {x for _y, x in positions} return min(ys), min(xs), max(ys), max(xs) @@ -69,35 +71,38 @@ def round( directions.append(directions.pop(0)) -POSITIONS = { - (i, j) - for i, row in enumerate(sys.stdin.read().splitlines()) - for j, col in enumerate(row) - if col == "#" -} +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + POSITIONS = { + (i, j) + for i, row in enumerate(input.splitlines()) + for j, col in enumerate(row) + if col == "#" + } -# === part 1 === + # === part 1 === -p1, d1 = POSITIONS.copy(), DIRECTIONS.copy() -for r in range(10): - round(p1, d1) + p1, d1 = POSITIONS.copy(), DIRECTIONS.copy() + for _ in range(10): + round(p1, d1) -min_y, min_x, max_y, max_x = min_max_yx(p1) -answer_1 = sum( - (y, x) not in p1 for y in range(min_y, max_y + 1) for x in range(min_x, max_x + 1) -) -print(f"answer 1 is {answer_1}") + min_y, min_x, max_y, max_x = min_max_yx(p1) + yield sum( + (y, x) not in p1 + for y in range(min_y, max_y + 1) + for x in range(min_x, max_x + 1) + ) -# === part 2 === + # === part 2 === -p2, d2 = POSITIONS.copy(), DIRECTIONS.copy() -answer_2 = 0 -while True: - answer_2 += 1 - backup = p2.copy() - round(p2, d2) + p2, d2 = POSITIONS.copy(), DIRECTIONS.copy() + answer_2 = 0 + while True: + answer_2 += 1 + backup = p2.copy() + round(p2, d2) - if backup == p2: - break + if backup == p2: + break -print(f"answer 2 is {answer_2}") + yield answer_2 diff --git a/src/holt59/aoc/2022/day24.py b/src/holt59/aoc/2022/day24.py index 6ea76b9..f7f5cc4 100644 --- a/src/holt59/aoc/2022/day24.py +++ b/src/holt59/aoc/2022/day24.py @@ -1,98 +1,117 @@ import heapq import math -import sys from collections import defaultdict +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() - -winds = { - (i - 1, j - 1, lines[i][j]) - for i in range(1, len(lines) - 1) - for j in range(1, len(lines[i]) - 1) - if lines[i][j] != "." -} - -n_rows, n_cols = len(lines) - 2, len(lines[0]) - 2 -CYCLE = math.lcm(n_rows, n_cols) - -east_winds = [{j for j in range(n_cols) if (i, j, ">") in winds} for i in range(n_rows)] -west_winds = [{j for j in range(n_cols) if (i, j, "<") in winds} for i in range(n_rows)] -north_winds = [ - {i for i in range(n_rows) if (i, j, "^") in winds} for j in range(n_cols) -] -south_winds = [ - {i for i in range(n_rows) if (i, j, "v") in winds} for j in range(n_cols) -] +from ..base import BaseSolver -def run(start: tuple[int, int], start_cycle: int, end: tuple[int, int]): - def heuristic(y: int, x: int) -> int: - return abs(end[0] - y) + abs(end[1] - x) +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = [line.strip() for line in input.splitlines()] - # (distance + heuristic, distance, (start_pos, cycle)) - queue = [(heuristic(start[0], start[1]), 0, ((start[0], start[1]), start_cycle))] - visited: set[tuple[tuple[int, int], int]] = set() - distances: dict[tuple[int, int], dict[int, int]] = defaultdict(lambda: {}) + winds = { + (i - 1, j - 1, lines[i][j]) + for i in range(1, len(lines) - 1) + for j in range(1, len(lines[i]) - 1) + if lines[i][j] != "." + } - while queue: - _, distance, ((y, x), cycle) = heapq.heappop(queue) + n_rows, n_cols = len(lines) - 2, len(lines[0]) - 2 + CYCLE = math.lcm(n_rows, n_cols) - if ((y, x), cycle) in visited: - continue + east_winds = [ + {j for j in range(n_cols) if (i, j, ">") in winds} for i in range(n_rows) + ] + west_winds = [ + {j for j in range(n_cols) if (i, j, "<") in winds} for i in range(n_rows) + ] + north_winds = [ + {i for i in range(n_rows) if (i, j, "^") in winds} for j in range(n_cols) + ] + south_winds = [ + {i for i in range(n_rows) if (i, j, "v") in winds} for j in range(n_cols) + ] - distances[y, x][cycle] = distance + def run(start: tuple[int, int], start_cycle: int, end: tuple[int, int]): + def heuristic(y: int, x: int) -> int: + return abs(end[0] - y) + abs(end[1] - x) - visited.add(((y, x), cycle)) + # (distance + heuristic, distance, (start_pos, cycle)) + queue = [ + (heuristic(start[0], start[1]), 0, ((start[0], start[1]), start_cycle)) + ] + visited: set[tuple[tuple[int, int], int]] = set() + distances: dict[tuple[int, int], dict[int, int]] = defaultdict(lambda: {}) - if (y, x) == (end[0], end[1]): - break + while queue: + _, distance, ((y, x), cycle) = heapq.heappop(queue) - for dy, dx in (0, 0), (-1, 0), (1, 0), (0, -1), (0, 1): - ty = y + dy - tx = x + dx - - n_cycle = (cycle + 1) % CYCLE - - if (ty, tx) == end: - heapq.heappush(queue, (distance + 1, distance + 1, ((ty, tx), n_cycle))) - break - - if ((ty, tx), n_cycle) in visited: - continue - - if (ty, tx) != start and (ty < 0 or tx < 0 or ty >= n_rows or tx >= n_cols): - continue - - if (ty, tx) != start: - if (ty - n_cycle) % n_rows in south_winds[tx]: - continue - if (ty + n_cycle) % n_rows in north_winds[tx]: - continue - if (tx + n_cycle) % n_cols in west_winds[ty]: - continue - if (tx - n_cycle) % n_cols in east_winds[ty]: + if ((y, x), cycle) in visited: continue - heapq.heappush( - queue, - ((heuristic(ty, tx) + distance + 1, distance + 1, ((ty, tx), n_cycle))), - ) + distances[y, x][cycle] = distance - return distances, next(iter(distances[end].values())) + visited.add(((y, x), cycle)) + if (y, x) == (end[0], end[1]): + break -start = ( - -1, - next(j for j in range(1, len(lines[0]) - 1) if lines[0][j] == ".") - 1, -) -end = ( - n_rows, - next(j for j in range(1, len(lines[-1]) - 1) if lines[-1][j] == ".") - 1, -) + for dy, dx in (0, 0), (-1, 0), (1, 0), (0, -1), (0, 1): + ty = y + dy + tx = x + dx -distances_1, forward_1 = run(start, 0, end) -print(f"answer 1 is {forward_1}") + n_cycle = (cycle + 1) % CYCLE -distances_2, return_1 = run(end, next(iter(distances_1[end].keys())), start) -distances_3, forward_2 = run(start, next(iter(distances_2[start].keys())), end) -print(f"answer 2 is {forward_1 + return_1 + forward_2}") + if (ty, tx) == end: + heapq.heappush( + queue, (distance + 1, distance + 1, ((ty, tx), n_cycle)) + ) + break + + if ((ty, tx), n_cycle) in visited: + continue + + if (ty, tx) != start and ( + ty < 0 or tx < 0 or ty >= n_rows or tx >= n_cols + ): + continue + + if (ty, tx) != start: + if (ty - n_cycle) % n_rows in south_winds[tx]: + continue + if (ty + n_cycle) % n_rows in north_winds[tx]: + continue + if (tx + n_cycle) % n_cols in west_winds[ty]: + continue + if (tx - n_cycle) % n_cols in east_winds[ty]: + continue + + heapq.heappush( + queue, + ( + ( + heuristic(ty, tx) + distance + 1, + distance + 1, + ((ty, tx), n_cycle), + ) + ), + ) + + return distances, next(iter(distances[end].values())) + + start = ( + -1, + next(j for j in range(1, len(lines[0]) - 1) if lines[0][j] == ".") - 1, + ) + end = ( + n_rows, + next(j for j in range(1, len(lines[-1]) - 1) if lines[-1][j] == ".") - 1, + ) + + distances_1, forward_1 = run(start, 0, end) + yield forward_1 + + distances_2, return_1 = run(end, next(iter(distances_1[end].keys())), start) + _distances_3, forward_2 = run(start, next(iter(distances_2[start].keys())), end) + yield forward_1 + return_1 + forward_2 diff --git a/src/holt59/aoc/2022/day25.py b/src/holt59/aoc/2022/day25.py index 4327247..778be4b 100644 --- a/src/holt59/aoc/2022/day25.py +++ b/src/holt59/aoc/2022/day25.py @@ -1,27 +1,28 @@ -import sys +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() - -coeffs = {"2": 2, "1": 1, "0": 0, "-": -1, "=": -2} +from ..base import BaseSolver -def snafu2number(number: str) -> int: - value = 0 - for c in number: - value *= 5 - value += coeffs[c] - return value +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = [line.strip() for line in input.splitlines()] + coeffs = {"2": 2, "1": 1, "0": 0, "-": -1, "=": -2} -def number2snafu(number: int) -> str: - values = ["0", "1", "2", "=", "-"] - res = "" - while number > 0: - mod = number % 5 - res = res + values[mod] - number = number // 5 + int(mod >= 3) - return "".join(reversed(res)) + def snafu2number(number: str) -> int: + value = 0 + for c in number: + value *= 5 + value += coeffs[c] + return value + def number2snafu(number: int) -> str: + values = ["0", "1", "2", "=", "-"] + res = "" + while number > 0: + mod = number % 5 + res = res + values[mod] + number = number // 5 + int(mod >= 3) + return "".join(reversed(res)) -answer_1 = number2snafu(sum(map(snafu2number, lines))) -print(f"answer 1 is {answer_1}") + yield number2snafu(sum(map(snafu2number, lines))) diff --git a/src/holt59/aoc/2022/day3.py b/src/holt59/aoc/2022/day3.py index b7740ed..26f0eef 100644 --- a/src/holt59/aoc/2022/day3.py +++ b/src/holt59/aoc/2022/day3.py @@ -1,23 +1,28 @@ import string -import sys +from typing import Any, Iterator -lines = [line.strip() for line in sys.stdin.readlines()] +from ..base import BaseSolver -# extract content of each part -parts = [(set(line[: len(line) // 2]), set(line[len(line) // 2 :])) for line in lines] -# priorities -priorities = {c: i + 1 for i, c in enumerate(string.ascii_letters)} +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = [line.strip() for line in input.splitlines()] -# part 1 -part1 = sum(priorities[c] for p1, p2 in parts for c in p1.intersection(p2)) -print(f"answer 1 is {part1}") + # extract content of each part + parts = [ + (set(line[: len(line) // 2]), set(line[len(line) // 2 :])) for line in lines + ] -# part 2 -n_per_group = 3 -part2 = sum( - priorities[c] - for i in range(0, len(lines), n_per_group) - for c in set(lines[i]).intersection(*lines[i + 1 : i + n_per_group]) -) -print(f"answer 2 is {part2}") + # priorities + priorities = {c: i + 1 for i, c in enumerate(string.ascii_letters)} + + # part 1 + yield sum(priorities[c] for p1, p2 in parts for c in p1.intersection(p2)) + + # part 2 + n_per_group = 3 + yield sum( + priorities[c] + for i in range(0, len(lines), n_per_group) + for c in set(lines[i]).intersection(*lines[i + 1 : i + n_per_group]) + ) diff --git a/src/holt59/aoc/2022/day4.py b/src/holt59/aoc/2022/day4.py index bf6aae1..dfbb713 100644 --- a/src/holt59/aoc/2022/day4.py +++ b/src/holt59/aoc/2022/day4.py @@ -1,6 +1,6 @@ -import sys +from typing import Any, Iterator -lines = [line.strip() for line in sys.stdin.readlines()] +from ..base import BaseSolver def make_range(value: str) -> set[int]: @@ -8,10 +8,13 @@ def make_range(value: str) -> set[int]: return set(range(int(parts[0]), int(parts[1]) + 1)) -sections = [tuple(make_range(part) for part in line.split(",")) for line in lines] +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = [line.strip() for line in input.splitlines()] -answer_1 = sum(s1.issubset(s2) or s2.issubset(s1) for s1, s2 in sections) -print(f"answer 1 is {answer_1}") + sections = [ + tuple(make_range(part) for part in line.split(",")) for line in lines + ] -answer_2 = sum(bool(s1.intersection(s2)) for s1, s2 in sections) -print(f"answer 1 is {answer_2}") + yield sum(s1.issubset(s2) or s2.issubset(s1) for s1, s2 in sections) + yield sum(bool(s1.intersection(s2)) for s1, s2 in sections) diff --git a/src/holt59/aoc/2022/day5.py b/src/holt59/aoc/2022/day5.py index 04ba94f..831470c 100644 --- a/src/holt59/aoc/2022/day5.py +++ b/src/holt59/aoc/2022/day5.py @@ -1,41 +1,43 @@ import copy -import sys +from typing import Any, Iterator -blocks_s, moves_s = (part.splitlines() for part in sys.stdin.read().split("\n\n")) +from ..base import BaseSolver -blocks: dict[str, list[str]] = {stack: [] for stack in blocks_s[-1].split()} -# this codes assumes that the lines are regular, i.e., 4 characters per "crate" in the -# form of '[X] ' (including the trailing space) -# -for block in blocks_s[-2::-1]: - for stack, index in zip(blocks, range(0, len(block), 4)): - crate = block[index + 1 : index + 2].strip() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + blocks_s, moves_s = (part.splitlines() for part in input.split("\n\n")) - if crate: - blocks[stack].append(crate) + blocks: dict[str, list[str]] = {stack: [] for stack in blocks_s[-1].split()} -# part 1 - deep copy for part 2 -blocks_1 = copy.deepcopy(blocks) + # this codes assumes that the lines are regular, i.e., 4 characters per "crate" in the + # form of '[X] ' (including the trailing space) + # + for block in blocks_s[-2::-1]: + for stack, index in zip(blocks, range(0, len(block), 4)): + crate = block[index + 1 : index + 2].strip() -for move in moves_s: - _, count_s, _, from_, _, to_ = move.strip().split() + if crate: + blocks[stack].append(crate) - for _i in range(int(count_s)): - blocks_1[to_].append(blocks_1[from_].pop()) + # part 1 - deep copy for part 2 + blocks_1 = copy.deepcopy(blocks) -# part 2 -blocks_2 = copy.deepcopy(blocks) + for move in moves_s: + _, count_s, _, from_, _, to_ = move.strip().split() -for move in moves_s: - _, count_s, _, from_, _, to_ = move.strip().split() - count = int(count_s) + for _i in range(int(count_s)): + blocks_1[to_].append(blocks_1[from_].pop()) - blocks_2[to_].extend(blocks_2[from_][-count:]) - del blocks_2[from_][-count:] + # part 2 + blocks_2 = copy.deepcopy(blocks) -answer_1 = "".join(s[-1] for s in blocks_1.values()) -print(f"answer 1 is {answer_1}") + for move in moves_s: + _, count_s, _, from_, _, to_ = move.strip().split() + count = int(count_s) -answer_2 = "".join(s[-1] for s in blocks_2.values()) -print(f"answer 2 is {answer_2}") + blocks_2[to_].extend(blocks_2[from_][-count:]) + del blocks_2[from_][-count:] + + yield "".join(s[-1] for s in blocks_1.values()) + yield "".join(s[-1] for s in blocks_2.values()) diff --git a/src/holt59/aoc/2022/day6.py b/src/holt59/aoc/2022/day6.py index 086f33e..db7faf6 100644 --- a/src/holt59/aoc/2022/day6.py +++ b/src/holt59/aoc/2022/day6.py @@ -1,4 +1,6 @@ -import sys +from typing import Any, Iterator + +from ..base import BaseSolver def index_of_first_n_differents(data: str, n: int) -> int: @@ -8,8 +10,7 @@ def index_of_first_n_differents(data: str, n: int) -> int: return -1 -data = sys.stdin.read().strip() - - -print(f"answer 1 is {index_of_first_n_differents(data, 4)}") -print(f"answer 2 is {index_of_first_n_differents(data, 14)}") +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + yield index_of_first_n_differents(input, 4) + yield index_of_first_n_differents(input, 14) diff --git a/src/holt59/aoc/2022/day7.py b/src/holt59/aoc/2022/day7.py index c95e1b9..a3cb585 100644 --- a/src/holt59/aoc/2022/day7.py +++ b/src/holt59/aoc/2022/day7.py @@ -1,80 +1,81 @@ -import sys from pathlib import Path +from typing import Any, Iterator -lines = sys.stdin.read().splitlines() - -# we are going to use Path to create path and go up/down in the file tree since it -# implements everything we need -# -# we can use .resolve() to get normalized path, although this will add C:\ to all paths -# on Windows but that is not an issue since only the sizes matter -# - -# mapping from path to list of files or directories -trees: dict[Path, list[Path]] = {} - -# mapping from paths to either size (for file) or -1 for directory -sizes: dict[Path, int] = {} - -# first line must be a cd otherwise we have no idea where we are -assert lines[0].startswith("$ cd") -base_path = Path(lines[0].strip("$").split()[1]).resolve() -cur_path = base_path - -trees[cur_path] = [] -sizes[cur_path] = -1 - -for line in lines[1:]: - # command - if line.startswith("$"): - parts = line.strip("$").strip().split() - command = parts[0] - - if command == "cd": - cur_path = cur_path.joinpath(parts[1]).resolve() - - # just initialize the lis of files if not already done - if cur_path not in trees: - trees[cur_path] = [] - else: - # nothing to do here - pass - - # fill the current path - else: - parts = line.split() - name: str = parts[1] - if line.startswith("dir"): - size = -1 - else: - size = int(parts[0]) - - path = cur_path.joinpath(name) - trees[cur_path].append(path) - sizes[path] = size +from ..base import BaseSolver -def compute_size(path: Path) -> int: - size = sizes[path] +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = [line.strip() for line in input.splitlines()] - if size >= 0: - return size + # we are going to use Path to create path and go up/down in the file tree since it + # implements everything we need + # + # we can use .resolve() to get normalized path, although this will add C:\ to all paths + # on Windows but that is not an issue since only the sizes matter + # - return sum(compute_size(sub) for sub in trees[path]) + # mapping from path to list of files or directories + trees: dict[Path, list[Path]] = {} + # mapping from paths to either size (for file) or -1 for directory + sizes: dict[Path, int] = {} -acc_sizes = {path: compute_size(path) for path in trees} + # first line must be a cd otherwise we have no idea where we are + assert lines[0].startswith("$ cd") + base_path = Path(lines[0].strip("$").split()[1]).resolve() + cur_path = base_path -# part 1 -answer_1 = sum(size for size in acc_sizes.values() if size <= 100_000) -print(f"answer 1 is {answer_1}") + trees[cur_path] = [] + sizes[cur_path] = -1 -# part 2 -total_space = 70_000_000 -update_space = 30_000_000 -free_space = total_space - acc_sizes[base_path] + for line in lines[1:]: + # command + if line.startswith("$"): + parts = line.strip("$").strip().split() + command = parts[0] -to_free_space = update_space - free_space + if command == "cd": + cur_path = cur_path.joinpath(parts[1]).resolve() -answer_2 = min(size for size in acc_sizes.values() if size >= to_free_space) -print(f"answer 2 is {answer_2}") + # just initialize the lis of files if not already done + if cur_path not in trees: + trees[cur_path] = [] + else: + # nothing to do here + pass + + # fill the current path + else: + parts = line.split() + name: str = parts[1] + if line.startswith("dir"): + size = -1 + else: + size = int(parts[0]) + + path = cur_path.joinpath(name) + trees[cur_path].append(path) + sizes[path] = size + + def compute_size(path: Path) -> int: + size = sizes[path] + + if size >= 0: + return size + + return sum(compute_size(sub) for sub in trees[path]) + + acc_sizes = {path: compute_size(path) for path in trees} + + # part 1 + yield sum(size for size in acc_sizes.values() if size <= 100_000) + + # part 2 + total_space = 70_000_000 + update_space = 30_000_000 + free_space = total_space - acc_sizes[base_path] + + to_free_space = update_space - free_space + + yield min(size for size in acc_sizes.values() if size >= to_free_space) diff --git a/src/holt59/aoc/2022/day8.py b/src/holt59/aoc/2022/day8.py index ee75ede..ba06574 100644 --- a/src/holt59/aoc/2022/day8.py +++ b/src/holt59/aoc/2022/day8.py @@ -1,53 +1,54 @@ -import sys +from typing import Any, Iterator import numpy as np from numpy.typing import NDArray -lines = sys.stdin.read().splitlines() +from ..base import BaseSolver -trees = np.array([[int(x) for x in row] for row in lines]) -# answer 1 -highest_trees = np.ones(trees.shape + (4,), dtype=int) * -1 -highest_trees[1:-1, 1:-1] = [ - [ - [ - trees[:i, j].max(), - trees[i + 1 :, j].max(), - trees[i, :j].max(), - trees[i, j + 1 :].max(), +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = [line.strip() for line in input.splitlines()] + + trees = np.array([[int(x) for x in row] for row in lines]) + + # answer 1 + highest_trees = np.ones(trees.shape + (4,), dtype=int) * -1 + highest_trees[1:-1, 1:-1] = [ + [ + [ + trees[:i, j].max(), + trees[i + 1 :, j].max(), + trees[i, :j].max(), + trees[i, j + 1 :].max(), + ] + for j in range(1, trees.shape[1] - 1) + ] + for i in range(1, trees.shape[0] - 1) ] - for j in range(1, trees.shape[1] - 1) - ] - for i in range(1, trees.shape[0] - 1) -] -answer_1 = (highest_trees.min(axis=2) < trees).sum() -print(f"answer 1 is {answer_1}") + yield (highest_trees.min(axis=2) < trees).sum() + def viewing_distance(row_of_trees: NDArray[np.int_], value: int) -> int: + w = np.where(row_of_trees >= value)[0] -def viewing_distance(row_of_trees: NDArray[np.int_], value: int) -> int: - w = np.where(row_of_trees >= value)[0] + if not w.size: + return len(row_of_trees) - if not w.size: - return len(row_of_trees) + return w[0] + 1 - return w[0] + 1 - - -# answer 2 -v_distances = np.zeros(trees.shape + (4,), dtype=int) -v_distances[1:-1, 1:-1, :] = [ - [ - [ - viewing_distance(trees[i - 1 :: -1, j], trees[i, j]), - viewing_distance(trees[i, j - 1 :: -1], trees[i, j]), - viewing_distance(trees[i, j + 1 :], trees[i, j]), - viewing_distance(trees[i + 1 :, j], trees[i, j]), + # answer 2 + v_distances = np.zeros(trees.shape + (4,), dtype=int) + v_distances[1:-1, 1:-1, :] = [ + [ + [ + viewing_distance(trees[i - 1 :: -1, j], trees[i, j]), + viewing_distance(trees[i, j - 1 :: -1], trees[i, j]), + viewing_distance(trees[i, j + 1 :], trees[i, j]), + viewing_distance(trees[i + 1 :, j], trees[i, j]), + ] + for j in range(1, trees.shape[1] - 1) + ] + for i in range(1, trees.shape[0] - 1) ] - for j in range(1, trees.shape[1] - 1) - ] - for i in range(1, trees.shape[0] - 1) -] -answer_2 = np.prod(v_distances, axis=2).max() -print(f"answer 2 is {answer_2}") + yield np.prod(v_distances, axis=2).max() diff --git a/src/holt59/aoc/2022/day9.py b/src/holt59/aoc/2022/day9.py index f81ec93..6bfa243 100644 --- a/src/holt59/aoc/2022/day9.py +++ b/src/holt59/aoc/2022/day9.py @@ -1,7 +1,10 @@ -import sys +import itertools as it +from typing import Any, Iterator import numpy as np +from ..base import BaseSolver + def move(head: tuple[int, int], command: str) -> tuple[int, int]: h_col, h_row = head @@ -43,17 +46,14 @@ def run(commands: list[str], n_blocks: int) -> list[tuple[int, int]]: return visited -lines = sys.stdin.read().splitlines() +class Solver(BaseSolver): + def solve(self, input: str) -> Iterator[Any]: + lines = [line.strip() for line in input.splitlines()] -# flatten the commands -commands: list[str] = [] -for line in lines: - d, c = line.split() - commands.extend(d * int(c)) + # flatten the commands + commands = list( + it.chain(*(p[0] * int(p[1]) for line in lines if (p := line.split()))) + ) - -visited_1 = run(commands, n_blocks=2) -print(f"answer 1 is {len(set(visited_1))}") - -visited_2 = run(commands, n_blocks=10) -print(f"answer 2 is {len(set(visited_2))}") + yield len(set(run(commands, n_blocks=2))) + yield len(set(run(commands, n_blocks=10))) diff --git a/src/holt59/aoc/2023/day25.py b/src/holt59/aoc/2023/day25.py index d75d1b7..0f80ab7 100644 --- a/src/holt59/aoc/2023/day25.py +++ b/src/holt59/aoc/2023/day25.py @@ -1,3 +1,5 @@ +# pyright: reportUnknownMemberType=false + from typing import Any, Iterator import networkx as nx diff --git a/src/holt59/aoc/__main__.py b/src/holt59/aoc/__main__.py index 1ff9c44..edf9b06 100644 --- a/src/holt59/aoc/__main__.py +++ b/src/holt59/aoc/__main__.py @@ -179,7 +179,7 @@ def main(): start = datetime.now() last = start - it = solver.solve(data.strip()) + it = solver.solve(data.rstrip()) if it is None: solver.logger.error(f"no implementation for {year} day {day}")