{ "cells": [ { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "# Letting it all out\n", "\n", "- https://adventofcode.com/2022/day/16\n", "\n", "The puzzles are getting tricker! This is, in essence, an optimisation problem.\n", "\n", "Start with figuring out how many steps it'll take to move from any given valve to any of the other valves, because there is no point in calculating this each time you want to evaluate what valve next to visit. You can use the [Floyd-Warshall algorithm](https://en.wikipedia.org/wiki/Floyd%E2%80%93Warshall_algorithm) to do this.\n", "\n", "Next, the number of valves with a non-zero flow rate is low enough that we can use a breath-first search across all the different possible paths; keeping a mapping from visited nodes to pressure relieved to only follow up on a path that visits the same valves in a different order leading to a better value.\n" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import re\n", "from collections import deque\n", "from contextlib import suppress\n", "from dataclasses import dataclass\n", "from functools import cached_property\n", "from itertools import permutations\n", "from typing import Final, Iterator, NamedTuple, Self\n", "\n", "LINE: Final[re.Pattern[str]] = re.compile(\n", " r\"^Valve (?P[A-Z]{2}) has flow rate=(?P\\d+); \"\n", " r\"(?:tunnels lead to valves|tunnel leads to valve) (?P[A-Z, ]*)$\"\n", ")\n", "\n", "\n", "class Valve(NamedTuple):\n", " name: str\n", " rate: int\n", " valves: frozenset[str]\n", "\n", " @classmethod\n", " def from_line(cls, line: str) -> Self:\n", " match = LINE.match(line)\n", " assert match is not None\n", " rate = int(match[\"rate\"])\n", " valves = frozenset(v.strip() for v in match[\"valves\"].split(\",\"))\n", " return cls(match[\"name\"], rate, valves)\n", "\n", " def __repr__(self) -> str:\n", " return (\n", " f\"{type(self).__name__}({self.name}, {self.rate}, \"\n", " f\"[{','.join(sorted(self.valves))}])\"\n", " )\n", "\n", "\n", "@dataclass\n", "class TunnelStep:\n", " valve: Valve\n", " time_left: int = 30\n", " total_released: int = 0\n", " visited: frozenset[Valve] = frozenset()\n", "\n", " def traverse(self, graph: \"Graph\") -> Iterator[Self]:\n", " for valve, steps in graph.distances[self.valve].items():\n", " if valve in self.visited or not valve.rate:\n", " # either we already opened the valve here, or it is not worth\n", " # stopping here as the effect would be 0.\n", " continue\n", " if (time_left := self.time_left - steps - 1) <= 0:\n", " # no point in going here, the result would be 0.\n", " continue\n", " yield __class__(\n", " valve,\n", " time_left,\n", " self.total_released + valve.rate * time_left,\n", " self.visited | {valve},\n", " )\n", "\n", "\n", "class Graph:\n", " def __init__(self, nodes: dict[str, Valve]):\n", " self.nodes = nodes\n", "\n", " @classmethod\n", " def from_text(cls, text: str) -> Self:\n", " return cls({(v := Valve.from_line(line)).name: v for line in text.splitlines()})\n", "\n", " @cached_property\n", " def distances(self) -> dict[Valve, dict[Valve, int]]:\n", " \"\"\"Minimal distances to move from one valve to another\n", "\n", " Uses the Floyd-Warshall algorithm to find the minimum distances from\n", " any node in the graph to any other node.\n", " \"\"\"\n", " graph = self.nodes\n", " dist: dict[Valve, dict[Valve, int]] = {\n", " v: {graph[n]: 1 for n in v.valves} for v in graph.values()\n", " }\n", " max = len(graph)\n", " for k, i, j in permutations(graph.values(), r=3):\n", " with suppress(KeyError):\n", " dist[i][j] = min(dist[i][k] + dist[k][j], dist[i].get(j, max))\n", " return dist\n", "\n", " def max_pressure_reliefs(self, remaining: int = 30) -> dict[frozenset[Valve], int]:\n", " max_relief: dict[frozenset[Valve], int] = {}\n", " queue = deque([TunnelStep(self.nodes[\"AA\"], remaining)])\n", " while queue:\n", " node = queue.popleft()\n", " for new in node.traverse(self):\n", " max_relief[new.visited] = max(\n", " max_relief.get(new.visited, 0), new.total_released\n", " )\n", " queue.append(new)\n", " return max_relief\n", "\n", " def optimise_pressure_relief(self) -> int:\n", " return max(self.max_pressure_reliefs().values())\n", "\n", "\n", "example = Graph.from_text(\n", " \"\"\"\\\n", "Valve AA has flow rate=0; tunnels lead to valves DD, II, BB\n", "Valve BB has flow rate=13; tunnels lead to valves CC, AA\n", "Valve CC has flow rate=2; tunnels lead to valves DD, BB\n", "Valve DD has flow rate=20; tunnels lead to valves CC, AA, EE\n", "Valve EE has flow rate=3; tunnels lead to valves FF, DD\n", "Valve FF has flow rate=0; tunnels lead to valves EE, GG\n", "Valve GG has flow rate=0; tunnels lead to valves FF, HH\n", "Valve HH has flow rate=22; tunnel leads to valve GG\n", "Valve II has flow rate=0; tunnels lead to valves AA, JJ\n", "Valve JJ has flow rate=21; tunnel leads to valve II\n", "\"\"\"\n", ")\n", "\n", "assert example.optimise_pressure_relief() == 1651" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Part 1: 1617\n" ] } ], "source": [ "import aocd\n", "\n", "graph = Graph.from_text(aocd.get_data(day=16, year=2022))\n", "print(\"Part 1:\", graph.optimise_pressure_relief())" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Part 2: What noise we make when we stamp!\n", "\n", "We get the help of an elephant, so we can open more valves!\n", "\n", "The trick here is to run through the same search as step one, so simply opening valves with one person. Because this produces a mapping of _set of valves_ -> _relieved pressure_, we can then find all combinations of (partial) paths where the set of valves are _disjoint_, and calculate the maximum pressure relief we can obtain.\n" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "from itertools import combinations\n", "\n", "\n", "def double_up(graph: Graph) -> int:\n", " paths = graph.max_pressure_reliefs(26)\n", " return max(\n", " paths[me] + paths[elephant]\n", " for me, elephant in combinations(paths, r=2)\n", " if me.isdisjoint(elephant)\n", " )\n", "\n", "\n", "assert double_up(example) == 1707" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Part 2: 2171\n" ] } ], "source": [ "print(\"Part 2:\", double_up(graph))" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "## Visualisation\n", "\n", "This is what the test valves look like. Red nodes are valves with no pressure to relieve:\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "orbits\n", "\n", "\n", "\n", "AA\n", "\n", "\n", "AA\n", "0\n", "\n", "\n", "\n", "DD\n", "\n", "DD\n", "20\n", "\n", "\n", "\n", "AA->DD\n", "\n", "\n", "\n", "\n", "II\n", "\n", "II\n", "0\n", "\n", "\n", "\n", "AA->II\n", "\n", "\n", "\n", "\n", "BB\n", "\n", "BB\n", "13\n", "\n", "\n", "\n", "AA->BB\n", "\n", "\n", "\n", "\n", "EE\n", "\n", "EE\n", "3\n", "\n", "\n", "\n", "DD->EE\n", "\n", "\n", "\n", "\n", "JJ\n", "\n", "JJ\n", "21\n", "\n", "\n", "\n", "II->JJ\n", "\n", "\n", "\n", "\n", "CC\n", "\n", "CC\n", "2\n", "\n", "\n", "\n", "BB->CC\n", "\n", "\n", "\n", "\n", "FF\n", "\n", "FF\n", "0\n", "\n", "\n", "\n", "EE->FF\n", "\n", "\n", "\n", "\n", "CC->DD\n", "\n", "\n", "\n", "\n", "GG\n", "\n", "GG\n", "0\n", "\n", "\n", "\n", "FF->GG\n", "\n", "\n", "\n", "\n", "HH\n", "\n", "HH\n", "22\n", "\n", "\n", "\n", "GG->HH\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import graphviz\n", "from IPython.display import Markdown, display\n", "\n", "\n", "def plot_graph(\n", " graph: Graph,\n", " edge_attr: dict[str, str] | None = None,\n", " node_attr: dict[str, str] | None = None,\n", " **graph_attr: str,\n", ") -> graphviz.Digraph:\n", " graph_attr = {\n", " \"ratio\": \"compress\",\n", " \"rankdir\": \"LR\",\n", " **graph_attr,\n", " }\n", " edge_attr = {\n", " \"fontname\": \"Arial,Helvetica Neue,Helvetica,sans-serif\",\n", " \"fontsize\": \"10.0\",\n", " \"dir\": \"none\",\n", " **(edge_attr or {}),\n", " }\n", " node_attr = {\n", " \"fontname\": \"Arial,Helvetica Neue,Helvetica,sans-serif\",\n", " \"fontsize\": \"10.0\",\n", " \"fixedsize\": \"true\",\n", " \"style\": \"filled\",\n", " **(node_attr or {}),\n", " }\n", " dot = graphviz.Digraph(\"orbits\", strict=True)\n", " dot.attr(**graph_attr)\n", " dot.attr(\"edge\", **edge_attr)\n", " dot.attr(\"node\", **node_attr)\n", " for valve in graph.nodes.values():\n", " dot.node(\n", " valve.name,\n", " f\"{valve.name}\\n{valve.rate}\",\n", " **node_attr,\n", " shape=\"doublecircle\" if valve.name == \"AA\" else \"circle\",\n", " fillcolor=\"darkseagreen1\" if valve.rate else \"tomato\",\n", " )\n", " for other in valve.valves:\n", " if other > valve.name:\n", " dot.edge(valve.name, other)\n", "\n", " return dot.unflatten(3)\n", "\n", "\n", "display(\n", " Markdown(\n", " \"\"\"\\\n", "## Visualisation\n", "\n", "This is what the test valves look like. Red nodes are valves with no pressure to relieve:\n", "\"\"\"\n", " )\n", ")\n", "plot_graph(example)" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "## Visualisation\n", "\n", "The puzzle input data is a lot larger:" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "orbits\n", "\n", "\n", "\n", "GV\n", "\n", "GV\n", "23\n", "\n", "\n", "\n", "WO\n", "\n", "WO\n", "0\n", "\n", "\n", "\n", "GV->WO\n", "\n", "\n", "\n", "\n", "TS\n", "\n", "TS\n", "0\n", "\n", "\n", "\n", "TX\n", "\n", "TX\n", "14\n", "\n", "\n", "\n", "TS->TX\n", "\n", "\n", "\n", "\n", "YH\n", "\n", "YH\n", "0\n", "\n", "\n", "\n", "TX->YH\n", "\n", "\n", "\n", "\n", "UC\n", "\n", "UC\n", "0\n", "\n", "\n", "\n", "XJ\n", "\n", "XJ\n", "0\n", "\n", "\n", "\n", "UC->XJ\n", "\n", "\n", "\n", "\n", "VZ\n", "\n", "VZ\n", "19\n", "\n", "\n", "\n", "UC->VZ\n", "\n", "\n", "\n", "\n", "TJ\n", "\n", "TJ\n", "0\n", "\n", "\n", "\n", "YV\n", "\n", "YV\n", "11\n", "\n", "\n", "\n", "TJ->YV\n", "\n", "\n", "\n", "\n", "KF\n", "\n", "KF\n", "0\n", "\n", "\n", "\n", "VP\n", "\n", "VP\n", "8\n", "\n", "\n", "\n", "KF->VP\n", "\n", "\n", "\n", "\n", "QY\n", "\n", "QY\n", "0\n", "\n", "\n", "\n", "KF->QY\n", "\n", "\n", "\n", "\n", "PO\n", "\n", "PO\n", "0\n", "\n", "\n", "\n", "PO->VP\n", "\n", "\n", "\n", "\n", "YF\n", "\n", "YF\n", "20\n", "\n", "\n", "\n", "PO->YF\n", "\n", "\n", "\n", "\n", "CV\n", "\n", "CV\n", "0\n", "\n", "\n", "\n", "VB\n", "\n", "VB\n", "7\n", "\n", "\n", "\n", "CV->VB\n", "\n", "\n", "\n", "\n", "QK\n", "\n", "QK\n", "10\n", "\n", "\n", "\n", "CV->QK\n", "\n", "\n", "\n", "\n", "QK->WO\n", "\n", "\n", "\n", "\n", "VS\n", "\n", "VS\n", "0\n", "\n", "\n", "\n", "QK->VS\n", "\n", "\n", "\n", "\n", "NK\n", "\n", "NK\n", "6\n", "\n", "\n", "\n", "NK->YH\n", "\n", "\n", "\n", "\n", "NK->QY\n", "\n", "\n", "\n", "\n", "QJ\n", "\n", "QJ\n", "0\n", "\n", "\n", "\n", "NK->QJ\n", "\n", "\n", "\n", "\n", "XO\n", "\n", "XO\n", "0\n", "\n", "\n", "\n", "QJ->XO\n", "\n", "\n", "\n", "\n", "IG\n", "\n", "IG\n", "4\n", "\n", "\n", "\n", "IG->TS\n", "\n", "\n", "\n", "\n", "UV\n", "\n", "UV\n", "0\n", "\n", "\n", "\n", "IG->UV\n", "\n", "\n", "\n", "\n", "OP\n", "\n", "OP\n", "0\n", "\n", "\n", "\n", "IG->OP\n", "\n", "\n", "\n", "\n", "MI\n", "\n", "MI\n", "0\n", "\n", "\n", "\n", "IG->MI\n", "\n", "\n", "\n", "\n", "MI->NK\n", "\n", "\n", "\n", "\n", "KN\n", "\n", "KN\n", "0\n", "\n", "\n", "\n", "RF\n", "\n", "RF\n", "16\n", "\n", "\n", "\n", "KN->RF\n", "\n", "\n", "\n", "\n", "KR\n", "\n", "KR\n", "0\n", "\n", "\n", "\n", "KR->VP\n", "\n", "\n", "\n", "\n", "MW\n", "\n", "MW\n", "0\n", "\n", "\n", "\n", "MW->VB\n", "\n", "\n", "\n", "\n", "UZ\n", "\n", "UZ\n", "0\n", "\n", "\n", "\n", "MW->UZ\n", "\n", "\n", "\n", "\n", "UZ->VP\n", "\n", "\n", "\n", "\n", "LJ\n", "\n", "LJ\n", "25\n", "\n", "\n", "\n", "LJ->XJ\n", "\n", "\n", "\n", "\n", "DI\n", "\n", "DI\n", "0\n", "\n", "\n", "\n", "DI->KR\n", "\n", "\n", "\n", "\n", "TO\n", "\n", "TO\n", "12\n", "\n", "\n", "\n", "CG\n", "\n", "CG\n", "0\n", "\n", "\n", "\n", "CG->TX\n", "\n", "\n", "\n", "\n", "CG->VP\n", "\n", "\n", "\n", "\n", "GJ\n", "\n", "GJ\n", "0\n", "\n", "\n", "\n", "GJ->TJ\n", "\n", "\n", "\n", "\n", "QL\n", "\n", "QL\n", "15\n", "\n", "\n", "\n", "GJ->QL\n", "\n", "\n", "\n", "\n", "RD\n", "\n", "RD\n", "0\n", "\n", "\n", "\n", "QL->RD\n", "\n", "\n", "\n", "\n", "RD->RF\n", "\n", "\n", "\n", "\n", "CY\n", "\n", "CY\n", "0\n", "\n", "\n", "\n", "CY->YV\n", "\n", "\n", "\n", "\n", "CY->KN\n", "\n", "\n", "\n", "\n", "AA\n", "\n", "\n", "AA\n", "0\n", "\n", "\n", "\n", "AA->VS\n", "\n", "\n", "\n", "\n", "AA->XO\n", "\n", "\n", "\n", "\n", "AA->UV\n", "\n", "\n", "\n", "\n", "AA->DI\n", "\n", "\n", "\n", "\n", "NB\n", "\n", "NB\n", "0\n", "\n", "\n", "\n", "AA->NB\n", "\n", "\n", "\n", "\n", "NB->VB\n", "\n", "\n", "\n", "\n", "SB\n", "\n", "SB\n", "0\n", "\n", "\n", "\n", "SB->YF\n", "\n", "\n", "\n", "\n", "PB\n", "\n", "PB\n", "0\n", "\n", "\n", "\n", "PB->TO\n", "\n", "\n", "\n", "\n", "TG\n", "\n", "TG\n", "0\n", "\n", "\n", "\n", "TG->TO\n", "\n", "\n", "\n", "\n", "DM\n", "\n", "DM\n", "0\n", "\n", "\n", "\n", "DM->TX\n", "\n", "\n", "\n", "\n", "DM->YF\n", "\n", "\n", "\n", "\n", "PW\n", "\n", "PW\n", "0\n", "\n", "\n", "\n", "PW->YV\n", "\n", "\n", "\n", "\n", "OM\n", "\n", "OM\n", "0\n", "\n", "\n", "\n", "OM->QK\n", "\n", "\n", "\n", "\n", "OM->OP\n", "\n", "\n", "\n", "\n", "RM\n", "\n", "RM\n", "0\n", "\n", "\n", "\n", "RM->TX\n", "\n", "\n", "\n", "\n", "RM->TG\n", "\n", "\n", "\n", "\n", "SH\n", "\n", "SH\n", "0\n", "\n", "\n", "\n", "LI\n", "\n", "LI\n", "0\n", "\n", "\n", "\n", "LI->LJ\n", "\n", "\n", "\n", "\n", "LI->PW\n", "\n", "\n", "\n", "\n", "FP\n", "\n", "FP\n", "0\n", "\n", "\n", "\n", "FP->VB\n", "\n", "\n", "\n", "\n", "FP->IG\n", "\n", "\n", "\n", "\n", "BZ\n", "\n", "BZ\n", "0\n", "\n", "\n", "\n", "BZ->TO\n", "\n", "\n", "\n", "\n", "BZ->SB\n", "\n", "\n", "\n", "\n", "DO\n", "\n", "DO\n", "0\n", "\n", "\n", "\n", "DO->VB\n", "\n", "\n", "\n", "\n", "DO->NK\n", "\n", "\n", "\n", "\n", "AU\n", "\n", "AU\n", "0\n", "\n", "\n", "\n", "AU->RF\n", "\n", "\n", "\n", "\n", "AU->SH\n", "\n", "\n", "\n", "\n", "HP\n", "\n", "HP\n", "17\n", "\n", "\n", "\n", "HP->PB\n", "\n", "\n", "\n", "\n", "HP->SH\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "display(Markdown(\"## Visualisation\\n\\nThe puzzle input data is a lot larger:\"))\n", "plot_graph(graph)" ] } ], "metadata": { "kernelspec": { "display_name": "adventofcode-mOkh6lsX", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.0" }, "orig_nbformat": 4, "vscode": { "interpreter": { "hash": "b1b6870d1e0a983b1943c858d70ac8a7c80477f9f3ca364eb8daa198319a8a87" } } }, "nbformat": 4, "nbformat_minor": 2 }