Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Day 12 #23

Merged
merged 15 commits into from
Dec 15, 2023
Empty file added aoc_2023/day_12/__init__.py
Empty file.
34 changes: 34 additions & 0 deletions aoc_2023/day_12/a.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from dataclasses import dataclass, field
from functools import cached_property
from aoc_2023.day_12.common import Record, solve_it
from aoc_2023.day_12.parser import Parser


@dataclass(frozen=True)
class Day12PartASolver:
data: list[Record] = field(hash=False)

@cached_property
def solution(self) -> int:
return sum(solve_it(r.chars, tuple(r.congruencies)) for r in self.records)

@cached_property
def records(self) -> list[Record]:
return self.data


def solve(input: str) -> int:
data = Parser.parse(input)
solver = Day12PartASolver(data)

return solver.solution


def get_solution() -> int:
with open("aoc_2023/day_12/input.txt", "r") as f:
input = f.read()
return solve(input)


if __name__ == "__main__":
print(get_solution())
40 changes: 40 additions & 0 deletions aoc_2023/day_12/b.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from dataclasses import dataclass
from functools import cached_property
from aoc_2023.day_12.common import Record, solve_it
from aoc_2023.day_12.parser import Parser


@dataclass(frozen=True)
class Day12PartBSolver:
original_records: list[Record]

@cached_property
def solution(self) -> int:
return sum(solve_it(r.chars, tuple(r.congruencies)) for r in self.records)

@cached_property
def records(self) -> list[Record]:
return [
Record(
chars="?".join([original.chars] * 5),
congruencies=original.congruencies * 5,
)
for original in self.original_records
]


def solve(input: str) -> int:
data = Parser.parse(input)
solver = Day12PartBSolver(data)

return solver.solution


def get_solution() -> int:
with open("aoc_2023/day_12/input.txt", "r") as f:
input = f.read()
return solve(input)


if __name__ == "__main__":
print(get_solution())
120 changes: 120 additions & 0 deletions aoc_2023/day_12/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
from dataclasses import dataclass
import re
from functools import cache


@dataclass(frozen=True)
class Record:
chars: str
congruencies: list[int]


PERIOD_PATTERN = re.compile(r"\.+")


@cache
def solve_it(
chars: str,
congruencies: tuple[int, ...],
) -> int:
if len(congruencies) == 0:
output = 1 if all(ch != "#" for ch in chars) else 0
return output
elif len(congruencies) == 1:
return congruency_matches(chars, congruencies[0])
else:
if len(chars) == 0:
return 0
mid = len(chars) // 2
mid_char = chars[mid]
if mid_char == ".":
return split_as_period(chars, congruencies)
elif mid_char == "#":
return split_as_hash(chars, congruencies)
elif mid_char == "?":
return sum(
[
split_as_period(chars, congruencies),
split_as_hash(chars, congruencies),
]
)
else:
assert False


@cache
def congruency_matches(chars: str, congruency: int) -> int:
full_string = "".join(chars)
if congruency == 0:
return 1 if "#" not in chars else 0
groups = [g for g in PERIOD_PATTERN.split(full_string) if g]
with_hashes = [g for g in groups if "#" in g]
match len(with_hashes):
case 0:
output = 0
for group in groups:
if len(group) >= congruency:
output += len(group) + 1 - congruency
return output
case 1:
string = with_hashes[0]
hash_start = string.index("#")
hash_end = len(string) - string[::-1].index("#")
length = hash_end - hash_start
if length > congruency:
return 0
else:
output = 0
for start in range(1 + len(string) - congruency):
end = start + congruency
if start > hash_start:
...
elif end < hash_end:
...
else:
output += 1
return output

case _:
return 0


def split_as_period(chars: str, congruencies: tuple[int, ...]) -> int:
mid = len(chars) // 2
assert chars[mid] != "#"
left_chars = chars[:mid]
right_chars = chars[mid + 1 :]
output = 0
for i in range(len(congruencies) + 1):
left = solve_it(left_chars, congruencies[:i])
right = solve_it(right_chars, congruencies[i:])
output += left * right
return output


def split_as_hash(chars: str, congruencies: tuple[int, ...]) -> int:
output = 0
mid = len(chars) // 2
assert chars[mid] != "."
congruency_count = len(congruencies)
output = 0
for c in range(congruency_count):
congruency = congruencies[c]
for to_the_left in range(congruency):
left = mid - to_the_left
right = left + congruency
if left < 0:
continue
if right > len(chars):
continue
if "." in chars[left:right]:
continue
if left > 0 and chars[left - 1] == "#":
continue
if right < len(chars) and chars[right] == "#":
continue

left_count = solve_it(chars[: max(0, left - 1)], congruencies[:c])
right_count = solve_it(chars[right + 1 :], congruencies[c + 1 :])
output += left_count * right_count
return output
Binary file added aoc_2023/day_12/from_prompt.py
Binary file not shown.
Binary file added aoc_2023/day_12/input.txt
Binary file not shown.
15 changes: 15 additions & 0 deletions aoc_2023/day_12/parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from aoc_2023.day_12.common import Record


class Parser:
@staticmethod
def parse(input: str) -> list[Record]:
lines = input.strip().splitlines()
return [Parser.parse_line(line) for line in lines]

@staticmethod
def parse_line(line: str) -> Record:
chars, congruencies = line.split()
return Record(
chars=chars, congruencies=[int(x) for x in congruencies.split(",")]
)
Empty file added tests/test_day_12/__init__.py
Empty file.
10 changes: 10 additions & 0 deletions tests/test_day_12/test_a.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from aoc_2023.day_12.a import get_solution, solve
from aoc_2023.day_12.from_prompt import SAMPLE_DATA, SAMPLE_SOLUTION_A


def test_solve():
assert solve(SAMPLE_DATA) == SAMPLE_SOLUTION_A


def test_my_solution():
assert get_solution() == 7047
10 changes: 10 additions & 0 deletions tests/test_day_12/test_b.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from aoc_2023.day_12.b import get_solution, solve
from aoc_2023.day_12.from_prompt import SAMPLE_DATA, SAMPLE_SOLUTION_B


def test_solve():
assert solve(SAMPLE_DATA) == SAMPLE_SOLUTION_B


def test_my_solution():
assert get_solution() == 17391848518844