advent-of-code/2023/day12.py

108 lines
2.8 KiB
Python
Raw Normal View History

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