Poetry stuff.

This commit is contained in:
Mikael CAPELLE
2023-12-19 15:39:10 +01:00
parent 41b07cfe83
commit 2959387bcd
238 changed files with 571 additions and 3 deletions

View File

View File

@@ -0,0 +1,14 @@
import sys
lines = sys.stdin.read().splitlines()
values = [int(line) for line in lines]
# part 1
answer_1 = sum(v2 > v1 for v1, v2 in zip(values[:-1], values[1:]))
print(f"answer 1 is {answer_1}")
# part 2
runnings = [sum(values[i : i + 3]) for i in range(len(values) - 2)]
answer_2 = sum(v2 > v1 for v1, v2 in zip(runnings[:-1], runnings[1:]))
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,40 @@
import sys
from math import prod
from typing import Literal, cast
lines = sys.stdin.read().splitlines()
commands = [
(cast(Literal["forward", "up", "down"], (p := line.split())[0]), int(p[1]))
for line in lines
]
def depth_and_position(use_aim: bool):
aim, pos, depth = 0, 0, 0
for command, value in commands:
d_depth = 0
match command:
case "forward":
pos += value
depth += value * aim
case "up":
d_depth = -value
case "down":
d_depth = value
if use_aim:
aim += d_depth
else:
depth += value
return depth, pos
# part 1
answer_1 = prod(depth_and_position(False))
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = prod(depth_and_position(True))
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,39 @@
import sys
from collections import Counter
from typing import Literal
def generator_rating(
values: list[str], most_common: bool, default: Literal["0", "1"]
) -> str:
index = 0
most_common_idx = 0 if most_common else 1
while len(values) > 1:
cnt = Counter(value[index] for value in values)
bit = cnt.most_common(2)[most_common_idx][0]
if cnt["0"] == cnt["1"]:
bit = default
values = [value for value in values if value[index] == bit]
index += 1
return values[0]
lines = sys.stdin.read().splitlines()
# part 1
most_and_least_common = [
tuple(Counter(line[col] for line in lines).most_common(2)[m][0] for m in range(2))
for col in range(len(lines[0]))
]
gamma_rate = int("".join(most for most, _ in most_and_least_common), base=2)
epsilon_rate = int("".join(least for _, least in most_and_least_common), base=2)
print(f"answer 1 is {gamma_rate * epsilon_rate}")
# part 2
oxygen_generator_rating = int(generator_rating(lines, True, "1"), base=2)
co2_scrubber_rating = int(generator_rating(lines, False, "0"), base=2)
answer_2 = oxygen_generator_rating * co2_scrubber_rating
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,45 @@
import sys
import numpy as np
lines = sys.stdin.read().splitlines()
numbers = [int(c) for c in lines[0].split(",")]
boards = np.asarray(
[
[[int(c) for c in line.split()] for line in lines[start : start + 5]]
for start in range(2, len(lines), 6)
]
)
# (round, score) for each board (-1 when not found)
winning_rounds: list[tuple[int, int]] = [(-1, -1) for _ in range(len(boards))]
marked = np.zeros_like(boards, dtype=bool)
for round, number in enumerate(numbers):
# mark boards
marked[boards == number] = True
# check each board for winning
for index in range(len(boards)):
if winning_rounds[index][0] > 0:
continue
if np.any(np.all(marked[index], axis=0) | np.all(marked[index], axis=1)):
winning_rounds[index] = (
round,
number * int(np.sum(boards[index][~marked[index]])),
)
# all boards are winning - break
if np.all(marked.all(axis=1) | marked.all(axis=2)):
break
# part 1
(_, score) = min(winning_rounds, key=lambda w: w[0])
print(f"answer 1 is {score}")
# part 2
(_, score) = max(winning_rounds, key=lambda w: w[0])
print(f"answer 2 is {score}")

View File

@@ -0,0 +1,48 @@
import sys
import numpy as np
lines: list[str] = sys.stdin.read().splitlines()
sections: list[tuple[tuple[int, int], tuple[int, int]]] = [
(
(
int(line.split(" -> ")[0].split(",")[0]),
int(line.split(" -> ")[0].split(",")[1]),
),
(
int(line.split(" -> ")[1].split(",")[0]),
int(line.split(" -> ")[1].split(",")[1]),
),
)
for line in lines
]
np_sections = np.array(sections).reshape(-1, 4)
x_min, x_max, y_min, y_max = (
min(np_sections[:, 0].min(), np_sections[:, 2].min()),
max(np_sections[:, 0].max(), np_sections[:, 2].max()),
min(np_sections[:, 1].min(), np_sections[:, 3].min()),
max(np_sections[:, 1].max(), np_sections[:, 3].max()),
)
counts_1 = np.zeros((y_max + 1, x_max + 1), dtype=int)
counts_2 = counts_1.copy()
for (x1, y1), (x2, y2) in sections:
x_rng = range(x1, x2 + 1, 1) if x2 >= x1 else range(x1, x2 - 1, -1)
y_rng = range(y1, y2 + 1, 1) if y2 >= y1 else range(y1, y2 - 1, -1)
if x1 == x2 or y1 == y2:
counts_1[list(y_rng), list(x_rng)] += 1
counts_2[list(y_rng), list(x_rng)] += 1
elif abs(x2 - x1) == abs(y2 - y1):
for i, j in zip(y_rng, x_rng):
counts_2[i, j] += 1
answer_1 = (counts_1 >= 2).sum()
print(f"answer 1 is {answer_1}")
answer_2 = (counts_2 >= 2).sum()
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,21 @@
import sys
values = [int(c) for c in sys.stdin.read().strip().split(",")]
days = 256
lanterns = {day: 0 for day in range(days)}
for value in values:
for day in range(value, days, 7):
lanterns[day] += 1
for day in range(days):
for day2 in range(day + 9, days, 7):
lanterns[day2] += lanterns[day]
# part 1
answer_1 = sum(v for k, v in lanterns.items() if k < 80) + len(values)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = sum(lanterns.values()) + len(values)
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,21 @@
import sys
import numpy as np
positions = np.asarray([int(c) for c in sys.stdin.read().strip().split(",")])
min_position, max_position = positions.min(), positions.max()
# part 1
answer_1 = min(
np.sum(np.abs(positions - position))
for position in range(min_position, max_position + 1)
)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = min(
np.sum(abs(positions - position) * (abs(positions - position) + 1) // 2)
for position in range(min_position, max_position + 1)
)
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,87 @@
import itertools
import os
import sys
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
digits = {
"abcefg": 0,
"cf": 1,
"acdeg": 2,
"acdfg": 3,
"bcdf": 4,
"abdfg": 5,
"abdefg": 6,
"acf": 7,
"abcdefg": 8,
"abcdfg": 9,
}
lines = sys.stdin.read().splitlines()
# part 1
lengths = {len(k) for k, v in digits.items() if v in (1, 4, 7, 8)}
answer_1 = sum(
len(p) in lengths for line in lines for p in line.split("|")[1].strip().split()
)
print(f"answer 1 is {answer_1}")
# part 2
values: list[int] = []
for line in lines:
parts = line.split("|")
broken_digits = sorted(parts[0].strip().split(), key=len)
per_length = {
k: list(v)
for k, v in itertools.groupby(sorted(broken_digits, key=len), key=len)
}
# a can be found immediately
a = next(u for u in per_length[3][0] if u not in per_length[2][0])
# c and f have only two possible values corresponding to the single entry of
# length 2
cf = list(per_length[2][0])
# the only digit of length 4 contains bcdf, so we can deduce bd by removing cf
bd = [u for u in per_length[4][0] if u not in cf]
# the 3 digits of length 5 have a, d and g in common
adg = [u for u in per_length[5][0] if all(u in pe for pe in per_length[5][1:])]
# we can remove a
dg = [u for u in adg if u != a]
# we can deduce d and g
d = next(u for u in dg if u in bd)
g = next(u for u in dg if u != d)
# then b
b = next(u for u in bd if u != d)
# f is in the three 6-length digits, while c is only in 2
f = next(u for u in cf if all(u in p for p in per_length[6]))
# c is not f
c = next(u for u in cf if u != f)
# e is the last one
e = next(u for u in "abcdefg" if u not in {a, b, c, d, f, g})
mapping = dict(zip((a, b, c, d, e, f, g), "abcdefg"))
value = 0
for number in parts[1].strip().split():
digit = "".join(sorted(mapping[c] for c in number))
value = 10 * value + digits[digit]
if VERBOSE:
print(value)
values.append(value)
answer_2 = sum(values)
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

View File

@@ -0,0 +1,7 @@
import sys
blocks = sys.stdin.read().split("\n\n")
values = sorted(sum(map(int, block.split())) for block in blocks)
print(f"answer 1 is {values[-1]}")
print(f"answer 2 is {sum(values[-3:])}")

View File

@@ -0,0 +1,38 @@
import sys
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}")
for i in range(6):
for j in range(40):
v = values[1 + i * 40 + j]
if j >= v - 1 and j <= v + 1:
print("#", end="")
else:
print(".", end="")
print()

View File

@@ -0,0 +1,142 @@
import copy
import sys
from functools import reduce
from typing import Callable, Final, Mapping, Sequence
class Monkey:
id: Final[int]
items: Final[Sequence[int]]
worry_fn: Final[Callable[[int], int]]
test_value: Final[int]
throw_targets: Final[Mapping[bool, int]]
def __init__(
self,
id: int,
items: list[int],
worry_fn: Callable[[int], int],
test_value: int,
throw_targets: dict[bool, int],
):
self.id = id
self.items = items
self.worry_fn = worry_fn
self.test_value = test_value
self.throw_targets = throw_targets
def __eq__(self, o: object) -> bool:
if not isinstance(o, Monkey):
return False
return self.id == o.id
def __hash__(self) -> int:
return hash(self.id)
def parse_monkey(lines: list[str]) -> Monkey:
assert lines[0].startswith("Monkey")
monkey_id = int(lines[0].split()[-1][:-1])
# parse items
items = [int(r.strip()) for r in lines[1].split(":")[1].split(",")]
# parse worry
worry_fn: Callable[[int], int]
worry_s = lines[2].split("new =")[1].strip()
operand = worry_s.split()[2].strip()
if worry_s.startswith("old *"):
if operand == "old":
worry_fn = lambda w: w * w # noqa: E731
else:
worry_fn = lambda w: w * int(operand) # noqa: E731
elif worry_s.startswith("old +"):
if operand == "old":
worry_fn = lambda w: w + w # noqa: E731
else:
worry_fn = lambda w: w + int(operand) # noqa: E731
else:
assert False, worry_s
# parse test
assert lines[3].split(":")[1].strip().startswith("divisible by")
test_value = int(lines[3].split()[-1])
assert lines[4].strip().startswith("If true")
assert lines[5].strip().startswith("If false")
throw_targets = {True: int(lines[4].split()[-1]), False: int(lines[5].split()[-1])}
assert monkey_id not in throw_targets.values()
return Monkey(monkey_id, items, worry_fn, test_value, throw_targets)
def run(
monkeys: list[Monkey], n_rounds: int, me_worry_fn: Callable[[int], int]
) -> dict[Monkey, int]:
"""
Perform a full run.
Args:
monkeys: Initial list of monkeys. The Monkey are not modified.
n_rounds: Number of rounds to run.
me_worry_fn: Worry function to apply after the Monkey operation (e.g., divide
by 3 for round 1).
Returns:
A mapping containing, for each monkey, the number of items inspected.
"""
# copy of the items
items = {monkey: list(monkey.items) for monkey in monkeys}
# number of inspects
inspects = {monkey: 0 for monkey in monkeys}
for _ in range(n_rounds):
for monkey in monkeys:
for item in items[monkey]:
inspects[monkey] += 1
# compute the new worry level
item = me_worry_fn(monkey.worry_fn(item))
# find the target
target = monkey.throw_targets[item % monkey.test_value == 0]
assert target != monkey.id
items[monkeys[target]].append(item)
# clear after the loop
items[monkey].clear()
return inspects
def monkey_business(inspects: dict[Monkey, int]) -> int:
sorted_levels = sorted(inspects.values())
return sorted_levels[-2] * sorted_levels[-1]
monkeys = [parse_monkey(block.splitlines()) for block in sys.stdin.read().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 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}")

View File

@@ -0,0 +1,160 @@
import heapq
import sys
from typing import Callable, Iterator, TypeVar
Node = TypeVar("Node")
def dijkstra(
start: Node,
neighbors: Callable[[Node], Iterator[Node]],
cost: Callable[[Node, Node], float],
) -> tuple[dict[Node, float], dict[Node, Node]]:
"""
Compute shortest paths from one node to all reachable ones.
Args:
start: Starting node.
neighbors: Function returning the neighbors of a node.
cost: Function to compute the cost of an edge.
Returns:
A tuple (lengths, parents) where lengths is a mapping from Node to distance
(from the starting node) and parents a mapping from parents Node (in the
shortest path). If keyset of lengths and parents is the same. If a Node is not
in the mapping, it cannot be reached from the starting node.
"""
queue: list[tuple[float, Node]] = []
visited: set[Node] = set()
lengths: dict[Node, float] = {start: 0}
parents: dict[Node, Node] = {}
heapq.heappush(queue, (0, start))
while queue:
length, current = heapq.heappop(queue)
if current in visited:
continue
visited.add(current)
for neighbor in neighbors(current):
if neighbor in visited:
continue
neighbor_cost = length + cost(current, neighbor)
if neighbor_cost < lengths.get(neighbor, float("inf")):
lengths[neighbor] = neighbor_cost
parents[neighbor] = current
heapq.heappush(queue, (neighbor_cost, neighbor))
return lengths, parents
def make_path(parents: dict[Node, Node], start: Node, end: Node) -> list[Node] | None:
if end not in parents:
return None
path: list[Node] = [end]
while path[-1] is not start:
path.append(parents[path[-1]])
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]]:
n_rows = len(grid)
n_cols = len(grid[0])
c_row, c_col = node
for n_row, n_col in (
(c_row - 1, c_col),
(c_row + 1, c_col),
(c_row, c_col - 1),
(c_row, c_col + 1),
):
if not (n_row >= 0 and n_row < n_rows and n_col >= 0 and n_col < n_cols):
continue
if up and grid[n_row][n_col] > grid[c_row][c_col] + 1:
continue
elif not up and grid[n_row][n_col] < grid[c_row][c_col] - 1:
continue
yield n_row, n_col
# === main code ===
lines = sys.stdin.read().splitlines()
grid = [[ord(cell) - ord("a") for cell in line] for line in lines]
start: tuple[int, int]
end: tuple[int, int]
# for part 2
start_s: list[tuple[int, int]] = []
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))
# 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
print_path(path_1, n_rows=len(grid), n_cols=len(grid[0]))
print(f"answer 1 is {lengths_1[end] - 1}")
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}")

View File

@@ -0,0 +1,41 @@
import json
import sys
from functools import cmp_to_key
from typing import 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]
Packet: TypeAlias = list[int | list["Packet"]]
def compare(lhs: Packet, rhs: Packet) -> int:
for lhs_a, rhs_a in zip(lhs, rhs):
if isinstance(lhs_a, int) and isinstance(rhs_a, int):
if lhs_a != rhs_a:
return rhs_a - lhs_a
else:
if not isinstance(lhs_a, list):
lhs_a = [lhs_a] # type: ignore
elif not isinstance(rhs_a, list):
rhs_a = [rhs_a] # type: ignore
assert isinstance(rhs_a, list) and isinstance(lhs_a, list)
r = compare(cast(Packet, lhs_a), cast(Packet, rhs_a))
if r != 0:
return r
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}")
dividers = [[[2]], [[6]]]
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]
print(f"answer 2 is {d_index[0] * d_index[1]}")

View File

@@ -0,0 +1,140 @@
import sys
from enum import Enum, auto
from typing import Callable, cast
class Cell(Enum):
AIR = auto()
ROCK = auto()
SAND = auto()
def __str__(self) -> str:
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],
fill_fn: Callable[[int, int], Cell],
) -> dict[tuple[int, int], Cell]:
"""
Flow sands onto the given set of blocks
Args:
blocks: Blocks containing ROCK position. Modified in-place.
stop_fn: Function called with the last (assumed) position of a grain of
sand BEFORE adding it to blocks. If the function returns True, the grain
is added and a new one is flowed, otherwise, the whole procedure stops
and the function returns (without adding the final grain).
fill_fn: Function called when the target position of a grain (during the
flowing process) is missing from blocks.
Returns:
The input blocks.
"""
y_max = max(y for _, y in blocks)
while True:
x, y = 500, 0
while y <= y_max:
moved = False
for cx, cy in ((x, y + 1), (x - 1, y + 1), (x + 1, y + 1)):
if (cx, cy) not in blocks and fill_fn(cx, cy) == Cell.AIR:
x, y = cx, cy
moved = True
elif blocks[cx, cy] == Cell.AIR:
x, y = cx, cy
moved = True
if moved:
break
if not moved:
break
if stop_fn(x, y):
break
blocks[x, y] = Cell.SAND
return blocks
# === 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
]
)
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 x in range(x_start, x_end):
for y in range(y_start, y_end):
blocks[x, y] = Cell.ROCK
print_blocks(blocks)
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),
)
# === part 1 ===
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()
# === 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
print_blocks(blocks_2)
print(f"answer 2 is {sum(v == Cell.SAND for v in blocks_2.values())}")

View File

@@ -0,0 +1,87 @@
import sys
import numpy as np
import parse
def part1(sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], row: int) -> int:
no_beacons_row_l: list[np.ndarray] = []
for (sx, sy), (bx, by) in sensor_to_beacon.items():
d = abs(sx - bx) + abs(sy - by) # closest
no_beacons_row_l.append(sx - np.arange(0, d - abs(sy - row) + 1))
no_beacons_row_l.append(sx + np.arange(0, d - abs(sy - row) + 1))
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)
return len(no_beacons_row)
def part2_intervals(
sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], xy_max: int
) -> tuple[int, int, int]:
from tqdm import trange
for y in trange(xy_max + 1):
its: list[tuple[int, int]] = []
for (sx, sy), (bx, by) in sensor_to_beacon.items():
d = abs(sx - bx) + abs(sy - by)
dx = d - abs(sy - y)
if dx >= 0:
its.append((max(0, sx - dx), min(sx + dx, xy_max)))
its = sorted(its)
_, e = its[0]
for si, ei in its[1:]:
if si > e + 1:
return si - 1, y, 4_000_000 * (si - 1) + y
if ei > e:
e = ei
return (0, 0, 0)
def part2_cplex(
sensor_to_beacon: dict[tuple[int, int], tuple[int, int]], xy_max: int
) -> tuple[int, int, int]:
from docplex.mp.model import Model
m = Model()
x, y = m.continuous_var_list(2, ub=xy_max, name=["x", "y"])
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}")
m.set_objective("min", x + y)
s = m.solve()
vx = int(s.get_value(x))
vy = int(s.get_value(y))
return vx, vy, 4_000_000 * vx + vy
lines = sys.stdin.read().splitlines()
sensor_to_beacon: dict[tuple[int, int], tuple[int, int]] = {}
for line in lines:
r = parse.parse(
"Sensor at x={sx}, y={sy}: closest beacon is at x={bx}, y={by}", line
)
sensor_to_beacon[int(r["sx"]), int(r["sy"])] = (int(r["bx"]), int(r["by"]))
xy_max = 4_000_000 if max(sensor_to_beacon) > (1_000, 0) else 20
row = 2_000_000 if max(sensor_to_beacon) > (1_000, 0) else 10
print(f"answer 1 is {part1(sensor_to_beacon, row)}")
# x, y, a2 = part2_cplex(sensor_to_beacon, xy_max)
x, y, a2 = part2_intervals(sensor_to_beacon, xy_max)
print(f"answer 2 is {a2} (x={x}, y={y})")

View File

@@ -0,0 +1,158 @@
from __future__ import annotations
import heapq
import itertools
import re
import sys
from collections import defaultdict
from typing import FrozenSet, NamedTuple
from tqdm import tqdm
class Pipe(NamedTuple):
name: str
flow: int
tunnels: list[str]
def __lt__(self, other: object) -> bool:
return isinstance(other, Pipe) and other.name < self.name
def __eq__(self, other: object) -> bool:
return isinstance(other, Pipe) and other.name == self.name
def __hash__(self) -> int:
return hash(self.name)
def __str__(self) -> str:
return self.name
def __repr__(self) -> str:
return self.name
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()
distances: dict[Pipe, int] = {}
while len(distances) < len(pipes):
distance, current = heapq.heappop(queue)
if current in visited:
continue
visited.add(current)
distances[current] = distance
for tunnel in current.tunnels:
heapq.heappush(queue, (distance + 1, pipes[tunnel]))
return distances
def update_with_better(
node_at_times: dict[FrozenSet[Pipe], int], flow: int, flowing: FrozenSet[Pipe]
) -> None:
node_at_times[flowing] = max(node_at_times[flowing], flow)
def part_1(
start_pipe: Pipe,
max_time: int,
distances: dict[tuple[Pipe, Pipe], int],
relevant_pipes: FrozenSet[Pipe],
):
node_at_times: dict[int, dict[Pipe, dict[FrozenSet[Pipe], int]]] = defaultdict(
lambda: defaultdict(lambda: defaultdict(lambda: 0))
)
node_at_times[0] = {start_pipe: {frozenset(): 0}}
for time in range(max_time):
for c_pipe, nodes in node_at_times[time].items():
for flowing, flow in nodes.items():
for target in relevant_pipes:
distance = distances[c_pipe, target] + 1
if time + distance >= max_time or target in flowing:
continue
update_with_better(
node_at_times[time + distance][target],
flow + sum(pipe.flow for pipe in flowing) * distance,
flowing | {target},
)
update_with_better(
node_at_times[max_time][c_pipe],
flow + sum(pipe.flow for pipe in flowing) * (max_time - time),
flowing,
)
return max(
flow
for nodes_of_pipe in node_at_times[max_time].values()
for flow in nodes_of_pipe.values()
)
def part_2(
start_pipe: Pipe,
max_time: int,
distances: dict[tuple[Pipe, Pipe], int],
relevant_pipes: FrozenSet[Pipe],
):
def compute(pipes_for_me: FrozenSet[Pipe]) -> int:
return part_1(start_pipe, max_time, distances, pipes_for_me) + part_1(
start_pipe, max_time, distances, relevant_pipes - pipes_for_me
)
combs = [
frozenset(relevant_pipes_1)
for r in range(2, len(relevant_pipes) // 2 + 1)
for relevant_pipes_1 in itertools.combinations(relevant_pipes, r)
]
return max(compute(comb) for comb in tqdm(combs))
# === MAIN ===
lines = sys.stdin.read().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
g = r.groups()
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()
}
)
# valves with flow
relevant_pipes = frozenset(pipe for pipe in pipes.values() if pipe.flow > 0)
# 1651, 1653
print(part_1(pipes["AA"], 30, distances, relevant_pipes))
# 1707, 2223
print(part_2(pipes["AA"], 26, distances, relevant_pipes))

View File

@@ -0,0 +1,120 @@
import sys
from typing import Sequence, TypeVar
import numpy as np
T = TypeVar("T")
def print_tower(tower: np.ndarray, out: str = "#"):
print("-" * (tower.shape[1] + 2))
non_empty = False
for row in reversed(range(1, tower.shape[0])):
if not non_empty and not tower[row, :].any():
continue
non_empty = True
print("|" + "".join(out if c else "." for c in tower[row, :]) + "|")
print("+" + "-" * tower.shape[1] + "+")
def tower_height(tower: np.ndarray) -> int:
return int(tower.shape[0] - tower[::-1, :].argmax(axis=0).min() - 1)
def next_cycle(sequence: Sequence[T], index: int) -> tuple[T, int]:
t = sequence[index]
index = (index + 1) % len(sequence)
return t, index
ROCKS = [
np.array([(0, 0), (0, 1), (0, 2), (0, 3)]),
np.array([(0, 1), (1, 0), (1, 1), (1, 2), (2, 1)]),
np.array([(0, 0), (0, 1), (0, 2), (1, 2), (2, 2)]),
np.array([(0, 0), (1, 0), (2, 0), (3, 0)]),
np.array([(0, 0), (0, 1), (1, 0), (1, 1)]),
]
WIDTH = 7
START_X = 2
EMPTY_BLOCKS = np.zeros((10, WIDTH), dtype=bool)
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]]:
tower = EMPTY_BLOCKS.copy()
tower[0, :] = init
done_at: dict[tuple[int, int], int] = {}
heights: dict[int, int] = {}
i_jet, i_rock = 0, 0
rock_count = 0
for rock_count in range(n_rocks):
if early_stop:
if i_rock == 0 and (i_rock, i_jet) in done_at:
break
done_at[i_rock, i_jet] = rock_count
y_start = tower.shape[0] - tower[::-1, :].argmax(axis=0).min() + 3
rock, i_rock = next_cycle(ROCKS, i_rock)
rock_y = rock[:, 0] + y_start
rock_x = rock[:, 1] + START_X
if rock_y.max() >= tower.shape[0]:
tower = np.concatenate([tower, EMPTY_BLOCKS], axis=0)
while True:
jet, i_jet = next_cycle(jets, i_jet)
dx = 0
if jet == ">" and rock_x.max() < WIDTH - 1:
dx = 1
elif jet == "<" and rock_x.min() > 0:
dx = -1
if dx != 0 and not tower[rock_y, rock_x + dx].any():
rock_x = rock_x + dx
# move down
rock_y -= 1
if tower[rock_y, rock_x].any():
rock_y += 1
break
heights[rock_count] = tower_height(tower)
tower[rock_y, rock_x] = True
return tower, rock_count, done_at.get((i_rock, i_jet), -1), heights
line = sys.stdin.read().strip()
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, 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
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}")

View File

@@ -0,0 +1,51 @@
import sys
from typing import FrozenSet
import numpy as np
xyz = np.asarray(
[
tuple(int(x) for x in row.split(",")) # type: ignore
for row in sys.stdin.read().splitlines()
]
)
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
n_dims = len(cubes.shape)
faces = [(-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, 1, 0), (0, 0, -1), (0, 0, 1)]
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}")
visited = np.zeros_like(cubes, dtype=bool)
queue = [(0, 0, 0)]
n_faces = 0
while queue:
x, y, z = queue.pop(0)
if visited[x, y, z]:
continue
visited[x, y, z] = True
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 visited[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}")

View File

@@ -0,0 +1,182 @@
import sys
from typing import Literal
import numpy as np
import parse
from tqdm import tqdm
Reagent = Literal["ore", "clay", "obsidian", "geode"]
REAGENTS: tuple[Reagent, ...] = (
"ore",
"clay",
"obsidian",
"geode",
)
IntOfReagent = dict[Reagent, int]
class State:
robots: IntOfReagent
reagents: IntOfReagent
def __init__(
self,
robots: IntOfReagent | None = None,
reagents: IntOfReagent | None = None,
):
if robots is None:
assert reagents is None
self.reagents = {reagent: 0 for reagent in REAGENTS}
self.robots = {reagent: 0 for reagent in REAGENTS}
self.robots["ore"] = 1
else:
assert robots is not None and reagents is not None
self.robots = robots
self.reagents = reagents
def __eq__(self, other) -> bool:
return (
isinstance(other, State)
and self.robots == other.robots
and self.reagents == other.reagents
)
def __hash__(self) -> int:
return hash(tuple((self.robots[r], self.reagents[r]) for r in REAGENTS))
def __str__(self) -> str:
return "State({}, {})".format(
"/".join(str(self.robots[k]) for k in REAGENTS),
"/".join(str(self.reagents[k]) for k in REAGENTS),
)
def __repr__(self) -> str:
return str(self)
def dominates(lhs: State, rhs: State):
return all(
lhs.robots[r] >= rhs.robots[r] and lhs.reagents[r] >= rhs.reagents[r]
for r in REAGENTS
)
lines = sys.stdin.read().splitlines()
blueprints: list[dict[Reagent, IntOfReagent]] = []
for line in lines:
r = parse.parse(
"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.,
# in the first toy blueprint, we need at most 4 ore robots, 14 clay ones and 7
# obsidian ones
maximums = {
name: max(blueprint[r].get(name, 0) for r in REAGENTS) for name in REAGENTS
}
state_after_t: dict[int, set[State]] = {0: [State()]}
for t in range(1, max_time + 1):
# list of new states at the end of step t that we are going to prune later
states_for_t: set[State] = set()
for state in state_after_t[t - 1]:
robots_that_can_be_built = [
robot
for robot in REAGENTS
if all(
state.reagents[reagent] >= blueprint[robot].get(reagent, 0)
for reagent in REAGENTS
)
]
states_for_t.add(
State(
robots=state.robots,
reagents={
reagent: state.reagents[reagent] + state.robots[reagent]
for reagent in REAGENTS
},
)
)
if "geode" in robots_that_can_be_built:
robots_that_can_be_built = ["geode"]
else:
robots_that_can_be_built = [
robot
for robot in robots_that_can_be_built
if state.robots[robot] < maximums[robot]
]
for robot in robots_that_can_be_built:
robots = state.robots.copy()
robots[robot] += 1
reagents = {
reagent: state.reagents[reagent]
+ state.robots[reagent]
- blueprint[robot].get(reagent, 0)
for reagent in REAGENTS
}
states_for_t.add(State(robots=robots, reagents=reagents))
# use numpy to switch computation of dominated states -> store each state
# as a 8 array and use numpy broadcasting to find dominated states
states_after = np.asarray(list(states_for_t))
np_states = np.array(
[
[state.robots[r] for r in REAGENTS]
+ [state.reagents[r] for r in REAGENTS]
for state in states_after
]
)
to_keep = []
while len(np_states) > 0:
first_dom = (np_states[1:] >= np_states[0]).all(axis=1).any()
if first_dom:
np_states = np_states[1:]
else:
to_keep.append(np_states[0])
np_states = np_states[1:][~(np_states[1:] <= np_states[0]).all(axis=1)]
state_after_t[t] = {
State(
robots=dict(zip(REAGENTS, row[:4])),
reagents=dict(zip(REAGENTS, row[4:])),
)
for row in to_keep
}
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}")
answer_2 = run(blueprints[0], 32) * run(blueprints[1], 32) * run(blueprints[2], 32)
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,53 @@
import sys
def score_1(ux: int, vx: int) -> int:
# here ux and vx are both moves: 0 = rock, 1 = paper, 2 = scissor
#
# 1. to get the score of the move/shape, we simply add 1 -> vx + 1
# 2. to get the score of the outcome (loss/draw/win), we use the fact that the
# winning hand is always the opponent hand (ux) + 1 in modulo-3 arithmetic:
# - (ux - vx) % 3 gives us 0 for a draw, 1 for a loss and 2 for a win
# - 1 - ((ux - vx) % 3) gives us -1 for a win, 0 for a loss and 1 for a draw
# - (1 - ((ux - vx) % 3)) gives us 0 / 1 / 2 for loss / draw / win
# - the above can be rewritten as ((1 - (ux - vx)) % 3)
# we can then simply multiply this by 3 to get the outcome score
#
return (vx + 1) + ((1 - (ux - vx)) % 3) * 3
def score_2(ux: int, vx: int) -> int:
# here ux is the opponent move (0 = rock, 1 = paper, 2 = scissor) and vx is the
# outcome (0 = loss, 1 = draw, 2 = win)
#
# 1. to get the score to the move/shape, we need to find it (as 0, 1 or 2) and then
# add 1 to it
# - (vx - 1) gives the offset from the opponent shape (-1 for a loss, 0 for a
# draw and 1 for a win)
# - from the offset, we can retrieve the shape by adding the opponent shape and
# using modulo-3 arithmetic -> (ux + vx - 1) % 3
# - we then add 1 to get the final shape score
# 2. to get the score of the outcome, we can simply multiply vx by 3 -> vx * 3
return (ux + vx - 1) % 3 + 1 + vx * 3
lines = sys.stdin.readlines()
# 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]
# part 1 - 13526
print(f"answer 1 is {sum(score_1(*v) for v in values)}")
# part 2 - 14204
print(f"answer 2 is {sum(score_2(*v) for v in values)}")

View File

@@ -0,0 +1,74 @@
from __future__ import annotations
import sys
class Number:
current: int
value: int
def __init__(self, value: int):
self.current = 0
self.value = value
def __str__(self):
return str(self.value)
def __repr__(self):
return str(self)
def decrypt(numbers: list[Number], key: int, rounds: int) -> int:
numbers = numbers.copy()
original = numbers.copy()
for index, number in enumerate(numbers):
number.current = index
for _ in range(rounds):
for number in original:
index = number.current
offset = (number.value * key) % (len(numbers) - 1)
target = index + offset
# need to wrap
if target >= len(numbers):
target = offset - (len(numbers) - index) + 1
for number_2 in numbers[target:index]:
number_2.current += 1
numbers = (
numbers[:target]
+ [number]
+ numbers[target:index]
+ numbers[index + 1 :]
)
else:
for number_2 in numbers[index : target + 1]:
number_2.current -= 1
numbers = (
numbers[:index]
+ numbers[index + 1 : target + 1]
+ [number]
+ numbers[target + 1 :]
)
number.current = target
index_of_0 = next(
filter(lambda index: numbers[index].value == 0, range(len(numbers)))
)
return sum(
numbers[(index_of_0 + offset) % len(numbers)].value * key
for offset in (1000, 2000, 3000)
)
numbers = [Number(int(x)) for i, x in enumerate(sys.stdin.readlines())]
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}")

View File

@@ -0,0 +1,107 @@
import operator
import sys
from typing import Callable
def compute(monkeys: dict[str, int | tuple[str, str, str]], monkey: str) -> int:
value = monkeys[monkey]
if isinstance(value, int):
return value
else:
op: dict[str, Callable[[int, int], int]] = {
"+": operator.add,
"-": operator.sub,
"*": operator.mul,
"/": operator.floordiv,
}
value = op[value[1]](compute(monkeys, value[0]), compute(monkeys, value[2]))
monkeys[monkey] = value
return value
def invert(
monkeys: dict[str, int | tuple[str, str, str]], monkey: str, target: int
) -> dict[str, int | tuple[str, str, str]]:
"""
Revert the given mapping from monkey name to value or operation such that
the value from 'monkey' is computable by inverting operation until the root is
found.
Args:
monkeys: Dictionary of monkeys, that will be updated and returned.
monkey: Name of the monkey to start from.
target: Target value to set for the monkey that depends on root.
Returns:
The given dictionary of monkeys.
"""
monkeys = monkeys.copy()
depends: dict[str, str] = {}
for m, v in monkeys.items():
if isinstance(v, int):
continue
op1, _, op2 = v
assert op1 not in depends
assert op2 not in depends
depends[op1] = m
depends[op2] = m
invert_op = {"+": "-", "-": "+", "*": "/", "/": "*"}
current = monkey
while True:
dep = depends[current]
if dep == "root":
monkeys[current] = target
break
val = monkeys[dep]
assert not isinstance(val, int)
op1, ope, op2 = val
if op1 == current:
monkeys[current] = (dep, invert_op[ope], op2)
elif ope in ("+", "*"):
monkeys[current] = (dep, invert_op[ope], op1)
else:
monkeys[current] = (op1, ope, dep)
current = dep
return monkeys
lines = sys.stdin.read().splitlines()
monkeys: dict[str, int | tuple[str, str, str]] = {}
op_monkeys: set[str] = set()
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)
op_monkeys.add(name)
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}")

View File

@@ -0,0 +1,223 @@
import re
import sys
from typing import Callable
import numpy as np
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")
# 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)
]
# 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)
# 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]
return y0, x0, r0
y1, x1, r1 = run(wrap_part_1)
answer_1 = 1000 * (1 + y1) + 4 * (1 + x1) + SCORES[r1]
print(f"answer 1 is {answer_1}")
y2, x2, r2 = run(wrap_part_2)
answer_2 = 1000 * (1 + y2) + 4 * (1 + x2) + SCORES[r2]
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,103 @@
import itertools
import sys
from collections import defaultdict
Directions = list[
tuple[
str, tuple[int, int], tuple[tuple[int, int], tuple[int, int], tuple[int, int]]
]
]
# (Y, X)
DIRECTIONS: Directions = [
("N", (-1, 0), ((-1, -1), (-1, 0), (-1, 1))),
("S", (1, 0), ((1, -1), (1, 0), (1, 1))),
("W", (0, -1), ((-1, -1), (0, -1), (1, -1))),
("E", (0, 1), ((-1, 1), (0, 1), (1, 1))),
]
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}
return min(ys), min(xs), max(ys), max(xs)
def print_positions(positions: set[tuple[int, int]]):
min_y, min_x, max_y, max_x = min_max_yx(positions)
print(
"\n".join(
"".join(
"#" if (y, x) in positions else "." for x in range(min_x - 1, max_x + 2)
)
for y in range(min_y - 1, max_y + 2)
)
)
def round(
positions: set[tuple[int, int]],
directions: Directions,
):
to_move: dict[tuple[int, int], list[tuple[int, int]]] = defaultdict(lambda: [])
for y, x in positions:
elves = {
(dy, dx): (y + dy, x + dx) in positions
for dy, dx in itertools.product((-1, 0, 1), (-1, 0, 1))
if (dy, dx) != (0, 0)
}
if not any(elves.values()):
to_move[y, x].append((y, x))
continue
found: str | None = None
for d, (dy, dx), d_yx_check in directions:
if not any(elves[dy, dx] for dy, dx in d_yx_check):
found = d
to_move[y + dy, x + dx].append((y, x))
break
if found is None:
to_move[y, x].append((y, x))
positions.clear()
for ty, tx in to_move:
if len(to_move[ty, tx]) > 1:
positions.update(to_move[ty, tx])
else:
positions.add((ty, tx))
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 == "#"
}
# === part 1 ===
p1, d1 = POSITIONS.copy(), DIRECTIONS.copy()
for r 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}")
# === part 2 ===
p2, d2 = POSITIONS.copy(), DIRECTIONS.copy()
answer_2 = 0
while True:
answer_2 += 1
backup = p2.copy()
round(p2, d2)
if backup == p2:
break
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,98 @@
import heapq
import math
import sys
from collections import defaultdict
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)
]
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)
# (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: {})
while queue:
_, distance, ((y, x), cycle) = heapq.heappop(queue)
if ((y, x), cycle) in visited:
continue
distances[y, x][cycle] = distance
visited.add(((y, x), cycle))
if (y, x) == (end[0], end[1]):
break
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]:
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)
print(f"answer 1 is {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)
print(f"answer 2 is {forward_1 + return_1 + forward_2}")

View File

@@ -0,0 +1,27 @@
import sys
lines = sys.stdin.read().splitlines()
coeffs = {"2": 2, "1": 1, "0": 0, "-": -1, "=": -2}
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}")

View File

@@ -0,0 +1,23 @@
import string
import sys
lines = [line.strip() for line in sys.stdin.readlines()]
# 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)}
# part 1
part1 = sum(priorities[c] for p1, p2 in parts for c in p1.intersection(p2))
print(f"answer 1 is {part1}")
# 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}")

View File

@@ -0,0 +1,17 @@
import sys
lines = [line.strip() for line in sys.stdin.readlines()]
def make_range(value: str) -> set[int]:
parts = value.split("-")
return set(range(int(parts[0]), int(parts[1]) + 1))
sections = [tuple(make_range(part) for part in line.split(",")) for line in lines]
answer_1 = sum(s1.issubset(s2) or s2.issubset(s1) for s1, s2 in sections)
print(f"answer 1 is {answer_1}")
answer_2 = sum(bool(s1.intersection(s2)) for s1, s2 in sections)
print(f"answer 1 is {answer_2}")

View File

@@ -0,0 +1,41 @@
import copy
import sys
blocks_s, moves_s = (part.splitlines() for part in sys.stdin.read().split("\n\n"))
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()
if crate:
blocks[stack].append(crate)
# part 1 - deep copy for part 2
blocks_1 = copy.deepcopy(blocks)
for move in moves_s:
_, count_s, _, from_, _, to_ = move.strip().split()
for _i in range(int(count_s)):
blocks_1[to_].append(blocks_1[from_].pop())
# part 2
blocks_2 = copy.deepcopy(blocks)
for move in moves_s:
_, count_s, _, from_, _, to_ = move.strip().split()
count = int(count_s)
blocks_2[to_].extend(blocks_2[from_][-count:])
del blocks_2[from_][-count:]
answer_1 = "".join(s[-1] for s in blocks_1.values())
print(f"answer 1 is {answer_1}")
answer_2 = "".join(s[-1] for s in blocks_2.values())
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,15 @@
import sys
def index_of_first_n_differents(data: str, n: int) -> int:
for i in range(len(data)):
if len(set(data[i : i + n])) == n:
return i + n
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)}")

View File

@@ -0,0 +1,80 @@
import sys
from pathlib import Path
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
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
answer_1 = sum(size for size in acc_sizes.values() if size <= 100_000)
print(f"answer 1 is {answer_1}")
# 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
answer_2 = min(size for size in acc_sizes.values() if size >= to_free_space)
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,53 @@
import sys
import numpy as np
from numpy.typing import NDArray
lines = sys.stdin.read().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)
]
answer_1 = (highest_trees.min(axis=2) < trees).sum()
print(f"answer 1 is {answer_1}")
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)
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]),
]
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}")

View File

@@ -0,0 +1,59 @@
import sys
import numpy as np
def move(head: tuple[int, int], command: str) -> tuple[int, int]:
h_col, h_row = head
if command == "L":
head = (h_col - 1, h_row)
elif command == "R":
head = (h_col + 1, h_row)
elif command == "U":
head = (h_col, h_row + 1)
elif command == "D":
head = (h_col, h_row - 1)
return head
def follow(head: tuple[int, int], tail: tuple[int, int]) -> tuple[int, int]:
h_col, h_row = head
t_col, t_row = tail
if abs(t_col - h_col) <= 1 and abs(t_row - h_row) <= 1:
return tail
return t_col + np.sign(h_col - t_col), t_row + np.sign(h_row - t_row)
def run(commands: list[str], n_blocks: int) -> list[tuple[int, int]]:
blocks: list[tuple[int, int]] = [(0, 0) for _ in range(n_blocks)]
visited = [blocks[-1]]
for command in commands:
blocks[0] = move(blocks[0], command)
for i in range(0, n_blocks - 1):
blocks[i + 1] = follow(blocks[i], blocks[i + 1])
visited.append(blocks[-1])
return visited
lines = sys.stdin.read().splitlines()
# flatten the commands
commands: list[str] = []
for line in lines:
d, c = line.split()
commands.extend(d * int(c))
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))}")

View File

View File

@@ -0,0 +1,45 @@
import sys
lines = sys.stdin.read().splitlines()
lookups_1 = {str(d): d for d in range(1, 10)}
lookups_2 = lookups_1 | {
d: i + 1
for i, d in enumerate(
(
"one",
"two",
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine",
)
)
}
def find_values(lookups: dict[str, int]) -> list[int]:
values: list[int] = []
for line in filter(bool, lines):
first_digit = min(
lookups,
key=lambda lookup: index
if (index := line.find(lookup)) >= 0
else len(line),
)
last_digit = max(
lookups,
key=lambda lookup: index if (index := line.rfind(lookup)) >= 0 else -1,
)
values.append(10 * lookups[first_digit] + lookups[last_digit])
return values
print(f"answer 1 is {sum(find_values(lookups_1))}")
print(f"answer 2 is {sum(find_values(lookups_2))}")

View File

@@ -0,0 +1,100 @@
import os
import sys
from typing import Literal, cast
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
Symbol = Literal["|", "-", "L", "J", "7", "F", ".", "S"]
lines: list[list[Symbol]] = [
[cast(Symbol, symbol) for symbol in line] for line in sys.stdin.read().splitlines()
]
# find starting point
si, sj = next(
(i, j)
for i in range(len(lines))
for j in range(len(lines[0]))
if lines[i][j] == "S"
)
# find one of the two outputs
ni, nj = si, sj
for ni, nj, chars in (
(si - 1, sj, "|7F"),
(si + 1, sj, "|LJ"),
(si, sj - 1, "-LF"),
(si, sj + 1, "-J7"),
):
if lines[ni][nj] in chars:
break
# part 1 - find the loop (re-used in part 2)
loop = [(si, sj), (ni, nj)]
while True:
pi, pj = loop[-2]
i, j = loop[-1]
sym = lines[i][j]
if sym == "|" and pi > i or sym in "JL" and pi == i:
i -= 1
elif sym == "|" and pi < i or sym in "7F" and pi == i:
i += 1
elif sym == "-" and pj > j or sym in "J7" and pj == j:
j -= 1
elif sym == "-" and pj < j or sym in "LF" and pj == j:
j += 1
if (i, j) == (si, sj):
break
loop.append((i, j))
answer_1 = len(loop) // 2
print(f"answer 1 is {answer_1}")
# part 2
# replace S by an appropriate character for the loop below
di1, dj1 = loop[1][0] - loop[0][0], loop[1][1] - loop[0][1]
di2, dj2 = loop[0][0] - loop[-1][0], loop[0][1] - loop[-1][1]
mapping: dict[tuple[int, int], dict[tuple[int, int], Symbol]] = {
(0, 1): {(0, 1): "-", (-1, 0): "F", (1, 0): "L"},
(0, -1): {(0, -1): "-", (-1, 0): "7", (1, 0): "J"},
(1, 0): {(1, 0): "|", (0, 1): "7", (0, -1): "F"},
(-1, 0): {(-1, 0): "|", (0, -1): "L", (0, 1): "J"},
}
lines[si][sj] = mapping[di1, dj1][di2, dj2]
# find the points inside the loop using an adaptation of ray casting for a discrete
# grid (https://stackoverflow.com/a/218081/2666289)
#
# use a set for faster '... in loop' check
#
loop_s = set(loop)
inside: set[tuple[int, int]] = set()
for i in range(len(lines)):
cnt = 0
for j in range(len(lines[0])):
if (i, j) not in loop_s and cnt % 2 == 1:
inside.add((i, j))
if (i, j) in loop_s and lines[i][j] in "|LJ":
cnt += 1
if VERBOSE:
for i in range(len(lines)):
for j in range(len(lines[0])):
if (i, j) == (si, sj):
print("\033[91mS\033[0m", end="")
elif (i, j) in loop:
print(lines[i][j], end="")
elif (i, j) in inside:
print("\033[92mI\033[0m", end="")
else:
print(".", end="")
print()
answer_2 = len(inside)
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,41 @@
import sys
import numpy as np
lines = sys.stdin.read().splitlines()
data = np.array([[c == "#" for c in line] for line in lines])
rows = {c for c in range(data.shape[0]) if not data[c, :].any()}
columns = {c for c in range(data.shape[1]) if not data[:, c].any()}
galaxies_y, galaxies_x = np.where(data) # type: ignore
def compute_total_distance(expansion: int) -> int:
distances: list[int] = []
for g1 in range(len(galaxies_y)):
x1, y1 = int(galaxies_x[g1]), int(galaxies_y[g1])
for g2 in range(g1 + 1, len(galaxies_y)):
x2, y2 = int(galaxies_x[g2]), int(galaxies_y[g2])
dx = sum(
1 + (expansion - 1) * (x in columns)
for x in range(min(x1, x2), max(x1, x2))
)
dy = sum(
1 + (expansion - 1) * (y in rows)
for y in range(min(y1, y2), max(y1, y2))
)
distances.append(dx + dy)
return sum(distances)
# part 1
answer_1 = compute_total_distance(2)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = compute_total_distance(1000000)
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,107 @@
import os
import sys
from functools import lru_cache
from typing import Iterable
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
@lru_cache
def compute_fitting_arrangements(pattern: str, counts: tuple[int, ...]) -> int:
"""
fn3p tries to fit ALL values in counts() inside the pattern.
"""
# no pattern -> ok if nothing to fit, otherwise ko
if not pattern:
count = 1 if not counts else 0
# no count -> ok if pattern has no mandatory entry, else ko
elif not counts:
count = 1 if pattern.find("#") == -1 else 0
# cannot fit all values -> ko
elif len(pattern) < sum(counts) + len(counts) - 1:
count = 0
elif len(pattern) < counts[0]:
count = 0
else:
count = 0
if pattern[0] == "?":
count += compute_fitting_arrangements(pattern[1:], counts)
if len(pattern) == counts[0]:
count += 1
elif pattern[counts[0]] != "#":
count += compute_fitting_arrangements(pattern[counts[0] + 1 :], counts[1:])
return count
@lru_cache
def compute_possible_arrangements(
patterns: tuple[str, ...], counts: tuple[int, ...]
) -> int:
if not patterns:
return 1 if not counts else 0
with_hash = sum(1 for p in patterns[1:] if p.find("#") >= 0)
if with_hash > len(counts):
return 0
to_fit = counts if with_hash == 0 else counts[:-with_hash]
remaining = () if with_hash == 0 else counts[-with_hash:]
if not to_fit:
if patterns[0].find("#") != -1:
return 0
return compute_possible_arrangements(patterns[1:], remaining)
elif patterns[0].find("#") != -1 and len(patterns[0]) < to_fit[0]:
return 0
elif patterns[0].find("?") == -1:
if len(patterns[0]) != to_fit[0]:
return 0
return compute_possible_arrangements(patterns[1:], counts[1:])
else:
return sum(
fp * compute_possible_arrangements(patterns[1:], to_fit[i:] + remaining)
for i in range(len(to_fit) + 1)
if (fp := compute_fitting_arrangements(patterns[0], to_fit[:i])) > 0
)
def compute_all_possible_arrangements(lines: Iterable[str], repeat: int) -> int:
count = 0
if VERBOSE:
from tqdm import tqdm
lines = tqdm(lines)
for line in lines:
parts = line.split(" ")
count += compute_possible_arrangements(
tuple(filter(len, "?".join(parts[0] for _ in range(repeat)).split("."))),
tuple(int(c) for c in parts[1].split(",")) * repeat,
)
return count
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = compute_all_possible_arrangements(lines, 1)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = compute_all_possible_arrangements(lines, 5)
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,43 @@
import sys
from typing import Callable, Literal
def split(block: list[str], axis: Literal[0, 1], count: int) -> int:
n_iter = len(block) if axis == 0 else len(block[0])
n_check = len(block) if axis == 1 else len(block[0])
at: Callable[[int, int], str] = (
(lambda i, j: block[i][j]) if axis == 0 else (lambda i, j: block[j][i])
)
for i in range(n_iter - 1):
size = min(i + 1, n_iter - i - 1)
if (
sum(
at(i - s, j) != at(i + 1 + s, j)
for s in range(0, size)
for j in range(n_check)
)
== count
):
return i + 1
return 0
blocks = [block.splitlines() for block in sys.stdin.read().split("\n\n")]
# part 1
answer_1 = sum(
split(block, axis=1, count=0) + 100 * split(block, axis=0, count=0)
for block in blocks
)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = sum(
split(block, axis=1, count=1) + 100 * split(block, axis=0, count=1)
for block in blocks
)
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,68 @@
import sys
from typing import TypeAlias
RockGrid: TypeAlias = list[list[str]]
rocks0 = [list(line) for line in sys.stdin.read().splitlines()]
def slide_rocks_top(rocks: RockGrid) -> RockGrid:
top = [0 if c == "." else 1 for c in rocks[0]]
for row in range(1, len(rocks)):
for col in range(len(rocks[0])):
match rocks[row][col]:
case "O":
if top[col] != row:
rocks[top[col]][col] = "O"
rocks[row][col] = "."
top[col] = top[col] + 1
case "#":
top[col] = row + 1
case _:
pass
return rocks
def cycle(rocks: RockGrid) -> RockGrid:
for _ in range(4):
rocks = slide_rocks_top(rocks)
rocks = [
[rocks[len(rocks) - j - 1][i] for j in range(len(rocks))]
for i in range(len(rocks[0]))
]
return rocks
rocks = slide_rocks_top([[c for c in r] for r in rocks0])
# part 1
answer_1 = sum(
(len(rocks) - i) * sum(1 for c in row if c == "O") for i, row in enumerate(rocks)
)
print(f"answer 1 is {answer_1}")
# part 2
rocks = rocks0
N = 1000000000
cycles: list[RockGrid] = []
i_cycle: int = -1
for i_cycle in range(N):
rocks = cycle(rocks)
if any(rocks == c for c in cycles):
break
cycles.append([[c for c in r] for r in rocks])
cycle_start = next(i for i in range(len(cycles)) if (rocks == cycles[i]))
cycle_length = i_cycle - cycle_start
ci = cycle_start + (N - cycle_start) % cycle_length - 1
answer_2 = sum(
(len(rocks) - i) * sum(1 for c in row if c == "O")
for i, row in enumerate(cycles[ci])
)
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,31 @@
import sys
from functools import reduce
steps = sys.stdin.read().strip().split(",")
def _hash(s: str) -> int:
return reduce(lambda v, u: ((v + ord(u)) * 17) % 256, s, 0)
# part 1
answer_1 = sum(map(_hash, steps))
print(f"answer 1 is {answer_1}")
# part 2
boxes: list[dict[str, int]] = [{} for _ in range(256)]
for step in steps:
if (i := step.find("=")) >= 0:
label, length = step[:i], int(step[i + 1 :])
boxes[_hash(label)][label] = length
else:
label = step[:-1]
boxes[_hash(label)].pop(label, None)
answer_2 = sum(
i_box * i_lens * length
for i_box, box in enumerate(boxes, start=1)
for i_lens, length in enumerate(box.values(), start=1)
)
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,110 @@
import os
import sys
from typing import Literal, TypeAlias, cast
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
CellType: TypeAlias = Literal[".", "|", "-", "\\", "/"]
Direction: TypeAlias = Literal["R", "L", "U", "D"]
Mappings: dict[
CellType,
dict[
Direction,
tuple[tuple[tuple[int, int, Direction], ...], tuple[Direction, ...]],
],
] = {
".": {
"R": (((0, +1, "R"),), ("R", "L")),
"L": (((0, -1, "L"),), ("R", "L")),
"U": (((-1, 0, "U"),), ("U", "D")),
"D": (((+1, 0, "D"),), ("U", "D")),
},
"-": {
"R": (((0, +1, "R"),), ("R", "L")),
"L": (((0, -1, "L"),), ("R", "L")),
"U": (((0, +1, "R"), (0, -1, "L")), ("U", "D")),
"D": (((0, +1, "R"), (0, -1, "L")), ("U", "D")),
},
"|": {
"U": (((-1, 0, "U"),), ("U", "D")),
"D": (((+1, 0, "D"),), ("U", "D")),
"R": (((-1, 0, "U"), (+1, 0, "D")), ("R", "L")),
"L": (((-1, 0, "U"), (+1, 0, "D")), ("R", "L")),
},
"/": {
"R": (((-1, 0, "U"),), ("R", "D")),
"L": (((+1, 0, "D"),), ("L", "U")),
"U": (((0, +1, "R"),), ("U", "L")),
"D": (((0, -1, "L"),), ("R", "D")),
},
"\\": {
"R": (((+1, 0, "D"),), ("R", "U")),
"L": (((-1, 0, "U"),), ("L", "D")),
"U": (((0, -1, "L"),), ("U", "R")),
"D": (((0, +1, "R"),), ("L", "D")),
},
}
def propagate(
layout: list[list[CellType]], start: tuple[int, int], direction: Direction
) -> list[list[tuple[Direction, ...]]]:
n_rows, n_cols = len(layout), len(layout[0])
beams: list[list[tuple[Direction, ...]]] = [
[() for _ in range(len(layout[0]))] for _ in range(len(layout))
]
queue = [(start, direction)]
while queue:
(row, col), direction = queue.pop()
if (
row not in range(0, n_rows)
or col not in range(0, n_cols)
or direction in beams[row][col]
):
continue
moves, update = Mappings[layout[row][col]][direction]
beams[row][col] += update
for move in moves:
queue.append(((row + move[0], col + move[1]), move[2]))
return beams
layout: list[list[CellType]] = [
[cast(CellType, col) for col in row] for row in sys.stdin.read().splitlines()
]
beams = propagate(layout, (0, 0), "R")
if VERBOSE:
print("\n".join(["".join("#" if col else "." for col in row) for row in beams]))
# part 1
answer_1 = sum(sum(map(bool, row)) for row in beams)
print(f"answer 1 is {answer_1}")
# part 2
n_rows, n_cols = len(layout), len(layout[0])
cases: list[tuple[tuple[int, int], Direction]] = []
for row in range(n_rows):
cases.append(((row, 0), "R"))
cases.append(((row, n_cols - 1), "L"))
for col in range(n_cols):
cases.append(((0, col), "D"))
cases.append(((n_rows - 1, col), "U"))
answer_2 = max(
sum(sum(map(bool, row)) for row in propagate(layout, start, direction))
for start, direction in cases
)
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,233 @@
from __future__ import annotations
import heapq
import os
import sys
from collections import defaultdict
from dataclasses import dataclass
from typing import Literal, TypeAlias
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
Direction: TypeAlias = Literal[">", "<", "^", "v"]
@dataclass(frozen=True, order=True)
class Label:
row: int
col: int
direction: Direction
count: int
parent: Label | None = None
# mappings from direction to row shift / col shift / opposite direction
MAPPINGS: dict[Direction, tuple[int, int, Direction]] = {
">": (0, +1, "<"),
"<": (0, -1, ">"),
"v": (+1, 0, "^"),
"^": (-1, 0, "v"),
}
def print_shortest_path(
grid: list[list[int]],
target: tuple[int, int],
per_cell: dict[tuple[int, int], list[tuple[Label, int]]],
):
assert len(per_cell[target]) == 1
label = per_cell[target][0][0]
path: list[Label] = []
while True:
path.insert(0, label)
if label.parent is None:
break
label = label.parent
p_grid = [[str(c) for c in r] for r in grid]
for i in range(len(grid)):
for j in range(len(grid[0])):
if per_cell[i, j]:
p_grid[i][j] = f"\033[94m{grid[i][j]}\033[0m"
prev_label = path[0]
for label in path[1:]:
for r in range(
min(prev_label.row, label.row), max(prev_label.row, label.row) + 1
):
for c in range(
min(prev_label.col, label.col),
max(prev_label.col, label.col) + 1,
):
if (r, c) != (prev_label.row, prev_label.col):
p_grid[r][c] = f"\033[93m{grid[r][c]}\033[0m"
p_grid[label.row][label.col] = f"\033[91m{grid[label.row][label.col]}\033[0m"
prev_label = label
p_grid[0][0] = f"\033[92m{grid[0][0]}\033[0m"
print("\n".join("".join(row) for row in p_grid))
def shortest_many_paths(grid: list[list[int]]) -> dict[tuple[int, int], int]:
n_rows, n_cols = len(grid), len(grid[0])
visited: dict[tuple[int, int], tuple[Label, int]] = {}
queue: list[tuple[int, Label]] = [
(0, Label(row=n_rows - 1, col=n_cols - 1, direction="^", count=0))
]
while queue and len(visited) != n_rows * n_cols:
distance, label = heapq.heappop(queue)
if (label.row, label.col) in visited:
continue
visited[label.row, label.col] = (label, distance)
for direction, (c_row, c_col, i_direction) in MAPPINGS.items():
if label.direction == i_direction:
continue
else:
row, col = (label.row + c_row, label.col + c_col)
# exclude labels outside the grid or with too many moves in the same
# direction
if row not in range(0, n_rows) or col not in range(0, n_cols):
continue
heapq.heappush(
queue,
(
distance
+ sum(
grid[r][c]
for r in range(min(row, label.row), max(row, label.row) + 1)
for c in range(min(col, label.col), max(col, label.col) + 1)
)
- grid[row][col],
Label(
row=row,
col=col,
direction=direction,
count=0,
parent=label,
),
),
)
return {(r, c): visited[r, c][1] for r in range(n_rows) for c in range(n_cols)}
def shortest_path(
grid: list[list[int]],
min_straight: int,
max_straight: int,
lower_bounds: dict[tuple[int, int], int],
) -> int:
n_rows, n_cols = len(grid), len(grid[0])
target = (len(grid) - 1, len(grid[0]) - 1)
# for each tuple (row, col, direction, count), the associated label when visited
visited: dict[tuple[int, int, str, int], Label] = {}
# list of all visited labels for a cell (with associated distance)
per_cell: dict[tuple[int, int], list[tuple[Label, int]]] = defaultdict(list)
# need to add two start labels, otherwise one of the two possible direction will
# not be possible
queue: list[tuple[int, int, Label]] = [
(lower_bounds[0, 0], 0, Label(row=0, col=0, direction="^", count=0)),
(lower_bounds[0, 0], 0, Label(row=0, col=0, direction="<", count=0)),
]
while queue:
_, distance, label = heapq.heappop(queue)
if (label.row, label.col, label.direction, label.count) in visited:
continue
visited[label.row, label.col, label.direction, label.count] = label
per_cell[label.row, label.col].append((label, distance))
if (label.row, label.col) == target:
break
for direction, (c_row, c_col, i_direction) in MAPPINGS.items():
# cannot move in the opposite direction
if label.direction == i_direction:
continue
# other direction, move 'min_straight' in the new direction
elif label.direction != direction:
row, col, count = (
label.row + min_straight * c_row,
label.col + min_straight * c_col,
min_straight,
)
# same direction, too many count
elif label.count == max_straight:
continue
# same direction, keep going and increment count
else:
row, col, count = (
label.row + c_row,
label.col + c_col,
label.count + 1,
)
# exclude labels outside the grid or with too many moves in the same
# direction
if row not in range(0, n_rows) or col not in range(0, n_cols):
continue
distance_to = (
distance
+ sum(
grid[r][c]
for r in range(min(row, label.row), max(row, label.row) + 1)
for c in range(min(col, label.col), max(col, label.col) + 1)
)
- grid[label.row][label.col]
)
heapq.heappush(
queue,
(
distance_to + lower_bounds[row, col],
distance_to,
Label(
row=row,
col=col,
direction=direction,
count=count,
parent=label,
),
),
)
if VERBOSE:
print_shortest_path(grid, target, per_cell)
return per_cell[target][0][1]
data = [[int(c) for c in r] for r in sys.stdin.read().splitlines()]
estimates = shortest_many_paths(data)
# part 1
answer_1 = shortest_path(data, 1, 3, lower_bounds=estimates)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = shortest_path(data, 4, 10, lower_bounds=estimates)
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,54 @@
import sys
from typing import Literal, TypeAlias, cast
Direction: TypeAlias = Literal["R", "L", "U", "D"]
DIRECTIONS: list[Direction] = ["R", "D", "L", "U"]
MOVES: dict[Direction, tuple[int, int]] = {
"R": (0, +1),
"L": (0, -1),
"U": (-1, 0),
"D": (+1, 0),
}
def area(corners: list[tuple[int, int]], perimeter: int) -> int:
area = abs(
sum(c0[0] * c1[1] - c0[1] * c1[0] for c0, c1 in zip(corners, corners[1::])) // 2
)
return 1 + area - perimeter // 2 + perimeter
def polygon(values: list[tuple[Direction, int]]) -> tuple[list[tuple[int, int]], int]:
perimeter = 0
corners: list[tuple[int, int]] = [(0, 0)]
for direction, amount in values:
perimeter += amount
corners.append(
(
corners[-1][0] + amount * MOVES[direction][0],
corners[-1][1] + amount * MOVES[direction][1],
)
)
return corners, perimeter
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = area(
*polygon([(cast(Direction, (p := line.split())[0]), int(p[1])) for line in lines])
)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = area(
*polygon(
[
(DIRECTIONS[int((h := line.split()[-1])[-2])], int(h[2:-2], 16))
for line in lines
]
)
)
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,132 @@
import logging
import operator
import os
import sys
from math import prod
from typing import Literal, TypeAlias, cast
VERBOSE = os.getenv("AOC_VERBOSE") == "True"
logging.basicConfig(level=logging.INFO if VERBOSE else logging.WARNING)
Category: TypeAlias = Literal["x", "m", "a", "s"]
Part: TypeAlias = dict[Category, int]
PartWithBounds: TypeAlias = dict[Category, tuple[int, int]]
OPERATORS = {"<": operator.lt, ">": operator.gt}
# None if there is no check (last entry), otherwise (category, sense, value)
Check: TypeAlias = tuple[Category, Literal["<", ">"], int] | None
# workflow as a list of check, in specified order, with target
Workflow: TypeAlias = list[tuple[Check, str]]
def accept(workflows: dict[str, Workflow], part: Part) -> bool:
workflow = "in"
decision: bool | None = None
while decision is None:
for check, target in workflows[workflow]:
ok = check is None
if check is not None:
category, sense, value = check
ok = OPERATORS[sense](part[category], value)
if ok:
if target in workflows:
workflow = target
else:
decision = target == "A"
break
return decision
def propagate(workflows: dict[str, Workflow], start: PartWithBounds) -> int:
def transfer_or_accept(
target: str, meta: PartWithBounds, queue: list[tuple[PartWithBounds, str]]
) -> int:
if target in workflows:
logging.info(f" transfer to {target}")
queue.append((meta, target))
return 0
elif target == "A":
logging.info(" accepted")
return prod((high - low + 1) for low, high in meta.values())
else:
logging.info(" rejected")
return 0
accepted = 0
queue: list[tuple[PartWithBounds, str]] = [(start, "in")]
while queue:
meta, workflow = queue.pop()
logging.info(f"{workflow}: {meta}")
for check, target in workflows[workflow]:
if check is None:
accepted += transfer_or_accept(target, meta, queue)
continue
category, sense, value = check
bounds, op = meta[category], OPERATORS[sense]
logging.info(f" splitting {meta} into {category} {sense} {value}")
if not op(bounds[0], value) and not op(bounds[1], value):
logging.info(" reject, always false")
continue
if op(meta[category][0], value) and op(meta[category][1], value):
logging.info(" accept, always true")
accepted += transfer_or_accept(target, meta, queue)
break
meta2 = meta.copy()
if sense == "<":
meta2[category] = (meta[category][0], value - 1)
meta[category] = (value, meta[category][1])
else:
meta2[category] = (value + 1, meta[category][1])
meta[category] = (meta[category][0], value)
logging.info(f" split {meta2} ({target}), {meta}")
accepted += transfer_or_accept(target, meta2, queue)
return accepted
workflows_s, parts_s = sys.stdin.read().strip().split("\n\n")
workflows: dict[str, Workflow] = {}
for workflow_s in workflows_s.split("\n"):
name, block_s = workflow_s.split("{")
workflows[name] = []
for block in block_s[:-1].split(","):
check: Check
if (i := block.find(":")) >= 0:
check, target = (
cast(Category, block[0]),
cast(Literal["<", ">"], block[1]),
int(block[2:i]),
), block[i + 1 :]
else:
check, target = None, block
workflows[name].append((check, target))
# part 1
parts: list[Part] = [
{cast(Category, s[0]): int(s[2:]) for s in part_s[1:-1].split(",")}
for part_s in parts_s.split("\n")
]
answer_1 = sum(sum(part.values()) for part in parts if accept(workflows, part))
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = propagate(
workflows, {cast(Category, c): (1, 4000) for c in ["x", "m", "a", "s"]}
)
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,43 @@
import math
import sys
from typing import Literal, TypeAlias, cast
CubeType: TypeAlias = Literal["red", "blue", "green"]
MAX_CUBES: dict[CubeType, int] = {"red": 12, "green": 13, "blue": 14}
# parse games
lines = sys.stdin.read().splitlines()
games: dict[int, list[dict[CubeType, int]]] = {}
for line in filter(bool, lines):
id_part, sets_part = line.split(":")
games[int(id_part.split(" ")[-1])] = [
{
cast(CubeType, s[1]): int(s[0])
for cube_draw in cube_set_s.strip().split(", ")
if (s := cube_draw.split(" "))
}
for cube_set_s in sets_part.strip().split(";")
]
# part 1
answer_1 = sum(
id
for id, set_of_cubes in games.items()
if all(
n_cubes <= MAX_CUBES[cube]
for cube_set in set_of_cubes
for cube, n_cubes in cube_set.items()
)
)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = sum(
math.prod(
max(cube_set.get(cube, 0) for cube_set in set_of_cubes) for cube in MAX_CUBES
)
for set_of_cubes in games.values()
)
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,13 @@
import sys
from collections import defaultdict
from dataclasses import dataclass
lines = sys.stdin.read().splitlines()
# part 1
answer_1 = ...
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = ...
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,53 @@
import string
import sys
from collections import defaultdict
NOT_A_SYMBOL = "." + string.digits
lines = sys.stdin.read().splitlines()
values: list[int] = []
gears: dict[tuple[int, int], list[int]] = defaultdict(list)
for i, line in enumerate(lines):
j = 0
while j < len(line):
# skip everything until a digit is found (start of a number)
if line[j] not in string.digits:
j += 1
continue
# extract the range of the number and its value
k = j + 1
while k < len(line) and line[k] in string.digits:
k += 1
value = int(line[j:k])
# lookup around the number if there is a symbol - we go through the number
# itself but that should not matter since it only contains digits
found = False
for i2 in range(max(0, i - 1), min(i + 1, len(lines) - 1) + 1):
for j2 in range(max(0, j - 1), min(k, len(line) - 1) + 1):
assert i2 >= 0 and i2 < len(lines)
assert j2 >= 0 and j2 < len(line)
if lines[i2][j2] not in NOT_A_SYMBOL:
found = True
if lines[i2][j2] == "*":
gears[i2, j2].append(value)
if found:
values.append(value)
# continue starting from the end of the number
j = k
# part 1
answer_1 = sum(values)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = sum(v1 * v2 for v1, v2 in filter(lambda vs: len(vs) == 2, gears.values()))
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,41 @@
import sys
from dataclasses import dataclass
@dataclass(frozen=True)
class Card:
id: int
numbers: list[int]
values: list[int]
lines = sys.stdin.read().splitlines()
cards: list[Card] = []
for line in lines:
id_part, e_part = line.split(":")
numbers_s, values_s = e_part.split("|")
cards.append(
Card(
id=int(id_part.split()[1]),
numbers=[int(v.strip()) for v in numbers_s.strip().split()],
values=[int(v.strip()) for v in values_s.strip().split()],
)
)
winnings = [sum(1 for n in card.values if n in card.numbers) for card in cards]
# part 1
answer_1 = sum(2 ** (winning - 1) for winning in winnings if winning > 0)
print(f"answer 1 is {answer_1}")
# part 2
card2cards = {i: list(range(i + 1, i + w + 1)) for i, w in enumerate(winnings)}
card2values = {i: 0 for i in range(len(cards))}
for i in range(len(cards)):
card2values[i] += 1
for j in card2cards[i]:
card2values[j] += card2values[i]
print(f"answer 2 is {sum(card2values.values())}")

129
src/holt59/aoc/2023/day5.py Normal file
View File

@@ -0,0 +1,129 @@
import sys
from typing import Sequence
MAP_ORDER = [
"seed",
"soil",
"fertilizer",
"water",
"light",
"temperature",
"humidity",
"location",
]
lines = sys.stdin.read().splitlines()
# mappings from one category to another, each list contains
# ranges stored as (source, target, length), ordered by start and
# completed to have no "hole"
maps: dict[tuple[str, str], list[tuple[int, int, int]]] = {}
# parsing
index = 2
while index < len(lines):
p1, _, p2 = lines[index].split()[0].split("-")
# extract the existing ranges from the file - we store as (source, target, length)
# whereas the file is in order (target, source, length)
index += 1
values: list[tuple[int, int, int]] = []
while index < len(lines) and lines[index]:
n1, n2, n3 = lines[index].split()
values.append((int(n2), int(n1), int(n3)))
index += 1
# sort by source value
values.sort()
# add a 'fake' interval starting at 0 if missing
if values[0][0] != 0:
values.insert(0, (0, 0, values[0][0]))
# fill gaps between intervals
for i in range(len(values) - 1):
next_start = values[i + 1][0]
end = values[i][0] + values[i][2]
if next_start != end:
values.insert(
i + 1,
(end, end, next_start - end),
)
# add an interval covering values up to at least 2**32 at the end
last_start, _, last_length = values[-1]
values.append((last_start + last_length, last_start + last_length, 2**32))
assert all(v1[0] + v1[2] == v2[0] for v1, v2 in zip(values[:-1], values[1:]))
assert values[0][0] == 0
assert values[-1][0] + values[-1][-1] >= 2**32
maps[p1, p2] = values
index += 1
def find_range(
values: tuple[int, int], map: list[tuple[int, int, int]]
) -> list[tuple[int, int]]:
"""
Given an input range, use the given mapping to find the corresponding list of
ranges in the target domain.
"""
r_start, r_length = values
ranges: list[tuple[int, int]] = []
# find index of the first and last intervals in map that overlaps the input
# interval
index_start, index_end = -1, -1
for index_start, (start, _, length) in enumerate(map):
if start <= r_start and start + length > r_start:
break
for index_end, (start, _, length) in enumerate(
map[index_start:], start=index_start
):
if r_start + r_length >= start and r_start + r_length < start + length:
break
assert index_start >= 0 and index_end >= 0
# special case if one interval contains everything
if index_start == index_end:
start, target, length = map[index_start]
ranges.append((target + r_start - start, r_length))
else:
# add the start interval part
start, target, length = map[index_start]
ranges.append((target + r_start - start, start + length - r_start))
# add all intervals between the first and last (excluding both)
index = index_start + 1
while index < index_end:
start, target, length = map[index]
ranges.append((target, length))
index += 1
# add the last interval
start, target, length = map[index_end]
ranges.append((target, r_start + r_length - start))
return ranges
def find_location_ranges(seeds: Sequence[tuple[int, int]]) -> Sequence[tuple[int, int]]:
for map1, map2 in zip(MAP_ORDER[:-1], MAP_ORDER[1:]):
seeds = [s2 for s1 in seeds for s2 in find_range(s1, maps[map1, map2])]
return seeds
# part 1 - use find_range() with range of length 1
seeds_p1 = [(int(s), 1) for s in lines[0].split(":")[1].strip().split()]
answer_1 = min(start for start, _ in find_location_ranges(seeds_p1))
print(f"answer 1 is {answer_1}")
# # part 2
parts = lines[0].split(":")[1].strip().split()
seeds_p2 = [(int(s), int(e)) for s, e in zip(parts[::2], parts[1::2])]
answer_2 = min(start for start, _ in find_location_ranges(seeds_p2))
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,47 @@
import math
import sys
def extreme_times_to_beat(time: int, distance: int) -> tuple[int, int]:
# formula (T = total time, D = target distance)
# - speed(t) = t,
# - distance(t) = (T - t) * speed(t)
# - distance(t) > D
# <=> (T - t) * t > D
# <=> -t^2 + T * t - D >= 0
a, b, c = -1, time, -distance
d = b * b - 4 * a * c
t1 = math.ceil(-(b - math.sqrt(d)) / (2 * a))
t2 = math.floor(-(b + math.sqrt(d)) / (2 * a))
if (time - t1) * t1 == distance:
t1 += 1
if (time - t2) * t2 == distance:
t2 -= 1
return t1, t2
lines = sys.stdin.read().splitlines()
# part 1
times = list(map(int, lines[0].split()[1:]))
distances = list(map(int, lines[1].split()[1:]))
answer_1 = math.prod(
t2 - t1 + 1
for t1, t2 in (
extreme_times_to_beat(time, distance)
for time, distance in zip(times, distances)
)
)
print(f"answer 1 is {answer_1}")
# part 2
time = int(lines[0].split(":")[1].strip().replace(" ", ""))
distance = int(lines[1].split(":")[1].strip().replace(" ", ""))
t1, t2 = extreme_times_to_beat(time, distance)
answer_2 = t2 - t1 + 1
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,49 @@
import sys
from collections import Counter, defaultdict
class HandTypes:
HIGH_CARD = 0
ONE_PAIR = 1
TWO_PAIR = 2
THREE_OF_A_KIND = 3
FULL_HOUSE = 4
FOUR_OF_A_KIND = 5
FIVE_OF_A_KIND = 6
# mapping from number of different cards + highest count of card to the hand type
LEN_LAST_TO_TYPE: dict[int, dict[int, int]] = {
1: defaultdict(lambda: HandTypes.FIVE_OF_A_KIND),
2: defaultdict(lambda: HandTypes.FULL_HOUSE, {4: HandTypes.FOUR_OF_A_KIND}),
3: defaultdict(lambda: HandTypes.TWO_PAIR, {3: HandTypes.THREE_OF_A_KIND}),
4: defaultdict(lambda: HandTypes.ONE_PAIR),
5: defaultdict(lambda: HandTypes.HIGH_CARD),
}
def extract_key(hand: str, values: dict[str, int], joker: str = "0") -> tuple[int, ...]:
# get count of each cards, in increasing count - the 'or' part handles the JJJJJ
# case when joker is True
cnt = sorted(Counter(hand.replace(joker, "")).values()) or [0]
return (LEN_LAST_TO_TYPE[len(cnt)][cnt[-1] + hand.count(joker)],) + tuple(
values[c] for c in hand
)
lines = sys.stdin.read().splitlines()
cards = [(t[0], int(t[1])) for line in lines if (t := line.split())]
# part 1
values = {card: value for value, card in enumerate("23456789TJQKA")}
cards.sort(key=lambda cv: extract_key(cv[0], values=values))
answer_1 = sum(rank * value for rank, (_, value) in enumerate(cards, start=1))
print(f"answer 1 is {answer_1}")
# part 2
values = {card: value for value, card in enumerate("J23456789TQKA")}
cards.sort(key=lambda cv: extract_key(cv[0], values=values, joker="J"))
answer_2 = sum(rank * value for rank, (_, value) in enumerate(cards, start=1))
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,29 @@
import itertools
import math
import sys
lines = sys.stdin.read().splitlines()
sequence = lines[0]
nodes = {
p[0]: {d: n for d, n in zip("LR", p[1].strip("()").split(", "))}
for line in lines[2:]
if (p := line.split(" = "))
}
def path(start: str):
path = [start]
it_seq = iter(itertools.cycle(sequence))
while not path[-1].endswith("Z"):
path.append(nodes[path[-1]][next(it_seq)])
return path
# part 1
answer_1 = len(path(next(node for node in nodes if node.endswith("A")))) - 1
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = math.lcm(*(len(path(node)) - 1 for node in nodes if node.endswith("A")))
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,29 @@
import sys
lines = sys.stdin.read().splitlines()
data = [[int(c) for c in line.split()] for line in lines]
right_values: list[int] = []
left_values: list[int] = []
for values in data:
diffs = [values]
while any(d != 0 for d in diffs[-1]):
diffs.append([rhs - lhs for lhs, rhs in zip(diffs[-1][:-1], diffs[-1][1:])])
rhs: list[int] = [0]
lhs: list[int] = [0]
for cx in range(len(diffs) - 1):
rhs.append(diffs[-cx - 2][-1] + rhs[cx])
lhs.append(diffs[-cx - 2][0] - lhs[cx])
right_values.append(rhs[-1])
left_values.append(lhs[-1])
# part 1
answer_1 = sum(right_values)
print(f"answer 1 is {answer_1}")
# part 2
answer_2 = sum(left_values)
print(f"answer 2 is {answer_2}")

View File

@@ -0,0 +1,49 @@
import argparse
import importlib
import os
import sys
from pathlib import Path
def main():
parser = argparse.ArgumentParser("Holt59 Advent-Of-Code Runner")
parser.add_argument("-v", "--verbose", action="store_true", help="verbose mode")
parser.add_argument("-t", "--test", action="store_true", help="test mode")
parser.add_argument(
"-u", "--user", type=str, default="holt59", help="user input to use"
)
parser.add_argument(
"-i",
"--input",
type=Path,
default=None,
help="input to use (override user and test)",
)
parser.add_argument("-y", "--year", type=int, help="year to run", default=2023)
parser.add_argument("day", type=int, help="day to run")
args = parser.parse_args()
verbose: bool = args.verbose
test: bool = args.test
user: str = args.user
input_path: Path | None = args.input
year: int = args.year
day: int = args.day
# TODO: change this
if verbose:
os.environ["AOC_VERBOSE"] = "True"
if input_path is None:
input_path = Path(__file__).parent.joinpath(
"inputs", "tests" if test else user, str(year), f"day{day}.txt"
)
assert input_path.exists(), f"{input_path} missing"
with open(input_path) as fp:
sys.stdin = fp
importlib.import_module(f".{year}.day{day}", __package__)
sys.stdin = sys.__stdin__

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,601 @@
46,12,57,37,14,78,31,71,87,52,64,97,10,35,54,36,27,84,80,94,99,22,0,11,30,44,86,59,66,7,90,21,51,53,92,8,76,41,39,77,42,88,29,24,60,17,68,13,79,67,50,82,25,61,20,16,6,3,81,19,85,9,28,56,75,96,2,26,1,62,33,63,32,73,18,48,43,65,98,5,91,69,47,4,38,23,49,34,55,83,93,45,72,95,40,15,58,74,70,89
37 72 60 35 89
32 49 4 77 82
30 26 27 63 88
29 43 16 34 58
48 33 96 79 94
41 94 77 43 87
2 17 82 96 25
95 49 32 12 9
59 33 67 71 64
88 54 93 85 30
78 84 73 64 81
6 66 54 21 15
72 88 69 5 93
11 96 38 95 44
13 41 94 55 48
5 14 2 82 33
56 26 0 84 92
8 95 24 54 25
68 67 15 85 47
20 91 36 13 88
39 26 33 65 32
78 72 80 51 0
35 64 60 18 31
93 59 83 54 74
86 5 9 98 69
0 8 20 18 70
5 29 65 21 57
68 61 83 63 51
91 73 77 75 80
35 62 16 32 10
51 78 58 67 93
50 14 99 5 31
6 21 48 30 83
22 33 23 1 34
2 72 57 54 42
15 68 4 24 49
12 9 74 88 51
91 19 50 76 75
80 84 23 17 53
67 42 22 85 36
41 78 11 69 9
90 25 98 65 77
97 53 37 84 89
58 63 5 55 1
24 10 74 20 82
42 19 95 89 49
61 31 50 76 3
34 47 32 69 86
78 68 99 11 91
55 12 73 45 23
24 53 95 64 14
40 29 71 57 97
62 70 25 22 2
88 68 33 82 59
72 38 76 78 43
73 36 84 90 40
16 4 57 9 29
38 97 46 51 83
86 88 99 44 32
54 49 37 43 62
18 66 17 49 27
24 93 91 87 72
54 37 77 43 10
88 80 60 15 79
47 68 12 2 69
9 23 13 57 68
38 97 63 88 98
96 62 65 82 58
61 83 29 47 40
21 86 20 16 56
27 90 37 97 52
14 96 76 21 79
0 43 63 81 56
42 62 23 55 74
45 72 77 44 47
8 78 63 24 87
9 23 12 17 68
36 83 45 61 50
84 77 18 86 37
31 26 19 49 94
72 84 59 48 40
92 98 35 1 80
83 15 85 63 39
2 64 58 13 20
29 88 60 12 74
21 94 52 6 4
89 70 39 23 64
96 87 31 54 14
88 35 83 13 56
84 10 98 48 68
70 33 48 21 37
91 95 65 38 77
92 14 26 96 60
12 6 73 13 81
54 55 2 45 80
60 11 67 95 28
5 32 0 71 12
47 78 13 54 43
49 89 82 66 77
26 53 19 79 3
81 9 53 72 29
56 35 60 44 45
42 94 96 88 64
15 92 4 6 14
97 11 17 61 63
24 43 33 9 34
36 28 69 35 7
47 4 14 82 38
11 1 52 0 49
93 87 98 41 5
37 79 99 34 77
38 26 25 95 70
28 78 40 33 86
41 57 96 10 24
9 74 72 50 81
18 96 52 29 61
38 90 1 48 51
78 11 27 55 97
33 21 87 93 67
79 46 94 45 2
27 63 6 90 10
3 60 24 5 89
78 72 76 54 8
33 22 87 51 58
4 37 64 91 43
63 73 87 80 89
29 14 95 48 3
71 55 69 9 67
30 99 19 2 86
26 72 88 85 37
12 57 81 78 40
35 4 55 15 39
33 45 25 60 70
86 79 88 52 3
90 20 28 59 85
92 51 98 47 99
41 78 65 4 46
19 87 39 89 17
12 23 36 29 44
6 82 71 16 37
8 34 81 67 80
83 92 13 11 41
39 89 93 49 43
20 69 3 74 76
44 72 68 70 45
66 39 94 98 28
72 4 25 77 76
56 41 84 59 40
36 87 18 44 73
29 45 79 55 95
45 91 2 92 16
21 47 86 81 56
31 11 62 5 95
39 1 30 65 33
42 60 17 18 83
86 11 77 30 43
51 88 73 98 94
72 63 38 56 10
57 92 49 7 41
79 75 34 23 54
56 95 3 43 65
39 62 93 19 27
61 41 99 96 52
4 92 77 98 70
16 54 11 17 57
6 63 10 71 58
64 70 50 92 0
7 14 99 45 26
78 17 44 46 73
77 38 62 53 37
31 82 67 55 27
57 58 84 6 15
14 41 49 8 85
12 32 91 42 19
23 1 87 54 29
54 60 43 26 4
78 17 28 67 5
87 93 90 71 22
13 30 16 21 85
55 74 52 1 29
50 16 70 32 33
6 94 52 66 22
97 64 98 72 39
27 69 99 34 26
36 91 37 21 14
7 97 64 28 18
85 80 14 37 34
72 1 22 58 73
53 3 68 17 0
29 44 56 95 32
30 66 93 24 92
48 80 79 86 27
89 13 62 94 81
70 65 61 8 54
96 97 20 90 34
87 76 4 7 43
92 55 80 25 62
79 6 88 35 30
10 32 5 45 17
36 27 33 68 63
72 69 27 88 41
34 53 42 84 3
58 18 22 66 65
9 47 85 12 62
73 90 91 57 33
67 16 50 58 52
68 70 84 98 69
4 72 9 64 0
93 97 39 26 5
3 37 79 7 82
61 57 88 54 70
77 8 94 81 63
39 48 18 13 10
55 23 27 4 73
3 5 64 0 96
62 27 0 52 19
28 57 83 25 41
5 59 24 33 80
37 85 2 86 43
22 94 50 8 20
54 32 34 47 87
71 22 43 85 24
11 68 58 36 46
35 56 61 67 18
70 23 72 5 59
3 96 41 45 32
68 2 56 28 24
87 38 40 75 26
53 64 73 80 81
54 88 20 6 18
64 55 51 96 47
59 35 49 67 71
36 91 61 76 68
6 94 20 8 27
60 88 45 7 82
87 94 51 91 1
96 60 28 97 37
26 27 74 53 35
88 89 11 77 8
73 47 18 59 6
46 50 19 36 83
69 28 4 44 70
45 20 63 27 1
53 38 9 47 67
91 31 79 73 86
45 3 98 91 60
40 7 78 34 83
52 73 59 13 4
38 15 82 86 79
42 11 17 20 62
65 86 38 20 72
78 45 73 74 25
62 42 24 75 3
81 8 35 50 51
44 11 94 85 57
13 86 55 65 96
53 18 43 76 20
41 14 32 52 38
90 59 80 68 7
2 23 92 39 50
96 62 85 24 14
37 5 11 91 45
61 28 23 34 77
43 48 20 0 21
10 35 2 26 97
89 5 40 34 84
90 6 72 68 10
13 64 71 31 76
53 60 9 92 62
69 98 8 50 3
17 86 10 75 79
67 94 78 40 56
11 85 82 50 46
53 39 22 9 61
59 73 72 33 45
65 22 18 96 95
55 86 67 52 69
10 2 60 83 98
43 61 87 88 66
41 24 8 84 33
31 53 98 70 91
33 34 48 83 9
40 39 29 71 65
69 10 62 30 4
52 21 11 93 75
8 94 53 85 89
13 84 58 59 29
97 7 21 25 96
45 54 34 22 63
37 17 49 68 67
86 87 84 24 10
82 32 36 59 50
8 62 79 71 43
49 23 85 69 58
21 66 42 25 56
65 88 43 25 19
26 36 63 5 6
37 54 75 1 38
95 46 83 66 28
4 90 80 99 85
78 83 7 77 34
27 92 93 96 82
40 95 52 32 43
17 28 69 41 85
21 65 39 58 19
11 84 28 90 36
74 4 62 5 46
22 8 45 40 98
12 6 30 9 82
37 2 53 29 41
17 65 31 86 57
73 16 24 67 53
60 93 88 45 26
14 80 94 7 44
55 78 49 8 82
95 38 81 25 76
29 13 83 47 12
17 69 4 43 28
63 84 39 52 34
1 97 41 88 8
70 40 16 83 3
15 49 20 74 48
71 30 21 28 84
29 10 97 1 18
57 50 63 35 69
40 13 67 9 41
71 76 8 54 24
15 97 92 49 96
61 34 23 81 31
11 38 48 37 86
77 36 32 75 7
38 18 84 26 2
19 13 99 83 20
35 51 74 6 27
71 48 15 66 69
91 57 41 3 99
74 55 81 77 43
36 52 47 49 45
85 65 5 38 50
90 68 70 16 0
1 90 28 86 27
73 36 67 11 14
71 31 10 65 55
78 21 16 69 12
87 24 33 83 68
90 17 10 84 45
5 68 69 27 92
6 63 98 3 46
94 48 59 34 43
39 88 12 33 73
12 31 33 98 63
65 51 94 83 92
41 38 84 91 66
47 28 76 54 3
48 36 11 13 27
51 84 96 16 8
64 26 74 30 48
29 41 68 97 87
9 38 1 15 39
98 3 45 53 14
53 70 90 95 86
35 22 85 45 66
93 0 83 30 88
64 57 68 36 3
5 51 19 20 89
9 36 69 46 44
37 7 99 57 45
79 10 86 58 30
49 98 52 90 27
14 51 88 60 81
73 97 91 19 48
76 43 18 83 67
62 9 11 82 55
24 17 33 53 22
75 8 56 1 21
27 97 53 0 89
30 70 3 80 54
56 93 40 64 35
46 82 1 44 65
6 59 45 32 34
87 58 73 45 69
24 49 89 71 83
94 6 53 68 50
28 25 88 47 0
36 13 31 18 55
52 63 37 66 9
34 77 57 6 55
85 80 97 78 74
95 75 67 96 29
22 73 92 69 47
79 97 80 36 73
38 77 35 32 53
2 37 29 6 89
78 91 15 47 34
11 52 64 84 0
69 30 21 99 46
72 4 15 25 42
67 98 81 91 63
70 20 57 65 14
0 78 19 8 87
20 4 98 33 85
76 17 94 65 35
95 69 72 52 71
23 25 50 38 27
43 49 96 53 99
16 27 34 65 36
10 40 84 60 82
80 2 54 67 70
52 94 79 17 56
5 14 77 91 88
32 90 50 66 39
30 16 14 20 10
4 42 88 59 12
75 84 54 51 48
33 24 13 89 43
78 42 34 65 51
75 72 3 99 61
15 50 59 8 89
71 18 9 54 53
43 39 97 56 19
50 43 83 4 30
89 97 58 35 39
11 24 61 41 25
87 99 93 15 34
31 57 3 45 44
70 21 63 24 38
34 23 88 7 51
43 18 76 46 49
60 78 47 8 12
11 66 98 25 74
30 17 23 10 92
12 85 69 81 91
47 80 28 29 58
73 44 77 50 32
76 54 78 75 60
71 53 86 48 98
90 37 79 8 56
99 42 97 36 15
31 85 34 10 40
43 89 57 72 51
48 0 65 55 90
45 76 69 97 4
42 52 46 77 56
64 62 68 35 72
71 10 27 30 16
41 69 63 88 57
25 56 23 78 80
8 92 59 66 97
48 61 77 15 14
87 47 91 12 71
51 46 15 2 49
48 33 23 16 4
80 41 43 59 83
62 13 20 63 85
99 30 7 87 8
69 80 96 43 47
61 75 45 62 15
32 22 91 83 58
82 13 50 52 8
89 20 63 73 14
40 2 96 52 73
25 27 26 43 34
60 38 80 78 5
83 63 48 10 66
97 46 53 74 86
46 7 0 69 15
79 19 85 27 73
63 45 5 49 54
93 29 84 28 66
72 23 99 8 33
20 72 85 99 49
69 0 10 52 23
88 56 28 67 21
16 91 83 54 81
14 73 32 30 59
31 52 63 12 3
96 20 82 6 89
55 38 8 95 40
5 60 84 81 75
51 14 65 27 61
46 93 1 47 76
8 98 7 16 63
44 78 17 14 92
42 62 20 12 68
56 3 74 6 21
8 94 11 40 44
43 92 78 91 18
75 80 12 54 26
67 9 45 22 21
86 1 90 36 30
21 19 83 90 8
50 28 45 65 75
59 88 25 29 70
58 23 0 95 49
36 68 76 78 66
77 28 43 56 97
73 71 8 72 46
23 25 70 69 41
90 17 34 67 48
32 75 81 63 21

View File

@@ -0,0 +1,500 @@
657,934 -> 657,926
130,34 -> 570,474
478,716 -> 226,464
861,110 -> 861,167
448,831 -> 370,831
75,738 -> 390,738
26,880 -> 864,42
965,658 -> 527,220
208,381 -> 80,381
523,475 -> 807,475
219,69 -> 219,434
793,538 -> 534,797
754,602 -> 754,148
443,327 -> 443,611
606,395 -> 546,395
980,56 -> 51,985
619,325 -> 354,325
342,123 -> 819,600
290,533 -> 374,533
598,77 -> 598,75
605,302 -> 605,636
97,981 -> 692,386
278,779 -> 278,800
661,377 -> 661,10
726,108 -> 518,316
271,883 -> 271,50
382,271 -> 606,271
963,358 -> 891,286
496,880 -> 496,855
211,142 -> 211,49
841,866 -> 260,285
841,849 -> 173,181
927,326 -> 391,862
396,558 -> 459,558
753,183 -> 953,183
941,698 -> 941,407
347,612 -> 347,476
18,340 -> 18,612
140,299 -> 797,956
714,907 -> 714,228
966,155 -> 194,927
769,674 -> 712,674
644,675 -> 948,979
703,872 -> 812,763
26,629 -> 120,535
844,738 -> 844,253
798,133 -> 798,795
27,318 -> 288,57
38,545 -> 872,545
827,351 -> 195,983
818,45 -> 21,842
257,559 -> 626,928
145,925 -> 886,184
83,618 -> 590,111
326,243 -> 53,243
489,278 -> 526,278
783,693 -> 783,525
495,636 -> 495,585
374,716 -> 215,557
839,536 -> 839,966
850,468 -> 955,468
55,799 -> 55,447
472,722 -> 296,898
390,731 -> 120,461
405,493 -> 208,296
807,42 -> 56,793
476,327 -> 655,327
24,965 -> 967,22
776,211 -> 776,850
489,20 -> 822,20
630,740 -> 871,499
743,493 -> 283,953
62,429 -> 62,720
806,270 -> 806,332
550,154 -> 107,597
71,713 -> 533,251
620,575 -> 620,156
726,829 -> 143,246
944,553 -> 468,553
185,582 -> 185,468
845,266 -> 212,899
654,97 -> 265,486
726,609 -> 726,147
631,76 -> 860,76
835,24 -> 928,24
712,719 -> 74,81
616,478 -> 616,117
903,226 -> 903,577
440,699 -> 136,395
215,705 -> 890,30
20,24 -> 981,985
102,144 -> 850,892
695,967 -> 582,967
219,284 -> 219,388
359,833 -> 665,833
389,55 -> 305,55
59,32 -> 957,930
815,198 -> 64,949
699,540 -> 717,558
215,682 -> 182,682
805,489 -> 328,489
43,546 -> 578,546
489,181 -> 489,363
266,391 -> 266,582
863,368 -> 448,368
83,236 -> 83,487
874,875 -> 874,413
799,90 -> 799,802
253,29 -> 253,905
136,446 -> 435,745
830,534 -> 550,534
183,785 -> 107,785
81,517 -> 159,517
359,941 -> 359,560
71,546 -> 948,546
596,811 -> 596,791
255,960 -> 255,159
788,15 -> 788,682
240,55 -> 240,244
51,423 -> 137,423
504,418 -> 809,723
131,842 -> 914,59
727,790 -> 82,145
281,509 -> 841,509
797,807 -> 834,807
333,499 -> 790,499
215,328 -> 215,139
500,898 -> 500,862
75,217 -> 777,919
17,264 -> 17,446
852,755 -> 150,755
865,186 -> 385,186
158,192 -> 158,733
196,261 -> 196,128
989,960 -> 131,102
807,393 -> 807,153
507,579 -> 507,764
468,76 -> 535,76
381,357 -> 659,357
794,277 -> 749,277
51,152 -> 546,647
797,959 -> 458,959
82,156 -> 967,156
261,624 -> 460,624
597,53 -> 197,53
153,507 -> 411,765
305,717 -> 768,717
344,954 -> 344,217
194,432 -> 545,432
346,46 -> 557,46
685,599 -> 685,312
49,719 -> 49,631
499,668 -> 304,863
262,405 -> 554,405
87,64 -> 295,64
859,675 -> 74,675
663,776 -> 99,212
232,189 -> 232,904
777,276 -> 703,276
704,492 -> 86,492
142,736 -> 514,364
418,611 -> 224,417
602,571 -> 602,424
152,603 -> 248,603
915,673 -> 143,673
538,32 -> 128,32
975,885 -> 975,344
870,511 -> 870,756
330,798 -> 46,798
440,195 -> 587,195
739,237 -> 568,66
54,838 -> 196,980
370,556 -> 47,556
124,575 -> 748,575
261,283 -> 880,902
784,91 -> 426,449
764,670 -> 148,670
32,51 -> 967,986
807,906 -> 10,906
470,488 -> 579,597
274,649 -> 285,649
221,540 -> 221,94
914,957 -> 914,510
879,825 -> 145,91
438,833 -> 438,775
191,844 -> 911,124
145,763 -> 595,763
504,81 -> 622,199
834,206 -> 834,704
908,308 -> 815,308
929,567 -> 929,322
805,50 -> 620,235
36,409 -> 133,312
345,375 -> 19,701
468,948 -> 468,108
109,547 -> 446,547
929,916 -> 69,56
927,857 -> 318,248
833,948 -> 833,61
559,787 -> 559,982
293,825 -> 293,775
508,744 -> 545,744
827,713 -> 753,639
88,775 -> 555,775
523,812 -> 684,812
307,142 -> 307,265
636,40 -> 355,321
891,875 -> 891,25
301,423 -> 712,12
922,187 -> 219,890
45,447 -> 230,262
114,568 -> 233,687
573,398 -> 677,398
334,101 -> 324,101
957,277 -> 957,652
943,834 -> 610,834
523,632 -> 523,379
958,361 -> 90,361
408,824 -> 380,824
647,314 -> 647,449
747,83 -> 59,83
776,104 -> 937,104
16,984 -> 989,11
362,581 -> 362,226
72,962 -> 940,94
319,877 -> 319,122
310,206 -> 986,882
794,877 -> 267,877
855,58 -> 976,58
699,971 -> 598,971
162,556 -> 162,440
494,859 -> 494,255
794,210 -> 142,862
275,510 -> 548,510
739,592 -> 739,793
376,985 -> 376,990
755,264 -> 280,739
187,34 -> 187,688
770,827 -> 770,548
10,68 -> 913,971
571,427 -> 571,944
153,211 -> 153,560
976,972 -> 55,51
103,611 -> 674,40
95,972 -> 924,143
929,94 -> 38,985
777,330 -> 60,330
312,430 -> 312,326
549,433 -> 269,433
477,267 -> 477,403
598,375 -> 19,375
512,799 -> 512,831
348,700 -> 348,43
165,97 -> 63,199
38,835 -> 38,828
282,334 -> 282,909
14,891 -> 390,515
930,657 -> 334,61
630,341 -> 630,85
671,464 -> 319,112
949,340 -> 894,285
663,916 -> 245,916
114,395 -> 286,223
335,804 -> 529,804
567,338 -> 14,891
623,705 -> 379,949
82,864 -> 545,401
932,128 -> 932,134
291,294 -> 291,101
739,765 -> 739,757
460,94 -> 892,94
375,673 -> 367,681
81,831 -> 90,831
890,402 -> 890,138
775,547 -> 790,547
49,927 -> 966,10
23,116 -> 257,116
923,75 -> 18,980
63,986 -> 687,362
369,844 -> 357,844
790,188 -> 644,188
557,282 -> 557,669
861,173 -> 390,644
480,529 -> 893,529
32,960 -> 830,162
368,725 -> 368,40
502,600 -> 701,600
63,977 -> 873,167
463,518 -> 788,193
738,406 -> 324,406
162,931 -> 822,931
377,487 -> 707,817
610,319 -> 901,319
586,658 -> 690,658
25,288 -> 53,288
760,602 -> 760,628
294,62 -> 951,62
222,773 -> 661,334
151,483 -> 646,483
272,852 -> 317,852
557,906 -> 503,960
736,445 -> 736,703
241,376 -> 241,692
835,41 -> 835,369
987,743 -> 987,210
42,700 -> 42,244
646,136 -> 646,440
544,751 -> 404,751
295,651 -> 295,805
687,878 -> 113,878
290,142 -> 604,142
579,920 -> 579,807
12,985 -> 987,10
919,940 -> 919,808
770,143 -> 770,832
114,76 -> 962,76
876,882 -> 428,434
861,139 -> 861,320
888,59 -> 888,39
629,823 -> 707,823
296,598 -> 296,305
61,54 -> 578,54
864,58 -> 253,58
71,861 -> 306,861
682,181 -> 326,537
307,418 -> 307,910
810,251 -> 810,431
151,836 -> 602,385
954,987 -> 243,276
724,272 -> 350,646
134,295 -> 434,295
178,235 -> 802,859
832,688 -> 832,573
165,334 -> 165,378
816,26 -> 114,728
668,192 -> 540,192
730,341 -> 969,341
951,169 -> 286,834
647,115 -> 886,115
664,288 -> 507,131
609,362 -> 609,295
747,479 -> 287,19
350,967 -> 350,725
117,383 -> 311,383
871,124 -> 292,124
654,271 -> 547,271
525,773 -> 345,953
401,670 -> 610,670
930,196 -> 301,825
336,37 -> 961,662
714,212 -> 714,667
454,848 -> 454,107
587,390 -> 587,577
530,437 -> 542,437
304,229 -> 517,229
340,571 -> 766,571
727,941 -> 138,352
831,325 -> 11,325
241,294 -> 403,456
788,658 -> 788,126
337,360 -> 337,589
799,402 -> 342,402
530,820 -> 530,319
982,27 -> 20,989
923,936 -> 923,721
581,395 -> 64,912
61,509 -> 61,827
989,580 -> 610,580
477,592 -> 219,592
296,775 -> 296,58
204,12 -> 204,457
190,171 -> 190,673
939,200 -> 939,457
472,282 -> 472,631
983,331 -> 734,331
365,609 -> 365,817
640,698 -> 145,698
103,618 -> 549,618
454,319 -> 454,346
650,815 -> 381,546
624,603 -> 507,603
966,445 -> 723,445
763,129 -> 763,784
695,145 -> 695,511
498,84 -> 435,147
188,716 -> 967,716
810,446 -> 810,924
731,483 -> 731,51
307,783 -> 307,533
15,956 -> 956,15
192,210 -> 882,210
303,173 -> 38,438
769,952 -> 769,863
135,781 -> 405,781
494,436 -> 494,892
705,394 -> 714,394
164,37 -> 164,633
813,232 -> 813,620
227,906 -> 222,906
542,432 -> 414,432
549,858 -> 88,397
200,101 -> 958,859
235,565 -> 469,331
492,871 -> 503,882
704,398 -> 869,563
450,736 -> 746,736
420,706 -> 420,635
717,493 -> 686,524
187,554 -> 717,24
31,851 -> 315,851
800,230 -> 466,230
226,324 -> 226,614
937,927 -> 937,798
143,26 -> 534,417
952,344 -> 12,344
181,361 -> 782,361
925,906 -> 415,396
685,944 -> 470,944
200,627 -> 290,627
728,285 -> 728,326
271,864 -> 271,34
802,558 -> 207,558
963,26 -> 84,905
504,60 -> 529,60
840,292 -> 180,292
914,272 -> 914,330
82,107 -> 925,950
33,245 -> 33,134
463,663 -> 463,82
27,305 -> 27,675
276,894 -> 891,279
746,325 -> 746,948
249,657 -> 341,749
530,848 -> 28,346
798,617 -> 798,609
119,767 -> 312,767
80,18 -> 674,18
723,374 -> 583,374
582,985 -> 239,642
217,765 -> 217,395
811,159 -> 609,159
689,896 -> 501,896
562,881 -> 562,96
244,621 -> 629,621
277,379 -> 277,287
856,153 -> 20,153
518,228 -> 518,898
230,789 -> 243,789
534,335 -> 534,592
240,790 -> 413,617
768,615 -> 768,560
773,101 -> 912,101
252,571 -> 767,56
370,595 -> 681,906
565,176 -> 565,318
750,465 -> 750,724
979,130 -> 120,989
160,153 -> 160,785
610,222 -> 610,191
873,124 -> 130,867
519,593 -> 519,32
525,947 -> 525,562
50,292 -> 291,533
558,927 -> 960,525
536,694 -> 249,981
954,896 -> 277,896
732,202 -> 732,288
447,989 -> 541,895
890,754 -> 367,231
368,89 -> 564,285
588,100 -> 588,156
282,313 -> 943,974
16,792 -> 495,792
111,591 -> 111,493
57,713 -> 685,85
676,632 -> 676,575
560,708 -> 560,602
489,288 -> 489,404
904,515 -> 443,54
70,977 -> 985,62
11,119 -> 11,403
215,859 -> 937,137
78,469 -> 110,437
747,605 -> 747,369
847,598 -> 847,299
742,695 -> 159,112
986,370 -> 986,460
631,900 -> 771,760
228,406 -> 683,861
189,639 -> 61,639
221,650 -> 820,650
558,569 -> 834,845
655,533 -> 558,630
967,921 -> 967,169
230,308 -> 429,308
873,762 -> 873,528
412,151 -> 412,538
881,587 -> 881,21
941,45 -> 26,960
377,126 -> 700,126

Some files were not shown because too many files have changed in this diff Show More