This commit is contained in:
Peter Norvig 2022-12-10 15:11:14 -08:00 committed by GitHub
parent 54a60050af
commit dee0d1700b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 3829 additions and 1190 deletions

File diff suppressed because one or more lines are too long

936
ipynb/Advent-2022.ipynb Normal file
View File

@ -0,0 +1,936 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<div style=\"text-align: right\" align=\"right\"><i>Peter Norvig, December 2022</i></div>\n",
"\n",
"# Advent of Code 2022\n",
"\n",
"I'm doing Advent of Code (AoC) again this year.\n",
"\n",
"Happily for us all, [@GaryJGrady](https://twitter.com/GaryJGrady/) is drawing his cartoons again too! Below, Gary's elf makes preparations on the eve of AoC:\n",
"\n",
"<img src=\"https://pbs.twimg.com/media/Fi0-6hLX0AAav2b?format=jpg&name=small\" width=400 title=\"Drawing by Gary Grady @GaryJGrady\">\n",
"\n",
"I prepared by loading up my [**AdventUtils.ipynb**](AdventUtils.ipynb) notebook from last year:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"ename": "NameError",
"evalue": "name 'mapt' is not defined",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)",
"\u001b[0;32m~/Google Drive/Python/AdventUtils.ipynb\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m## TESTS\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0;32massert\u001b[0m \u001b[0mparse\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"hello\\nworld\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mshow\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;34m'hello'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'world'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 4\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0mparse\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"123\\nabc7\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdigits\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mshow\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m2\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m3\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0;36m7\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0;32massert\u001b[0m \u001b[0mtruncate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'hello world'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m99\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m'hello world'\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;32m~/Google Drive/Python/AdventUtils.ipynb\u001b[0m in \u001b[0;36mparse\u001b[0;34m(day_or_text, parser, sep, show)\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[0mtext\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mget_text\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mday_or_text\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 9\u001b[0m \u001b[0mprint_parse_items\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Puzzle input'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtext\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msplitlines\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mshow\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'line'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 10\u001b[0;31m \u001b[0mrecords\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmapt\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mparser\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtext\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrstrip\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msplit\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msep\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 11\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mparser\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 12\u001b[0m \u001b[0mprint_parse_items\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Parsed representation'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mrecords\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mshow\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34mf'{type(records[0]).__name__}'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;31mNameError\u001b[0m: name 'mapt' is not defined"
]
}
],
"source": [
"%run AdventUtils.ipynb"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"You might want to [take a look](AdventUtils.ipynb) to see how the `parse` and `answer` functions work, since they will be used for each day's puzzles. You'll really have to read [each day's puzzle description](https://adventofcode.com/2022/day/1). Each solution will have three parts:\n",
"- **Reading the Input**, e.g. for Day 1, `in1 = parse(1, ints, sep=paragraph)`. The function `parse` splits the input file for day 1 into records (by default each line is a record, but the `sep` keyword argument can be used to split by paragraph or other separators), and then applies a function (here `ints`, which returns a tuple of all integers in a string) to each record. `parse` prints the first few lines of the input file and the first few records of the parsed result.\n",
"- **Solving Part One**, e.g. `answer(1.1, ..., lambda: ...)`. The function `answer` takes three arguments:\n",
" 1. The puzzle we are answering, in the form *day*.*part*\n",
" 2. The correct answer as verified by AoC (recorded here so that if I modify and re-run the notebook, I can verify that it still works), \n",
" 3. A function to call to compute the answer. (It is passed as a function so we can time how long it takes to run.)\n",
"- **Solving Part Two**, e.g. `answer(1.2, ..., lambda: ...)`.\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 1](https://adventofcode.com/2022/day/1): Calorie Counting\n",
"\n",
"There is a complex backstory involving food for the elves and calories, but computationally all we have to know is that the input is a sequence of paragraphs, where each paragraph contains some integers. My `parse` function knows how to handle that:"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 2275 lines:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"15931\n",
"8782\n",
"16940\n",
"14614\n",
"\n",
"4829\n",
"...\n"
]
},
{
"ename": "NameError",
"evalue": "name 'mapt' is not defined",
"output_type": "error",
"traceback": [
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
"\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)",
"\u001b[0;32m<ipython-input-2-dd26501d74ae>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0min1\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mparse\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mints\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mparagraphs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
"\u001b[0;32m<ipython-input-1-8de51a529160>\u001b[0m in \u001b[0;36mparse\u001b[0;34m(day_or_text, parser, sep, show)\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[0mtext\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mget_text\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mday_or_text\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 9\u001b[0m \u001b[0mprint_parse_items\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Puzzle input'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtext\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msplitlines\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mshow\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'line'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 10\u001b[0;31m \u001b[0mrecords\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmapt\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mparser\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtext\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrstrip\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msplit\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msep\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 11\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mparser\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 12\u001b[0m \u001b[0mprint_parse_items\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Parsed representation'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mrecords\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mshow\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34mf'{type(records[0]).__name__}'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
"\u001b[0;31mNameError\u001b[0m: name 'mapt' is not defined"
]
}
],
"source": [
"in1 = parse(1, ints, paragraphs)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Part 1: Find the Elf carrying the most Calories. How many total Calories is that Elf carrying?\n",
"\n",
"Find the maximum sum among all the tuples:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"answer(1.1, 70116, lambda: max(sum(elf) for elf in in1))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Part 2: Find the top three Elves carrying the most Calories. How many Calories are those Elves carrying in total?\n",
"\n",
"Find the sum of the 3 biggest sums:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"answer(1.2, 206582, lambda: sum(sorted(sum(elf) for elf in in1)[-3:]))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To be clear, here is exactly what I did to solve the day's puzzle:\n",
"\n",
"1. Typed and executed `in1 = parse(1, ints, paragraphs)` in a Jupyter Notebook cell, and examined the output. Looked good to me.\n",
"2. Solved Part 1: typed and executed `max(sum(elf) for elf in in1)` in a cell, and saw the output, `70116`.\n",
"3. Copy/pasted `70116` into the [AoC Day 1](https://adventofcode.com/2022/day/1) input box and submitted it.\n",
"4. Verified that AoC agreed the answer was correct. (On some other days, the first such submission was not correct.)\n",
"5. Typed and executed `answer(1.1, 70116, lambda: max(sum(elf) for elf in in1))` in a cell, for when I re-run the notebook.\n",
"6. Repeated steps 25 for Part 2."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src=\"https://pbs.twimg.com/media/Fi6Ryc0XEBIHBXq?format=jpg&name=small\" title=\"Drawing by Gary Grady @GaryJGrady\" width=400>"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 2](https://adventofcode.com/2022/day/2): Rock Paper Scissors \n",
"\n",
"The input is two one-letter strings per line indicating the two player's plays:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"in2 = parse(2, atoms)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Part 1: What would your total score be if everything goes exactly according to your strategy guide?\n",
"\n",
"One confusing aspect: there are multiple encodings. Rock/Paper/Scissors corresponds to A/B/C, and X/Y/Z, and scores of 1/2/3. I decided the least confusing approach would be to translate everything to 1/2/3:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"RPS = Rock, Paper, Scissors = 1, 2, 3\n",
"rps_winner = {Rock: Paper, Paper: Scissors, Scissors: Rock}\n",
"\n",
"def rps_score(you: int, me: int) -> int:\n",
" \"\"\"My score for a round is my play plus 3 for draw and 6 for win.\"\"\"\n",
" return me + (6 if rps_winner[you] == me else 3 if me == you else 0)\n",
" \n",
"answer(2.1, 13268, lambda: sum(rps_score('.ABC'.index(a), '.XYZ'.index(x)) for a, x in in2))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Part 2: What would your total score be if everything goes exactly according to your strategy guide?\n",
"\n",
"In Part 2 the X/Y/Z does not mean that I should play rock/paper/scissors; rather it means that I should lose/draw/win:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"rps_loser = {rps_winner[x]: x for x in RPS} # Invert the dict\n",
"\n",
"def rps_score2(you: int, x: Char) -> int:\n",
" \"\"\"First letter means A=Rock/B=Paper/C=Scissors; second means X=lose/Y=draw/Z=win.\"\"\"\n",
" me = rps_loser[you] if x == 'X' else you if x == 'Y' else rps_winner[you]\n",
" return rps_score(you, me)\n",
"\n",
"answer(2.2, 15508, lambda: sum(rps_score2('.ABC'.index(a), x) for a, x in in2))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 3](https://adventofcode.com/2022/day/3): Rucksack Reorganization\n",
"\n",
"Each line of input is just a string of letters; the simplest input to parse:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"in3 = parse(3)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Part 1: Find the item type that appears in both compartments of each rucksack. What is the sum of the priorities of those item types?\n",
"\n",
"The two \"compartments\" are the two halves of the string. Find the common item by set intersection. The function `the` makes sure there is exactly one letter in the interesection:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def common_item(rucksack: str) -> Char:\n",
" \"\"\"The one letter that appears in both left and right halves of the input string.\"\"\"\n",
" left, right = split_at(rucksack, len(rucksack) // 2)\n",
" return the(set(left) & set(right))\n",
"\n",
"priority = {c: i + 1 for i, c in enumerate(string.ascii_letters)}\n",
"\n",
"answer(3.1, 8401, lambda: sum(priority[common_item(rucksack)] for rucksack in in3))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Part 2: Find the item type that corresponds to the badges of each three-Elf group. What is the sum of the priorities of those item types?\n",
"\n",
"My utility function `batched(in3, 3)` (from the [itertools recipes](https://docs.python.org/3/library/itertools.html#itertools-recipes) groups a sequence into subsequences of length 3; then we find the intersection and get its priority:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"answer(3.2, 2641, lambda: sum(priority[the(intersection(group))] for group in batched(in3, 3)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src=\"https://pbs.twimg.com/media/FjE7eyPWAAAy2be?format=jpg&name=small\" title=\"Drawing by Gary Grady @GaryJGrady\" width=500>"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 4](https://adventofcode.com/2022/day/4): Camp Cleanup\n",
"\n",
"Each input line corresponds to two ranges of integers, which I'll represent with a 4-tuple of endpoints:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"in4 = parse(4, positive_ints)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Part 1: In how many assignment pairs does one range fully contain the other?\n",
"\n",
"I could have turned each range into a set of integers and compared the sets, but I was concerned that a huge range would mean a huge set, so instead I directly compare the endpoints of the ranges:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def fully_contained(lo, hi, LO, HI) -> bool:\n",
" \"\"\"Is the range `lo-hi` fully contained in `LO-HI`, or vice-veresa?\"\"\"\n",
" return (lo <= LO <= HI <= hi) or (LO <= lo <= hi <= HI)\n",
"\n",
"answer(4.1, 477, lambda: quantify(fully_contained(*line) for line in in4))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Part 2: In how many assignment pairs do the ranges overlap?"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def overlaps(lo, hi, LO, HI) -> bool:\n",
" \"\"\"Do the two ranges have any overlap?\"\"\"\n",
" return (lo <= LO <= hi) or (LO <= lo <= HI)\n",
"\n",
"answer(4.2, 830, lambda: quantify(overlaps(*line) for line in in4))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 5](https://adventofcode.com/2022/day/5): Supply Stacks\n",
"\n",
"My `parse` function is primarily intended for the case where every record is parsed the same way. In today's puzzle, the input has two sections (in two paragraphs), each of which should be parsed differently. The function `parse_sections` is designed to handle this case. It takes as input a list of parsers (in this case two of them), which will be applied in order to parse the corresponding section:\n",
"- The first section is a **diagram**, which is parsed by picking out the characters in each stack; that is, in columns 1 and every 4th column after. \n",
"- The second section is a list of **moves**, which can be parsed with `ints` to get a 3-tuple of numbers. \n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"in5 = parse(5, parse_sections([lambda line: line[1::4], ints]), sep=paragraphs, show=12)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Part 1: After the rearrangement procedure completes, what crate ends up on top of each stack?\n",
"\n",
"Rearranging means repeatedly popping a crate from one stack and putting it on top of another stack, according to the move commands:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def rearrange(diagram, moves) -> str:\n",
" \"\"\"Given a diagram of crates in stacks, apply move commands.\n",
" Then return a string of the crates that are on top of each stack.\"\"\"\n",
" stacks = {int(row[-1]): [L for L in reversed(row[:-1]) if L != ' '] for row in T(diagram)}\n",
" for (n, source, dest) in moves:\n",
" for _ in range(n):\n",
" stacks[dest].append(stacks[source].pop())\n",
" return cat(stacks[i].pop() for i in stacks)\n",
"\n",
"answer(5.1, 'SHQWSRBDL', lambda: rearrange(*in5))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Part 2: After the rearrangement procedure completes, what crate ends up on top of each stack?\n",
"\n",
"In part 1, when *n* crates were moved with a model 9000 crane, it was done one-at-a-time, so the stack ends up reversed at its destination. In part 2 we have the more advanced model 9001 crane, which can lift all *n* crates at once, and place them down without reversing them. I'll rewrite `rearrange` to handle either way. I'll rerun part 1 to make sure the new function definition is backwards compatible."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def rearrange(diagram, moves, model=9000) -> str:\n",
" stacks = {int(row[-1]): [L for L in row[-1::-1] if L != ' '] for row in T(diagram)}\n",
" for (n, source, dest) in moves:\n",
" stacks[source], crates = split_at(stacks[source], -n)\n",
" if model == 9000: crates = crates[::-1]\n",
" stacks[dest].extend(crates)\n",
" return cat(stacks[i].pop() for i in stacks)\n",
"\n",
"answer(5.1, 'SHQWSRBDL', lambda: rearrange(*in5))\n",
"answer(5.2, 'CDTQZHBRS', lambda: rearrange(*in5, model=9001))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 6](https://adventofcode.com/2022/day/6): Tuning Trouble\n",
"\n",
"The input is a single line of characters:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"in6 = parse(6)[0]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Part 1: How many characters need to be processed before the first start-of-packet marker is detected?\n",
"\n",
"A start-of-packet marker is when there are *n* distinct characters in a row. I initially made a mistake: I read the instructions hastily and assumed they were asking for the *start* of the start-of-packet marker, not the *end* of it. When AoC told me I had the wrong answer, I went back and figured it out."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def first_marker(stream, n=4) -> int:\n",
" \"\"\"The number of characters read before the first start-of-packet marker is detected.\"\"\"\n",
" return first(i + n for i in range(len(stream)) if len(set(stream[i:i+n])) == n)\n",
"\n",
"answer(6.1, 1987, lambda: first_marker(in6, 4))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Part 2: How many characters need to be processed before the first start-of-message marker is detected?\n",
"\n",
"Now we're looking for 14 distinct characters, not just 4."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"answer(6.2, 3059, lambda: first_marker(in6, 14))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src=\"https://pbs.twimg.com/media/FjUoJ0TXEBIHU46?format=jpg&name=small\" title=\"Drawing by Gary Grady @GaryJGrady\" width=400>"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 7](https://adventofcode.com/2022/day/7): No Space Left On Device \n",
"\n",
"The input is a sequence of shell commands; I'll make `parse` split each line into words:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"in7 = parse(7, str.split)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Part 1: Find all of the directories with a total size of at most 100000. What is the sum of the total sizes of those directories?\n",
"\n",
"I'll keep track of a stack of directories (as Unix/Linux does with the `pushd`, `popd`, and `dirs` commands). All I need to track is `cd` commands (which change the `dirs` stack) and file size listings. I can ignore `ls` command lines and \"`dir` *name*\" output. The `browse` command examines the lines of the transcript and returns a Counter of `{directory_name: total_size}`. From that Counter I can sum the directory sizes that are under 100,000. "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def browse(transcript) -> Counter:\n",
" \"\"\"Return a Counter of {directory_name: total_size}, as revealed by the transcript of commands and output.\"\"\"\n",
" dirs = ['/'] # A stack of directories\n",
" sizes = Counter() # Mapping of directory name to total size\n",
" for tokens in transcript:\n",
" if tokens[0].isnumeric():\n",
" for dir in dirs: # All parent directories get credit for this file's size\n",
" sizes[dir] += int(tokens[0])\n",
" elif tokens[0] == '$' and tokens[1] == 'cd':\n",
" dir = tokens[2]\n",
" if dir == '/':\n",
" dirs = ['/']\n",
" elif dir == '..':\n",
" dirs.pop()\n",
" else:\n",
" dirs.append(dirs[-1] + dir + '/')\n",
" return sizes \n",
"\n",
"answer(7.1, 1232307, lambda: sum(v for v in browse(in7).values() if v <= 100_000))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Part 2: Find the smallest directory that, if deleted, would free up enough space on the filesystem to run the update. What is the total size of that directory?"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def free_up(transcript, available=70_000_000, needed=30_000_000) -> int:\n",
" \"\"\"What is the size of the smallest directory you can delete to free up enough space?\"\"\"\n",
" sizes = browse(transcript)\n",
" unused = available - sizes['/']\n",
" return min(sizes[d] for d in sizes if unused + sizes[d] >= needed)\n",
"\n",
"answer(7.2, 7268994, lambda: free_up(in7))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 8](https://adventofcode.com/2022/day/8): Treetop Tree House\n",
"\n",
"The input is a grid of heights of trees; my `Grid` class handles this well:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"in8 = Grid(parse(8, digits))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Part 1: Consider your map; how many trees are visible from outside the grid?\n",
"\n",
"In the worst case this is *O*(*n*<sup>2</sup>), so I don't feel too bad about taking the brute force approach of considering every location in the grid, and checking if for **any** direction, **all** the points in that direction have a shorter tree:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def visible_from_outside(grid) -> int:\n",
" \"\"\"How many points on grid are visible from the outside?\n",
" Points such that, for some direction, all the points in that direction have a shorter tree.\"\"\"\n",
" return quantify(any(all(grid[p] < grid[loc] for p in go_in_direction(loc, dir, grid))\n",
" for dir in directions4)\n",
" for loc in grid)\n",
"\n",
"def go_in_direction(start, direction, grid) -> Iterable[Point]:\n",
" \"\"\"All the points in grid that are beyond `start` in `direction`.\"\"\"\n",
" (x, y), (dx, dy) = start, direction\n",
" while True:\n",
" (x, y) = (x + dx, y + dy)\n",
" if (x, y) not in grid:\n",
" return\n",
" yield (x, y)\n",
"\n",
"answer(8.1, 1829, lambda: visible_from_outside(in8))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Part 2: Consider each tree on your map. What is the highest scenic score possible for any tree?\n",
"\n",
"If I had chosen better abstraction for Part 1, perhaps I could re-use some \"visible\" function. As it is, I can only re-use `go_in_direction`:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def scenic_score(loc, grid) -> int:\n",
" \"\"\"The product of the number of trees you can see in each of the 4 directions.\"\"\"\n",
" return prod(viewing_distance(loc, direction, grid) for direction in directions4)\n",
"\n",
"def viewing_distance(loc, direction, grid):\n",
" \"\"\"How many trees can you see from this location in this direction?\"\"\"\n",
" seen = 0\n",
" for seen, p in enumerate(go_in_direction(loc, direction, grid), 1):\n",
" if grid[p] >= grid[loc]:\n",
" break\n",
" return seen\n",
"\n",
"answer(8.2, 291840, lambda: max(scenic_score(loc, in8) for loc in in8))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Part 3: Exploration"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"*Note*: Up to now, I haven't worried about the efficiency of the code, since every day's code ran in about a millisecond. But today took 50 times longer, so I'm starting to get nervous. Maybe in the coming days I will need to be more aware of efficiency issues.\n",
"\n",
"I can plot the trees. Darker green means taller, and the red dot is the most scenic spot. We see that the taller growth is towards the center of the forest:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"square_plot([max(in8, key=lambda p: scenic_score(p, in8))], 'ro',\n",
" extra=lambda: plt.scatter(*T(in8), c=list(in8.values()), \n",
" cmap=plt.get_cmap('YlGn')))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 9](https://adventofcode.com/2022/day/9): Rope Bridge\n",
"\n",
"The input consists of command lines, which we can parse as one tuple of two atoms (a command name and an integer) per line:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"in9 = parse(9, atoms)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"These are motion commands for the head of a rope; the tail (one knot away) must follow, so that it is always on or adjacent to the head's location.\n",
"\n",
"#### Part 1: Simulate your complete hypothetical series of motions. How many positions does the tail of the rope visit at least once?\n",
"\n",
"The rules for how the tail moves are a bit tricky, but otherwise the control flow is easy. I'll return the set of visited squares, in case I need it in part 2, but for this part I just need the size of the set. I provide for an optional starting position; this is arbitrary, but it makes it eeasier to follow the example in the puzzle description if I start at the same place they start at."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def move_rope(motions, start=(0, 4)) -> Set[Point]:\n",
" \"\"\"Move rope according to `motions`; return set of points visited by tail.\"\"\"\n",
" deltas = dict(R=East, L=West, U=North, D=South)\n",
" H = T = start # Head and Tail oof the rope\n",
" visited = {start}\n",
" for (op, n) in motions:\n",
" for _ in range(n):\n",
" H = add(H, deltas[op])\n",
" T = move_tail(T, H)\n",
" visited.add(T)\n",
" return visited\n",
"\n",
"def move_tail(T: Point, H: Point) -> Point:\n",
" \"\"\"Move tail to be close to head if it is not already adjacent.\"\"\"\n",
" dx, dy = sub(H, T)\n",
" if max(abs(dx), abs(dy)) > 1:\n",
" if dx: # Different column\n",
" T = add(T, (sign(dx), 0))\n",
" if dy: # Different row\n",
" T = add(T, (0, sign(dy)))\n",
" return T\n",
" \n",
"answer(9.1, 6236, lambda: len(move_rope(in9, (0, 4))))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Part 2: Simulate your complete series of motions on a larger rope with ten knots. How many positions does the tail of the rope visit at least once?\n",
"\n",
"I'll re-write `move_rope` to take an optional argument giving the number of knots in the rope. Then instead of just one `move_tail` per loop, I'll move all the non-head knots in the rope, each one to follow the one immediately in front of it. I'll show that the re-write is backwards compatible by repeating the two-knot solution."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def move_rope(motions, start=(0, 4), knots=2) -> Set[Point]:\n",
" deltas = dict(R=East, L=West, U=North, D=South)\n",
" rope = [start] * knots\n",
" visited = {start}\n",
" for (op, n) in motions:\n",
" for _ in range(n):\n",
" rope[0] = add(rope[0], deltas[op])\n",
" for k in range(1, knots):\n",
" rope[k] = move_tail(rope[k], rope[k - 1])\n",
" visited.add(rope[-1])\n",
" return visited\n",
"\n",
"answer(9.1, 6236, lambda: len(move_rope(in9, (0, 4))))\n",
"answer(9.2, 2449, lambda: len(move_rope(in9, (0, 4), knots=10)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Part 3: Exploration\n",
"\n",
"Because I chose to return the set of visited points, I can plot the tail of the rope (for various size ropes):"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"square_plot(move_rope(in9, knots=2), '.');"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"square_plot(move_rope(in9, knots=10), '.');"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"square_plot(move_rope(in9, knots=20), '.');"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src=\"https://pbs.twimg.com/media/FjkFSH_XEAM5BRy?format=jpg&name=medium\" width=500 title=\"Drawing by Gary Grady @GaryJGrady\">"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# [Day 10](https://adventofcode.com/2022/day/10): Cathode-Ray Tube \n",
"\n",
"Another puzzle involving running an interpreter on a program. The program is a sequence of lines, each containing one or two atoms:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"in10 = parse(10, atoms)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We're never sure what we will need in Part 2, so I'll make the program interpreter output the cycle number and value of X for every cycle. For Part 1, we sum the product of these two for cycles in {20, 60, 100, ... 220}:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def run(program) -> Iterable[Tuple[int, int]]:\n",
" \"\"\"Execute the program, oputputing (cycle_number, X_register_value) on each cycle.\n",
" Remember that an `addx` instruction takes 2 cycles.\"\"\"\n",
" X = 1\n",
" cycle = 0\n",
" results = []\n",
" for (op, *args) in program:\n",
" cycle += 1\n",
" results.append((cycle, X))\n",
" if op == 'addx':\n",
" cycle += 1\n",
" results.append((cycle, X))\n",
" X += args[0]\n",
" return results\n",
"\n",
"answer(10.1, 12560, lambda: sum(c * X for c, X in run(in10) if c in range(20, 221, 40)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"For Part 2 I'm glad I kept all the `(cycle, X)` pairs. I just need to map `cycle` to `(x, y)` positions on the screen, and plot the result. Then I'll use my eyeballs (not an OCR program) to determine what letters are indicated."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def render(program):\n",
" \"\"\"As the cycle number scans a 40-pixel wide CRT, turn on pixels\n",
" where register X and the scan position differ by 1 or less.x\"\"\"\n",
" points = []\n",
" for (c, X) in run(program):\n",
" x, y = (c - 1) % 40, (c - 1) // 40\n",
" if abs(X - x) <= 1:\n",
" points.append((x, y))\n",
" square_plot(points)\n",
" \n",
"answer(10.2, \"PLPAFBCL\", lambda: render(in10) or \"PLPAFBCL\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Summary\n",
"\n",
"The results so far, with run times:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"answers"
]
},
{
"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
}

596
ipynb/AdventUtils.ipynb Normal file
View File

@ -0,0 +1,596 @@
{
"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": 1,
"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": 2,
"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",
" print_parse_items('Puzzle input', text.splitlines(), show, 'line')\n",
" records = mapt(parser, text.rstrip().split(sep))\n",
" if parser != str:\n",
" print_parse_items('Parsed representation', records, show, f'{type(records[0]).__name__}')\n",
" return records\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 print_parse_items(source, items, show:int, name:str, sep=\"─\"*100):\n",
" \"\"\"Print verbose output from `parse` for lines or records.\"\"\"\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))\n",
" if show < len(items):\n",
" print('...')\n",
" \n",
"def truncate(object, width=100) -> 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": 3,
"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'[+-]?\\d+\\.?\\d*|\\w+', 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\n",
" \n",
"def mapt(function: Callable, sequence) -> tuple:\n",
" \"\"\"`map`, with the result as a tuple.\"\"\"\n",
" return tuple(map(function, sequence))"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"## TESTS\n",
"\n",
"assert parse(\"hello\\nworld\", show=0) == ('hello', 'world')\n",
"assert parse(\"123\\nabc7\", digits, show=0) == ((1, 2, 3), (7,))\n",
"assert truncate('hello world', 99) == 'hello world'\n",
"assert truncate('hello world', 8) == 'hell ...'\n",
"\n",
"assert atoms('hello, cruel_world! 24-7') == ('hello', 'cruel_world', 24, -7)\n",
"assert words('hello, cruel_world! 24-7') == ('hello', 'cruel', 'world')\n",
"assert digits('hello, cruel_world! 24-7') == (2, 4, 7)\n",
"assert ints('hello, cruel_world! 24-7') == (24, -7)\n",
"assert positive_ints('hello, cruel_world! 24-7') == (24, 7)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Daily Answers\n",
"\n",
"Here is the `answer` function, which gives verification of a correct computation (or an error message for an incorrect computation), times how long the computation took, ans stores the result in the dict `answers`."
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"# `answers` is a dict of {puzzle_number_id: message_about_results}\n",
"answers = {} \n",
"\n",
"def answer(puzzle, correct, code: callable) -> str:\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 truncate(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",
" return msg"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Additional utility functions \n",
"\n",
"All of the following have been used in solutions to multiple puzzles in the past, so I pulled them all in here:"
]
},
{
"cell_type": "code",
"execution_count": 6,
"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",
"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": "code",
"execution_count": 17,
"metadata": {},
"outputs": [],
"source": [
"## TESTS\n",
"\n",
"assert quantify(words('This is a test'), str.islower) == 3\n",
"assert mapt(first, words('This is a test')) == ('T', 'i', 'a', 't')\n",
"assert multimap(((i % 3), i) for i in range(9)) == {0: [0, 3, 6], 1: [1, 4, 7], 2: [2, 5, 8]}\n",
"assert prod([2, 3, 5]) == 30\n",
"assert total(Counter('hello, world')) == 12\n",
"assert cover(3, 1, 4, 1, 5) == range(1, 6)\n",
"assert minmax([3, 1, 4, 1, 5, 9]) == (1, 9)\n",
"assert first('abc') == 'a'\n",
"assert T([(1, 2, 3), (4, 5, 6)]) == [(1, 4), (2, 5), (3, 6)]\n",
"assert the({1}) == 1\n",
"assert split_at('hello, world', 6) == ('hello,', ' world')\n",
"assert batched('abcdefghi', 3) == ['abc', 'def', 'ghi']\n",
"assert list(sliding_window('abcdefghi', 3)) == ['abc', 'bcd', 'cde', 'def', 'efg', 'fgh', 'ghi']\n",
"assert is_int(-42) and not is_int('one')\n",
"assert sign(-42) == -1 and sign(0) == 0 and sign(42) == +1\n",
"assert append(([1, 2], [3, 4], [5, 6])) == [1, 2, 3, 4, 5, 6]\n",
"assert union([{1, 2}, {3, 4}, {5, 6}]) == {1, 2, 3, 4, 5, 6}\n",
"assert intersection([{1, 2, 3}, {2, 3, 4}, {2, 4, 6, 8}]) == {2}\n",
"assert clock_mod(24, 12) == 12 and 24 % 12 == 0\n",
"assert list(flatten(['abc', 'def', '123'])) == ['a', 'b', 'c', 'd', 'e', 'f', '1', '2', '3']\n",
"assert cat(['hello', 'world']) == 'helloworld'"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Points on a Grid\n",
"\n",
"Many puzzles seem to involve a two-dimensional rectangular grid with integer coordinates. First we'll define the two-dimensional `Point`, then the `Grid`."
]
},
{
"cell_type": "code",
"execution_count": 7,
"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": 16,
"metadata": {},
"outputs": [],
"source": [
"## TESTS\n",
"\n",
"p, q = (0, 3), (4, 0)\n",
"assert Y_(p) == 3 and X_(q) == 4\n",
"assert distance(p, q) == 5\n",
"assert manhatten_distance(p, q) == 7\n",
"assert add(p, q) == (4, 3)\n",
"assert sub(p, q) == (-4, 3)\n",
"assert add(North, South) == (0,0)"
]
},
{
"cell_type": "code",
"execution_count": 8,
"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\n",
"\n",
"Many puzzles involve searching over a branching tree of possibilities. For many puzzles, an ad-hoc solution is fine. But when there is a larger search space, it is useful to have a pre-defined efficient best-first search algorithm, and in particular an A* search, which incorporates a heuristic function to estimate the remaining distance to the goal. This is a somewhat heavy-weight approach, as it requires the solver to define a subclass of `SearchProblem`."
]
},
{
"cell_type": "code",
"execution_count": 18,
"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) -> 'Node':\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": 10,
"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 state 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": 11,
"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)"
]
}
],
"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
}