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 -}