from tools.aoc import AOCDay from tools.int_seq import triangular from tools.tools import list_combinations_of_sum from typing import Any 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]]): 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[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] 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) 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 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)) return open_groups def get_arrangements(springs: str, groups: list[int]) -> int: if sum(groups) + len(groups) - 1 == len(springs): return 1 open_groups = get_open_groups(springs) print(f"{groups} => {springs} => {open_groups}") return 0 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 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 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)