Delete AdventUtils.ipynb

This commit is contained in:
Peter Norvig
2023-01-05 13:46:43 -08:00
committed by GitHub
parent e76f6cdf4c
commit fb661727f8

View File

@@ -1,530 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<div style=\"text-align: right\" align=\"right\"><i>Peter Norvig<br>Decembers 20162021</i></div>\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 09), 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
}