Compare commits

...

48 commits

Author SHA1 Message Date
4fc6136588 2018: d25: ex2: add solution 2024-12-31 13:16:24 -05:00
a6c0017553 2018: d25: ex2: add input 2024-12-31 13:16:19 -05:00
220afe0b8a 2018: d25: ex1: add solution 2024-12-31 13:16:13 -05:00
00ed576a42 2018: d25: ex1: add input 2024-12-31 13:16:08 -05:00
759def15c9 2018: d24: ex2: add solution 2024-12-31 12:40:01 -05:00
da0803985b 2018: d24: ex2: add input 2024-12-31 12:39:47 -05:00
5f9a7cb80a 2018: d24: ex1: add solution 2024-12-31 12:39:39 -05:00
0d8ba9e62b 2018: d24: ex1: add input 2024-12-31 12:39:35 -05:00
df08b37dd2 2018: d23: ex2: add solution 2024-12-31 00:47:41 -05:00
3353a22586 2018: d23: ex2: add input 2024-12-31 00:47:30 -05:00
c7538c902a 2018: d23: ex1: add solution 2024-12-31 00:47:24 -05:00
25aeec5676 2018: d23: ex1: add input 2024-12-31 00:47:19 -05:00
0624b4e44d 2018: d22: ex2: add solution 2024-12-30 23:58:48 -05:00
aab563b9ff 2018: d22: ex2: add input 2024-12-30 23:58:27 -05:00
49d7160617 2018: d22: ex1: add solution 2024-12-30 23:58:21 -05:00
f18a8f45f6 2018: d22: ex1: add input 2024-12-30 23:58:08 -05:00
cd832a2bb0 2018: d21: ex2: add solution 2024-12-30 23:06:17 -05:00
955d5ec6c2 2018: d21: ex2: add input 2024-12-30 23:06:05 -05:00
47cce9f0f3 2018: d21: ex1: add solution 2024-12-30 23:06:00 -05:00
763793ab0b 2018: d21: ex1: add input 2024-12-30 23:05:52 -05:00
4cb48ee71c 2018: d20: ex2: add solution 2024-12-30 21:47:39 -05:00
9b6cb7bd45 2018: d20: ex2: add input 2024-12-30 21:47:39 -05:00
6dbc98412e 2018: d20: ex1: add solution 2024-12-30 21:47:39 -05:00
add0b47894 2018: d20: ex1: add input 2024-12-30 21:43:50 -05:00
a0e1e0e223 2018: d19: ex2: add solution 2024-12-30 20:18:10 -05:00
93b4eea6f8 2018: d19: ex2: add input 2024-12-30 20:18:00 -05:00
52a891aa57 2018: d19: ex1: add solution 2024-12-30 20:17:54 -05:00
aa767b9781 2018: d19: ex1: add input 2024-12-30 20:17:49 -05:00
11b32839f4 2018: d18: ex2: add solution 2024-12-30 19:41:27 -05:00
91cee79a22 2018: d18: ex2: add input 2024-12-30 19:41:14 -05:00
c1f47d34ad 2018: d18: ex1: add solution 2024-12-30 19:41:08 -05:00
f0395b2fae 2018: d18: ex1: add input 2024-12-30 19:40:58 -05:00
f2ccb2cd5a 2018: d17: ex2: add solution 2024-12-30 19:10:22 -05:00
5ea92f305e 2018: d17: ex2: add input 2024-12-30 19:10:18 -05:00
dd31b1369d 2018: d17: ex1: add solution 2024-12-30 19:10:12 -05:00
69b8894c54 2018: d17: ex1: add input 2024-12-30 19:10:07 -05:00
27c2cc0463 2018: d16: ex2: add solution 2024-12-30 17:25:39 -05:00
43de4e9dba 2018: d16: ex2: add input 2024-12-30 17:25:39 -05:00
1aea4b9bcd 2018: d16: ex1: add solution 2024-12-30 17:25:39 -05:00
d0013a1e17 2018: d16: ex1: add input 2024-12-30 17:23:38 -05:00
fd604ff8a4 2018: d15: ex2: add solution 2024-12-30 16:26:46 -05:00
a7a5f62afe 2018: d15: ex2: add input 2024-12-30 16:22:58 -05:00
cea7f60e46 2018: d15: ex1: add solution 2024-12-30 16:22:52 -05:00
583c913bbe 2018: d15: ex1: add input 2024-12-30 16:22:45 -05:00
1010cacbe9 2018: d14: ex2: add solution 2024-12-29 23:50:09 -05:00
05e7c39142 2018: d14: ex2: add input 2024-12-29 23:49:57 -05:00
a7eac8ea74 2018: d14: ex1: add solution 2024-12-29 23:49:51 -05:00
9bbcf41657 2018: d14: ex1: add input 2024-12-29 23:49:45 -05:00
48 changed files with 18909 additions and 0 deletions

27
2018/d14/ex1/ex1.py Executable file
View file

@ -0,0 +1,27 @@
#!/usr/bin/env python
import functools
import sys
def solve(input: str) -> int:
n_recipes = int(input)
scores = [3, 7]
elves = [0, 1]
while (len(scores) - 10) < n_recipes:
sum = scores[elves[0]] + scores[elves[1]]
scores.extend(map(int, str(sum)))
elves = [(elf + 1 + scores[elf]) % len(scores) for elf in elves]
return functools.reduce(lambda lhs, rhs: lhs * 10 + rhs, scores[-10:])
def main() -> None:
input = sys.stdin.read()
print(solve(input))
if __name__ == "__main__":
main()

1
2018/d14/ex1/input Normal file
View file

@ -0,0 +1 @@
556061

27
2018/d14/ex2/ex2.py Executable file
View file

@ -0,0 +1,27 @@
#!/usr/bin/env python
import sys
def solve(input: str) -> int:
digits = [int(n) for n in input.strip()]
scores = [3, 7]
elves = [0, 1]
while scores[-len(digits) :] != digits and scores[-len(digits) - 1 : -1] != digits:
sum = scores[elves[0]] + scores[elves[1]]
scores.extend(map(int, str(sum)))
elves = [(elf + 1 + scores[elf]) % len(scores) for elf in elves]
left_of_digits = len(scores) - len(digits) - (scores[-len(digits) :] != digits)
return left_of_digits
def main() -> None:
input = sys.stdin.read()
print(solve(input))
if __name__ == "__main__":
main()

1
2018/d14/ex2/input Normal file
View file

@ -0,0 +1 @@
556061

192
2018/d15/ex1/ex1.py Executable file
View file

@ -0,0 +1,192 @@
#!/usr/bin/env python
import dataclasses
import enum
import sys
from typing import Iterator, NamedTuple
class Point(NamedTuple):
x: int
y: int
# Returned in reading order
def neighbours(self) -> Iterator["Point"]:
for dx, dy in (
(-1, 0),
(0, -1),
(0, 1),
(1, 0),
):
yield Point(self.x + dx, self.y + dy)
class Unit(enum.StrEnum):
ELF = "E"
GOBLIN = "G"
def ennemy(self) -> "Unit":
if self == Unit.ELF:
return Unit.GOBLIN
if self == Unit.GOBLIN:
return Unit.ELF
assert False # Sanity check
@dataclasses.dataclass
class UnitData:
hp: int = 200
power: int = 3
def solve(input: str) -> int:
def parse(input: list[str]) -> tuple[set[Point], dict[Unit, set[Point]]]:
walls: set[Point] = set()
units: dict[Unit, set[Point]] = {u: set() for u in Unit}
for x, line in enumerate(input):
for y, c in enumerate(line):
p = Point(x, y)
if c in Unit:
units[Unit(c)].add(p)
if c == "#":
walls.add(p)
return walls, units
def double_bfs(
unit_type: Unit,
unit_pos: Point,
walls: set[Point],
units: dict[Unit, set[Point]],
) -> Point | None:
def bfs(
start: Point,
targets: set[Point],
blockers: set[Point],
) -> Point | None:
frontier = [start]
seen: set[Point] = set()
while frontier:
new_frontier: set[Point] = set()
for p in frontier:
if p in targets:
return p
seen.add(p)
for n in p.neighbours():
if n in seen:
continue
if n in blockers:
continue
new_frontier.add(n)
frontier = sorted(new_frontier)
return None
blockers = walls | units[unit_type]
ennemies = units[unit_type.ennemy()]
# First BFS from start to square next to an ennemy
targets_in_range = {
n for ennemy in ennemies for n in ennemy.neighbours() if n not in blockers
}
if (target := bfs(unit_pos, targets_in_range, blockers)) is None:
return None
# Then back from chosen target to one of the movement squares
movement_squares = {n for n in unit_pos.neighbours() if n not in blockers}
return bfs(target, movement_squares, blockers)
def do_move(
unit_type: Unit,
unit_pos: Point,
walls: set[Point],
units: dict[Unit, set[Point]],
unit_data: dict[Point, UnitData],
) -> Point:
# If already next to an ennemy, do not move
if any(n in units[unit_type.ennemy()] for n in unit_pos.neighbours()):
return unit_pos
new_pos = double_bfs(unit_type, unit_pos, walls, units)
# Nowhere to move to, no-op
if new_pos is None:
return unit_pos
assert new_pos != unit_pos # Sanity check
assert unit_pos in units[unit_type] # Sanity check
assert new_pos not in units[unit_type] # Sanity check
# Make the movement in-place
units[unit_type] ^= {unit_pos, new_pos}
unit_data[new_pos] = unit_data.pop(unit_pos)
return new_pos
def do_attack(
unit_type: Unit,
unit_pos: Point,
units: dict[Unit, set[Point]],
unit_data: dict[Point, UnitData],
) -> None:
# Look for an attack target
target = min(
(n for n in unit_pos.neighbours() if n in units[unit_type.ennemy()]),
key=lambda p: unit_data[p].hp,
default=None,
)
# If not in range, no-op
if target is None:
return
assert target not in units[unit_type] # Sanity check
assert target in units[unit_type.ennemy()] # Sanity check
assert unit_data[target].hp > 0 # Sanity check
# Make the attack in-place
unit_data[target].hp -= unit_data[unit_pos].power
# And if we killed it, remove it from `units`
if unit_data[target].hp <= 0:
units[unit_type.ennemy()].remove(target)
unit_data.pop(target)
def turn(
walls: set[Point],
units: dict[Unit, set[Point]],
unit_data: dict[Point, UnitData],
) -> bool:
turn_order = sorted((p, u) for u, points in units.items() for p in points)
for p, u in turn_order:
# Don't do anything if the unit is dead
if p not in units[u]:
continue
# If no ennemies left, finish the turn early and indicate that we're done
if not units[u.ennemy()]:
return False
# Movements and attacks are made in-place
p = do_move(u, p, walls, units, unit_data)
do_attack(u, p, units, unit_data)
return True
walls, units = parse(input.splitlines())
unit_data = {p: UnitData() for points in units.values() for p in points}
turns = 0
while turn(walls, units, unit_data):
turns += 1
return turns * sum(data.hp for data in unit_data.values())
def main() -> None:
input = sys.stdin.read()
print(solve(input))
if __name__ == "__main__":
main()

32
2018/d15/ex1/input Normal file
View file

@ -0,0 +1,32 @@
################################
######################........##
####################...........#
##############G.G..G.#.......#.#
#############.....G...#....###.#
############..##.G.............#
#############...#...GG......####
#############......G......######
##############G....EG.....######
#############.......G.....######
############.....G......#.######
###########......E...G.#########
##########....#####......#######
##########G..#######......######
######......#########....#######
#####....G..#########....#######
###.......#.#########....#######
###.G.....#.#########E...#######
#........##.#########E...#######
#.......###..#######...E.#######
#.#.#.........#####.......######
#.###.#.###.G..............#####
####.....###..........E.##.#####
#......G####.E..........########
###..G..####...........####..###
####..########..E......###...###
###..............#...E...#.#####
##.........##....##........#####
#.......#.####.........#########
#...##G.##########....E#########
#...##...#######################
################################

239
2018/d15/ex2/ex2.py Executable file
View file

@ -0,0 +1,239 @@
#!/usr/bin/env python
import copy
import dataclasses
import enum
import itertools
import sys
from typing import Iterator, NamedTuple
class Point(NamedTuple):
x: int
y: int
# Returned in reading order
def neighbours(self) -> Iterator["Point"]:
for dx, dy in (
(-1, 0),
(0, -1),
(0, 1),
(1, 0),
):
yield Point(self.x + dx, self.y + dy)
class Unit(enum.StrEnum):
ELF = "E"
GOBLIN = "G"
def ennemy(self) -> "Unit":
if self == Unit.ELF:
return Unit.GOBLIN
if self == Unit.GOBLIN:
return Unit.ELF
assert False # Sanity check
@dataclasses.dataclass
class UnitData:
hp: int = 200
power: int = 3
class ElfDiedError(Exception):
pass
def solve(input: str) -> int:
def parse(input: list[str]) -> tuple[set[Point], dict[Unit, set[Point]]]:
walls: set[Point] = set()
units: dict[Unit, set[Point]] = {u: set() for u in Unit}
for x, line in enumerate(input):
for y, c in enumerate(line):
p = Point(x, y)
if c in Unit:
units[Unit(c)].add(p)
if c == "#":
walls.add(p)
return walls, units
def double_bfs(
unit_type: Unit,
unit_pos: Point,
walls: set[Point],
units: dict[Unit, set[Point]],
) -> Point | None:
def bfs(
start: Point,
targets: set[Point],
blockers: set[Point],
) -> Point | None:
frontier = [start]
seen: set[Point] = set()
while frontier:
new_frontier: set[Point] = set()
for p in frontier:
if p in targets:
return p
seen.add(p)
for n in p.neighbours():
if n in seen:
continue
if n in blockers:
continue
new_frontier.add(n)
frontier = sorted(new_frontier)
return None
blockers = walls | units[unit_type]
ennemies = units[unit_type.ennemy()]
# First BFS from start to square next to an ennemy
targets_in_range = {
n for ennemy in ennemies for n in ennemy.neighbours() if n not in blockers
}
if (target := bfs(unit_pos, targets_in_range, blockers)) is None:
return None
# Then back from chosen target to one of the movement squares
movement_squares = {n for n in unit_pos.neighbours() if n not in blockers}
return bfs(target, movement_squares, blockers)
def do_move(
unit_type: Unit,
unit_pos: Point,
walls: set[Point],
units: dict[Unit, set[Point]],
unit_data: dict[Point, UnitData],
) -> Point:
# If already next to an ennemy, do not move
if any(n in units[unit_type.ennemy()] for n in unit_pos.neighbours()):
return unit_pos
new_pos = double_bfs(unit_type, unit_pos, walls, units)
# Nowhere to move to, no-op
if new_pos is None:
return unit_pos
assert new_pos != unit_pos # Sanity check
assert unit_pos in units[unit_type] # Sanity check
assert new_pos not in units[unit_type] # Sanity check
# Make the movement in-place
units[unit_type] ^= {unit_pos, new_pos}
unit_data[new_pos] = unit_data.pop(unit_pos)
return new_pos
def do_attack(
unit_type: Unit,
unit_pos: Point,
units: dict[Unit, set[Point]],
unit_data: dict[Point, UnitData],
) -> None:
# Look for an attack target
target = min(
(n for n in unit_pos.neighbours() if n in units[unit_type.ennemy()]),
key=lambda p: unit_data[p].hp,
default=None,
)
# If not in range, no-op
if target is None:
return
assert target not in units[unit_type] # Sanity check
assert target in units[unit_type.ennemy()] # Sanity check
assert unit_data[target].hp > 0 # Sanity check
# Make the attack in-place
unit_data[target].hp -= unit_data[unit_pos].power
# And if we killed it, remove it from `units`
if unit_data[target].hp <= 0:
if unit_type.ennemy() == Unit.ELF:
raise ElfDiedError
units[unit_type.ennemy()].remove(target)
unit_data.pop(target)
def turn(
walls: set[Point],
units: dict[Unit, set[Point]],
unit_data: dict[Point, UnitData],
) -> bool:
turn_order = sorted((p, u) for u, points in units.items() for p in points)
for p, u in turn_order:
# Don't do anything if the unit is dead
if p not in units[u]:
continue
# If no ennemies left, finish the turn early and indicate that we're done
if not units[u.ennemy()]:
return False
# Movements and attacks are made in-place
p = do_move(u, p, walls, units, unit_data)
do_attack(u, p, units, unit_data)
return True
def print_map(walls: set[Point], units: dict[Unit, set[Point]]) -> None:
max_x, max_y = max(p.x for p in walls), max(p.y for p in walls)
for x in range(0, max_x + 1):
for y in range(0, max_y + 1):
p = Point(x, y)
for u in Unit:
if p in units[u]:
print(str(u), end="")
break
else:
print("#" if p in walls else ".", end="")
print()
print()
def run_to_completion(
walls: set[Point],
units: dict[Unit, set[Point]],
unit_data: dict[Point, UnitData],
) -> int:
turns = 0
while turn(walls, units, unit_data):
turns += 1
return turns * sum(data.hp for data in unit_data.values())
def arm_elves(
walls: set[Point],
units: dict[Unit, set[Point]],
) -> int:
for elf_power in itertools.count(start=3):
try:
unit_data = {
p: UnitData(power=elf_power if u == Unit.ELF else 3)
for u, points in units.items()
for p in points
}
return run_to_completion(
copy.deepcopy(walls),
copy.deepcopy(units),
unit_data,
)
except ElfDiedError:
pass
assert False # Sanity check
walls, units = parse(input.splitlines())
return arm_elves(walls, units)
def main() -> None:
input = sys.stdin.read()
print(solve(input))
if __name__ == "__main__":
main()

32
2018/d15/ex2/input Normal file
View file

@ -0,0 +1,32 @@
################################
######################........##
####################...........#
##############G.G..G.#.......#.#
#############.....G...#....###.#
############..##.G.............#
#############...#...GG......####
#############......G......######
##############G....EG.....######
#############.......G.....######
############.....G......#.######
###########......E...G.#########
##########....#####......#######
##########G..#######......######
######......#########....#######
#####....G..#########....#######
###.......#.#########....#######
###.G.....#.#########E...#######
#........##.#########E...#######
#.......###..#######...E.#######
#.#.#.........#####.......######
#.###.#.###.G..............#####
####.....###..........E.##.#####
#......G####.E..........########
###..G..####...........####..###
####..########..E......###...###
###..............#...E...#.#####
##.........##....##........#####
#.......#.####.........#########
#...##G.##########....E#########
#...##...#######################
################################

110
2018/d16/ex1/ex1.py Executable file
View file

@ -0,0 +1,110 @@
#!/usr/bin/env python
import copy
import enum
import sys
from typing import NamedTuple
class OpCode(enum.StrEnum):
ADDR = "addr"
ADDI = "addi"
MULR = "mulr"
MULI = "muli"
BANR = "banr"
BANI = "bani"
BORR = "borr"
BORI = "bori"
SETR = "setr"
SETI = "seti"
GTIR = "gtir"
GTRI = "gtri"
GTRR = "gtrr"
EQIR = "eqir"
EQRI = "eqri"
EQRR = "eqrr"
def apply(self, registers: list[int], a: int, b: int, c: int) -> list[int]:
registers = copy.deepcopy(registers)
if self == OpCode.ADDR:
registers[c] = registers[a] + registers[b]
if self == OpCode.ADDI:
registers[c] = registers[a] + b
if self == OpCode.MULR:
registers[c] = registers[a] * registers[b]
if self == OpCode.MULI:
registers[c] = registers[a] * b
if self == OpCode.BANR:
registers[c] = registers[a] & registers[b]
if self == OpCode.BANI:
registers[c] = registers[a] & b
if self == OpCode.BORR:
registers[c] = registers[a] | registers[b]
if self == OpCode.BORI:
registers[c] = registers[a] | b
if self == OpCode.SETR:
registers[c] = registers[a]
if self == OpCode.SETI:
registers[c] = a
if self == OpCode.GTIR:
registers[c] = a > registers[b]
if self == OpCode.GTRI:
registers[c] = registers[a] > b
if self == OpCode.GTRR:
registers[c] = registers[a] > registers[b]
if self == OpCode.EQIR:
registers[c] = a == registers[b]
if self == OpCode.EQRI:
registers[c] = registers[a] == b
if self == OpCode.EQRR:
registers[c] = registers[a] == registers[b]
return registers
Instruction = list[int]
class Example(NamedTuple):
before: list[int]
data: Instruction
after: list[int]
def solve(input: str) -> int:
def parse_example(input: list[str]) -> Example:
before = input[0].removeprefix("Before: [").removesuffix("]")
data = input[1]
after = input[2].removeprefix("After: [").removesuffix("]")
return Example(
[int(n) for n in before.split(", ")],
[int(n) for n in data.split()],
[int(n) for n in after.split(", ")],
)
def parse_examples(input: str) -> list[Example]:
return [parse_example(example.splitlines()) for example in input.split("\n\n")]
def parse_data(input: list[str]) -> list[Instruction]:
return [[int(n) for n in line.split()] for line in input]
def parse(input: str) -> tuple[list[Example], list[Instruction]]:
examples, data = input.split("\n\n\n\n")
return parse_examples(examples), parse_data(data.splitlines())
def num_candidates(example: Example) -> int:
return sum(
op.apply(example.before, *example.data[1:]) == example.after
for op in OpCode
)
examples, data = parse(input)
return sum(num_candidates(example) >= 3 for example in examples)
def main() -> None:
input = sys.stdin.read()
print(solve(input))
if __name__ == "__main__":
main()

4148
2018/d16/ex1/input Normal file

File diff suppressed because it is too large Load diff

133
2018/d16/ex2/ex2.py Executable file
View file

@ -0,0 +1,133 @@
#!/usr/bin/env python
import copy
import enum
import sys
from typing import NamedTuple
class OpCode(enum.StrEnum):
ADDR = "addr"
ADDI = "addi"
MULR = "mulr"
MULI = "muli"
BANR = "banr"
BANI = "bani"
BORR = "borr"
BORI = "bori"
SETR = "setr"
SETI = "seti"
GTIR = "gtir"
GTRI = "gtri"
GTRR = "gtrr"
EQIR = "eqir"
EQRI = "eqri"
EQRR = "eqrr"
def apply(self, registers: list[int], a: int, b: int, c: int) -> list[int]:
registers = copy.deepcopy(registers)
if self == OpCode.ADDR:
registers[c] = registers[a] + registers[b]
if self == OpCode.ADDI:
registers[c] = registers[a] + b
if self == OpCode.MULR:
registers[c] = registers[a] * registers[b]
if self == OpCode.MULI:
registers[c] = registers[a] * b
if self == OpCode.BANR:
registers[c] = registers[a] & registers[b]
if self == OpCode.BANI:
registers[c] = registers[a] & b
if self == OpCode.BORR:
registers[c] = registers[a] | registers[b]
if self == OpCode.BORI:
registers[c] = registers[a] | b
if self == OpCode.SETR:
registers[c] = registers[a]
if self == OpCode.SETI:
registers[c] = a
if self == OpCode.GTIR:
registers[c] = a > registers[b]
if self == OpCode.GTRI:
registers[c] = registers[a] > b
if self == OpCode.GTRR:
registers[c] = registers[a] > registers[b]
if self == OpCode.EQIR:
registers[c] = a == registers[b]
if self == OpCode.EQRI:
registers[c] = registers[a] == b
if self == OpCode.EQRR:
registers[c] = registers[a] == registers[b]
return registers
Instruction = list[int]
class Example(NamedTuple):
before: list[int]
data: Instruction
after: list[int]
def solve(input: str) -> int:
def parse_example(input: list[str]) -> Example:
before = input[0].removeprefix("Before: [").removesuffix("]")
data = input[1]
after = input[2].removeprefix("After: [").removesuffix("]")
return Example(
[int(n) for n in before.split(", ")],
[int(n) for n in data.split()],
[int(n) for n in after.split(", ")],
)
def parse_examples(input: str) -> list[Example]:
return [parse_example(example.splitlines()) for example in input.split("\n\n")]
def parse_data(input: list[str]) -> list[Instruction]:
return [[int(n) for n in line.split()] for line in input]
def parse(input: str) -> tuple[list[Example], list[Instruction]]:
examples, data = input.split("\n\n\n\n")
return parse_examples(examples), parse_data(data.splitlines())
def find_opcodes(examples: list[Example]) -> dict[int, OpCode]:
candidates: dict[int, set[OpCode]] = {n: set(OpCode) for n in range(16)}
for example in examples:
opcode, a, b, c = example.data
candidates[opcode] &= {
op
for op in candidates[opcode]
if op.apply(example.before, a, b, c) == example.after
}
while not all(len(ops) == 1 for ops in candidates.values()):
singles = {
n: next(iter(ops)) for n, ops in candidates.items() if len(ops) == 1
}
for n in candidates:
if n in singles:
continue
candidates[n] -= set(singles.values())
return {n: ops.pop() for n, ops in candidates.items()}
def run_program(data: list[Instruction], opcodes: dict[int, OpCode]) -> list[int]:
registers = [0] * 4
for opcode, a, b, c in data:
registers = opcodes[opcode].apply(registers, a, b, c)
return registers
examples, data = parse(input)
opcodes = find_opcodes(examples)
registers = run_program(data, opcodes)
return registers[0]
def main() -> None:
input = sys.stdin.read()
print(solve(input))
if __name__ == "__main__":
main()

4148
2018/d16/ex2/input Normal file

File diff suppressed because it is too large Load diff

108
2018/d17/ex1/ex1.py Executable file
View file

@ -0,0 +1,108 @@
#!/usr/bin/env python
import enum
import itertools
import sys
from collections.abc import Iterator
from typing import NamedTuple
class Point(NamedTuple):
x: int
y: int
class Direction(enum.Enum):
DOWN = Point(0, 1)
LEFT = Point(-1, 0)
RIGHT = Point(1, 0)
def apply(self, p: Point) -> Point:
dx, dy = self.value
return Point(p.x + dx, p.y + dy)
def solve(input: str) -> int:
def parse_range(input: str) -> range:
if ".." not in input:
input = input + ".." + input
start, end = map(int, input.split(".."))
return range(start, end + 1)
def parse_line(input: str) -> Iterator[Point]:
xs, ys = sorted(input.split(", "))
yield from map(
Point._make,
itertools.product(parse_range(xs[2:]), parse_range(ys[2:])),
)
def parse(input: list[str]) -> set[Point]:
return {p for line in input for p in parse_line(line)}
def flow(clay: set[Point], source: Point) -> set[Point]:
max_y = max(p.y for p in clay)
def helper(
source: Point,
water: set[Point],
settled: set[Point],
direction: Direction = Direction.DOWN,
) -> bool:
# Clay is considered "settled"
if source in clay:
return True
# We've already seen this, return early
if source in water:
return source in settled
# Account for this new source
water.add(source)
below = Direction.DOWN.apply(source)
if below not in clay:
if below.y <= max_y:
helper(below, water, settled)
if below not in settled:
return False
left = Direction.LEFT.apply(source)
right = Direction.RIGHT.apply(source)
l_filled = helper(left, water, settled, Direction.LEFT)
r_filled = helper(right, water, settled, Direction.RIGHT)
if direction == Direction.DOWN and l_filled and r_filled:
settled.add(source)
while left in water:
settled.add(left)
left = Direction.LEFT.apply(left)
while right in water:
settled.add(right)
right = Direction.RIGHT.apply(right)
return True
return (direction == Direction.LEFT and l_filled) or (
direction == Direction.RIGHT and r_filled
)
assert source not in clay # Sanity check
water: set[Point] = set()
settled: set[Point] = set()
helper(source, water, settled)
assert settled <= water # Sanity check
return water
clay = parse(input.splitlines())
sys.setrecursionlimit(5000) # HACK
water = flow(clay, Point(500, 0))
min_y, max_y = min(p.y for p in clay), max(p.y for p in clay)
return sum(min_y <= p.y <= max_y for p in water)
def main() -> None:
input = sys.stdin.read()
print(solve(input))
if __name__ == "__main__":
main()

1470
2018/d17/ex1/input Normal file

File diff suppressed because it is too large Load diff

107
2018/d17/ex2/ex2.py Executable file
View file

@ -0,0 +1,107 @@
#!/usr/bin/env python
import enum
import itertools
import sys
from collections.abc import Iterator
from typing import NamedTuple
class Point(NamedTuple):
x: int
y: int
class Direction(enum.Enum):
DOWN = Point(0, 1)
LEFT = Point(-1, 0)
RIGHT = Point(1, 0)
def apply(self, p: Point) -> Point:
dx, dy = self.value
return Point(p.x + dx, p.y + dy)
def solve(input: str) -> int:
def parse_range(input: str) -> range:
if ".." not in input:
input = input + ".." + input
start, end = map(int, input.split(".."))
return range(start, end + 1)
def parse_line(input: str) -> Iterator[Point]:
xs, ys = sorted(input.split(", "))
yield from map(
Point._make,
itertools.product(parse_range(xs[2:]), parse_range(ys[2:])),
)
def parse(input: list[str]) -> set[Point]:
return {p for line in input for p in parse_line(line)}
def flow(clay: set[Point], source: Point) -> set[Point]:
max_y = max(p.y for p in clay)
def helper(
source: Point,
water: set[Point],
settled: set[Point],
direction: Direction = Direction.DOWN,
) -> bool:
# Clay is considered "settled"
if source in clay:
return True
# We've already seen this, return early
if source in water:
return source in settled
# Account for this new source
water.add(source)
below = Direction.DOWN.apply(source)
if below not in clay:
if below.y <= max_y:
helper(below, water, settled)
if below not in settled:
return False
left = Direction.LEFT.apply(source)
right = Direction.RIGHT.apply(source)
l_filled = helper(left, water, settled, Direction.LEFT)
r_filled = helper(right, water, settled, Direction.RIGHT)
if direction == Direction.DOWN and l_filled and r_filled:
settled.add(source)
while left in water:
settled.add(left)
left = Direction.LEFT.apply(left)
while right in water:
settled.add(right)
right = Direction.RIGHT.apply(right)
return True
return (direction == Direction.LEFT and l_filled) or (
direction == Direction.RIGHT and r_filled
)
assert source not in clay # Sanity check
water: set[Point] = set()
settled: set[Point] = set()
helper(source, water, settled)
assert settled <= water # Sanity check
return settled
clay = parse(input.splitlines())
sys.setrecursionlimit(5000) # HACK
settled = flow(clay, Point(500, 0))
return len(settled)
def main() -> None:
input = sys.stdin.read()
print(solve(input))
if __name__ == "__main__":
main()

1470
2018/d17/ex2/input Normal file

File diff suppressed because it is too large Load diff

68
2018/d18/ex1/ex1.py Executable file
View file

@ -0,0 +1,68 @@
#!/usr/bin/env python
import enum
import itertools
import sys
from collections.abc import Iterator
from typing import NamedTuple
class Point(NamedTuple):
x: int
y: int
def neighbours(self) -> Iterator["Point"]:
for dx, dy in itertools.product(range(-1, 1 + 1), repeat=2):
if dx == 0 and dy == 0:
continue
yield Point(self.x + dx, self.y + dy)
class Cell(enum.StrEnum):
OPEN = "."
TREE = "|"
LUMBERYARD = "#"
def solve(input: str) -> int:
def parse(input: list[str]) -> dict[Point, Cell]:
return {
Point(x, y): Cell(c)
for x, line in enumerate(input)
for y, c in enumerate(line)
}
def step_cell(p: Point, grid: dict[Point, Cell]) -> Cell:
neighbours = (n for n in p.neighbours() if n in grid)
if grid[p] == Cell.OPEN:
trees = sum(grid[n] == Cell.TREE for n in neighbours)
return Cell.TREE if trees >= 3 else Cell.OPEN
if grid[p] == Cell.TREE:
lumberyards = sum(grid[n] == Cell.LUMBERYARD for n in neighbours)
return Cell.LUMBERYARD if lumberyards >= 3 else Cell.TREE
if grid[p] == Cell.LUMBERYARD:
continues = {Cell.TREE, Cell.LUMBERYARD} <= {grid[n] for n in neighbours}
return Cell.LUMBERYARD if continues else Cell.OPEN
assert False # Sanity check
def step(grid: dict[Point, Cell]) -> dict[Point, Cell]:
res: dict[Point, Cell] = {}
for p in map(Point._make, itertools.product(range(50), repeat=2)):
res[p] = step_cell(p, grid)
return res
grid = parse(input.splitlines())
for _ in range(10):
grid = step(grid)
trees = sum(c == Cell.TREE for c in grid.values())
lumberyards = sum(c == Cell.LUMBERYARD for c in grid.values())
return trees * lumberyards
def main() -> None:
input = sys.stdin.read()
print(solve(input))
if __name__ == "__main__":
main()

50
2018/d18/ex1/input Normal file
View file

@ -0,0 +1,50 @@
.#|#.##....#|....|.#.#.|||.#.|....||....|...|..#..
..|#||.|#..|...|#|..#...|#...#..#..|.....||..#.|#.
#|||#..||.....||.#................|..#.##|.#...#.|
|#..#.|...##...#..#|#|#..|#.#...|....#..#...##....
.###.........|.||#...#|.|#.||||#..|...||....#..#..
###.|..|#|...|..||..##.....|..#.|.#.............|.
..|.|.||.#....|...|....#|.........##||..#||..|.##.
#||#|...#|..|.|.||#...#|...|#.......|...#.....|...
....||.....|.|.....#...|.......|...|..|...|......|
#......#..#|#|..|....#.|.|.#...#.#.|..#.|.....#.#.
.|#...|...........#|.#....#.#...#.|..|...|....|.|.
..||.#.|...||#|....#.#..||#..#...#|..#..|..#|.....
|..|.|..#...|.....#.|..|#.||..#|.|.||#|#..|#...##|
..|..|#......||##..|........#.|...#.|.|#.#...||..#
#.|...#.||#..|.|..|..|.#....|.||....|.|....#....#.
#||.|.#..#..|...#....##|#..#...#.#...|.#...#.....#
#.|.##.|##..#.##|##........#.|...#...|..#|.#|#|...
.|#|....|.#...#..|||.#.||..#||.||.|..#.|....|..##.
|.#.||#|.##.|.||.....#...#.#..###|.#......||#|....
.|.#..|#||......|##..##.#|..#|.|#.|.|#......|#.|#.
#..|........|||..|###..|#..|||#.|.|.....#|..|...|#
..####||#......|#||..###.|...|....#..|.#|.||....||
|##.......|||..........|..||.#.|#.......##...|...|
|.......#......####|#|....#....|......#.|#.###...#
#|.#.|||...|..|.....#....|...|......|#|#|......||.
...#.|......#..||||.#|.....|.|.|||.|.|.|#|.#...#.#
#.#.##.|.#|.|...|...|...#|...#.|#..|..##.|....#..|
|...#.......#....#....#.#....#.#|.|#||.|.|.|#...#.
#..|.||..|.#..|.#.....#|##.|.|....|....||.......|.
..||.#..|#|.###....#.#|..#|.#..........#...|...#|.
|#||.|.#..|....|....#.#||#.|......#..|#.#.|||||#|.
.|#.|#.##.....#.|.#.....|....|.#..#.#..|#.#.....|.
#.||.#.......|..|......|#||.|..#....#...|...|...|.
|.....#.|.....#||.....##...#.#...||.|..#........|.
||#..|.##.#...........#..|..|.|..#....|...#..||.#.
..||.##.##.|.||......#...|.#.#.#..#.#...##.#.|.#..
.|.#......#|#||.|.#|......||.#.|.|..|....#...||...
....|.##.....|#|####.#..#..#|.....|.#.#|......|...
...#..|......#....|#.#...|...|.#.#.......#.#.##..#
.|||#.||||...|..|#||.|.#|#||..|..#..|..|..#||.....
.....|..#..|#|.||.#||.||......|||..|..#|.|##......
.#...#|..#..|||..||.|..|.#.#.......||..|...|.|....
.##.||..|..||.|.......#.|||.|.|..|.#.#..|.||.|#|||
.|..##|..#.#|#|....|.#.#.#|#.#|.##|........###...#
..#..|#|...#.........#.#.####..#.#..#..#||#|...#|#
#.|...|.......|.#.#..#.|#..#|#|..#..|.....|..|...|
.##.|..#.....|...#..|#..|.|.#..##.#.|..#.|..|.##..
....|..|.|..||....|...|.....#..|.|.....|.#|......#
...##.|#..#..|.#|.##....|.#...||#|.....#...##.#|..
.|....##.....||...#.#.....#|#...#...#|.|..#.#.#.##

90
2018/d18/ex2/ex2.py Executable file
View file

@ -0,0 +1,90 @@
#!/usr/bin/env python
import enum
import itertools
import sys
from collections.abc import Iterator
from typing import NamedTuple
class Point(NamedTuple):
x: int
y: int
def neighbours(self) -> Iterator["Point"]:
for dx, dy in itertools.product(range(-1, 1 + 1), repeat=2):
if dx == 0 and dy == 0:
continue
yield Point(self.x + dx, self.y + dy)
class Cell(enum.StrEnum):
OPEN = "."
TREE = "|"
LUMBERYARD = "#"
def solve(input: str) -> int:
def parse(input: list[str]) -> dict[Point, Cell]:
return {
Point(x, y): Cell(c)
for x, line in enumerate(input)
for y, c in enumerate(line)
}
def step_cell(p: Point, grid: dict[Point, Cell]) -> Cell:
neighbours = (n for n in p.neighbours() if n in grid)
if grid[p] == Cell.OPEN:
trees = sum(grid[n] == Cell.TREE for n in neighbours)
return Cell.TREE if trees >= 3 else Cell.OPEN
if grid[p] == Cell.TREE:
lumberyards = sum(grid[n] == Cell.LUMBERYARD for n in neighbours)
return Cell.LUMBERYARD if lumberyards >= 3 else Cell.TREE
if grid[p] == Cell.LUMBERYARD:
continues = {Cell.TREE, Cell.LUMBERYARD} <= {grid[n] for n in neighbours}
return Cell.LUMBERYARD if continues else Cell.OPEN
assert False # Sanity check
def step(grid: dict[Point, Cell]) -> dict[Point, Cell]:
res: dict[Point, Cell] = {}
for p in map(Point._make, itertools.product(range(50), repeat=2)):
res[p] = step_cell(p, grid)
return res
def frozen(grid: dict[Point, Cell]) -> tuple[Cell, ...]:
return tuple(grid[p] for p in sorted(grid.keys()))
def thawed(hashed_grid: tuple[Cell, ...]) -> dict[Point, Cell]:
return {Point(i // 50, i % 50): c for i, c in enumerate(hashed_grid)}
def do_cycles(grid: dict[Point, Cell], end: int) -> dict[Point, Cell]:
hashed_grid = frozen(grid)
cache = {hashed_grid: 0}
t = 0
while t < end:
hashed_grid = frozen(step(thawed(hashed_grid)))
t += 1
if hashed_grid in cache:
previous_t = cache[hashed_grid]
cycle_length = t - previous_t
num_cycles = (end - t) // cycle_length
t += num_cycles * cycle_length
else:
cache[hashed_grid] = t
return thawed(hashed_grid)
grid = parse(input.splitlines())
grid = do_cycles(grid, 1000000000)
trees = sum(c == Cell.TREE for c in grid.values())
lumberyards = sum(c == Cell.LUMBERYARD for c in grid.values())
return trees * lumberyards
def main() -> None:
input = sys.stdin.read()
print(solve(input))
if __name__ == "__main__":
main()

50
2018/d18/ex2/input Normal file
View file

@ -0,0 +1,50 @@
.#|#.##....#|....|.#.#.|||.#.|....||....|...|..#..
..|#||.|#..|...|#|..#...|#...#..#..|.....||..#.|#.
#|||#..||.....||.#................|..#.##|.#...#.|
|#..#.|...##...#..#|#|#..|#.#...|....#..#...##....
.###.........|.||#...#|.|#.||||#..|...||....#..#..
###.|..|#|...|..||..##.....|..#.|.#.............|.
..|.|.||.#....|...|....#|.........##||..#||..|.##.
#||#|...#|..|.|.||#...#|...|#.......|...#.....|...
....||.....|.|.....#...|.......|...|..|...|......|
#......#..#|#|..|....#.|.|.#...#.#.|..#.|.....#.#.
.|#...|...........#|.#....#.#...#.|..|...|....|.|.
..||.#.|...||#|....#.#..||#..#...#|..#..|..#|.....
|..|.|..#...|.....#.|..|#.||..#|.|.||#|#..|#...##|
..|..|#......||##..|........#.|...#.|.|#.#...||..#
#.|...#.||#..|.|..|..|.#....|.||....|.|....#....#.
#||.|.#..#..|...#....##|#..#...#.#...|.#...#.....#
#.|.##.|##..#.##|##........#.|...#...|..#|.#|#|...
.|#|....|.#...#..|||.#.||..#||.||.|..#.|....|..##.
|.#.||#|.##.|.||.....#...#.#..###|.#......||#|....
.|.#..|#||......|##..##.#|..#|.|#.|.|#......|#.|#.
#..|........|||..|###..|#..|||#.|.|.....#|..|...|#
..####||#......|#||..###.|...|....#..|.#|.||....||
|##.......|||..........|..||.#.|#.......##...|...|
|.......#......####|#|....#....|......#.|#.###...#
#|.#.|||...|..|.....#....|...|......|#|#|......||.
...#.|......#..||||.#|.....|.|.|||.|.|.|#|.#...#.#
#.#.##.|.#|.|...|...|...#|...#.|#..|..##.|....#..|
|...#.......#....#....#.#....#.#|.|#||.|.|.|#...#.
#..|.||..|.#..|.#.....#|##.|.|....|....||.......|.
..||.#..|#|.###....#.#|..#|.#..........#...|...#|.
|#||.|.#..|....|....#.#||#.|......#..|#.#.|||||#|.
.|#.|#.##.....#.|.#.....|....|.#..#.#..|#.#.....|.
#.||.#.......|..|......|#||.|..#....#...|...|...|.
|.....#.|.....#||.....##...#.#...||.|..#........|.
||#..|.##.#...........#..|..|.|..#....|...#..||.#.
..||.##.##.|.||......#...|.#.#.#..#.#...##.#.|.#..
.|.#......#|#||.|.#|......||.#.|.|..|....#...||...
....|.##.....|#|####.#..#..#|.....|.#.#|......|...
...#..|......#....|#.#...|...|.#.#.......#.#.##..#
.|||#.||||...|..|#||.|.#|#||..|..#..|..|..#||.....
.....|..#..|#|.||.#||.||......|||..|..#|.|##......
.#...#|..#..|||..||.|..|.#.#.......||..|...|.|....
.##.||..|..||.|.......#.|||.|.|..|.#.#..|.||.|#|||
.|..##|..#.#|#|....|.#.#.#|#.#|.##|........###...#
..#..|#|...#.........#.#.####..#.#..#..#||#|...#|#
#.|...|.......|.#.#..#.|#..#|#|..#..|.....|..|...|
.##.|..#.....|...#..|#..|.|.#..##.#.|..#.|..|.##..
....|..|.|..||....|...|.....#..|.|.....|.#|......#
...##.|#..#..|.#|.##....|.#...||#|.....#...##.#|..
.|....##.....||...#.#.....#|#...#...#|.|..#.#.#.##

98
2018/d19/ex1/ex1.py Executable file
View file

@ -0,0 +1,98 @@
#!/usr/bin/env python
import copy
import enum
import sys
from typing import NamedTuple
class OpCode(enum.StrEnum):
ADDR = "addr"
ADDI = "addi"
MULR = "mulr"
MULI = "muli"
BANR = "banr"
BANI = "bani"
BORR = "borr"
BORI = "bori"
SETR = "setr"
SETI = "seti"
GTIR = "gtir"
GTRI = "gtri"
GTRR = "gtrr"
EQIR = "eqir"
EQRI = "eqri"
EQRR = "eqrr"
def apply(self, registers: list[int], a: int, b: int, c: int) -> list[int]:
registers = copy.deepcopy(registers)
if self == OpCode.ADDR:
registers[c] = registers[a] + registers[b]
if self == OpCode.ADDI:
registers[c] = registers[a] + b
if self == OpCode.MULR:
registers[c] = registers[a] * registers[b]
if self == OpCode.MULI:
registers[c] = registers[a] * b
if self == OpCode.BANR:
registers[c] = registers[a] & registers[b]
if self == OpCode.BANI:
registers[c] = registers[a] & b
if self == OpCode.BORR:
registers[c] = registers[a] | registers[b]
if self == OpCode.BORI:
registers[c] = registers[a] | b
if self == OpCode.SETR:
registers[c] = registers[a]
if self == OpCode.SETI:
registers[c] = a
if self == OpCode.GTIR:
registers[c] = a > registers[b]
if self == OpCode.GTRI:
registers[c] = registers[a] > b
if self == OpCode.GTRR:
registers[c] = registers[a] > registers[b]
if self == OpCode.EQIR:
registers[c] = a == registers[b]
if self == OpCode.EQRI:
registers[c] = registers[a] == b
if self == OpCode.EQRR:
registers[c] = registers[a] == registers[b]
return registers
class Instruction(NamedTuple):
op: OpCode
a: int
b: int
c: int
def apply(self, registers: list[int]) -> list[int]:
return self.op.apply(registers, self.a, self.b, self.c)
def solve(input: str) -> int:
def parse_instruction(input: str) -> Instruction:
op, *values = input.split()
return Instruction(OpCode(op), *map(int, values))
def parse(input: list[str]) -> tuple[int, list[Instruction]]:
ip = int(input[0].removeprefix("#ip "))
return ip, [parse_instruction(line) for line in input[1:]]
ip_reg, instructions = parse(input.splitlines())
registers = [0] * 6
while (ip := registers[ip_reg]) < len(instructions):
registers = instructions[ip].apply(registers)
registers[ip_reg] += 1
return registers[0]
def main() -> None:
input = sys.stdin.read()
print(solve(input))
if __name__ == "__main__":
main()

37
2018/d19/ex1/input Normal file
View file

@ -0,0 +1,37 @@
#ip 4
addi 4 16 4
seti 1 5 3
seti 1 9 1
mulr 3 1 2
eqrr 2 5 2
addr 2 4 4
addi 4 1 4
addr 3 0 0
addi 1 1 1
gtrr 1 5 2
addr 4 2 4
seti 2 9 4
addi 3 1 3
gtrr 3 5 2
addr 2 4 4
seti 1 8 4
mulr 4 4 4
addi 5 2 5
mulr 5 5 5
mulr 4 5 5
muli 5 11 5
addi 2 4 2
mulr 2 4 2
addi 2 5 2
addr 5 2 5
addr 4 0 4
seti 0 9 4
setr 4 2 2
mulr 2 4 2
addr 4 2 2
mulr 4 2 2
muli 2 14 2
mulr 2 4 2
addr 5 2 5
seti 0 0 0
seti 0 8 4

104
2018/d19/ex2/ex2.py Executable file
View file

@ -0,0 +1,104 @@
#!/usr/bin/env python
import copy
import enum
import sys
from typing import NamedTuple
class OpCode(enum.StrEnum):
ADDR = "addr"
ADDI = "addi"
MULR = "mulr"
MULI = "muli"
BANR = "banr"
BANI = "bani"
BORR = "borr"
BORI = "bori"
SETR = "setr"
SETI = "seti"
GTIR = "gtir"
GTRI = "gtri"
GTRR = "gtrr"
EQIR = "eqir"
EQRI = "eqri"
EQRR = "eqrr"
def apply(self, registers: list[int], a: int, b: int, c: int) -> list[int]:
registers = copy.deepcopy(registers)
if self == OpCode.ADDR:
registers[c] = registers[a] + registers[b]
if self == OpCode.ADDI:
registers[c] = registers[a] + b
if self == OpCode.MULR:
registers[c] = registers[a] * registers[b]
if self == OpCode.MULI:
registers[c] = registers[a] * b
if self == OpCode.BANR:
registers[c] = registers[a] & registers[b]
if self == OpCode.BANI:
registers[c] = registers[a] & b
if self == OpCode.BORR:
registers[c] = registers[a] | registers[b]
if self == OpCode.BORI:
registers[c] = registers[a] | b
if self == OpCode.SETR:
registers[c] = registers[a]
if self == OpCode.SETI:
registers[c] = a
if self == OpCode.GTIR:
registers[c] = a > registers[b]
if self == OpCode.GTRI:
registers[c] = registers[a] > b
if self == OpCode.GTRR:
registers[c] = registers[a] > registers[b]
if self == OpCode.EQIR:
registers[c] = a == registers[b]
if self == OpCode.EQRI:
registers[c] = registers[a] == b
if self == OpCode.EQRR:
registers[c] = registers[a] == registers[b]
return registers
class Instruction(NamedTuple):
op: OpCode
a: int
b: int
c: int
def apply(self, registers: list[int]) -> list[int]:
return self.op.apply(registers, self.a, self.b, self.c)
def solve(input: str) -> int:
def parse_instruction(input: str) -> Instruction:
op, *values = input.split()
return Instruction(OpCode(op), *map(int, values))
def parse(input: list[str]) -> tuple[int, list[Instruction]]:
ip = int(input[0].removeprefix("#ip "))
return ip, [parse_instruction(line) for line in input[1:]]
def get_seed(ip_reg: int, instructions: list[Instruction]) -> int:
registers = [0] * 6
registers[0] = 1
while (ip := registers[ip_reg]) != 1:
registers = instructions[ip].apply(registers)
registers[ip_reg] += 1
return max(registers)
ip_reg, instructions = parse(input.splitlines())
seed = get_seed(ip_reg, instructions)
return sum(i for i in range(1, seed + 1) if seed % i == 0)
def main() -> None:
input = sys.stdin.read()
print(solve(input))
if __name__ == "__main__":
main()

37
2018/d19/ex2/input Normal file
View file

@ -0,0 +1,37 @@
#ip 4
addi 4 16 4
seti 1 5 3
seti 1 9 1
mulr 3 1 2
eqrr 2 5 2
addr 2 4 4
addi 4 1 4
addr 3 0 0
addi 1 1 1
gtrr 1 5 2
addr 4 2 4
seti 2 9 4
addi 3 1 3
gtrr 3 5 2
addr 2 4 4
seti 1 8 4
mulr 4 4 4
addi 5 2 5
mulr 5 5 5
mulr 4 5 5
muli 5 11 5
addi 2 4 2
mulr 2 4 2
addi 2 5 2
addr 5 2 5
addr 4 0 4
seti 0 9 4
setr 4 2 2
mulr 2 4 2
addr 4 2 2
mulr 4 2 2
muli 2 14 2
mulr 2 4 2
addr 5 2 5
seti 0 0 0
seti 0 8 4

89
2018/d20/ex1/ex1.py Executable file
View file

@ -0,0 +1,89 @@
#!/usr/bin/env python
import collections
import copy
import enum
import sys
from typing import NamedTuple
class Point(NamedTuple):
x: int
y: int
class Direction(enum.StrEnum):
NORTH = "N"
SOUTH = "S"
WEST = "W"
EAST = "E"
def apply(self, p: Point) -> Point:
delta: Point
match self:
case Direction.NORTH:
delta = Point(-1, 0)
case Direction.SOUTH:
delta = Point(1, 0)
case Direction.WEST:
delta = Point(0, -1)
case Direction.EAST:
delta = Point(0, 1)
return Point(p.x + delta.x, p.y + delta.y)
START = Point(0, 0)
def solve(input: str) -> int:
def to_graph(regex: str) -> dict[Point, set[Point]]:
res: dict[Point, set[Point]] = collections.defaultdict(set)
stack: list[set[Point]] = [{START}]
current_branches: set[Point] = set()
for c in regex.removeprefix("^").removesuffix("$"):
if c == "(":
stack.append(copy.deepcopy(stack[-1]))
current_branches = set()
elif c == "|":
current_branches |= stack.pop()
stack.append(copy.deepcopy(stack[-1]))
elif c == ")":
current_branches |= stack.pop()
stack[-1] = current_branches
else:
dir = Direction(c)
for p in stack[-1]:
neighbour = dir.apply(p)
res[p].add(neighbour)
res[neighbour].add(p)
stack[-1] = {dir.apply(p) for p in stack[-1]}
return dict(res)
def start_distances(graph: dict[Point, set[Point]]) -> dict[Point, int]:
queue = collections.deque([(0, START)])
distances: dict[Point, int] = {}
while queue:
dist, p = queue.popleft()
if p in distances:
continue
distances[p] = dist
for n in graph.get(p, set()):
queue.append((dist + 1, n))
return distances
# Remove the anchors, we don't use them in the parsing code
graph = to_graph(input.strip())
distances = start_distances(graph)
return max(distances.values())
def main() -> None:
input = sys.stdin.read()
print(solve(input))
if __name__ == "__main__":
main()

1
2018/d20/ex1/input Normal file

File diff suppressed because one or more lines are too long

89
2018/d20/ex2/ex2.py Executable file
View file

@ -0,0 +1,89 @@
#!/usr/bin/env python
import collections
import copy
import enum
import sys
from typing import NamedTuple
class Point(NamedTuple):
x: int
y: int
class Direction(enum.StrEnum):
NORTH = "N"
SOUTH = "S"
WEST = "W"
EAST = "E"
def apply(self, p: Point) -> Point:
delta: Point
match self:
case Direction.NORTH:
delta = Point(-1, 0)
case Direction.SOUTH:
delta = Point(1, 0)
case Direction.WEST:
delta = Point(0, -1)
case Direction.EAST:
delta = Point(0, 1)
return Point(p.x + delta.x, p.y + delta.y)
START = Point(0, 0)
def solve(input: str) -> int:
def to_graph(regex: str) -> dict[Point, set[Point]]:
res: dict[Point, set[Point]] = collections.defaultdict(set)
stack: list[set[Point]] = [{START}]
current_branches: set[Point] = set()
for c in regex.removeprefix("^").removesuffix("$"):
if c == "(":
stack.append(copy.deepcopy(stack[-1]))
current_branches = set()
elif c == "|":
current_branches |= stack.pop()
stack.append(copy.deepcopy(stack[-1]))
elif c == ")":
current_branches |= stack.pop()
stack[-1] = current_branches
else:
dir = Direction(c)
for p in stack[-1]:
neighbour = dir.apply(p)
res[p].add(neighbour)
res[neighbour].add(p)
stack[-1] = {dir.apply(p) for p in stack[-1]}
return dict(res)
def start_distances(graph: dict[Point, set[Point]]) -> dict[Point, int]:
queue = collections.deque([(0, START)])
distances: dict[Point, int] = {}
while queue:
dist, p = queue.popleft()
if p in distances:
continue
distances[p] = dist
for n in graph.get(p, set()):
queue.append((dist + 1, n))
return distances
# Remove the anchors, we don't use them in the parsing code
graph = to_graph(input.strip())
distances = start_distances(graph)
return sum(d >= 1000 for d in distances.values())
def main() -> None:
input = sys.stdin.read()
print(solve(input))
if __name__ == "__main__":
main()

1
2018/d20/ex2/input Normal file

File diff suppressed because one or more lines are too long

107
2018/d21/ex1/ex1.py Executable file
View file

@ -0,0 +1,107 @@
#!/usr/bin/env python
import copy
import enum
import sys
from typing import NamedTuple
class OpCode(enum.StrEnum):
ADDR = "addr"
ADDI = "addi"
MULR = "mulr"
MULI = "muli"
BANR = "banr"
BANI = "bani"
BORR = "borr"
BORI = "bori"
SETR = "setr"
SETI = "seti"
GTIR = "gtir"
GTRI = "gtri"
GTRR = "gtrr"
EQIR = "eqir"
EQRI = "eqri"
EQRR = "eqrr"
def apply(self, registers: list[int], a: int, b: int, c: int) -> list[int]:
registers = copy.deepcopy(registers)
if self == OpCode.ADDR:
registers[c] = registers[a] + registers[b]
if self == OpCode.ADDI:
registers[c] = registers[a] + b
if self == OpCode.MULR:
registers[c] = registers[a] * registers[b]
if self == OpCode.MULI:
registers[c] = registers[a] * b
if self == OpCode.BANR:
registers[c] = registers[a] & registers[b]
if self == OpCode.BANI:
registers[c] = registers[a] & b
if self == OpCode.BORR:
registers[c] = registers[a] | registers[b]
if self == OpCode.BORI:
registers[c] = registers[a] | b
if self == OpCode.SETR:
registers[c] = registers[a]
if self == OpCode.SETI:
registers[c] = a
if self == OpCode.GTIR:
registers[c] = a > registers[b]
if self == OpCode.GTRI:
registers[c] = registers[a] > b
if self == OpCode.GTRR:
registers[c] = registers[a] > registers[b]
if self == OpCode.EQIR:
registers[c] = a == registers[b]
if self == OpCode.EQRI:
registers[c] = registers[a] == b
if self == OpCode.EQRR:
registers[c] = registers[a] == registers[b]
return registers
class Instruction(NamedTuple):
op: OpCode
a: int
b: int
c: int
def apply(self, registers: list[int]) -> list[int]:
return self.op.apply(registers, self.a, self.b, self.c)
def solve(input: str) -> int:
def parse_instruction(input: str) -> Instruction:
op, *values = input.split()
return Instruction(OpCode(op), *map(int, values))
def parse(input: list[str]) -> tuple[int, list[Instruction]]:
ip = int(input[0].removeprefix("#ip "))
return ip, [parse_instruction(line) for line in input[1:]]
# Relies on the input having a singular `EQRR` instruction
def find_comparison(ip_reg: int, instructions: list[Instruction]) -> int:
registers = [0] * 6
while (ip := registers[ip_reg]) < len(instructions):
instr = instructions[ip]
if instr.op == OpCode.EQRR:
operands = {instr.a, instr.b}
assert 0 in operands # Sanity check
operands.remove(0)
return registers[operands.pop()]
registers = instr.apply(registers)
registers[ip_reg] += 1
assert False # Sanity check
ip_reg, instructions = parse(input.splitlines())
return find_comparison(ip_reg, instructions)
def main() -> None:
input = sys.stdin.read()
print(solve(input))
if __name__ == "__main__":
main()

32
2018/d21/ex1/input Normal file
View file

@ -0,0 +1,32 @@
#ip 1
seti 123 0 3
bani 3 456 3
eqri 3 72 3
addr 3 1 1
seti 0 0 1
seti 0 9 3
bori 3 65536 5
seti 15028787 4 3
bani 5 255 2
addr 3 2 3
bani 3 16777215 3
muli 3 65899 3
bani 3 16777215 3
gtir 256 5 2
addr 2 1 1
addi 1 1 1
seti 27 3 1
seti 0 9 2
addi 2 1 4
muli 4 256 4
gtrr 4 5 4
addr 4 1 1
addi 1 1 1
seti 25 1 1
addi 2 1 2
seti 17 8 1
setr 2 4 5
seti 7 3 1
eqrr 3 0 2
addr 2 1 1
seti 5 3 1

138
2018/d21/ex2/ex2.py Executable file
View file

@ -0,0 +1,138 @@
#!/usr/bin/env python
import copy
import enum
import sys
from typing import NamedTuple
class OpCode(enum.StrEnum):
ADDR = "addr"
ADDI = "addi"
MULR = "mulr"
MULI = "muli"
BANR = "banr"
BANI = "bani"
BORR = "borr"
BORI = "bori"
SETR = "setr"
SETI = "seti"
GTIR = "gtir"
GTRI = "gtri"
GTRR = "gtrr"
EQIR = "eqir"
EQRI = "eqri"
EQRR = "eqrr"
def apply(self, registers: list[int], a: int, b: int, c: int) -> list[int]:
registers = copy.deepcopy(registers)
if self == OpCode.ADDR:
registers[c] = registers[a] + registers[b]
if self == OpCode.ADDI:
registers[c] = registers[a] + b
if self == OpCode.MULR:
registers[c] = registers[a] * registers[b]
if self == OpCode.MULI:
registers[c] = registers[a] * b
if self == OpCode.BANR:
registers[c] = registers[a] & registers[b]
if self == OpCode.BANI:
registers[c] = registers[a] & b
if self == OpCode.BORR:
registers[c] = registers[a] | registers[b]
if self == OpCode.BORI:
registers[c] = registers[a] | b
if self == OpCode.SETR:
registers[c] = registers[a]
if self == OpCode.SETI:
registers[c] = a
if self == OpCode.GTIR:
registers[c] = a > registers[b]
if self == OpCode.GTRI:
registers[c] = registers[a] > b
if self == OpCode.GTRR:
registers[c] = registers[a] > registers[b]
if self == OpCode.EQIR:
registers[c] = a == registers[b]
if self == OpCode.EQRI:
registers[c] = registers[a] == b
if self == OpCode.EQRR:
registers[c] = registers[a] == registers[b]
return registers
class Instruction(NamedTuple):
op: OpCode
a: int
b: int
c: int
def apply(self, registers: list[int]) -> list[int]:
return self.op.apply(registers, self.a, self.b, self.c)
def solve(input: str) -> int:
def parse_instruction(input: str) -> Instruction:
op, *values = input.split()
return Instruction(OpCode(op), *map(int, values))
def parse(input: list[str]) -> tuple[int, list[Instruction]]:
ip = int(input[0].removeprefix("#ip "))
return ip, [parse_instruction(line) for line in input[1:]]
def hash_loop(n: int, seed: int, perturb: int) -> int:
n |= 0x10000 #
while n:
seed += n & 0xFF
seed &= 0xFFFFFF # Keeps 24-bit
seed *= perturb
seed &= 0xFFFFFF # Keeps 24-bit
n >>= 8
return seed
# Relies heavily on input having a specific shape
def hash_params(ip_reg: int, instructions: list[Instruction]) -> tuple[int, int]:
def seed_index() -> int:
for i, instr in enumerate(instructions):
if instr.op == OpCode.BORI and instr.b == 65536:
return i + 1
assert False # Sanity check
def perturb_index() -> int:
for i, instr in enumerate(instructions):
if instr.op == OpCode.BANI and instr.b == 16777215:
return i + 1
assert False # Sanity check
seed_instr = instructions[seed_index()]
perturb_instr = instructions[perturb_index()]
assert seed_instr.op == OpCode.SETI # Sanity check
assert perturb_instr.op == OpCode.MULI # Sanity check
assert perturb_instr.a == perturb_instr.c # Sanity check
return seed_instr.a, perturb_instr.b
def find_comparison(ip_reg: int, instructions: list[Instruction]) -> int:
seed, perturb = hash_params(ip_reg, instructions)
value = 0
count = 0
seen: set[int] = set()
while True:
count += (value << 8) + (value << 16)
if (new_value := hash_loop(value, seed, perturb)) in seen:
return value
seen.add(new_value)
value = new_value
assert False # Sanity check
ip_reg, instructions = parse(input.splitlines())
return find_comparison(ip_reg, instructions)
def main() -> None:
input = sys.stdin.read()
print(solve(input))
if __name__ == "__main__":
main()

32
2018/d21/ex2/input Normal file
View file

@ -0,0 +1,32 @@
#ip 1
seti 123 0 3
bani 3 456 3
eqri 3 72 3
addr 3 1 1
seti 0 0 1
seti 0 9 3
bori 3 65536 5
seti 15028787 4 3
bani 5 255 2
addr 3 2 3
bani 3 16777215 3
muli 3 65899 3
bani 3 16777215 3
gtir 256 5 2
addr 2 1 1
addi 1 1 1
seti 27 3 1
seti 0 9 2
addi 2 1 4
muli 4 256 4
gtrr 4 5 4
addr 4 1 1
addi 1 1 1
seti 25 1 1
addi 2 1 2
seti 17 8 1
setr 2 4 5
seti 7 3 1
eqrr 3 0 2
addr 2 1 1
seti 5 3 1

67
2018/d22/ex1/ex1.py Executable file
View file

@ -0,0 +1,67 @@
#!/usr/bin/env python
import enum
import itertools
import sys
from typing import NamedTuple
class Point(NamedTuple):
x: int
y: int
class Region(enum.IntEnum):
ROCKY = 0
WET = 1
NARROW = 2
def solve(input: str) -> int:
def parse(input: list[str]) -> tuple[int, Point]:
depth = input[0].removeprefix("depth: ")
target = input[1].removeprefix("target: ")
return int(depth), Point(*(int(n) for n in target.split(",")))
def compute_erosions(depth: int, target: Point) -> dict[Point, int]:
res: dict[Point, int] = {}
for x in range(0, target.x + 1):
for y in range(0, target.y + 1):
p = Point(x, y)
if p == Point(0, 0) or p == target:
res[p] = 0
elif p.y == 0:
res[p] = p.x * 16807
elif p.x == 0:
res[p] = p.y * 48271
else:
res[p] = res[Point(p.x - 1, p.y)] * res[Point(p.x, p.y - 1)]
# Go from geologic index to erosion level
res[p] += depth
res[p] %= 20183
return res
def compute_regions(depth: int, target: Point) -> dict[Point, Region]:
return {
p: Region(erosion % 3)
for p, erosion in compute_erosions(depth, target).items()
}
depth, target = parse(input.splitlines())
regions = compute_regions(depth, target)
return sum(
regions[p]
for p in map(
Point._make,
itertools.product(range(0, target.x + 1), range(target.y + 1)),
)
)
def main() -> None:
input = sys.stdin.read()
print(solve(input))
if __name__ == "__main__":
main()

2
2018/d22/ex1/input Normal file
View file

@ -0,0 +1,2 @@
depth: 7740
target: 12,763

128
2018/d22/ex2/ex2.py Executable file
View file

@ -0,0 +1,128 @@
#!/usr/bin/env python
import dataclasses
import enum
import heapq
import sys
from collections.abc import Iterator
from typing import NamedTuple
class Point(NamedTuple):
x: int
y: int
def neighbours(self) -> Iterator["Point"]:
for dx, dy in (
(-1, 0),
(1, 0),
(0, -1),
(0, 1),
):
yield Point(self.x + dx, self.y + dy)
class Region(enum.IntEnum):
ROCKY = 0
WET = 1
NARROW = 2
@dataclasses.dataclass
class Cave:
depth: int
target: Point
erosion: dict[Point, int] = dataclasses.field(init=False)
def __post_init__(self) -> None:
self.erosion = {}
def erosion_at(self, p: Point) -> int:
if p in self.erosion:
return self.erosion[p]
if p == Point(0, 0) or p == self.target:
self.erosion[p] = 0
elif p.y == 0:
self.erosion[p] = p.x * 16807
elif p.x == 0:
self.erosion[p] = p.y * 48271
else:
self.erosion[p] = self.erosion_at(Point(p.x - 1, p.y)) * self.erosion_at(
Point(p.x, p.y - 1)
)
# Go from geologic index to erosion level
self.erosion[p] += self.depth
self.erosion[p] %= 20183
return self.erosion[p]
def region_at(self, p: Point) -> Region:
return Region(self.erosion_at(p) % 3)
class Gear(enum.IntEnum):
NEITHER = 0
TORCH = 1
CLIMBING = 2
class Explorer(NamedTuple):
pos: Point
gear: Gear
def solve(input: str) -> int:
def parse(input: list[str]) -> tuple[int, Point]:
depth = input[0].removeprefix("depth: ")
target = input[1].removeprefix("target: ")
return int(depth), Point(*(int(n) for n in target.split(",")))
def next_state(explorer: Explorer, cave: Cave) -> Iterator[tuple[int, Explorer]]:
for n in explorer.pos.neighbours():
if n.x < 0 or n.y < 0:
continue
region = cave.region_at(n)
if region == Region.ROCKY:
for gear in (Gear.CLIMBING, Gear.TORCH):
yield 1 + (7 if gear != explorer.gear else 0), Explorer(n, gear)
if region == Region.WET:
for gear in (Gear.CLIMBING, Gear.NEITHER):
yield 1 + (7 if gear != explorer.gear else 0), Explorer(n, gear)
if region == Region.NARROW:
for gear in (Gear.TORCH, Gear.NEITHER):
yield 1 + (7 if gear != explorer.gear else 0), Explorer(n, gear)
def djikstra(start: Explorer, end: Explorer, cave: Cave) -> int:
# Priority queue of (distance, point)
queue = [(0, start)]
seen: set[Explorer] = set()
while len(queue) > 0:
cost, explorer = heapq.heappop(queue)
if explorer == end:
return cost
# We must have seen p with a smaller distance before
if explorer in seen:
continue
# First time encountering p, must be the smallest distance to it
seen.add(explorer)
# Add all neighbours to be visited
for time, n in next_state(explorer, cave):
heapq.heappush(queue, (cost + time, n))
assert False # Sanity check
depth, target = parse(input.splitlines())
cave = Cave(depth, target)
start = Explorer(Point(0, 0), Gear.TORCH)
end = Explorer(target, Gear.TORCH)
return djikstra(start, end, cave)
def main() -> None:
input = sys.stdin.read()
print(solve(input))
if __name__ == "__main__":
main()

2
2018/d22/ex2/input Normal file
View file

@ -0,0 +1,2 @@
depth: 7740
target: 12,763

42
2018/d23/ex1/ex1.py Executable file
View file

@ -0,0 +1,42 @@
#!/usr/bin/env python
import sys
from typing import NamedTuple
class Point(NamedTuple):
x: int
y: int
z: int
class NanoBot(NamedTuple):
pos: Point
r: int
def solve(input: str) -> int:
def parse_nanobot(input: str) -> NanoBot:
pos, r = input.split(", ")
pos = pos.removeprefix("pos=<").removesuffix(">")
r = r.removeprefix("r=")
return NanoBot(Point(*(int(n) for n in pos.split(","))), int(r))
def parse(input: list[str]) -> list[NanoBot]:
return [parse_nanobot(line) for line in input]
def dist(lhs: Point, rhs: Point) -> int:
return sum(abs(l - r) for l, r in zip(lhs, rhs))
bots = parse(input.splitlines())
strongest = max(bots, key=lambda b: b.r)
return sum(dist(strongest.pos, bot.pos) <= strongest.r for bot in bots)
def main() -> None:
input = sys.stdin.read()
print(solve(input))
if __name__ == "__main__":
main()

1000
2018/d23/ex1/input Normal file

File diff suppressed because it is too large Load diff

71
2018/d23/ex2/ex2.py Executable file
View file

@ -0,0 +1,71 @@
#!/usr/bin/env python
import sys
from typing import NamedTuple
import z3
class Point(NamedTuple):
x: int
y: int
z: int
class NanoBot(NamedTuple):
pos: Point
r: int
def solve(input: str) -> int:
def parse_nanobot(input: str) -> NanoBot:
pos, r = input.split(", ")
pos = pos.removeprefix("pos=<").removesuffix(">")
r = r.removeprefix("r=")
return NanoBot(Point(*(int(n) for n in pos.split(","))), int(r))
def parse(input: list[str]) -> list[NanoBot]:
return [parse_nanobot(line) for line in input]
def dist(lhs: Point, rhs: Point) -> int:
return sum(abs(l - r) for l, r in zip(lhs, rhs))
def find_best(bots: list[NanoBot]) -> Point:
def z3_abs(n: z3.ArithRef) -> z3.ArithRef:
return z3.If(n > 0, n, -n) # type: ignore
def z3_dist(lhs: tuple[z3.ArithRef, ...], rhs: Point) -> z3.ArithRef:
return sum(z3_abs(l - r) for l, r in zip(lhs, rhs)) # type: ignore
pos = tuple(z3.Int(c) for c in ("x", "y", "z"))
in_range = [z3.Int(f"in_range_{i}") for i in range(len(bots))]
total = z3.Int("total")
optimizer = z3.Optimize()
for i, bot in enumerate(bots):
optimizer.add(in_range[i] == (z3_dist(pos, bot.pos) <= bot.r))
optimizer.add(total == sum(in_range))
dist_to_origin = z3.Int("dist_to_origin")
optimizer.add(dist_to_origin == z3_dist(pos, Point(0, 0, 0)))
optimizer.maximize(total)
optimizer.minimize(dist_to_origin)
assert optimizer.check() == z3.sat # Sanity check
model = optimizer.model()
return Point(*(map(lambda v: model.eval(v).as_long(), pos))) # type: ignore
bots = parse(input.splitlines())
return dist(find_best(bots), Point(0, 0, 0))
def main() -> None:
input = sys.stdin.read()
print(solve(input))
if __name__ == "__main__":
main()

1000
2018/d23/ex2/input Normal file

File diff suppressed because it is too large Load diff

188
2018/d24/ex1/ex1.py Executable file
View file

@ -0,0 +1,188 @@
#!/usr/bin/env python
import dataclasses
import enum
import sys
@dataclasses.dataclass
class Group:
units: int
hp: int
weaknesses: set[str]
immunities: set[str]
attack: int
attack_type: str
initiative: int
@classmethod
def from_raw(cls, input: str) -> "Group":
def split_sections(input: str) -> tuple[str, str, str]:
points_idx = input.index("hit points ")
with_idx = input.index(" with an attack")
return (
input[:points_idx].strip(),
input[points_idx:with_idx].removeprefix("(").removesuffix(")"),
input[with_idx:].strip(),
)
def parse_weak_immune(weak_immune: str) -> tuple[set[str], set[str]]:
weaknesses: set[str] = set()
immunities: set[str] = set()
for part in weak_immune.split("; "):
for start, values in (
("weak to ", weaknesses),
("immune to ", immunities),
):
if not part.startswith(start):
continue
values.update(part.removeprefix(start).split(", "))
return weaknesses, immunities
group_str, weak_immune, attack_str = split_sections(input)
group_list, attack_list = group_str.split(), attack_str.split()
weaknesses, immunities = parse_weak_immune(
weak_immune.removeprefix("hit points (")
)
return cls(
units=int(group_list[0]),
hp=int(group_list[4]),
weaknesses=weaknesses,
immunities=immunities,
attack=int(attack_list[5]),
attack_type=attack_list[6],
initiative=int(attack_list[10]),
)
@property
def alive(self) -> bool:
return self.units > 0
@property
def effective_power(self) -> int:
return self.units * self.attack
def potential_attack(self, ennemy: "Group") -> int:
multiplier = 1
if self.attack_type in ennemy.weaknesses:
multiplier = 2
if self.attack_type in ennemy.immunities:
multiplier = 0
return self.effective_power * multiplier
class Army(enum.StrEnum):
INFECTION = "INFECTION"
IMMUNE = "IMMUNE"
def ennemy(self) -> "Army":
if self == Army.INFECTION:
return Army.IMMUNE
if self == Army.IMMUNE:
return Army.INFECTION
assert False # Sanity check
@dataclasses.dataclass
class Armies:
immune: list[Group]
infection: list[Group]
@classmethod
def from_raw(cls, input: str) -> "Armies":
immune, infection = map(str.splitlines, input.split("\n\n"))
assert "Immune System:" == immune[0] # Sanity check
assert "Infection:" == infection[0] # Sanity check
return cls(
list(map(Group.from_raw, immune[1:])),
list(map(Group.from_raw, infection[1:])),
)
def army(self, army: Army) -> list[Group]:
if army == Army.IMMUNE:
return self.immune
if army == Army.INFECTION:
return self.infection
assert False # Sanity check
def active_groups(self, army: Army) -> set[int]:
return {i for i, group in enumerate(self.army(army)) if group.alive}
def selection_phase(self) -> dict[tuple[Army, int], int]:
# Armies are sorted by decreasing power, initiative
def power_order(group: Group) -> tuple[int, int]:
return group.effective_power, group.initiative
# Targets are ordered in decreasing potential attack, power, initiative
def target_order(group: Group, ennemy: Group) -> tuple[int, int, int]:
return (
group.potential_attack(ennemy),
ennemy.effective_power,
ennemy.initiative,
)
res: dict[tuple[Army, int], int] = {}
for army in Army:
army_indices = sorted(
self.active_groups(army),
key=lambda i: power_order(self.army(army)[i]),
reverse=True,
)
ennemies = self.army(army.ennemy())
indices = set(self.active_groups(army.ennemy()))
for i in army_indices:
group = self.army(army)[i]
if not indices:
break
target = max(indices, key=lambda j: target_order(group, ennemies[j]))
# Skip target if we cannot deal damage to it
if group.potential_attack(ennemies[target]) == 0:
continue
res[(army, i)] = target
# Targets must be different for each attack
indices.remove(target)
return res
def attack_phase(self, targets: dict[tuple[Army, int], int]) -> None:
# Armies take turn by initiative, regardless of type
turn_order = sorted(
((army, i) for army in Army for i in self.active_groups(army)),
key=lambda t: self.army(t[0])[t[1]].initiative,
reverse=True,
)
for army, i in turn_order:
# Empty armies do not fight
if not self.army(army)[i].alive:
continue
# Army must have a target selected
if (target := targets.get((army, i))) is None:
continue
attackers = self.army(army)[i]
defender = self.army(army.ennemy())[target]
damage = attackers.potential_attack(defender)
defender.units -= min(damage // defender.hp, defender.units)
def fight(self) -> None:
while self.active_groups(Army.IMMUNE) and self.active_groups(Army.INFECTION):
targets = self.selection_phase()
self.attack_phase(targets)
def solve(input: str) -> int:
def parse(input: str) -> Armies:
return Armies.from_raw(input)
armies = parse(input)
armies.fight()
return sum(
group.units for army in (armies.immune, armies.infection) for group in army
)
def main() -> None:
input = sys.stdin.read()
print(solve(input))
if __name__ == "__main__":
main()

23
2018/d24/ex1/input Normal file
View file

@ -0,0 +1,23 @@
Immune System:
916 units each with 3041 hit points (weak to cold, fire) with an attack that does 29 fire damage at initiative 13
1959 units each with 7875 hit points (weak to cold; immune to slashing, bludgeoning) with an attack that does 38 radiation damage at initiative 20
8933 units each with 5687 hit points with an attack that does 6 slashing damage at initiative 15
938 units each with 8548 hit points with an attack that does 89 radiation damage at initiative 4
1945 units each with 3360 hit points (immune to cold; weak to radiation) with an attack that does 16 cold damage at initiative 1
2211 units each with 7794 hit points (weak to slashing) with an attack that does 30 fire damage at initiative 12
24 units each with 3693 hit points with an attack that does 1502 fire damage at initiative 5
2004 units each with 4141 hit points (immune to radiation) with an attack that does 18 slashing damage at initiative 19
3862 units each with 3735 hit points (immune to bludgeoning, fire) with an attack that does 9 fire damage at initiative 10
8831 units each with 3762 hit points (weak to radiation) with an attack that does 3 fire damage at initiative 7
Infection:
578 units each with 55836 hit points with an attack that does 154 radiation damage at initiative 9
476 units each with 55907 hit points (weak to fire) with an attack that does 208 cold damage at initiative 18
496 units each with 33203 hit points (weak to fire, radiation; immune to cold, bludgeoning) with an attack that does 116 slashing damage at initiative 14
683 units each with 12889 hit points (weak to fire) with an attack that does 35 bludgeoning damage at initiative 11
1093 units each with 29789 hit points (immune to cold, fire) with an attack that does 51 radiation damage at initiative 17
2448 units each with 40566 hit points (immune to bludgeoning, fire; weak to cold) with an attack that does 25 slashing damage at initiative 16
1229 units each with 6831 hit points (weak to fire, cold; immune to slashing) with an attack that does 8 bludgeoning damage at initiative 8
3680 units each with 34240 hit points (immune to bludgeoning; weak to fire, cold) with an attack that does 17 radiation damage at initiative 3
4523 units each with 9788 hit points (immune to bludgeoning, fire, slashing) with an attack that does 3 bludgeoning damage at initiative 6
587 units each with 49714 hit points (weak to bludgeoning) with an attack that does 161 fire damage at initiative 2

219
2018/d24/ex2/ex2.py Executable file
View file

@ -0,0 +1,219 @@
#!/usr/bin/env python
import copy
import dataclasses
import enum
import sys
@dataclasses.dataclass
class Group:
units: int
hp: int
weaknesses: set[str]
immunities: set[str]
attack: int
attack_type: str
initiative: int
@classmethod
def from_raw(cls, input: str) -> "Group":
def split_sections(input: str) -> tuple[str, str, str]:
points_idx = input.index("hit points ")
with_idx = input.index(" with an attack")
return (
input[:points_idx].strip(),
input[points_idx:with_idx].removeprefix("(").removesuffix(")"),
input[with_idx:].strip(),
)
def parse_weak_immune(weak_immune: str) -> tuple[set[str], set[str]]:
weaknesses: set[str] = set()
immunities: set[str] = set()
for part in weak_immune.split("; "):
for start, values in (
("weak to ", weaknesses),
("immune to ", immunities),
):
if not part.startswith(start):
continue
values.update(part.removeprefix(start).split(", "))
return weaknesses, immunities
group_str, weak_immune, attack_str = split_sections(input)
group_list, attack_list = group_str.split(), attack_str.split()
weaknesses, immunities = parse_weak_immune(
weak_immune.removeprefix("hit points (")
)
return cls(
units=int(group_list[0]),
hp=int(group_list[4]),
weaknesses=weaknesses,
immunities=immunities,
attack=int(attack_list[5]),
attack_type=attack_list[6],
initiative=int(attack_list[10]),
)
@property
def alive(self) -> bool:
return self.units > 0
@property
def effective_power(self) -> int:
return self.units * self.attack
def potential_attack(self, ennemy: "Group") -> int:
multiplier = 1
if self.attack_type in ennemy.weaknesses:
multiplier = 2
if self.attack_type in ennemy.immunities:
multiplier = 0
return self.effective_power * multiplier
class LoopError(Exception):
pass
class Army(enum.StrEnum):
INFECTION = "INFECTION"
IMMUNE = "IMMUNE"
def ennemy(self) -> "Army":
if self == Army.INFECTION:
return Army.IMMUNE
if self == Army.IMMUNE:
return Army.INFECTION
assert False # Sanity check
@dataclasses.dataclass
class Armies:
immune: list[Group]
infection: list[Group]
@classmethod
def from_raw(cls, input: str) -> "Armies":
immune, infection = map(str.splitlines, input.split("\n\n"))
assert "Immune System:" == immune[0] # Sanity check
assert "Infection:" == infection[0] # Sanity check
return cls(
list(map(Group.from_raw, immune[1:])),
list(map(Group.from_raw, infection[1:])),
)
def army(self, army: Army) -> list[Group]:
if army == Army.IMMUNE:
return self.immune
if army == Army.INFECTION:
return self.infection
assert False # Sanity check
def active_groups(self, army: Army) -> set[int]:
return {i for i, group in enumerate(self.army(army)) if group.alive}
def selection_phase(self) -> dict[tuple[Army, int], int]:
# Armies are sorted by decreasing power, initiative
def power_order(group: Group) -> tuple[int, int]:
return group.effective_power, group.initiative
# Targets are ordered in decreasing potential attack, power, initiative
def target_order(group: Group, ennemy: Group) -> tuple[int, int, int]:
return (
group.potential_attack(ennemy),
ennemy.effective_power,
ennemy.initiative,
)
res: dict[tuple[Army, int], int] = {}
for army in Army:
army_indices = sorted(
self.active_groups(army),
key=lambda i: power_order(self.army(army)[i]),
reverse=True,
)
ennemies = self.army(army.ennemy())
indices = set(self.active_groups(army.ennemy()))
for i in army_indices:
group = self.army(army)[i]
if not indices:
break
target = max(indices, key=lambda j: target_order(group, ennemies[j]))
# Skip target if we cannot deal damage to it
if group.potential_attack(ennemies[target]) == 0:
continue
res[(army, i)] = target
# Targets must be different for each attack
indices.remove(target)
return res
def attack_phase(self, targets: dict[tuple[Army, int], int]) -> None:
# Armies take turn by initiative, regardless of type
turn_order = sorted(
((army, i) for army in Army for i in self.active_groups(army)),
key=lambda t: self.army(t[0])[t[1]].initiative,
reverse=True,
)
any_kills = False
for army, i in turn_order:
# Empty armies do not fight
if not self.army(army)[i].alive:
continue
# Army must have a target selected
if (target := targets.get((army, i))) is None:
continue
attackers = self.army(army)[i]
defender = self.army(army.ennemy())[target]
damage = attackers.potential_attack(defender)
killed_units = min(damage // defender.hp, defender.units)
defender.units -= killed_units
# Detect if no kills were done to avoid loops
any_kills |= bool(killed_units)
# If no units were killed, we're about to enter an infinite loop
if not any_kills:
raise LoopError
def fight(self) -> None:
while self.active_groups(Army.IMMUNE) and self.active_groups(Army.INFECTION):
targets = self.selection_phase()
self.attack_phase(targets)
def solve(input: str) -> int:
def parse(input: str) -> Armies:
return Armies.from_raw(input)
def apply_boost(armies: Armies, boost: int) -> int:
armies = copy.deepcopy(armies)
for group in armies.immune:
group.attack += boost
try:
armies.fight()
except LoopError:
return 0
return sum(group.units for group in armies.immune)
def bisect_boost(armies: Armies) -> int:
# Winning the fight feels like it should be monotonic
low, high = 0, 100000 # Probably good enough
while low < high:
mid = low + (high - low) // 2
if apply_boost(armies, mid) != 0:
high = mid
else:
low = mid + 1
# Wastefully re-run the fight to get the number of remaining units
return apply_boost(armies, low)
armies = parse(input)
return bisect_boost(armies)
def main() -> None:
input = sys.stdin.read()
print(solve(input))
if __name__ == "__main__":
main()

23
2018/d24/ex2/input Normal file
View file

@ -0,0 +1,23 @@
Immune System:
916 units each with 3041 hit points (weak to cold, fire) with an attack that does 29 fire damage at initiative 13
1959 units each with 7875 hit points (weak to cold; immune to slashing, bludgeoning) with an attack that does 38 radiation damage at initiative 20
8933 units each with 5687 hit points with an attack that does 6 slashing damage at initiative 15
938 units each with 8548 hit points with an attack that does 89 radiation damage at initiative 4
1945 units each with 3360 hit points (immune to cold; weak to radiation) with an attack that does 16 cold damage at initiative 1
2211 units each with 7794 hit points (weak to slashing) with an attack that does 30 fire damage at initiative 12
24 units each with 3693 hit points with an attack that does 1502 fire damage at initiative 5
2004 units each with 4141 hit points (immune to radiation) with an attack that does 18 slashing damage at initiative 19
3862 units each with 3735 hit points (immune to bludgeoning, fire) with an attack that does 9 fire damage at initiative 10
8831 units each with 3762 hit points (weak to radiation) with an attack that does 3 fire damage at initiative 7
Infection:
578 units each with 55836 hit points with an attack that does 154 radiation damage at initiative 9
476 units each with 55907 hit points (weak to fire) with an attack that does 208 cold damage at initiative 18
496 units each with 33203 hit points (weak to fire, radiation; immune to cold, bludgeoning) with an attack that does 116 slashing damage at initiative 14
683 units each with 12889 hit points (weak to fire) with an attack that does 35 bludgeoning damage at initiative 11
1093 units each with 29789 hit points (immune to cold, fire) with an attack that does 51 radiation damage at initiative 17
2448 units each with 40566 hit points (immune to bludgeoning, fire; weak to cold) with an attack that does 25 slashing damage at initiative 16
1229 units each with 6831 hit points (weak to fire, cold; immune to slashing) with an attack that does 8 bludgeoning damage at initiative 8
3680 units each with 34240 hit points (immune to bludgeoning; weak to fire, cold) with an attack that does 17 radiation damage at initiative 3
4523 units each with 9788 hit points (immune to bludgeoning, fire, slashing) with an attack that does 3 bludgeoning damage at initiative 6
587 units each with 49714 hit points (weak to bludgeoning) with an attack that does 161 fire damage at initiative 2

111
2018/d25/ex1/ex1.py Executable file
View file

@ -0,0 +1,111 @@
#!/usr/bin/env python
import collections
import itertools
import sys
from collections.abc import Iterable
from typing import Generic, Hashable, NamedTuple, TypeVar
class Point(NamedTuple):
x: int
y: int
z: int
t: int
class UnionFind:
_parent: list[int]
_rank: list[int]
def __init__(self, size: int):
# Each node is in its own set, making it its own parent...
self._parent = list(range(size))
# ... And its rank 0
self._rank = [0] * size
def find(self, elem: int) -> int:
while (parent := self._parent[elem]) != elem:
# Replace each parent link by a link to the grand-parent
elem, self._parent[elem] = parent, self._parent[parent]
return elem
def union(self, lhs: int, rhs: int) -> int:
lhs = self.find(lhs)
rhs = self.find(rhs)
# Bail out early if they already belong to the same set
if lhs == rhs:
return lhs
# Always keep `lhs` as the taller tree
if self._rank[lhs] < self._rank[rhs]:
lhs, rhs = rhs, lhs
# Merge the smaller tree into the taller one
self._parent[rhs] = lhs
# Update the rank when merging trees of approximately the same size
if self._rank[lhs] == self._rank[rhs]:
self._rank[lhs] += 1
return lhs
def sets(self) -> dict[int, set[int]]:
res: dict[int, set[int]] = collections.defaultdict(set)
for elem in range(len(self._parent)):
res[self.find(elem)].add(elem)
return dict(res)
# PEP 695 still not supported by MyPy...
T = TypeVar("T", bound=Hashable)
class DisjointSet(Generic[T]):
_values: list[T]
_to_index: dict[T, int]
_sets: UnionFind
def __init__(self, values: Iterable[T]) -> None:
self._values = list(values)
self._to_index = {v: i for i, v in enumerate(self._values)}
self._sets = UnionFind(len(self._values))
def find(self, elem: T) -> T:
return self._values[self._sets.find(self._to_index[elem])]
def union(self, lhs: T, rhs: T) -> T:
return self._values[self._sets.union(self._to_index[lhs], self._to_index[rhs])]
def sets(self) -> dict[T, set[T]]:
sets = self._sets.sets()
return {
self._values[r]: {self._values[i] for i in values}
for r, values in sets.items()
}
def solve(input: str) -> int:
def parse(input: list[str]) -> list[Point]:
return [Point(*map(int, line.split(","))) for line in input]
def dist(lhs: Point, rhs: Point) -> int:
return sum(abs(l - r) for l, r in zip(lhs, rhs))
def count_constellations(points: list[Point]) -> int:
sets = DisjointSet(points)
for a, b in itertools.combinations(points, 2):
if dist(a, b) > 3:
continue
sets.union(a, b)
return len(sets.sets())
points = parse(input.splitlines())
return count_constellations(points)
def main() -> None:
input = sys.stdin.read()
print(solve(input))
if __name__ == "__main__":
main()

1378
2018/d25/ex1/input Normal file

File diff suppressed because it is too large Load diff

9
2018/d25/ex2/ex2.py Executable file
View file

@ -0,0 +1,9 @@
#!/usr/bin/env python
def main() -> None:
print("There is no part two...")
if __name__ == "__main__":
main()

1378
2018/d25/ex2/input Normal file

File diff suppressed because it is too large Load diff