diff --git a/AdventUtils.ipynb b/AdventUtils.ipynb
deleted file mode 100644
index 2dacabe..0000000
--- a/AdventUtils.ipynb
+++ /dev/null
@@ -1,530 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "
Peter Norvig
Decembers 2016–2021
\n",
- "\n",
- "# Advent of Code Utilities\n",
- "\n",
- "Stuff I might need for [Advent of Code](https://adventofcode.com). First, some imports that I have used in past AoC years:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 10,
- "metadata": {},
- "outputs": [],
- "source": [
- "from collections import Counter, defaultdict, namedtuple, deque, abc\n",
- "from dataclasses import dataclass\n",
- "from itertools import permutations, combinations, cycle, chain\n",
- "from itertools import count as count_from, product as cross_product\n",
- "from typing import *\n",
- "from statistics import mean, median\n",
- "from math import ceil, floor, factorial, gcd, log, log2, log10, sqrt, inf\n",
- "from functools import reduce\n",
- "\n",
- "import matplotlib.pyplot as plt\n",
- "import time\n",
- "import heapq\n",
- "import string\n",
- "import functools\n",
- "import pathlib\n",
- "import re"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Daily Input Parsing\n",
- "\n",
- "Each day's work will consist of three tasks, denoted by three sections in the notebook:\n",
- "- **Input**: Parse the day's input file. I will use the function `parse(day, parser, sep)`, which:\n",
- " - Reads the input file for `day`.\n",
- " - Breaks the file into a sequence of *items* separated by `sep` (default newline).\n",
- " - Applies `parser` to each item and returns the results as a tuple.\n",
- " - Useful parser functions include `ints`, `digits`, `atoms`, `words`, and the built-ins `int` and `str`.\n",
- " - Prints the first few input lines and output records. This is useful to me as a debugging tool, and to the reader.\n",
- "- **Part 1**: Understand the day's instructions and:\n",
- " - Write code to compute the answer to Part 1.\n",
- " - Once I have computed the answer and submitted it to the AoC site to verify it is correct, I record it with the `answer` function.\n",
- "- **Part 2**: Repeat the above steps for Part 2.\n",
- "- Occasionally I'll introduce a **Part 3** where I explore beyond the official instructions.\n",
- "\n",
- "Here is `parse` and some helper functions for it:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 18,
- "metadata": {},
- "outputs": [],
- "source": [
- "current_year = 2022 # Subdirectory name for input files\n",
- "lines = '\\n' # For inputs where each record is a line\n",
- "paragraphs = '\\n\\n' # For inputs where each record is a paragraph \n",
- "\n",
- "def parse(day_or_text:Union[int, str], parser:Callable=str, sep:Callable=lines, show=6) -> tuple:\n",
- " \"\"\"Split the input text into items separated by `sep`, and apply `parser` to each.\n",
- " The first argument is either the text itself, or the day number of a text file.\"\"\"\n",
- " text = get_text(day_or_text)\n",
- " show_parse_items('Puzzle input', text.splitlines(), show, 'line')\n",
- " items = mapt(parser, text.rstrip().split(sep))\n",
- " if parser != str:\n",
- " show_parse_items('Parsed representation', items, show, f'{type(items[0]).__name__}')\n",
- " return items\n",
- "\n",
- "def get_text(day_or_text:Union[int, str]) -> str:\n",
- " \"\"\"The text used as input to the puzzle: either a string or the day number of a file.\"\"\"\n",
- " if isinstance(day_or_text, int):\n",
- " return pathlib.Path(f'AOC/{current_year}/input{day_or_text}.txt').read_text()\n",
- " else:\n",
- " return day_or_text\n",
- "\n",
- "def show_parse_items(source, items, show:int, name:str, sep=\"─\"*100):\n",
- " \"\"\"Show verbose output from `parse` for lines or items.\"\"\"\n",
- " if not show:\n",
- " return\n",
- " count = f'1 {name}' if len(items) == 1 else f'{len(items)} {name}s'\n",
- " for line in (sep, f'{source} ➜ {count}:', sep, *items[:show]):\n",
- " print(truncate(line, 100))\n",
- " if show < len(items):\n",
- " print('...')\n",
- " \n",
- "def truncate(object, width) -> str:\n",
- " \"\"\"Use elipsis to truncate `str(object)` to `width` characters, if necessary.\"\"\"\n",
- " string = str(object)\n",
- " return string if len(string) <= width else string[:width-4] + ' ...'\n",
- "\n",
- "def parse_sections(specs: Iterable) -> Callable:\n",
- " \"\"\"Return a parser that uses the first spec to parse the first section, the second for second, etc.\n",
- " Each spec is either parser or [parser, sep].\"\"\"\n",
- " specs = ([spec] if callable(spec) else spec for spec in specs)\n",
- " fns = ((lambda section: parse(section, *spec, show=0)) for spec in specs)\n",
- " return lambda section: next(fns)(section)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 38,
- "metadata": {},
- "outputs": [],
- "source": [
- "## Functions that can be used by `parse`\n",
- "\n",
- "Char = str # Intended as the type of a one-character string\n",
- "Atom = Union[str, float, int] # The type of a string or number\n",
- "\n",
- "def ints(text: str) -> Tuple[int]:\n",
- " \"\"\"A tuple of all the integers in text, ignoring non-number characters.\"\"\"\n",
- " return mapt(int, re.findall(r'-?[0-9]+', text))\n",
- "\n",
- "def positive_ints(text: str) -> Tuple[int]:\n",
- " \"\"\"A tuple of all the integers in text, ignoring non-number characters.\"\"\"\n",
- " return mapt(int, re.findall(r'[0-9]+', text))\n",
- "\n",
- "def digits(text: str) -> Tuple[int]:\n",
- " \"\"\"A tuple of all the digits in text (as ints 0–9), ignoring non-digit characters.\"\"\"\n",
- " return mapt(int, re.findall(r'[0-9]', text))\n",
- "\n",
- "def words(text: str) -> Tuple[str]:\n",
- " \"\"\"A tuple of all the alphabetic words in text, ignoring non-letters.\"\"\"\n",
- " return tuple(re.findall(r'[a-zA-Z]+', text))\n",
- "\n",
- "def atoms(text: str) -> Tuple[Atom]:\n",
- " \"\"\"A tuple of all the atoms (numbers or identifiers) in text. Skip punctuation.\"\"\"\n",
- " return mapt(atom, re.findall(r'[_a-zA-Z0-9.+-]+', text))\n",
- "\n",
- "def atom(text: str) -> Atom:\n",
- " \"\"\"Parse text into a single float or int or str.\"\"\"\n",
- " try:\n",
- " x = float(text)\n",
- " return round(x) if x.is_integer() else x\n",
- " except ValueError:\n",
- " return text"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Daily Answers\n",
- "\n",
- "Here is the `answer` function:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "metadata": {},
- "outputs": [],
- "source": [
- "# `answers` is a dict of {puzzle_number_id: message_about_results}\n",
- "answers = {} \n",
- "\n",
- "def answer(puzzle, correct, code: callable) -> None:\n",
- " \"\"\"Verify that calling `code` computes the `correct` answer for `puzzle`. \n",
- " Record results in the dict `answers`. Prints execution time.\"\"\"\n",
- " def pretty(x): return f'{x:,d}' if is_int(x) else str(x)\n",
- " start = time.time()\n",
- " got = code()\n",
- " dt = time.time() - start\n",
- " ans = pretty(got)\n",
- " msg = f'{dt:5.3f} seconds for ' + (\n",
- " f'correct answer: {ans}' if (got == correct) else\n",
- " f'WRONG!! answer: {ans}; expected {pretty(correct)}')\n",
- " answers[puzzle] = msg\n",
- " assert got == correct, msg\n",
- " print(msg)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Additional utility functions "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 16,
- "metadata": {},
- "outputs": [],
- "source": [
- "def quantify(iterable, pred=bool) -> int:\n",
- " \"\"\"Count the number of items in iterable for which pred is true.\"\"\"\n",
- " return sum(1 for item in iterable if pred(item))\n",
- "\n",
- "def mapt(function: Callable, sequence) -> tuple:\n",
- " \"\"\"`map`, with the result as a tuple.\"\"\"\n",
- " return tuple(map(function, sequence))\n",
- "\n",
- "class multimap(defaultdict):\n",
- " \"\"\"A mapping of {key: [val1, val2, ...]}.\"\"\"\n",
- " def __init__(self, pairs: Iterable[tuple], symmetric=False):\n",
- " \"\"\"Given (key, val) pairs, return {key: [val, ...], ...}.\n",
- " If `symmetric` is True, treat (key, val) as (key, val) plus (val, key).\"\"\"\n",
- " self.default_factory = list\n",
- " for (key, val) in pairs:\n",
- " self[key].append(val)\n",
- " if symmetric:\n",
- " self[val].append(key)\n",
- "\n",
- "def prod(numbers) -> float: # Will be math.prod in Python 3.8\n",
- " \"\"\"The product formed by multiplying `numbers` together.\"\"\"\n",
- " result = 1\n",
- " for x in numbers:\n",
- " result *= x\n",
- " return result\n",
- "\n",
- "def total(counter: Counter) -> int: \n",
- " \"\"\"The sum of all the counts in a Counter.\"\"\"\n",
- " return sum(counter.values())\n",
- "\n",
- "def minmax(numbers) -> Tuple[int, int]:\n",
- " \"\"\"A tuple of the (minimum, maximum) of numbers.\"\"\"\n",
- " numbers = list(numbers)\n",
- " return min(numbers), max(numbers)\n",
- "\n",
- "def first(iterable) -> Optional[object]: \n",
- " \"\"\"The first element in an iterable, or None.\"\"\"\n",
- " return next(iter(iterable), None)\n",
- "\n",
- "def T(matrix: Sequence[Sequence]) -> List[Tuple]:\n",
- " \"\"\"The transpose of a matrix: T([(1,2,3), (4,5,6)]) == [(1,4), (2,5), (3,6)]\"\"\"\n",
- " return list(zip(*matrix))\n",
- "\n",
- "def cover(*integers) -> range:\n",
- " \"\"\"A `range` that covers all the given integers, and any in between them.\n",
- " cover(lo, hi) is a an inclusive (or closed) range, equal to range(lo, hi + 1).\"\"\"\n",
- " return range(min(integers), max(integers) + 1)\n",
- "\n",
- "def the(sequence) -> object:\n",
- " \"\"\"Return the one item in a sequence. Raise error if not exactly one.\"\"\"\n",
- " items = list(sequence)\n",
- " if not len(items) == 1:\n",
- " raise ValueError(f'Expected exactly one item in the sequence {items}')\n",
- " return items[0]\n",
- "\n",
- "def split_at(sequence, i) -> Tuple[Sequence, Sequence]:\n",
- " \"\"\"The sequence split into two pieces: (before position i, and i-and-after).\"\"\"\n",
- " return sequence[:i], sequence[i:]\n",
- "\n",
- "def batched(data, n) -> list:\n",
- " \"Batch data into lists of length n. The last batch may be shorter.\"\n",
- " # batched('ABCDEFG', 3) --> ABC DEF G\n",
- " return [data[i:i+n] for i in range(0, len(data), n)]\n",
- "\n",
- "def sliding_window(sequence, n) -> Iterable[Sequence]:\n",
- " \"\"\"All length-n subsequences of sequence.\"\"\"\n",
- " return (sequence[i:i+n] for i in range(len(sequence) + 1 - n))\n",
- "\n",
- "def ignore(*args) -> None: \"Just return None.\"; return None\n",
- "\n",
- "def is_int(x) -> bool: \"Is x an int?\"; return isinstance(x, int) \n",
- "\n",
- "def sign(x) -> int: \"0, +1, or -1\"; return (0 if x == 0 else +1 if x > 0 else -1)\n",
- "\n",
- "def append(sequences) -> Sequence: \"Append sequences into a list\"; return list(flatten(sequences))\n",
- "\n",
- "def union(sets) -> set: \"Union of several sets\"; return set().union(*sets)\n",
- "\n",
- "def intersection(sets):\n",
- " \"Intersection of several sets.\"\n",
- " first, *rest = sets\n",
- " return set(first).intersection(*rest)\n",
- "\n",
- "def square_plot(points, marker='o', size=12, extra=None, **kwds):\n",
- " \"\"\"Plot `points` in a square of given `size`, with no axis labels.\n",
- " Calls `extra()` to do more plt.* stuff if defined.\"\"\"\n",
- " plt.figure(figsize=(size, size))\n",
- " plt.plot(*T(points), marker, **kwds)\n",
- " if extra: extra()\n",
- " plt.axis('square'); plt.axis('off'); plt.gca().invert_yaxis()\n",
- " \n",
- "def clock_mod(i, m) -> int:\n",
- " \"\"\"i % m, but replace a result of 0 with m\"\"\"\n",
- " # This is like a clock, where 24 mod 12 is 12, not 0.\n",
- " return (i % m) or m\n",
- "\n",
- "flatten = chain.from_iterable # Yield items from each sequence in turn\n",
- "cat = ''.join\n",
- "cache = functools.lru_cache(None)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Points on a Grid"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 8,
- "metadata": {},
- "outputs": [],
- "source": [
- "Point = Tuple[int, int] # (x, y) points on a grid\n",
- "\n",
- "def X_(point) -> int: \"X coordinate\"; return point[0]\n",
- "def Y_(point) -> int: \"Y coordinate\"; return point[1]\n",
- "\n",
- "def distance(p: Point, q: Point) -> float:\n",
- " \"\"\"Distance between two points.\"\"\"\n",
- " dx, dy = abs(X_(p) - X_(q)), abs(Y_(p) - Y_(q))\n",
- " return dx + dy if dx == 0 or dy == 0 else (dx ** 2 + dy ** 2) ** 0.5\n",
- "\n",
- "def manhatten_distance(p: Point, q: Point) -> int:\n",
- " \"\"\"Distance along grid lines between two points.\"\"\"\n",
- " return sum(abs(pi - qi) for pi, qi in zip(p, q))\n",
- "\n",
- "def add(p: Point, q: Point) -> Point:\n",
- " \"\"\"Add two points.\"\"\"\n",
- " return (X_(p) + X_(q), Y_(p) + Y_(q))\n",
- "\n",
- "def sub(p: Point, q: Point) -> Point:\n",
- " \"\"\"Subtract point q from point p.\"\"\"\n",
- " return (X_(p) - X_(q), Y_(p) - Y_(q))\n",
- "\n",
- "directions4 = North, South, East, West = ((0, -1), (0, 1), (1, 0), (-1, 0))\n",
- "directions8 = directions4 + ((1, 1), (1, -1), (-1, 1), (-1, -1))"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 13,
- "metadata": {},
- "outputs": [],
- "source": [
- "class Grid(dict):\n",
- " \"\"\"A 2D grid, implemented as a mapping of {(x, y): cell_contents}.\"\"\"\n",
- " def __init__(self, mapping_or_rows, directions=directions4):\n",
- " \"\"\"Initialize with either (e.g.) `Grid({(0, 0): 1, (1, 0): 2, ...})`, or\n",
- " `Grid([(1, 2, 3), (4, 5, 6)]).\"\"\"\n",
- " self.update(mapping_or_rows if isinstance(mapping_or_rows, abc.Mapping) else\n",
- " {(x, y): val \n",
- " for y, row in enumerate(mapping_or_rows) \n",
- " for x, val in enumerate(row)})\n",
- " self.width = max(map(X_, self)) + 1\n",
- " self.height = max(map(Y_, self)) + 1\n",
- " self.directions = directions\n",
- " \n",
- " def copy(self): return Grid(self, directions=self.directions)\n",
- " \n",
- " def neighbors(self, point) -> List[Point]:\n",
- " \"\"\"Points on the grid that neighbor `point`.\"\"\"\n",
- " return [add(point, Δ) for Δ in self.directions if add(point, Δ) in self]\n",
- " \n",
- " def to_rows(self, default='.') -> List[List[object]]:\n",
- " \"\"\"The contents of the grid in a rectangular list of lists.\"\"\"\n",
- " return [[self.get((x, y), default) for x in range(self.width)]\n",
- " for y in range(self.height)]\n",
- " \n",
- " def to_picture(self, sep='', default='.') -> str:\n",
- " \"\"\"The contents of the grid as a picture.\"\"\"\n",
- " return '\\n'.join(map(cat, self.to_rows(default)))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# A* Search"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 10,
- "metadata": {},
- "outputs": [],
- "source": [
- "def A_star_search(problem, h=None):\n",
- " \"\"\"Search nodes with minimum f(n) = path_cost(n) + h(n) value first.\"\"\"\n",
- " h = h or problem.h\n",
- " return best_first_search(problem, f=lambda n: n.path_cost + h(n))\n",
- "\n",
- "def best_first_search(problem, f):\n",
- " \"Search nodes with minimum f(node) value first.\"\n",
- " node = Node(problem.initial)\n",
- " frontier = PriorityQueue([node], key=f)\n",
- " reached = {problem.initial: node}\n",
- " while frontier:\n",
- " node = frontier.pop()\n",
- " if problem.is_goal(node.state):\n",
- " return node\n",
- " for child in expand(problem, node):\n",
- " s = child.state\n",
- " if s not in reached or child.path_cost < reached[s].path_cost:\n",
- " reached[s] = child\n",
- " frontier.add(child)\n",
- " return search_failure"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 8,
- "metadata": {},
- "outputs": [],
- "source": [
- "class SearchProblem:\n",
- " \"\"\"The abstract class for a search problem. A new domain subclasses this,\n",
- " overriding `actions` and perhaps other methods.\n",
- " The default heuristic is 0 and the default action cost is 1 for all states.\n",
- " When you create an instance of a subclass, specify `initial`, and `goal` states \n",
- " (or give an `is_goal` method) and perhaps other keyword args for the subclass.\"\"\"\n",
- "\n",
- " def __init__(self, initial=None, goal=None, **kwds): \n",
- " self.__dict__.update(initial=initial, goal=goal, **kwds) \n",
- " \n",
- " def __str__(self):\n",
- " return '{}({!r}, {!r})'.format(type(self).__name__, self.initial, self.goal)\n",
- " \n",
- " def actions(self, state): raise NotImplementedError\n",
- " def result(self, state, action): return action # Simplest case: action is result state\n",
- " def is_goal(self, state): return state == self.goal\n",
- " def action_cost(self, s, a, s1): return 1\n",
- " def h(self, node): return 0 # Never overestimate!\n",
- " \n",
- "class GridProblem(SearchProblem):\n",
- " \"\"\"Problem for searching a grid from a start to a goal location.\n",
- " A states is just an (x, y) location in the grid.\"\"\"\n",
- " def actions(self, loc): return self.grid.neighbors(loc)\n",
- " def result(self, loc1, loc2): return loc2\n",
- " def action_cost(self, s1, a, s2): return self.grid[s2]\n",
- " def h(self, node): return manhatten_distance(node.state, self.goal) \n",
- "\n",
- "class Node:\n",
- " \"A Node in a search tree.\"\n",
- " def __init__(self, state, parent=None, action=None, path_cost=0):\n",
- " self.__dict__.update(state=state, parent=parent, action=action, path_cost=path_cost)\n",
- "\n",
- " def __repr__(self): return f'Node({self.state})'\n",
- " def __len__(self): return 0 if self.parent is None else (1 + len(self.parent))\n",
- " def __lt__(self, other): return self.path_cost < other.path_cost\n",
- " \n",
- "search_failure = Node('failure', path_cost=inf) # Indicates an algorithm couldn't find a solution.\n",
- " \n",
- "def expand(problem, node):\n",
- " \"Expand a node, generating the children nodes.\"\n",
- " s = node.state\n",
- " for action in problem.actions(s):\n",
- " s2 = problem.result(s, action)\n",
- " cost = node.path_cost + problem.action_cost(s, action, s2)\n",
- " yield Node(s2, node, action, cost)\n",
- " \n",
- "def path_actions(node):\n",
- " \"The sequence of actions to get to this node.\"\n",
- " if node.parent is None:\n",
- " return [] \n",
- " return path_actions(node.parent) + [node.action]\n",
- "\n",
- "def path_states(node):\n",
- " \"The sequence of states to get to this node.\"\n",
- " if node in (search_failure, None): \n",
- " return []\n",
- " return path_states(node.parent) + [node.state]"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 9,
- "metadata": {},
- "outputs": [],
- "source": [
- "class PriorityQueue:\n",
- " \"\"\"A queue in which the item with minimum key(item) is always popped first.\"\"\"\n",
- "\n",
- " def __init__(self, items=(), key=lambda x: x): \n",
- " self.key = key\n",
- " self.items = [] # a heap of (score, item) pairs\n",
- " for item in items:\n",
- " self.add(item)\n",
- " \n",
- " def add(self, item):\n",
- " \"\"\"Add item to the queue.\"\"\"\n",
- " pair = (self.key(item), item)\n",
- " heapq.heappush(self.items, pair)\n",
- "\n",
- " def pop(self):\n",
- " \"\"\"Pop and return the item with min f(item) value.\"\"\"\n",
- " return heapq.heappop(self.items)[1]\n",
- " \n",
- " def top(self): return self.items[0][1]\n",
- "\n",
- " def __len__(self): return len(self.items)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": []
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": "Python 3",
- "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.7.6"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 4
-}