diff --git a/2022/day19.py b/2022/day19.py new file mode 100644 index 0000000..4838fdd --- /dev/null +++ b/2022/day19.py @@ -0,0 +1,416 @@ +# -*- encoding: utf-8 -*- + +import heapq +import math +import sys +from collections import defaultdict +from typing import Literal, TypedDict + +Reagent = Literal["ore", "clay", "obsidian", "geode"] +REAGENTS: tuple[Reagent] = ( + "ore", + "clay", + "obsidian", + "geode", +) + +IntOfReagent = dict[Reagent, int] + +lines = sys.stdin.read().splitlines() + +blueprints: list[dict[Reagent, IntOfReagent]] = [ + { + "ore": {"ore": 4}, + "clay": {"ore": 2}, + "obsidian": {"ore": 3, "clay": 14}, + "geode": {"ore": 2, "obsidian": 7}, + }, + { + "ore": {"ore": 2}, + "clay": {"ore": 3}, + "obsidian": {"ore": 3, "clay": 8}, + "geode": {"ore": 3, "obsidian": 12}, + }, +] + + +class State: + robots: IntOfReagent + reagents: IntOfReagent + + def __init__( + self, + robots: IntOfReagent | None = None, + reagents: IntOfReagent | None = None, + ): + if robots is None: + assert reagents is None + self.reagents = {reagent: 0 for reagent in REAGENTS} + self.robots = {reagent: 0 for reagent in REAGENTS} + self.robots["ore"] = 1 + else: + assert robots is not None and reagents is not None + self.robots = robots + self.reagents = reagents + + def __eq__(self, other) -> bool: + return ( + isinstance(other, State) + and self.robots == other.robots + and self.reagents == other.reagents + ) + + def __lt__(self, other) -> bool: + return isinstance(other, State) and tuple( + (self.robots[r], self.reagents[r]) for r in REAGENTS + ) < tuple((other.robots[r], other.reagents[r]) for r in REAGENTS) + + def __hash__(self) -> int: + return hash(tuple((self.robots[r], self.reagents[r]) for r in REAGENTS)) + + def __str__(self) -> str: + return "State({}, {})".format( + "/".join(str(self.robots[k]) for k in REAGENTS), + "/".join(str(self.reagents[k]) for k in REAGENTS), + ) + + def __repr__(self) -> str: + return str(self) + + +def dominates(lhs: State, rhs: State): + return all( + lhs.robots[r] >= rhs.robots[r] and lhs.reagents[r] >= rhs.reagents[r] + for r in REAGENTS + ) + + +MAX_TIME = 24 +blueprint = blueprints[0] + +parents: dict[State, tuple[State | None, int]] = {State(): (None, 0)} +queue = [(0, State())] +visited: set[State] = set() +at_time: dict[int, list[State]] = defaultdict(lambda: []) + +while queue: + time, state = heapq.heappop(queue) + if state in visited: + continue + + visited.add(state) + + # if any(dominates(state_3, state) for state_3 in at_time[time]): + # continue + + at_time[time].append(state) + + if time > MAX_TIME: + continue + + if len(queue) % 500 == 0: + print(len(queue), len(visited), time) + + can_build_any: bool = False + for reagent in REAGENTS: + needed = blueprint[reagent] + + if any(state.robots[r] == 0 for r in needed): + continue + + time_to_complete = max( + max( + math.ceil((needed[r] - state.reagents[r]) / state.robots[r]) + for r in needed + ), + 0, + ) + + if time + time_to_complete + 1 > MAX_TIME: + continue + + wait = time_to_complete + 1 + + reagents = { + r: state.reagents[r] + wait * state.robots[r] - needed.get(r, 0) + for r in REAGENTS + } + + robots = state.robots.copy() + robots[reagent] += 1 + + state_2 = State(reagents=reagents, robots=robots) + + print(time + wait) + if any(dominates(state_3, state_2) for state_3 in at_time[time + wait]): + continue + + if state_2 not in parents or parents[state_2][1] > time + wait: + parents[state_2] = (state, time + wait) + heapq.heappush(queue, (time + wait, state_2)) + can_build_any = True + + if not can_build_any: + state_2 = State( + reagents={ + r: state.reagents[r] + state.robots[r] * (MAX_TIME - time) + for r in REAGENTS + }, + robots=state.robots, + ) + + if state_2 not in parents or parents[state_2][1] > time + wait: + parents[state_2] = (state, time + wait) + heapq.heappush(queue, (time + wait, state_2)) + +print(len(visited)) +print(max(state.reagents["geode"] for state in visited)) + +exit() + +while states: + state = states.pop() + processed.append(state) + + if state.time > MAX_TIME: + continue + + if len(states) % 100 == 0: + print(len(states), len(processed), min((s.time for s in states), default=1)) + + can_build_any: bool = False + for reagent in REAGENTS: + needed = blueprint[reagent] + + if any(state.robots[r] == 0 for r in needed): + continue + + time_to_complete = max( + max( + math.ceil((needed[r] - state.reagents[r]) / state.robots[r]) + for r in needed + ), + 0, + ) + + if state.time + time_to_complete + 1 > MAX_TIME: + continue + + wait = time_to_complete + 1 + + reagents = { + r: state.reagents[r] + wait * state.robots[r] - needed.get(r, 0) + for r in REAGENTS + } + + robots = state.robots.copy() + robots[reagent] += 1 + + can_build_any = True + state_2 = State(time=state.time + wait, reagents=reagents, robots=robots) + # print(f"{state} -> {state_2}") + states.add(state_2) + + if not any(dominates(s2, state_2) for s2 in states): + states.add(state) + + # print(f"can build {reagent} in {time_to_complete}") + + if not can_build_any: + states.add( + State( + time=MAX_TIME + 1, + reagents={ + r: state.reagents[r] + state.robots[r] * (MAX_TIME - state.time) + for r in REAGENTS + }, + robots=state.robots, + ) + ) + + if len(states) % 1000 == 0: + print("filtering") + states = { + s1 + for s1 in states + if not any(dominates(s2, s1) for s2 in states if s2 is not s1) + } + + # if len(states) > 4: + # break + + # break + +print(len(processed)) +print(max(state.reagents["geode"] for state in processed)) + +exit() + +for t in range(1, 25): + states = set() + for state in state_after_t[t - 1]: + robots_that_can_be_built = [ + robot + for robot in REAGENTS + if all( + state.reagents[reagent] >= blueprint[robot].get(reagent, 0) + for reagent in REAGENTS + ) + ] + + new_states = set() + + # new reagents + reagents = { + reagent: state.reagents[reagent] + state.robots[reagent] + for reagent in REAGENTS + } + + # if we can build anything, there is no point in waiting + if len(robots_that_can_be_built) != len(REAGENTS): + new_states.add(State(robots=state.robots, reagents=reagents)) + + for robot in robots_that_can_be_built: + robots = state.robots.copy() + robots[robot] += 1 + reagents = { + reagent: state.reagents[reagent] + + state.robots[reagent] + - blueprint[robot].get(reagent, 0) + for reagent in REAGENTS + } + new_states.add(State(robots=robots, reagents=reagents)) + + new_states = [ + s1 + for s1 in new_states + if not any(s1 is not s2 and dominates(s2, s1) for s2 in new_states) + ] + + states = { + s1 for s1 in states if not any(dominates(s2, s1) for s2 in new_states) + } + states.update(new_states) + + state_after_t[t] = states + +exit() + + +for t in range(1, 25): + print(t, len(state_after_t[t - 1])) + state_after_t[t] = set() + + bests_for_robots: dict[tuple[int, ...], set[State]] = {} + bests_for_reagents: dict[tuple[int, ...], set[State]] = {} + + for state in state_after_t[t - 1]: + robots_that_can_be_built = [ + robot + for robot in REAGENTS + if all( + state.reagents[reagent] >= blueprint[robot].get(reagent, 0) + for reagent in REAGENTS + ) + ] + + # print(t, robots_that_can_be_built) + new_states: set[State] = set() + + # new reagents + reagents = { + reagent: state.reagents[reagent] + state.robots[reagent] + for reagent in REAGENTS + } + + # if we can build anything, there is no point in waiting + new_states.add(State(robots=state.robots, reagents=reagents, last=None)) + + for robot in robots_that_can_be_built: + if robot == state.last: + continue + robots = state.robots.copy() + robots[robot] += 1 + reagents = { + reagent: state.reagents[reagent] + + state.robots[reagent] + - blueprint[robot].get(reagent, 0) + for reagent in REAGENTS + } + new_states.add(State(robots=robots, reagents=reagents, last=robot)) + + for s1 in new_states: + r1 = tuple(s1.robots[r] for r in REAGENTS) + if r1 not in bests_for_robots: + bests_for_robots[r1] = {s1} + else: + is_dominated = False + for s2 in bests_for_robots[r1]: + if all(s2.reagents[r] >= s1.reagents[r] for r in REAGENTS): + is_dominated = True + break + if not is_dominated: + bests_for_robots[r1].add(s1) + + r2 = tuple(s1.reagents[r] for r in REAGENTS) + if r2 not in bests_for_reagents: + bests_for_reagents[r2] = {s1} + else: + is_dominated = False + for s2 in bests_for_reagents[r2]: + if all(s2.robots[r] >= s1.robots[r] for r in REAGENTS): + is_dominated = True + break + if not is_dominated: + bests_for_reagents[r2].add(s1) + + state_after_t[t] = set() + for bests in bests_for_robots.values(): + dominated = [False for _ in range(len(bests))] + for i_s1, s1 in enumerate(bests): + if dominated[i_s1]: + continue + for i_s2, s2 in enumerate(bests): + if s1 is s2 or dominated[i_s2]: + continue + if all(s1.reagents[r] >= s2.reagents[r] for r in REAGENTS): + dominated[i_s2] = True + state_after_t[t].update( + s1 for i_s1, s1 in enumerate(bests) if not dominated[i_s1] + ) + for bests in bests_for_reagents.values(): + dominated = [False for _ in range(len(bests))] + for i_s1, s1 in enumerate(bests): + if dominated[i_s1]: + continue + for i_s2, s2 in enumerate(bests): + if s1 is s2 or dominated[i_s2]: + continue + if all(s1.robots[r] >= s2.robots[r] for r in REAGENTS): + dominated[i_s2] = True + state_after_t[t].update( + s1 for i_s1, s1 in enumerate(bests) if not dominated[i_s1] + ) + + # dominated = [False for _ in range(len(state_after_t[t]))] + # print(t, "->", len(state_after_t[t])) + # for i_s1, s1 in enumerate(state_after_t[t]): + # if dominated[i_s1]: + # continue + # for i_s2, s2 in enumerate(state_after_t[t]): + # if s1 is s2 or dominated[i_s2]: + # continue + # if all(s1.robots[r] >= s2.robots[r] for r in REAGENTS) and all( + # s1.reagents[r] >= s2.reagents[r] for r in REAGENTS + # ): + # dominated[i_s2] = True + + # state_after_t[t] = { + # s1 for i_s1, s1 in enumerate(state_after_t[t]) if not dominated[i_s1] + # } + + # print(len(state_after_t[t])) + # print(sum(dominated)) + # break + +print(max(state.reagents["geode"] for state in state_after_t[24])) diff --git a/2022/tests/day19.txt b/2022/tests/day19.txt new file mode 100644 index 0000000..f39c094 --- /dev/null +++ b/2022/tests/day19.txt @@ -0,0 +1,2 @@ +Blueprint 1: Each ore robot costs 4 ore. Each clay robot costs 2 ore. Each obsidian robot costs 3 ore and 14 clay. Each geode robot costs 2 ore and 7 obsidian. +Blueprint 2: Each ore robot costs 2 ore. Each clay robot costs 3 ore. Each obsidian robot costs 3 ore and 8 clay. Each geode robot costs 3 ore and 12 obsidian.