From 55d885f41648f32be390ca0d9796023f688b7d2d Mon Sep 17 00:00:00 2001 From: Stefan Harmuth Date: Mon, 1 Jan 2024 21:47:03 +0100 Subject: [PATCH] Day 18 - remove additional perimeter calculation --- day12.py | 279 +++++++++++++++++++++++++--------------------------- day12_bf.py | 212 +++++++++++++++++++++++++++++++++++++++ day18.py | 5 +- 3 files changed, 346 insertions(+), 150 deletions(-) create mode 100644 day12_bf.py diff --git a/day12.py b/day12.py index 5c663e2..b300094 100644 --- a/day12.py +++ b/day12.py @@ -1,78 +1,13 @@ +import re +import sys + from tools.aoc import AOCDay -from tools.int_seq import triangular -from tools.tools import list_combinations_of_sum -from typing import Any +from tools.itertools import combinations_of_sum, len_combinations_of_sum +from typing import Any, Iterator +from tqdm.auto import tqdm -def _get_placement_options(open_groups: list[tuple[int, str]], groups: list[int], not_found: list[tuple[int, int, int]]) -> list: - to_determine = [] - min_group_len = min(groups[x[2]] for x in not_found) - for x, group_str in open_groups.copy(): - if len(group_str) < min_group_len: - open_groups.remove((x, group_str)) - - assert not not_found or open_groups, f"No open_groups left to fill with {not_found}" - # open_groups = list(sorted(open_groups)) - # open_groups should always be sorted by default - for possible_combination in list_combinations_of_sum(len(not_found), len(open_groups)): - # try to put possible_combination[x] in open_groups[x] - nf_idx = 0 - for i, to_place in enumerate(possible_combination): - # try to place to_place not_founds in open_groups[i] - pass - - for start, group_str in open_groups: - print("look in", group_str, "for", [groups[x[2]] for x in not_found]) - assigned = [] - for min_x, max_x, idx_need in list(not_found): - print("check", min_x, "<", start, "<", max_x, idx_need, groups[idx_need]) - if groups[idx_need] > len(group_str): - continue - elif (not assigned or sum(assigned) + len(assigned) + groups[idx_need] <= len(group_str)) and ( - min_x <= start <= max_x - ): - assigned.append(groups[idx_need]) - not_found = not_found[1:] - else: - break - print("append", assigned) - if assigned: - to_determine.append((group_str, assigned)) - - return to_determine - - -def _get_arrangements(springs: str, groups: list[int]): - springs, not_found = clean_springs(springs, groups) - print("not_found", not_found) - open_groups = get_open_groups(springs) - to_determine = get_placement_options(open_groups, groups, not_found) - arrangements = 1 - for group_str, values in to_determine: - if len(values) == 1 and len(group_str) == values[0]: - continue - elif sum(values) + len(values) - 1 == len(group_str): - continue - elif len(values) == 1: - arrangements *= len(group_str) - values[0] + 1 - else: - arrangements *= triangular(len(group_str) - (sum(values) + len(values) - 1) + 1) - # print( - # group_str, - # "with", - # values, - # "has multiple options:", - # triangular(len(group_str) - (sum(values) + len(values) - 1) + 1), - # ) - - if arrangements < 0: - print(springs, groups, to_determine) - exit(1) - - return arrangements - - -def _clean_springs(springs: str, groups: list[int]) -> (str, list[tuple[int, int, int]]): +def clean_springs(springs: str, groups: tuple[int, ...]) -> str: hashes = [] start = None c_size = 0 @@ -90,87 +25,136 @@ def _clean_springs(springs: str, groups: list[int]) -> (str, list[tuple[int, int if start is not None: hashes.append((start, c_size)) - print("cleaning", springs, "with hashes", hashes, "and groups", groups) + # print("cleaning", springs, "with hashes", hashes, "and groups", groups) found = {} for i, s in enumerate(groups): - for h in hashes: - if h[0] >= sum(groups[:i]) + i and h[0] <= len(springs) - (sum(groups[i:]) + len(groups) - i) + 1: - # print(h[0], ">", sum(groups[:i]) + i) - # print(h[0], "<", len(springs) - (sum(groups[i:]) + len(groups) - i) + 1) - if h[1] == s: - found[i] = (h[0], s) - if h[0] == 0: - springs = "#" * h[1] + "." + springs[h[0] + h[1] + 1 :] - elif h[0] + h[1] == len(springs): - springs = springs[: -(h[1] + 1)] + "." + "#" * h[1] + 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[: h[0] - 1] + "." + "#" * h[1] + "." + springs[h[0] + h[1] + 1 :] - print( - "found index", - i, - "value", - groups[i], - "at", - h[0], - "new springs", - springs, - ) + 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, "found", found) - not_found = [] - # not_found entry = (min_x, max_x, index) - nf = [] - last_min = 0 - for i in range(len(groups)): - if i in found: - if nf: - for x in nf: - not_found.append((last_min, found[i][0], x)) - nf = [] - last_min = sum(found[i]) - else: - nf.append(i) - if nf: - for x in nf: - not_found.append((last_min, len(springs), x)) - - return springs, not_found + # print("cleaned springs:", springs) + return springs -def get_open_groups(springs: str) -> list[tuple[int, str]]: - open_groups = [] - start = None - group_str = "" - for i, c in enumerate(springs): - if c == ".": - if start is not None: - open_groups.append((start, group_str)) - start = None - group_str = "" - else: - if start is None: - start = i - group_str += c - - if start is not None: - open_groups.append((start, group_str)) - +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_arrangements(springs: str, groups: list[int]) -> int: +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} => {open_groups}") + # 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 - return 0 class Day(AOCDay): inputs = [ [ + (101, "input12_debug"), (21, "input12_test"), (6981, "input12"), ], @@ -180,28 +164,31 @@ class Day(AOCDay): ], ] + 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 line in self.getInput(): - springs, groups = line.split() - groups = list(map(int, groups.split(","))) + for springs, groups in self.parse_input(): f = get_arrangements(springs, groups) print("RESULT", springs, "=>", f) ans += f - print("Final result:", ans, "=>", self._current_test_solution) - exit(0) return ans def part2(self) -> Any: - if not self.is_test(): - return "" ans = 0 - for i, line in enumerate(self.getInput()): - springs, groups = line.split() - groups = list(map(int, groups.split(","))) - springs = "?".join([springs] * 5) - groups = groups * 5 + # 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 diff --git a/day12_bf.py b/day12_bf.py new file mode 100644 index 0000000..1c6014f --- /dev/null +++ b/day12_bf.py @@ -0,0 +1,212 @@ +import re +from tools.aoc import AOCDay +from tools.int_seq import triangular +from typing import Any, Iterable + + +def is_Valid(springs: str, groups: list[int]) -> bool: + foo = [] + h_count = 0 + for c in springs: + if c == "#": + h_count += 1 + elif h_count > 0: + foo.append(h_count) + h_count = 0 + + if h_count > 0: + foo.append(h_count) + + return foo == groups + + +def sums(length: int, total_sum: int) -> Iterable[tuple]: + if length == 1: + yield (total_sum,) + else: + for v in range(total_sum + 1): + for p in sums(length - 1, total_sum - v): + yield (v,) + p + + +def get_options(groups: list[int], fill: list[int]) -> Iterable[str]: + missing_len = sum(fill) + # print("fill_len", len(fill), "miss_len", missing_len) + for opt in sums(len(fill), missing_len): + ret = "" + for i, c in enumerate(opt): + if 0 < i < len(fill) - 1 and c == 0: + break + ret += " " * c + if i < len(groups): + ret += "#" * groups[i] + else: + yield ret + + +def get_arrangements(springs: str, groups: list[int]) -> int: + arrangements = 0 + springs = springs.replace(".", " ") + + after_fill = len(springs) - sum(groups) - len(groups) + 1 + if after_fill == 0: + return 1 + + fill = [0] + [1] * (len(groups) - 1) + [after_fill] + p = re.compile(springs.replace("?", ".")) + for s in get_options(groups, fill): + if p.match(s): + arrangements += 1 + + return arrangements + + +def _get_arrangements(springs: str, groups: list[int]): + springs, not_found = clean_springs(springs, groups) + # print("not_found", not_found) + open_groups = get_open_groups(springs) + to_determine = [] + for start, group_str in open_groups: + # print("look in", group_str, "for", [groups[x] for x in not_found]) + assigned = [] + for need in list(not_found): + # print("check", need, groups[need]) + if groups[need] > len(group_str): + continue + elif not assigned or sum(assigned) + len(assigned) + groups[need] <= len(group_str): + assigned.append(groups[need]) + not_found = not_found[1:] + else: + break + # print("append", assigned) + if assigned: + to_determine.append((group_str, assigned)) + + arrangements = 1 + for group_str, values in to_determine: + if len(values) == 1 and len(group_str) == values[0]: + continue + elif sum(values) + len(values) - 1 == len(group_str): + continue + elif len(values) == 1: + arrangements *= len(group_str) - values[0] + 1 + else: + arrangements *= triangular(len(group_str) - (sum(values) + len(values) - 1) + 1) + # print( + # group_str, + # "with", + # values, + # "has multiple options:", + # triangular(len(group_str) - (sum(values) + len(values) - 1) + 1), + # ) + + if arrangements < 0: + # print(springs, groups, to_determine) + exit(1) + + return arrangements + + +def clean_springs(springs: str, groups: list[int]) -> (str, list[int]): + 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 h in hashes: + if h[0] >= sum(groups[:i]) + i and h[0] <= len(springs) - (sum(groups[i:]) + len(groups) - i) + 1: + # print(h[0], ">", sum(groups[:i]) + i) + # print(h[0], "<", len(springs) - (sum(groups[i:]) + len(groups) - i) + 1) + if h[1] == s: + found.append(i) + if i == 0: + springs = "#" * h[1] + "." + springs[h[0] + h[1] + 1 :] + elif i == len(springs) - 1: + springs = springs[: -(h[0] + h[1] + 1)] + "." + "#" * h[1] + else: + springs = springs[: h[0] - 1] + "." + "#" * h[1] + "." + springs[h[0] + h[1] + 1 :] + # print("found index", i, "value", groups[i], "at", h[0], "new springs", springs) + break + + # print("cleaned springs", springs, "found", found) + + return springs, [x for x in range(len(groups)) if x not in found] + + +def get_open_groups(springs: str) -> list[tuple[int, str]]: + open_groups = [] + start = None + group_str = "" + for i, c in enumerate(springs): + if c == ".": + if "?" in group_str: + open_groups.append((start, group_str)) + start = None + group_str = "" + else: + if start is None: + start = i + group_str += c + + if "?" in group_str: + open_groups.append((start, group_str)) + + return open_groups + + +class Day(AOCDay): + inputs = [ + [ + (21, "input12_test"), + (6981, "input12"), + ], + [ + (525152, "input12_test"), + (None, "input12"), + ], + ] + + def part1(self) -> Any: + ans = 0 + for line in self.getInput(): + springs, groups = line.split() + groups = list(map(int, groups.split(","))) + f = get_arrangements(springs, groups) + print("RESULT", springs, "=>", f) + ans += f + + return ans + + def part2(self) -> Any: + return "" + ans = 0 + for i, line in enumerate(self.getInput()): + springs, groups = line.split() + groups = list(map(int, groups.split(","))) + springs = "?".join([springs] * 5) + groups = groups * 5 + 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) diff --git a/day18.py b/day18.py index 0d236bf..9b26dc6 100644 --- a/day18.py +++ b/day18.py @@ -33,16 +33,13 @@ class Day(AOCDay): def get_lagoon_area(self, part2: bool = False) -> int: start = Coordinate(0, 0) points = [] - perimeter = 0 for d, l in self.parse_input(part2): - perimeter += l end = start + DIRECTIONS[d] * l points.append(end) start = end p = Polygon(points) - a = abs(p.get_area()) - return int(p.get_area()) + perimeter // 2 + 1 + return int(p.get_area()) + int(p.get_circumference()) // 2 + 1 def part1(self) -> Any: return self.get_lagoon_area()