Compare commits
48 commits
ad739b7fec
...
4fc6136588
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fc6136588 | |||
| a6c0017553 | |||
| 220afe0b8a | |||
| 00ed576a42 | |||
| 759def15c9 | |||
| da0803985b | |||
| 5f9a7cb80a | |||
| 0d8ba9e62b | |||
| df08b37dd2 | |||
| 3353a22586 | |||
| c7538c902a | |||
| 25aeec5676 | |||
| 0624b4e44d | |||
| aab563b9ff | |||
| 49d7160617 | |||
| f18a8f45f6 | |||
| cd832a2bb0 | |||
| 955d5ec6c2 | |||
| 47cce9f0f3 | |||
| 763793ab0b | |||
| 4cb48ee71c | |||
| 9b6cb7bd45 | |||
| 6dbc98412e | |||
| add0b47894 | |||
| a0e1e0e223 | |||
| 93b4eea6f8 | |||
| 52a891aa57 | |||
| aa767b9781 | |||
| 11b32839f4 | |||
| 91cee79a22 | |||
| c1f47d34ad | |||
| f0395b2fae | |||
| f2ccb2cd5a | |||
| 5ea92f305e | |||
| dd31b1369d | |||
| 69b8894c54 | |||
| 27c2cc0463 | |||
| 43de4e9dba | |||
| 1aea4b9bcd | |||
| d0013a1e17 | |||
| fd604ff8a4 | |||
| a7a5f62afe | |||
| cea7f60e46 | |||
| 583c913bbe | |||
| 1010cacbe9 | |||
| 05e7c39142 | |||
| a7eac8ea74 | |||
| 9bbcf41657 |
48 changed files with 18909 additions and 0 deletions
27
2018/d14/ex1/ex1.py
Executable file
27
2018/d14/ex1/ex1.py
Executable 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
1
2018/d14/ex1/input
Normal file
|
|
@ -0,0 +1 @@
|
|||
556061
|
||||
27
2018/d14/ex2/ex2.py
Executable file
27
2018/d14/ex2/ex2.py
Executable 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
1
2018/d14/ex2/input
Normal file
|
|
@ -0,0 +1 @@
|
|||
556061
|
||||
192
2018/d15/ex1/ex1.py
Executable file
192
2018/d15/ex1/ex1.py
Executable 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
32
2018/d15/ex1/input
Normal 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
239
2018/d15/ex2/ex2.py
Executable 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
32
2018/d15/ex2/input
Normal 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
110
2018/d16/ex1/ex1.py
Executable 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
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
133
2018/d16/ex2/ex2.py
Executable 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
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
108
2018/d17/ex1/ex1.py
Executable 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
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
107
2018/d17/ex2/ex2.py
Executable 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
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
68
2018/d18/ex1/ex1.py
Executable 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
50
2018/d18/ex1/input
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
.#|#.##....#|....|.#.#.|||.#.|....||....|...|..#..
|
||||
..|#||.|#..|...|#|..#...|#...#..#..|.....||..#.|#.
|
||||
#|||#..||.....||.#................|..#.##|.#...#.|
|
||||
|#..#.|...##...#..#|#|#..|#.#...|....#..#...##....
|
||||
.###.........|.||#...#|.|#.||||#..|...||....#..#..
|
||||
###.|..|#|...|..||..##.....|..#.|.#.............|.
|
||||
..|.|.||.#....|...|....#|.........##||..#||..|.##.
|
||||
#||#|...#|..|.|.||#...#|...|#.......|...#.....|...
|
||||
....||.....|.|.....#...|.......|...|..|...|......|
|
||||
#......#..#|#|..|....#.|.|.#...#.#.|..#.|.....#.#.
|
||||
.|#...|...........#|.#....#.#...#.|..|...|....|.|.
|
||||
..||.#.|...||#|....#.#..||#..#...#|..#..|..#|.....
|
||||
|..|.|..#...|.....#.|..|#.||..#|.|.||#|#..|#...##|
|
||||
..|..|#......||##..|........#.|...#.|.|#.#...||..#
|
||||
#.|...#.||#..|.|..|..|.#....|.||....|.|....#....#.
|
||||
#||.|.#..#..|...#....##|#..#...#.#...|.#...#.....#
|
||||
#.|.##.|##..#.##|##........#.|...#...|..#|.#|#|...
|
||||
.|#|....|.#...#..|||.#.||..#||.||.|..#.|....|..##.
|
||||
|.#.||#|.##.|.||.....#...#.#..###|.#......||#|....
|
||||
.|.#..|#||......|##..##.#|..#|.|#.|.|#......|#.|#.
|
||||
#..|........|||..|###..|#..|||#.|.|.....#|..|...|#
|
||||
..####||#......|#||..###.|...|....#..|.#|.||....||
|
||||
|##.......|||..........|..||.#.|#.......##...|...|
|
||||
|.......#......####|#|....#....|......#.|#.###...#
|
||||
#|.#.|||...|..|.....#....|...|......|#|#|......||.
|
||||
...#.|......#..||||.#|.....|.|.|||.|.|.|#|.#...#.#
|
||||
#.#.##.|.#|.|...|...|...#|...#.|#..|..##.|....#..|
|
||||
|...#.......#....#....#.#....#.#|.|#||.|.|.|#...#.
|
||||
#..|.||..|.#..|.#.....#|##.|.|....|....||.......|.
|
||||
..||.#..|#|.###....#.#|..#|.#..........#...|...#|.
|
||||
|#||.|.#..|....|....#.#||#.|......#..|#.#.|||||#|.
|
||||
.|#.|#.##.....#.|.#.....|....|.#..#.#..|#.#.....|.
|
||||
#.||.#.......|..|......|#||.|..#....#...|...|...|.
|
||||
|.....#.|.....#||.....##...#.#...||.|..#........|.
|
||||
||#..|.##.#...........#..|..|.|..#....|...#..||.#.
|
||||
..||.##.##.|.||......#...|.#.#.#..#.#...##.#.|.#..
|
||||
.|.#......#|#||.|.#|......||.#.|.|..|....#...||...
|
||||
....|.##.....|#|####.#..#..#|.....|.#.#|......|...
|
||||
...#..|......#....|#.#...|...|.#.#.......#.#.##..#
|
||||
.|||#.||||...|..|#||.|.#|#||..|..#..|..|..#||.....
|
||||
.....|..#..|#|.||.#||.||......|||..|..#|.|##......
|
||||
.#...#|..#..|||..||.|..|.#.#.......||..|...|.|....
|
||||
.##.||..|..||.|.......#.|||.|.|..|.#.#..|.||.|#|||
|
||||
.|..##|..#.#|#|....|.#.#.#|#.#|.##|........###...#
|
||||
..#..|#|...#.........#.#.####..#.#..#..#||#|...#|#
|
||||
#.|...|.......|.#.#..#.|#..#|#|..#..|.....|..|...|
|
||||
.##.|..#.....|...#..|#..|.|.#..##.#.|..#.|..|.##..
|
||||
....|..|.|..||....|...|.....#..|.|.....|.#|......#
|
||||
...##.|#..#..|.#|.##....|.#...||#|.....#...##.#|..
|
||||
.|....##.....||...#.#.....#|#...#...#|.|..#.#.#.##
|
||||
90
2018/d18/ex2/ex2.py
Executable file
90
2018/d18/ex2/ex2.py
Executable 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
50
2018/d18/ex2/input
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
.#|#.##....#|....|.#.#.|||.#.|....||....|...|..#..
|
||||
..|#||.|#..|...|#|..#...|#...#..#..|.....||..#.|#.
|
||||
#|||#..||.....||.#................|..#.##|.#...#.|
|
||||
|#..#.|...##...#..#|#|#..|#.#...|....#..#...##....
|
||||
.###.........|.||#...#|.|#.||||#..|...||....#..#..
|
||||
###.|..|#|...|..||..##.....|..#.|.#.............|.
|
||||
..|.|.||.#....|...|....#|.........##||..#||..|.##.
|
||||
#||#|...#|..|.|.||#...#|...|#.......|...#.....|...
|
||||
....||.....|.|.....#...|.......|...|..|...|......|
|
||||
#......#..#|#|..|....#.|.|.#...#.#.|..#.|.....#.#.
|
||||
.|#...|...........#|.#....#.#...#.|..|...|....|.|.
|
||||
..||.#.|...||#|....#.#..||#..#...#|..#..|..#|.....
|
||||
|..|.|..#...|.....#.|..|#.||..#|.|.||#|#..|#...##|
|
||||
..|..|#......||##..|........#.|...#.|.|#.#...||..#
|
||||
#.|...#.||#..|.|..|..|.#....|.||....|.|....#....#.
|
||||
#||.|.#..#..|...#....##|#..#...#.#...|.#...#.....#
|
||||
#.|.##.|##..#.##|##........#.|...#...|..#|.#|#|...
|
||||
.|#|....|.#...#..|||.#.||..#||.||.|..#.|....|..##.
|
||||
|.#.||#|.##.|.||.....#...#.#..###|.#......||#|....
|
||||
.|.#..|#||......|##..##.#|..#|.|#.|.|#......|#.|#.
|
||||
#..|........|||..|###..|#..|||#.|.|.....#|..|...|#
|
||||
..####||#......|#||..###.|...|....#..|.#|.||....||
|
||||
|##.......|||..........|..||.#.|#.......##...|...|
|
||||
|.......#......####|#|....#....|......#.|#.###...#
|
||||
#|.#.|||...|..|.....#....|...|......|#|#|......||.
|
||||
...#.|......#..||||.#|.....|.|.|||.|.|.|#|.#...#.#
|
||||
#.#.##.|.#|.|...|...|...#|...#.|#..|..##.|....#..|
|
||||
|...#.......#....#....#.#....#.#|.|#||.|.|.|#...#.
|
||||
#..|.||..|.#..|.#.....#|##.|.|....|....||.......|.
|
||||
..||.#..|#|.###....#.#|..#|.#..........#...|...#|.
|
||||
|#||.|.#..|....|....#.#||#.|......#..|#.#.|||||#|.
|
||||
.|#.|#.##.....#.|.#.....|....|.#..#.#..|#.#.....|.
|
||||
#.||.#.......|..|......|#||.|..#....#...|...|...|.
|
||||
|.....#.|.....#||.....##...#.#...||.|..#........|.
|
||||
||#..|.##.#...........#..|..|.|..#....|...#..||.#.
|
||||
..||.##.##.|.||......#...|.#.#.#..#.#...##.#.|.#..
|
||||
.|.#......#|#||.|.#|......||.#.|.|..|....#...||...
|
||||
....|.##.....|#|####.#..#..#|.....|.#.#|......|...
|
||||
...#..|......#....|#.#...|...|.#.#.......#.#.##..#
|
||||
.|||#.||||...|..|#||.|.#|#||..|..#..|..|..#||.....
|
||||
.....|..#..|#|.||.#||.||......|||..|..#|.|##......
|
||||
.#...#|..#..|||..||.|..|.#.#.......||..|...|.|....
|
||||
.##.||..|..||.|.......#.|||.|.|..|.#.#..|.||.|#|||
|
||||
.|..##|..#.#|#|....|.#.#.#|#.#|.##|........###...#
|
||||
..#..|#|...#.........#.#.####..#.#..#..#||#|...#|#
|
||||
#.|...|.......|.#.#..#.|#..#|#|..#..|.....|..|...|
|
||||
.##.|..#.....|...#..|#..|.|.#..##.#.|..#.|..|.##..
|
||||
....|..|.|..||....|...|.....#..|.|.....|.#|......#
|
||||
...##.|#..#..|.#|.##....|.#...||#|.....#...##.#|..
|
||||
.|....##.....||...#.#.....#|#...#...#|.|..#.#.#.##
|
||||
98
2018/d19/ex1/ex1.py
Executable file
98
2018/d19/ex1/ex1.py
Executable 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
37
2018/d19/ex1/input
Normal 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
104
2018/d19/ex2/ex2.py
Executable 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
37
2018/d19/ex2/input
Normal 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
89
2018/d20/ex1/ex1.py
Executable 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
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
89
2018/d20/ex2/ex2.py
Executable 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
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
107
2018/d21/ex1/ex1.py
Executable 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
32
2018/d21/ex1/input
Normal 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
138
2018/d21/ex2/ex2.py
Executable 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
32
2018/d21/ex2/input
Normal 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
67
2018/d22/ex1/ex1.py
Executable 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
2
2018/d22/ex1/input
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
depth: 7740
|
||||
target: 12,763
|
||||
128
2018/d22/ex2/ex2.py
Executable file
128
2018/d22/ex2/ex2.py
Executable 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
2
2018/d22/ex2/input
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
depth: 7740
|
||||
target: 12,763
|
||||
42
2018/d23/ex1/ex1.py
Executable file
42
2018/d23/ex1/ex1.py
Executable 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
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
71
2018/d23/ex2/ex2.py
Executable 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
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
188
2018/d24/ex1/ex1.py
Executable 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
23
2018/d24/ex1/input
Normal 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
219
2018/d24/ex2/ex2.py
Executable 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
23
2018/d24/ex2/input
Normal 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
111
2018/d25/ex1/ex1.py
Executable 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
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
9
2018/d25/ex2/ex2.py
Executable 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
1378
2018/d25/ex2/input
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue