106 lines
3.2 KiB
Python
106 lines
3.2 KiB
Python
|
#!/usr/bin/env python
|
||
|
|
||
|
import itertools
|
||
|
import sys
|
||
|
from typing import NamedTuple, Optional
|
||
|
|
||
|
|
||
|
class AlmanacMapLine(NamedTuple):
|
||
|
dest_start: int
|
||
|
source_start: int
|
||
|
map_len: int
|
||
|
|
||
|
|
||
|
class AlmanacMap(NamedTuple):
|
||
|
lines: list[AlmanacMapLine]
|
||
|
|
||
|
def map(self, input: int) -> int:
|
||
|
for l in self.lines:
|
||
|
if input < l.source_start:
|
||
|
continue
|
||
|
if (l.source_start + l.map_len) <= input:
|
||
|
continue
|
||
|
return l.dest_start + (input - l.source_start)
|
||
|
return input
|
||
|
|
||
|
|
||
|
Almanac = dict[str, tuple[str, AlmanacMap]]
|
||
|
SeedRanges = list[tuple[int, int]]
|
||
|
|
||
|
|
||
|
def solve(input: str) -> int:
|
||
|
def parse_almanac_map_line(line: str) -> AlmanacMapLine:
|
||
|
dest_start, source_start, map_len = map(int, line.split(" "))
|
||
|
return AlmanacMapLine(dest_start, source_start, map_len)
|
||
|
|
||
|
def parse_almanac_map(lines: list[str]) -> tuple[str, str, AlmanacMap]:
|
||
|
source, dest = lines[0].split(" ")[0].split("-")[::2]
|
||
|
|
||
|
map_lines = [parse_almanac_map_line(line) for line in lines[1:]]
|
||
|
|
||
|
return source, dest, AlmanacMap(map_lines)
|
||
|
|
||
|
def parse_almanac(paragraphs: list[str]) -> Almanac:
|
||
|
res: Almanac = {}
|
||
|
|
||
|
for raw_map in paragraphs:
|
||
|
source, dest, map = parse_almanac_map(raw_map.splitlines())
|
||
|
res[source] = dest, map
|
||
|
|
||
|
return res
|
||
|
|
||
|
def parse(input: str) -> tuple[SeedRanges, Almanac]:
|
||
|
raw_seeds, *raw_almanac = input.split("\n\n")
|
||
|
|
||
|
parsed_seed = [int(n) for n in raw_seeds.removeprefix("seeds: ").split(" ")]
|
||
|
seed_ranges = list(zip(parsed_seed[::2], parsed_seed[1::2]))
|
||
|
|
||
|
return seed_ranges, parse_almanac(raw_almanac)
|
||
|
|
||
|
# Each input is piped to exactly one output type, so we can reverse it easily
|
||
|
def reverse_almanac(almanac: Almanac) -> Almanac:
|
||
|
def reverse_map_line(line: AlmanacMapLine) -> AlmanacMapLine:
|
||
|
return AlmanacMapLine(
|
||
|
dest_start=line.source_start,
|
||
|
source_start=line.dest_start,
|
||
|
map_len=line.map_len,
|
||
|
)
|
||
|
|
||
|
def reverse_map(map: AlmanacMap) -> AlmanacMap:
|
||
|
return AlmanacMap([reverse_map_line(line) for line in map.lines])
|
||
|
|
||
|
reversed: Almanac = {}
|
||
|
|
||
|
for source, (dest, map) in almanac.items():
|
||
|
reversed[dest] = source, reverse_map(map)
|
||
|
|
||
|
return reversed
|
||
|
|
||
|
def lowest_location(seeds: SeedRanges, inverse_almanac: Almanac) -> int:
|
||
|
def recurse(input: int, input_type: str) -> Optional[int]:
|
||
|
if input_type == "seed":
|
||
|
for start, length in seeds:
|
||
|
if start <= input and input < (start + length):
|
||
|
return input
|
||
|
return None
|
||
|
|
||
|
new_input_type, map = inverse_almanac[input_type]
|
||
|
return recurse(map.map(input), new_input_type)
|
||
|
|
||
|
for location in itertools.count():
|
||
|
if recurse(location, "location") is not None:
|
||
|
return location
|
||
|
assert False # Sanity check
|
||
|
|
||
|
seeds, almanac = parse(input)
|
||
|
return lowest_location(seeds, reverse_almanac(almanac))
|
||
|
|
||
|
|
||
|
def main() -> None:
|
||
|
input = sys.stdin.read()
|
||
|
print(solve(input))
|
||
|
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
main()
|