aoc2023/day12.py

202 lines
6.7 KiB
Python

import re
import sys
from tools.aoc import AOCDay
from tools.itertools import combinations_of_sum, len_combinations_of_sum
from typing import Any, Iterator
from tqdm.auto import tqdm
def clean_springs(springs: str, groups: tuple[int, ...]) -> str:
hashes = []
start = None
c_size = 0
for i, c in enumerate(springs):
if c == "#":
if start is None:
start = i
c_size += 1
else:
if start is not None:
hashes.append((start, c_size))
start = None
c_size = 0
if start is not None:
hashes.append((start, c_size))
# print("cleaning", springs, "with hashes", hashes, "and groups", groups)
found = {}
for i, s in enumerate(groups):
for hash_start, hash_len in hashes:
if sum(groups[: i + 1]) + i + 1 < hash_start < len(springs) - 1 - (sum(groups[i:]) + len(groups) - i):
if hash_len == s:
# print(
# f"PLACING HASH {i=} {groups[:i+1]} -> {sum(groups[:i + 1]) + i} <= {hash_start} <= {len(springs)=} - ({groups[i:]} -> {sum(groups[i:]) + len(groups) - i})"
# )
found[i] = (hash_start, s)
if hash_start == 0:
if len(springs) > hash_len:
springs = "#" * hash_len + "." + springs[hash_start + hash_len + 1 :]
else:
springs = "#" * hash_len
elif hash_start + hash_len == len(springs) - 1:
springs = springs[: -(hash_len + 1)] + "." + "#" * hash_len
else:
springs = (
springs[: hash_start - 1]
+ "."
+ "#" * hash_len
+ "."
+ springs[hash_start + hash_len + 1 :]
)
# print("found index", i, "value", groups[i], "at", hash_start, "new springs", springs,)
break
# print("cleaned springs:", springs)
return springs
def get_open_groups(springs: str) -> list[str]:
open_groups = re.split(r"\.+", springs)
while "" in open_groups:
open_groups.remove("")
return open_groups
def get_placement_options(groups: tuple[int, ...], open_groups: list[str]) -> Iterator[tuple]:
print(
f"get_placement_options({len(groups)}, {len(open_groups)}) returns",
len_combinations_of_sum(len(groups), len(open_groups)),
)
t = tqdm(total=len_combinations_of_sum(len(groups), len(open_groups)), leave=False, file=sys.stdout)
for options in combinations_of_sum(total_sum=len(groups), length=len(open_groups)):
t.update(1)
idx = 0
possible = True
for i, x in enumerate(options):
group_str = open_groups[i]
if sum(groups[idx : idx + x]) + x - 1 > len(group_str) or ("#" in group_str and x == 0):
possible = False
break # not possible
idx += x
if possible:
yield options
t.close()
def brute_force_group(group_str: str, groups: tuple[int, ...]) -> int:
count = 0
p = re.compile(group_str.replace("?", "."))
g_len = len(groups)
t = tqdm(
total=len_combinations_of_sum(len(group_str) - sum(groups), len(groups)),
leave=False,
file=sys.stdout,
postfix="brute_force",
)
for x in combinations_of_sum(total_sum=len(group_str) - sum(groups), length=len(groups) + 1):
t.update()
test_str = ""
valid = True
for i, c in enumerate(x):
test_str += " " * c
if 0 < i < g_len and c == 0:
valid = False
break
if i < len(groups):
test_str += groups[i] * "#"
if valid and p.match(test_str):
count += 1
t.close()
return count
def get_arrangements(springs: str, groups: tuple[int, ...]) -> int:
if sum(groups) + len(groups) - 1 == len(springs):
return 1
open_groups = get_open_groups(springs)
# print(f"{groups} => {springs} =>", list((x, len(x)) for x in open_groups))
arrangements = 0
for placement_option in get_placement_options(groups, open_groups):
# print("PLACEMENT OPTION", placement_option)
sub_arrangements = 1
group_idx = 0
for i, x in enumerate(placement_option):
if x == 0:
continue
numbers_to_place = groups[group_idx : group_idx + x]
group_idx += x
group_str = open_groups[i]
cleaned_group_str = clean_springs(group_str, numbers_to_place)
# print(f"CLEAN ({i}, {x}, {group_idx})", group_str, "=>", cleaned_group_str, "from", numbers_to_place)
if cleaned_group_str != group_str:
f = get_arrangements(cleaned_group_str, numbers_to_place)
# print("RECURSE CALL", cleaned_group_str, numbers_to_place, "=>", f)
sub_arrangements *= f
else:
f = brute_force_group(group_str, numbers_to_place)
# print("MISSING CASE:", group_str, numbers_to_place, "=>", f)
sub_arrangements *= f
# print("SUB", placement_option, "=>", sub_arrangements)
arrangements += sub_arrangements
return arrangements
class Day(AOCDay):
inputs = [
[
(101, "input12_debug"),
(21, "input12_test"),
(6981, "input12"),
],
[
(525152, "input12_test"),
(None, "input12"),
],
]
def parse_input(self, part2: bool = False) -> list[tuple[str, tuple[int, ...]]]:
records = []
for line in self.getInput():
springs, groupd = line.split()
groups = tuple(map(int, groupd.split(",")))
if part2:
springs = "?".join([springs] * 5)
groups *= 5
records.append((springs, groups))
return records
def part1(self) -> Any:
ans = 0
for springs, groups in self.parse_input():
f = get_arrangements(springs, groups)
print("RESULT", springs, "=>", f)
ans += f
return ans
def part2(self) -> Any:
ans = 0
# explore moving windows; or some other way to make use of DP
for i, (springs, groups) in enumerate(self.parse_input(part2=True)):
f = get_arrangements(springs, groups)
print(i + 1, "/", len(self.input), "=>", f)
ans += f
return ans
if __name__ == "__main__":
day = Day(2023, 12)
day.run(verbose=True)