Add files via upload

This commit is contained in:
Peter Norvig 2024-12-09 13:43:31 -08:00 committed by GitHub
parent ecefbabb55
commit a2d090645d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -118,7 +118,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 1.1: .0002 seconds, answer 1830467 ok" "Puzzle 1.1: .0003 seconds, answer 1830467 ok"
] ]
}, },
"execution_count": 3, "execution_count": 3,
@ -312,7 +312,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 2.2: .0076 seconds, answer 328 ok" "Puzzle 2.2: .0081 seconds, answer 328 ok"
] ]
}, },
"execution_count": 9, "execution_count": 9,
@ -400,7 +400,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 3.1: .0015 seconds, answer 156388521 ok" "Puzzle 3.1: .0018 seconds, answer 156388521 ok"
] ]
}, },
"execution_count": 12, "execution_count": 12,
@ -513,7 +513,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 28, "execution_count": 16,
"id": "72d48abb-7a82-452f-b91d-838b3836a90f", "id": "72d48abb-7a82-452f-b91d-838b3836a90f",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
@ -527,22 +527,22 @@
"\n", "\n",
"def grid_can_spell(grid, start, dir, word):\n", "def grid_can_spell(grid, start, dir, word):\n",
" \"\"\"Does `word` appear in grid starting at `start` and going in direction `dir`?\"\"\"\n", " \"\"\"Does `word` appear in grid starting at `start` and going in direction `dir`?\"\"\"\n",
" return all(grid[add(start, mul(dir, i))] == word[i] for i in range(len(word)))" " return all(grid[add2(start, mul(dir, i))] == word[i] for i in range(len(word)))"
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 29, "execution_count": 17,
"id": "6175362b-d8b4-45d1-b70c-d8575a0fe188", "id": "6175362b-d8b4-45d1-b70c-d8575a0fe188",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 4.1: .1282 seconds, answer 2401 ok" "Puzzle 4.1: .0785 seconds, answer 2401 ok"
] ]
}, },
"execution_count": 29, "execution_count": 17,
"metadata": {}, "metadata": {},
"output_type": "execute_result" "output_type": "execute_result"
} }
@ -577,7 +577,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 4.2: .0785 seconds, answer 1822 ok" "Puzzle 4.2: .0669 seconds, answer 1822 ok"
] ]
}, },
"execution_count": 18, "execution_count": 18,
@ -608,6 +608,8 @@
"\n", "\n",
"Today's puzzle involves a **sleigh launch safety manual** that needs to be updated. The day's input is in two parts: the first a set of **rules** such as \"47|53\", which means that page 47 must be printed before page 53; and the second a list of **updates** of the form \"75,47,61,53,29\", meaning that those pages are to be printed in that order.\n", "Today's puzzle involves a **sleigh launch safety manual** that needs to be updated. The day's input is in two parts: the first a set of **rules** such as \"47|53\", which means that page 47 must be printed before page 53; and the second a list of **updates** of the form \"75,47,61,53,29\", meaning that those pages are to be printed in that order.\n",
"\n", "\n",
"<img src=\"https://pbs.twimg.com/media/GeEU0XgWAAARMw-?format=jpg&name=medium\" width=400>\n",
"\n",
"I mostly like my `parse` function, but I admit it is not ideal when an input file has two parts like this. I'll parse the two parts into paragraphs, and then call `parse` again on each paragraph:" "I mostly like my `parse` function, but I admit it is not ideal when an input file has two parts like this. I'll parse the two parts into paragraphs, and then call `parse` again on each paragraph:"
] ]
}, },
@ -706,7 +708,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 5.1: .0010 seconds, answer 5762 ok" "Puzzle 5.1: .0015 seconds, answer 5762 ok"
] ]
}, },
"execution_count": 21, "execution_count": 21,
@ -791,7 +793,7 @@
{ {
"data": { "data": {
"text/plain": [ "text/plain": [
"Puzzle 5.2: .0016 seconds, answer 4130 ok" "Puzzle 5.2: .0017 seconds, answer 4130 ok"
] ]
}, },
"execution_count": 24, "execution_count": 24,
@ -814,17 +816,702 @@
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"id": "c3317844-2b4a-4756-8a59-b765aa467445", "id": "38258423-e3b8-4bae-8aeb-28f07f0d5a35",
"metadata": {}, "metadata": {},
"source": [ "source": [
"# Summary\n", "# [Day 6](https://adventofcode.com/2024/day/6): Guard Gallivant\n",
"\n", "\n",
"So far, I've solved all the puzzles, each with a run time of less than a tenth of a second." "Today's input is a 2D map of the manufacturing lab, with \".\" indicating an empty space, \"#\" indicating an obstruction, and \"^\" indicating the position of the security guard."
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": 25, "execution_count": 25,
"id": "6ec71cf8-c43d-457e-8e14-0e9eb99b956a",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 130 strs:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"........#........................................#......#........#.............................. ...\n",
"....................................#......#.....#............#.............#..........#........ ...\n",
"......................#.......................................................#................. ...\n",
".......#..#..#....#...#...#....#..............#......#.......#...#................#.......#..... ...\n",
"......................#....##...#.......#....#.......................................#.......... ...\n",
"...#............................#........................................#...................... ...\n",
"....................#............#...............#......#.........#...........#................. ...\n",
"............................#......#...#................#.............#......................... ...\n",
"...\n"
]
}
],
"source": [
"lab_grid = Grid(parse(6))"
]
},
{
"cell_type": "markdown",
"id": "4ba233f4-90aa-4249-9569-10288c34940d",
"metadata": {},
"source": [
"### Part 1: How many distinct positions will the guard visit before leaving the mapped area?\n",
"\n",
"The guard follows this protocol: If there is something directly in front of you, turn right 90 degrees.\n",
"Otherwise, take a step forward.\n",
"\n",
"I'll define `follow_path` to output a list of all the positions the guard occupies. I realize the puzzle is only asking for a count of the positions, but the path might be useful for Part 2, or for debugging, so I'll return it. I worried that it is also possible for a path to become a loop, but the problem statement says that can't happen, so I won't test for it."
]
},
{
"cell_type": "code",
"execution_count": 26,
"id": "aecb67fd-2c8a-40a8-9e67-e495b7cb8043",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 6.1: .0046 seconds, answer 5329 ok"
]
},
"execution_count": 26,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def follow_path(grid: Grid, guard='^', facing=North) -> List[Point]:\n",
" \"\"\"A list of all points in the path followed by the guard.\"\"\"\n",
" path = grid.findall(guard) # A one-element list of positions, e.g. [(3, 4)]\n",
" while True:\n",
" ahead = add2(path[-1], facing)\n",
" if ahead not in grid:\n",
" return path\n",
" elif grid[ahead] == '#':\n",
" facing = make_turn(facing, 'R')\n",
" else:\n",
" path.append(ahead)\n",
"\n",
"answer(6.1, 5329, lambda: len(set(follow_path(lab_grid))))"
]
},
{
"cell_type": "markdown",
"id": "eaf72ac3-ade0-4479-a090-1d0f292ecc27",
"metadata": {},
"source": [
"I initially had a **bug**; I asked for the length of the path, not the length of the **set** of positions in the path.\n",
" \n",
"### Part 2: How many different positions could you choose for an obstruction to put the guard in a loop?\n",
"\n",
"The historians would like to place a single obstacle so that the guard *will* get stuck in a loop, rather than exiting the grid. They want to know all possible positions for the obstacle. What do we know about such positions?\n",
"- An obstacle position must be somewhere on the guard's path, otherwise it would have no effect.\n",
"- The instructions say it can't be the guard's initial position.\n",
"- A loop is when the guard's path returns to the same position with the same facing. This suggests that my Part 1 solution was not completely helpful: to find duplicate positions in the path I would need a set of position/facing pairs, not just positions.\n",
"- Alternatively, I note that the grid is only of size 130 x 130; I could declare a loop after 130<sup>2</sup> steps; that seems simpler. \n",
"- The simplest approach for finding obstacles is to test each point on the path, one at a time.\n",
"- There are 5,329 such points, so the runtime should be about 5,000 times longer than Part 1; on the order of 10 seconds. I'll try it, and if it seems too slow, I'll try to think of something else."
]
},
{
"cell_type": "code",
"execution_count": 27,
"id": "fb992592-1b35-47c1-a1dc-8bb0c0653d6d",
"metadata": {},
"outputs": [],
"source": [
"def is_loopy_path(grid: Grid, guard_pos, facing=North) -> bool:\n",
" \"\"\"Does the path followed by the guard form a loop?\"\"\"\n",
" for step in range(prod(grid.size)):\n",
" ahead = add2(guard_pos, facing)\n",
" if ahead not in grid:\n",
" return False # Not a loop\n",
" elif grid[ahead] == '#':\n",
" facing = make_turn(facing, 'R')\n",
" else:\n",
" guard_pos = ahead\n",
" return True # Ran too many steps; must be a loop\n",
" \n",
"def find_loopy_obstacles(grid: Grid, guard='^', facing=North) -> Set[Point]:\n",
" \"\"\"All positions in which placing an obstacle would result in a loopy path for the guard.\"\"\"\n",
" guard_pos = the(grid.findall(guard))\n",
" obstacle_positions = set()\n",
" for pos in set(follow_path(grid)) - {guard_pos}:\n",
" grid[pos] = '#' # Place obstacle \n",
" if is_loopy_path(grid, guard_pos):\n",
" obstacle_positions.add(pos)\n",
" grid[pos] = '.' # Remove obstacle\n",
" return obstacle_positions"
]
},
{
"cell_type": "code",
"execution_count": 28,
"id": "0dfcbee9-7cfd-417c-8bd2-14544f0a100b",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 6.2: 14.9723 seconds, answer 2162 ok"
]
},
"execution_count": 28,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(6.2, 2162, lambda:\n",
" len(find_loopy_obstacles(lab_grid)))"
]
},
{
"cell_type": "markdown",
"id": "9f3ee6f9-7ec7-4248-ae52-1804fdc81dbd",
"metadata": {},
"source": [
"That was a bit slow, but I'll take it. I had a **bug** when I was keeping a set of previously visited states to detect loops; the bug went away when I switched to the step-count limit."
]
},
{
"cell_type": "markdown",
"id": "9eae8cf2-8c97-418e-b00b-3ea0187da526",
"metadata": {},
"source": [
"# [Day 7](https://adventofcode.com/2024/day/7): Bridge Repair\n",
"\n",
"The narrative for today involves calibrating a bridge, and our input consists of lines of integers, with a colon separating the first integer from the rest: "
]
},
{
"cell_type": "code",
"execution_count": 29,
"id": "c1c6cee8-122c-43c9-8c7d-ed8980ea2b76",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 850 strs:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"202998336: 686 9 7 62 2 673\n",
"19275222: 361 3 7 170 65 5 223\n",
"23101: 7 694 916 4 6\n",
"2042426: 6 34 2 423 3\n",
"40369523: 8 880 91 45 23\n",
"46629044796: 990 471 4 4 796\n",
"1839056: 3 42 2 4 3 258 703 4 8\n",
"26205: 2 9 5 9 9 4 3 7 44 5 8 7\n",
"...\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 850 tuples:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"(202998336, 686, 9, 7, 62, 2, 673)\n",
"(19275222, 361, 3, 7, 170, 65, 5, 223)\n",
"(23101, 7, 694, 916, 4, 6)\n",
"(2042426, 6, 34, 2, 423, 3)\n",
"(40369523, 8, 880, 91, 45, 23)\n",
"(46629044796, 990, 471, 4, 4, 796)\n",
"(1839056, 3, 42, 2, 4, 3, 258, 703, 4, 8)\n",
"(26205, 2, 9, 5, 9, 9, 4, 3, 7, 44, 5, 8, 7)\n",
"...\n"
]
}
],
"source": [
"calibrations = parse(7, ints)"
]
},
{
"cell_type": "markdown",
"id": "2e31d28f-97b1-4a3d-a298-18fcad297150",
"metadata": {},
"source": [
"### Part 1: What is the total calibration result of possibly true equations?\n",
"\n",
"Our task is to treat each line as an equation and find operators to balance it. An input line such as \"3267: 81 40 27\", can be made into the equation \"3267 = 81 + 40 * 27\", with the understanding that all evaluations are done left-to-right, so this is \"3267 = ((81 + 40) * 27)\". The two allowable operators are addition and multiplication. Our task is to compute the sum of all the equations that can be balanced.\n",
"\n",
"The straightforward approach would be to try both operators on every number. If there are *n* numbers on the right hand side of an equation then there will be 2<sup>*n*</sup> possible equations; is that going to be a problem?"
]
},
{
"cell_type": "code",
"execution_count": 30,
"id": "6fa3907c-0e1a-4d4a-9fc3-f809b9325674",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"13"
]
},
"execution_count": 30,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"max(map(len, calibrations))"
]
},
{
"cell_type": "markdown",
"id": "e0d9b0b2-fe1e-434e-b84e-c044da3d3673",
"metadata": {},
"source": [
"No; with 13 numbers on a line there are 11 places to choose an operator, and 2<sup>11</sup> = 2048; a small number. I'll define `can_be_calibrated` to keep a set of `results`, updating the set for each new number and each possible operator."
]
},
{
"cell_type": "code",
"execution_count": 31,
"id": "5dfe0edf-cf29-4623-bb2c-6180f832f4d7",
"metadata": {},
"outputs": [],
"source": [
"def can_be_calibrated(numbers: ints, operators=(operator.add, operator.mul)) -> bool:\n",
" \"\"\"Can the tuple of numbers be calibrated as a correct equation using '+' and '*' ?\"\"\"\n",
" target, first, *rest = numbers\n",
" results = {first}\n",
" for y in rest:\n",
" results = {op(x, y) for x in results for op in operators}\n",
" return target in results"
]
},
{
"cell_type": "code",
"execution_count": 32,
"id": "3085596d-f5ec-4ba8-b05a-cf70cf276a0c",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 7.1: .0483 seconds, answer 1985268524462 ok"
]
},
"execution_count": 32,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(7.1, 1985268524462, lambda:\n",
" sum(numbers[0] for numbers in calibrations if can_be_calibrated(numbers)))"
]
},
{
"cell_type": "markdown",
"id": "62a5fe5f-e23f-4420-87a8-47d8be02fbc0",
"metadata": {},
"source": [
"### Part 2: What is the total calibration result of possibly true equations, allowing concatenation?\n",
"\n",
"In Part 2, the equation \"192: 17 8 14\" can be balanced by using a concatenate operator, \"192 = ((17 || 8) + 14)\". With three operators, the equation with 11 operators now has 3<sup>11</sup> = 177,147, almost 100 times more than Part 1, so this will take a few seconds:"
]
},
{
"cell_type": "code",
"execution_count": 33,
"id": "5bdcb999-7f38-4814-bca5-13db88f4e214",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 7.2: 5.5535 seconds, answer 150077710195188 ok"
]
},
"execution_count": 33,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"operators3 = (operator.add, operator.mul, lambda x, y: int(str(x) + str(y)))\n",
" \n",
"answer(7.2, 150077710195188, lambda:\n",
" sum(numbers[0] for numbers in calibrations if can_be_calibrated(numbers, operators3)))"
]
},
{
"cell_type": "markdown",
"id": "2e5693b7-dab8-4f89-a000-c69ee75a11c9",
"metadata": {},
"source": [
"# [Day 8](https://adventofcode.com/2024/day/8): Resonant Collinearity\n",
"\n",
"Another grid input, this one a map of antenna locations. Each different non-\".\" character denotes an antenna of a given frequency."
]
},
{
"cell_type": "code",
"execution_count": 34,
"id": "cf6361a7-e3bc-42ec-ae16-f9eec166055e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 50 strs:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"..................................................\n",
".................................C................\n",
".e..........7O....................................\n",
".....................................z............\n",
"......................t.........C.......k.........\n",
"............h................................9....\n",
".............5.7....O.............9C..............\n",
".......5.O................T.......................\n",
"...\n"
]
}
],
"source": [
"antennas = Grid(parse(8))"
]
},
{
"cell_type": "markdown",
"id": "c8e1006d-37bc-432e-bf1f-7a639287382a",
"metadata": {},
"source": [
"### Part 1: How many unique locations within the bounds of the map contain an antinode?\n",
"\n",
"An **antinode** occurs at a point that is perfectly in line with two antennas of the same frequency, but only when one of the antennas is twice as far away as the other.\n",
"\n",
"That means that if two antennas are at points *A* and *B*, then the two antinodal points are at 2*A* - *B* and 2*B* - A. If there are three or more antennas with the same frequency then we consider each pair of them in turn. So all we have to do is group the antennas by frequency, compute the antinodes for each pair with the same frequency, and determine which of those antinodal points are on the grid."
]
},
{
"cell_type": "code",
"execution_count": 35,
"id": "33199a01-6e69-4d7d-aef2-e59b3b804398",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 8.1: .0007 seconds, answer 220 ok"
]
},
"execution_count": 35,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def antinodes(antennas: Grid) -> Set[Point]:\n",
" \"\"\"The set of all antinodal points in the grid.\n",
" (That is, points that are of distance d and 2d from same frequency antennas.)\"\"\"\n",
" return union(antinodes2(A, B, antennas)\n",
" for points in group_all(antennas).values()\n",
" for A, B in combinations(points, 2))\n",
"\n",
"def antinodes2(A: Point, B: Point, antennas: Grid) -> Set[Point]:\n",
" \"\"\"The set of antinodal points for two antenna points, A and B.\"\"\"\n",
" return {P for P in {sub(mul(A, 2), B), sub(mul(B, 2), A)}\n",
" if P in antennas}\n",
"\n",
"def group_all(antennas: Grid) -> Dict[Char, List[Point]]:\n",
" \"\"\"A dict of {frequency: [all_points_with_an_antenna_of_that_frequency]}.\"\"\"\n",
" frequencies = defaultdict(list)\n",
" for point, f in antennas.items():\n",
" if f != '.':\n",
" frequencies[f].append(point)\n",
" return frequencies\n",
"\n",
"answer(8.1, 220, \n",
" lambda: len(antinodes(antennas)))"
]
},
{
"cell_type": "markdown",
"id": "ff79d605-813a-46ac-8473-a1198be0e99f",
"metadata": {},
"source": [
"### How many unique locations within the bounds of the map contain an updated antinode?\n",
"\n",
"For Part 2, the updated definition of antinodes means that they can now occur at any point that is exactly on line with two antennas of the same frequency, regardless of distance. So if the two antennas are *A* and *B* then the antinbodal points are of the form *A* + *k*(*A* - *B*) for any scalar value *k* (positive or negative) that makes the resulting point fall within the grid."
]
},
{
"cell_type": "code",
"execution_count": 36,
"id": "d30f8ce9-f186-46a0-a2e7-f74eceae6905",
"metadata": {},
"outputs": [],
"source": [
"def updated_antinodes(antennas: Grid) -> Set[Point]:\n",
" \"\"\"The set of all updated antinodal points in the grid.\n",
" (That is, points that are on a line with two same frequency antennas.)\"\"\"\n",
" return union(updated_antinodes2(A, B, antennas)\n",
" for points in group_all(antennas).values()\n",
" for A, B in combinations(points, 2))\n",
"\n",
"def updated_antinodes2(A: Point, B: Point, antennas: Grid) -> Set[Point]:\n",
" \"\"\"The set of updated antinodal points for two antenna points, A and B.\"\"\"\n",
" antinodes = set()\n",
" D = sub(A, B)\n",
" for step in (+1, -1):\n",
" for k in count_from(0, step):\n",
" P = add2(A, mul(D, k))\n",
" if P in antennas:\n",
" antinodes.add(P)\n",
" else:\n",
" break\n",
" return antinodes\n",
" "
]
},
{
"cell_type": "code",
"execution_count": 37,
"id": "f232952c-5fc6-4696-a8b1-d0b54137ac02",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 8.2: .0022 seconds, answer 813 ok"
]
},
"execution_count": 37,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(8.2, 813, lambda:\n",
" len(updated_antinodes(antennas)))"
]
},
{
"cell_type": "markdown",
"id": "9696a986-b6a2-4530-b55b-9db959ef7485",
"metadata": {},
"source": [
"I got both of these right the first time (except for some simple typos like mismatched parens and typing `grid` when I meant the grid called `antennas`), but maybe I should have parameterized the first version of `antinodes` to allow for a different `antinodes2` function."
]
},
{
"cell_type": "markdown",
"id": "d4835cad-7777-4636-b9af-52cc9782b2b8",
"metadata": {},
"source": [
"# [Day 9](https://adventofcode.com/2024/day/9): Disk Fragmenter\n",
"\n",
"Today we're confronted with a computer disk that needs to be compacted to gain some contiguous free space. The contents of the disk is represented in the **disk map** format: a string of digits, where the digits alternate between the number of blocks of a file, followed by the number of blocks of free space. We'll parse that as a tuple of digits:"
]
},
{
"cell_type": "code",
"execution_count": 38,
"id": "0e944f9e-5c16-440c-b12e-178058a87048",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Puzzle input ➜ 1 str:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"692094513253604282899448234539616972499153261626907217394161512944107098953354935354419233821564 ...\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"Parsed representation ➜ 1 tuple:\n",
"────────────────────────────────────────────────────────────────────────────────────────────────────\n",
"(6, 9, 2, 0, 9, 4, 5, 1, 3, 2, 5, 3, 6, 0, 4, 2, 8, 2, 8, 9, 9, 4, 4, 8, 2, 3, 4, 5, 3, 9, 6, 1, ...\n"
]
}
],
"source": [
"disk_map = the(parse(9, digits))"
]
},
{
"cell_type": "markdown",
"id": "99d40379-65e1-4872-8c68-17ba4925c24e",
"metadata": {},
"source": [
"### Part 1: Compact the hard drive. What is the resulting filesystem checksum? \n",
"\n",
"The disk map \"`12345`\" means that there is 1 block for the first file (which has ID number 0), followed by 2 empty blocks, then 3 blocks for the second file (with ID number 1), followed by 4 empty blocks, and finally 5 blocks for the third file (with ID number 2). It makes sense to convert this into a **disk layout** format, which would be \"`0..111....22222`\", where \"`.`\" represents an empty block.\n",
"\n",
"To **compact** a disk layout, move file blocks one at a time starting by taking the rightmost non-empty block and moving it to the leftmost empty position; repeat until no more moves are possible.\n",
"\n",
"The final answer is a **checksum** of the compacted disk: the sum of the product of the block position times the file ID number for all non-empty blocks."
]
},
{
"cell_type": "code",
"execution_count": 39,
"id": "76e8454d-a2f3-4b6b-92df-182116cf46e0",
"metadata": {},
"outputs": [],
"source": [
"empty = '.'\n",
"\n",
"def disk_layout(disk_map: Ints) -> list:\n",
" \"\"\"Convert a disk map into a disk layout.\"\"\"\n",
" layout = []\n",
" for id, i in enumerate(range(0, len(disk_map), 2)):\n",
" layout.extend(disk_map[i] * [id])\n",
" if i + 1 < len(disk_map):\n",
" layout.extend(disk_map[i + 1] * [empty])\n",
" return layout\n",
"\n",
"def compact_layout(layout: list) -> list:\n",
" \"\"\"Mutate layout by moving blocks one at a time from the end to the leftmost free space.\"\"\"\n",
" N = len(layout)\n",
" free = -1 # Start looking for free space from the left\n",
" end = N # Start looking for non-empty blocks from the right\n",
" while True:\n",
" free = first(i for i in range(free + 1, N) if layout[i] is empty)\n",
" end = first(i for i in range(end - 1, 0, -1) if layout[i] is not empty)\n",
" if free is None or free >= end:\n",
" return layout\n",
" layout[free], layout[end] = layout[end], empty\n",
"\n",
"def checksum(layout: list) -> list:\n",
" \"\"\"The sum of the product of the block position times the file ID number for all non-empty blocks.\"\"\"\n",
" return sum(i * id for i, id in enumerate(layout) if id is not empty)"
]
},
{
"cell_type": "code",
"execution_count": 40,
"id": "2aa7e2b9-844e-49ed-b41b-4a4cecff86b7",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 9.1: .0430 seconds, answer 6332189866718 ok"
]
},
"execution_count": 40,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(9.1, 6332189866718, lambda:\n",
" checksum(compact_layout(disk_layout(disk_map))))"
]
},
{
"cell_type": "markdown",
"id": "2c05a497-cc66-4698-b88b-25c33eea224a",
"metadata": {},
"source": [
"### Part 2: Compact the hard drive with the new method. What is the resulting filesystem checksum? \n",
"\n",
"In Part 2, there is a new method of compacting the disk, where we move full files rather than a block at a time. Again we start on the right, and try to move a file to the leftmost position where it will fit. If there is no such position, the file doesn't move. `compact_layout2` implements this new method, performing a move by swapping two [**slices**](https://docs.python.org/3/library/functions.html#slice) of the disk layout: \n",
"\n",
" layout[file], layout[free] = layout[free], layout[file]`\n",
"\n",
"To find all the slices that indicate files, it is easier to run through the disk map than the disk layout. The function `file_slices` quickly finds all such slices.\n",
"\n",
"Finding a free space for a file is more difficult, because we need to find one that is big enough. I'll run through the whole layout from left-to-right each time. This will make it *O*(*n*<sup>2</sup>) rather than *O*(*n*), but hopefully it won't be too slow. (If I wanted to speed it up I could have an array of starting positions for each desired size of free space.)"
]
},
{
"cell_type": "code",
"execution_count": 41,
"id": "fcf4d832-3d7d-4987-aa57-e6e0f1df16bf",
"metadata": {},
"outputs": [],
"source": [
"def compact_layout2(disk_map: Ints) -> list:\n",
" \"\"\"Mutate layout by moving files one at a time from the end to the leftmost free space.\"\"\"\n",
" layout = disk_layout(disk_map)\n",
" for file in file_slices(disk_map):\n",
" free = find_freespace(layout, file)\n",
" if free:\n",
" layout[file], layout[free] = layout[free], layout[file]\n",
" return layout\n",
"\n",
"def file_slices(disk_map: Ints) -> List[slice]:\n",
" \"\"\"Given a disk map, find all the slice positions of files in the disk layout (last one first).\"\"\"\n",
" slices = []\n",
" block = 0\n",
" for i, length in enumerate(disk_map):\n",
" if i % 2 == 0:\n",
" slices.append(slice(block, block + length))\n",
" block += length\n",
" slices.reverse()\n",
" return slices\n",
"\n",
"def find_freespace(layout, file_slice) -> Optional[slice]:\n",
" \"\"\"Find a slice position big enough to fit the given file slice, or return None if there is no position.\"\"\"\n",
" length = file_slice.stop - file_slice.start\n",
" run = 0\n",
" for i in range(layout.index(empty), len(layout)):\n",
" if i >= file_slice.start:\n",
" return None # We only want to move a file left, not right\n",
" elif layout[i] is empty:\n",
" run += 1\n",
" if run == length:\n",
" return slice(i + 1 - length, i + 1)\n",
" else:\n",
" run = 0\n",
" return None"
]
},
{
"cell_type": "code",
"execution_count": 42,
"id": "e3036875-88d0-496e-9d2f-facd0e80a5b2",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 9.2: 6.5013 seconds, answer 6353648390778 ok"
]
},
"execution_count": 42,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(9.2, 6353648390778, lambda:\n",
" checksum(compact_layout2(disk_map)))"
]
},
{
"cell_type": "markdown",
"id": "24c0e7d7-6ac7-4e4b-9557-bd4e215ad0a9",
"metadata": {},
"source": [
"I got the right answer, but I confess I had an off-by-one **bug** in `find_freespace` on the first try."
]
},
{
"cell_type": "markdown",
"id": "c3317844-2b4a-4756-8a59-b765aa467445",
"metadata": {},
"source": [
"# Summary\n",
"\n",
"So far, I've solved all the puzzles. Most of them run in well under a second, but three of them take multiple seconds."
]
},
{
"cell_type": "code",
"execution_count": 43,
"id": "34813fc9-a000-4cd8-88ae-692851b3242c", "id": "34813fc9-a000-4cd8-88ae-692851b3242c",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@ -832,16 +1519,24 @@
"name": "stdout", "name": "stdout",
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"Puzzle 1.1: .0002 seconds, answer 1830467 ok\n", "Puzzle 1.1: .0003 seconds, answer 1830467 ok\n",
"Puzzle 1.2: .0002 seconds, answer 26674158 ok\n", "Puzzle 1.2: .0002 seconds, answer 26674158 ok\n",
"Puzzle 2.1: .0013 seconds, answer 257 ok\n", "Puzzle 2.1: .0013 seconds, answer 257 ok\n",
"Puzzle 2.2: .0076 seconds, answer 328 ok\n", "Puzzle 2.2: .0081 seconds, answer 328 ok\n",
"Puzzle 3.1: .0015 seconds, answer 156388521 ok\n", "Puzzle 3.1: .0018 seconds, answer 156388521 ok\n",
"Puzzle 3.2: .0009 seconds, answer 75920122 ok\n", "Puzzle 3.2: .0009 seconds, answer 75920122 ok\n",
"Puzzle 4.1: .0990 seconds, answer 2401 ok\n", "Puzzle 4.1: .0785 seconds, answer 2401 ok\n",
"Puzzle 4.2: .0785 seconds, answer 1822 ok\n", "Puzzle 4.2: .0669 seconds, answer 1822 ok\n",
"Puzzle 5.1: .0010 seconds, answer 5762 ok\n", "Puzzle 5.1: .0015 seconds, answer 5762 ok\n",
"Puzzle 5.2: .0016 seconds, answer 4130 ok\n" "Puzzle 5.2: .0017 seconds, answer 4130 ok\n",
"Puzzle 6.1: .0046 seconds, answer 5329 ok\n",
"Puzzle 6.2: 14.9723 seconds, answer 2162 ok\n",
"Puzzle 7.1: .0483 seconds, answer 1985268524462 ok\n",
"Puzzle 7.2: 5.5535 seconds, answer 150077710195188 ok\n",
"Puzzle 8.1: .0007 seconds, answer 220 ok\n",
"Puzzle 8.2: .0022 seconds, answer 813 ok\n",
"Puzzle 9.1: .0430 seconds, answer 6332189866718 ok\n",
"Puzzle 9.2: 6.5013 seconds, answer 6353648390778 ok\n"
] ]
} }
], ],