diff --git a/ipynb/Advent-2024.ipynb b/ipynb/Advent-2024.ipynb
index ed096a6..7d1b63e 100644
--- a/ipynb/Advent-2024.ipynb
+++ b/ipynb/Advent-2024.ipynb
@@ -424,31 +424,31 @@
},
{
"cell_type": "code",
- "execution_count": 692,
+ "execution_count": 1119,
"id": "bf6366b1-6952-47d8-8b3c-09f8d05ec093",
"metadata": {},
"outputs": [],
"source": [
"def execute(program: str) -> int:\n",
" \"\"\"The sum of the results of the multiply instructions.\"\"\"\n",
- " return sum(prod(ints(m)) for m in findall_multiplications(program))\n",
+ " return sum(prod(ints(m)) for m in all_multiplications(program))\n",
"\n",
- "findall_multiplications = re.compile(r'mul\\(\\d+,\\d+\\)').findall"
+ "all_multiplications = re.compile(r'mul\\(\\d+,\\d+\\)').findall"
]
},
{
"cell_type": "code",
- "execution_count": 693,
+ "execution_count": 1121,
"id": "2032c903-5f23-4c16-ba68-410b6c1750e1",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
- "Puzzle 3.1: .001 seconds, answer 156388521 ok"
+ "Puzzle 3.1: .002 seconds, answer 156388521 ok"
]
},
- "execution_count": 693,
+ "execution_count": 1121,
"metadata": {},
"output_type": "execute_result"
}
@@ -463,17 +463,17 @@
"id": "d39477c7-5c81-41a9-83a3-5b1bfdef273a",
"metadata": {},
"source": [
- "Here's an example of `findall_multiplications`:"
+ "Here's an example of `all_multiplications`:"
]
},
{
"cell_type": "code",
- "execution_count": 695,
+ "execution_count": 1124,
"id": "85844f51-1396-4299-ba5b-c61064ee02b6",
"metadata": {},
"outputs": [],
"source": [
- "assert findall_multiplications(\"mul(1,2) + mul(34,5) + mul(67,89] + mul(x,15)\") == ['mul(1,2)', 'mul(34,5)']"
+ "assert all_multiplications(\"mul(1,2) + mul(34,5) - mul(67,89] / mul(x,15)\") == ['mul(1,2)', 'mul(34,5)']"
]
},
{
@@ -616,7 +616,7 @@
"id": "eabe90c4-b668-4d9e-a345-b09f4b8ee42b",
"metadata": {},
"source": [
- "### Part 1: How many times does an X-MAS appear?\n",
+ "### Part 2: How many times does an X-MAS appear?\n",
"\n",
"Upon further review, the goal is not to find \"XMAS\" byt rather X-\"MAS\"; that is, two \"MAS\" words in an X pattern. The pattern can be any of these four:\n",
"\n",
@@ -844,20 +844,20 @@
"source": [
"That's not great. With 23 page numbers there are 23! permutations, which is over 25 sextillion. So instead, here's my strategy:\n",
"\n",
- "- Instead of generating all permutations, `correction` will **sort** an update, obeying the rules. It used to be that Python's `sort` method allowed a `cmp` keyword to compare two values; there is vestigial support for this with the `functools.cmp_to_key` function. I will sort each update so that page *m* comes before page *n* if (*m*, *n*) is in the rules. Sorting will be about a sextillion times faster than enumerating permutations.\n",
+ "- Instead of generating all permutations, `correction` will **sort** an update, returning a corrected reordering that obeys all the rules. This is a [topological sort](https://en.wikipedia.org/wiki/Topological_sorting) and is guaranteed to give a correct reordering (unless the rules contain a contradiction like \"2 must be before 3 and 3 must be before 2\"). There may be multiple correct orderings (for example, if there are no rules involving the pages in the update, then any ordering is correct). It used to be that Python's `sort` method allowed a `cmp` keyword to compare two values; there is vestigial support for this with the `functools.cmp_to_key` function. I will sort each update so that page *m* comes before page *n* if (*m*, *n*) is in the rules, and *m* comes after *n* if (*n*, *m*) is in the rules. Sorting will be about a sextillion times faster than enumerating permutations.\n",
"- `corrected` will find all the incorrect updates and correct them."
]
},
{
"cell_type": "code",
- "execution_count": 717,
+ "execution_count": 1135,
"id": "7222dc1c-067f-4bb5-84e1-3c2fc72fd53a",
"metadata": {},
"outputs": [],
"source": [
"def correction(update: Ints, rules) -> Ints:\n",
" \"\"\"Reorder the update to make it correctly obey all the rules.\"\"\"\n",
- " def rule_lookup(m, n): return +1 if (m, n) in rules else -1 \n",
+ " def rule_lookup(m, n): return +1 if (m, n) in rules else -1 if (n, m) in rules else 0\n",
" return sorted(update, key=functools.cmp_to_key(rule_lookup))\n",
"\n",
"def corrected(updates, rules) -> List[Ints]:\n",
@@ -867,17 +867,17 @@
},
{
"cell_type": "code",
- "execution_count": 718,
+ "execution_count": 1137,
"id": "494cda6e-6b07-4054-9b03-45f61bd4f973",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
- "Puzzle 5.2: .001 seconds, answer 4130 ok"
+ "Puzzle 5.2: .003 seconds, answer 4130 ok"
]
},
- "execution_count": 718,
+ "execution_count": 1137,
"metadata": {},
"output_type": "execute_result"
}
@@ -941,21 +941,23 @@
"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",
+ "The guard follows this protocol: \n",
+ "- 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 puzzle statement says that can't happen, so I won't test for it."
+ "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 puzzle statement specifically says that can't happen (the guard will always march off the grid), so I won't test for it."
]
},
{
"cell_type": "code",
- "execution_count": 723,
+ "execution_count": 1146,
"id": "95f0b409-a6d6-47bc-8ce5-1c2df80f2b18",
"metadata": {},
"outputs": [],
"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",
+ " \"\"\"A list of all points in the path followed by the guard.\n",
+ " The guard turns right when there is an obstacle ahead, otherwise goes forward.\"\"\"\n",
" path = grid.findall(guard) # A one-element list of positions, e.g. [(3, 4)]\n",
" while (ahead := add2(path[-1], facing)) in grid:\n",
" if grid[ahead] == '#':\n",
@@ -992,17 +994,17 @@
"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; since ther path crosses itself these two numbers are different.\n",
+ "I initially had a **bug**; I returned for the length of the **path**, not the length of the **set** of positions in the path. Since the path crosses itself these two numbers are different.\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",
- "- I can make slightly less work by only storing the corners of the path: the places where the guard turns. \n",
+ "- A loop is when the guard's path returns to the same position with the same facing. This means my Part 1 solution, which returns the list of visited positions, is not helpful: it is not a loop if the guard is in position *p* facing East and then later returns to position *p* facing South. \n",
+ "- Thus, I can detect a loop by keeping a set of previously visited position/facing pairs.\n",
+ "- I can make slightly less work by only storing the *corners* of the path: the places where the guard turns. \n",
"- The simplest approach for finding obstacle positions is to temporarily place an obstacle on each point on the path, one at a time, and see if it leads to a loop.\n",
- "- I can detect a loop by keeping a set of previously visited position/facing pairs.\n",
"- There are 5,329 positions on the path, so the runtime should be about 5,000 times longer than Part 1; on the order of 10 seconds or so. I'll try it, and if it seems too slow, I'll try to think of something better."
]
},
@@ -1063,9 +1065,9 @@
"id": "9f3ee6f9-7ec7-4248-ae52-1804fdc81dbd",
"metadata": {},
"source": [
- "That was the first run time over a second, but faster than I thought it would be. \n",
+ "That was my first run time over a second, but still faster than I thought it would be. I guess many of the obstacles force the guard into a small, fast loop, or an early exit from the grid. Note that in `the(grid.findall('^'))`, `findall` is a method from my `Grid` class that finds all locations where a `'^'` character resides, and `the` is my utility function that returns the first element of a one-element list (and raises an error if there is not exactly one element).\n",
"\n",
- "I had a **bug** initially, and never figured out what it was; it went away when I refactored to make the program prettier."
+ "I had a **bug** initially, and never figured out what it was; it went away when I refactored to make the program nicer."
]
},
{
@@ -1136,7 +1138,7 @@
"\n",
"Our task is to find operators to balance each equation. The input \"`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 is to try both operators on every number. If there are *n* numbers in an equation then there will be 2*n*-2 possible equations; is that going to be a problem?"
+ "The straightforward approach is to try both operators on every number. If there are *n* numbers in an equation, then there are *n* - 1 numbers on the right-hand side, and *n* - 2 missing operators, so there will be 2*n*-2 possible equations. Is that going to be a problem?"
]
},
{
@@ -1165,47 +1167,49 @@
"id": "e0d9b0b2-fe1e-434e-b84e-c044da3d3673",
"metadata": {},
"source": [
- "No problem! With 13 numbers on a line there are 211 = 2048 equations; a small number. I'll define `can_be_calibrated` to try all possible operator combinations. It goes left-to-right, one number at a time, keeping a set of partial `results` which is updated for each new number in the equation. \n",
+ "No problem! With 13 numbers on a line there are just 211 = 2048 equations; a small number. I'll define `can_be_calibrated` to try all possible operator combinations. It goes left-to-right, one number at a time, keeping a list of partial `results` which is updated for each new number in the equation. \n",
"\n",
- "Although the instructions were a bit vague, it appears that when they say \"numbers\" they mean \"positive integers\". Therefore, neither addition nor multiplication can cause a partal result to decrease, so once a partial result exceeds the target, we can drop it."
+ "Although the puzzle instructions are a bit vague, it appears that when they say \"numbers\" they mean \"positive integers\". Therefore, neither addition nor multiplication can cause a partal result to decrease, so once a partial result exceeds the target, we can drop it."
]
},
{
"cell_type": "code",
- "execution_count": 735,
+ "execution_count": 1207,
"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} # A set of all possible results of the partial computation\n",
+ "def can_be_calibrated(equation: ints, operators=(operator.add, operator.mul)) -> bool:\n",
+ " \"\"\"Can the equation be balanced using '+' and '*' operators?\"\"\"\n",
+ " target, first, *rest = equation\n",
+ " results = [first] # A list of all possible results of the partial computation\n",
" for y in rest:\n",
" results = [op(x, y) for x in results if x <= target for op in operators]\n",
- " return target in results"
+ " return target in results\n",
+ "\n",
+ "def lhs(equation) -> int: \"Left-hand side\"; return equation[0]"
]
},
{
"cell_type": "code",
- "execution_count": 736,
+ "execution_count": 1209,
"id": "3085596d-f5ec-4ba8-b05a-cf70cf276a0c",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
- "Puzzle 7.1: .014 seconds, answer 1985268524462 ok"
+ "Puzzle 7.1: .026 seconds, answer 1985268524462 ok"
]
},
- "execution_count": 736,
+ "execution_count": 1209,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(7.1, 1985268524462, lambda:\n",
- " sum(numbers[0] for numbers in equations if can_be_calibrated(numbers)))"
+ " sum(lhs(equation) for equation in equations if can_be_calibrated(equation)))"
]
},
{
@@ -1215,12 +1219,12 @@
"source": [
"### Part 2: What is the total calibration result of possibly true equations, allowing concatenation?\n",
"\n",
- "In Part 2, we add a third operator: concatentation. The equation \"`192: 17 8 14`\" can be balanced by concatenated 17 and 8 to get 178, and then adding 14: \"`192 = ((17 || 8) + 14)`\". With three operators, the equation with 11 operators now has 311 = 177,147 possibilities, almost 100 times more than Part 1, so this will take longer:"
+ "In Part 2, a third operator is allowed: concatentation. The equation \"`192: 17 8 14`\" can be balanced by concatenated 17 and 8 to get 178, and then adding 14 to get 192: \"`192 = ((17 || 8) + 14)`\". With three operators, the equation with 11 operators now has 311 = 177,147 possibilities, almost 100 times more than Part 1, so this will take longer:"
]
},
{
"cell_type": "code",
- "execution_count": 738,
+ "execution_count": 1219,
"id": "393a50cf-f136-446a-a97e-c501669ce89f",
"metadata": {},
"outputs": [],
@@ -1230,24 +1234,24 @@
},
{
"cell_type": "code",
- "execution_count": 739,
+ "execution_count": 1221,
"id": "f8e75ea3-e8ba-4b33-8efe-8bf74357e35d",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
- "Puzzle 7.2: .799 seconds, answer 150077710195188 ok"
+ "Puzzle 7.2: .807 seconds, answer 150077710195188 ok"
]
},
- "execution_count": 739,
+ "execution_count": 1221,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(7.2, 150077710195188, lambda:\n",
- " sum(numbers[0] for numbers in equations if can_be_calibrated(numbers, operators3)))"
+ " sum(lhs(equation) for equation in equations if can_be_calibrated(equation, operators3)))"
]
},
{
@@ -1255,12 +1259,12 @@
"id": "128b4857-ed3e-49f5-97f5-9d9afd46408d",
"metadata": {},
"source": [
- "That was easy, but it is the second-slowest runtime so far. I can make it a little faster by implementing the concatenation operator without any string operations:"
+ "That was easy, but it is the second-slowest runtime so far. I can make it faster by implementing concatenation by multiplying and adding rather than by string operations."
]
},
{
"cell_type": "code",
- "execution_count": 741,
+ "execution_count": 1223,
"id": "6fe6adad-a3a6-49b8-b49e-6098b27e3a44",
"metadata": {},
"outputs": [],
@@ -1281,17 +1285,17 @@
},
{
"cell_type": "code",
- "execution_count": 742,
+ "execution_count": 1225,
"id": "ffb673f1-af9d-4d15-8f8d-92e29489dd78",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
- "Puzzle 7.2: .593 seconds, answer 150077710195188 ok"
+ "Puzzle 7.2: .616 seconds, answer 150077710195188 ok"
]
},
- "execution_count": 742,
+ "execution_count": 1225,
"metadata": {},
"output_type": "execute_result"
}
@@ -1347,14 +1351,14 @@
"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",
+ "An **antinode** is defined as 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": 746,
+ "execution_count": 1229,
"id": "22180ce8-5d03-4aee-8c73-62f2afbddf71",
"metadata": {},
"outputs": [],
@@ -1375,17 +1379,17 @@
},
{
"cell_type": "code",
- "execution_count": 747,
+ "execution_count": 1231,
"id": "dd173ce9-cbbb-4282-b43f-c7cff662bd90",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
- "Puzzle 8.1: .003 seconds, answer 220 ok"
+ "Puzzle 8.1: .004 seconds, answer 220 ok"
]
},
- "execution_count": 747,
+ "execution_count": 1231,
"metadata": {},
"output_type": "execute_result"
}
@@ -1402,21 +1406,20 @@
"source": [
"### Part 2: 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 antinodal points can be found by starting at *A* and going step by step in the direction of the vector *A* - *B* and also in the direction *B* - *A*, going as far as you can while staying on the grid. The `Grid.follow_line` method facilitates that.\n",
+ "For Part 2, an **updated antinode** 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 antinodal points can be found by starting at *A* and going step by step in the direction of the vector *A* - *B* and also in the direction *B* - *A*, going as far as you can while staying on the grid. The `Grid.follow_line` method facilitates that.\n",
"\n",
- "I'll parametrize `updated_antinodes` so it can handle both parts:"
+ "I'll refactor `antinodes` to take an `antinodes2` parameter so it can handle both parts:"
]
},
{
"cell_type": "code",
- "execution_count": 749,
+ "execution_count": 1237,
"id": "d30f8ce9-f186-46a0-a2e7-f74eceae6905",
"metadata": {},
"outputs": [],
"source": [
"def antinodes(antennas: Grid, antinodes2=antinodes2) -> 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",
+ " \"\"\"The set of all antinodal points in the grid, according to the `antinode2` function.\"\"\"\n",
" groups = [antennas.findall(f) for f in set(antennas.values()) if f != '.']\n",
" return union(antinodes2(A, B, antennas)\n",
" for points in groups\n",
@@ -1430,17 +1433,17 @@
},
{
"cell_type": "code",
- "execution_count": 750,
+ "execution_count": 1239,
"id": "6bf85b57-8b8f-4196-9903-6d5fe082f404",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
- "Puzzle 8.1: .003 seconds, answer 220 ok"
+ "Puzzle 8.1: .004 seconds, answer 220 ok"
]
},
- "execution_count": 750,
+ "execution_count": 1239,
"metadata": {},
"output_type": "execute_result"
}
@@ -1452,17 +1455,17 @@
},
{
"cell_type": "code",
- "execution_count": 751,
+ "execution_count": 1241,
"id": "f232952c-5fc6-4696-a8b1-d0b54137ac02",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
- "Puzzle 8.2: .003 seconds, answer 813 ok"
+ "Puzzle 8.2: .006 seconds, answer 813 ok"
]
},
- "execution_count": 751,
+ "execution_count": 1241,
"metadata": {},
"output_type": "execute_result"
}
@@ -1524,26 +1527,38 @@
"\n",
"### Part 1: Compress 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",
+ "We will deal with two formats to represent disks. Consider this **disk map** format:\n",
"\n",
- "To **compress** 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",
+ " 1, 2, 3, 4, 5 ## Disk map format\n",
"\n",
- "The final answer is a **checksum** of the compressed disk: the sum of the product of the block position times the file ID number for all non-empty blocks."
+ "This 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). \n",
+ "\n",
+ "It makes sense to convert this into a **disk layout** format: which would be \"`\", where \"`.`\" represents an empty block.\n",
+ "\n",
+ " 0..111....22222 ## Disk layout format\n",
+ "\n",
+ "Here the integers represent file ID numbers, and the `.` indicates an empty block.\n",
+ "\n",
+ "To **compress** 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. For the example above, that would give us:\n",
+ "\n",
+ " 022111222...... ## Disk layout format (compressed)\n",
+ "\n",
+ "The final answer to the puzzle is a **checksum** of the compressed 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": 756,
+ "execution_count": 1273,
"id": "76e8454d-a2f3-4b6b-92df-182116cf46e0",
"metadata": {},
"outputs": [],
"source": [
- "empty = '.'\n",
+ "empty = -1 # An empty block\n",
"\n",
"def disk_layout(disk_map: Ints) -> list:\n",
" \"\"\"Convert a disk map into a disk layout.\"\"\"\n",
- " def empties(i): return (disk_map[i] * [empty] if i < len(disk_map) else [])\n",
- " return append(disk_map[i] * [id] + empties(i + 1)\n",
+ " def empties(j) -> int: return (disk_map[j] if j < len(disk_map) else 0)\n",
+ " return append(disk_map[i] * [id] + empties(i + 1) * [empty]\n",
" for id, i in enumerate(range(0, len(disk_map), 2)))\n",
"\n",
"def compress_layout(layout: list) -> list:\n",
@@ -1565,17 +1580,17 @@
},
{
"cell_type": "code",
- "execution_count": 757,
+ "execution_count": 1275,
"id": "2aa7e2b9-844e-49ed-b41b-4a4cecff86b7",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
- "Puzzle 9.1: .020 seconds, answer 6332189866718 ok"
+ "Puzzle 9.1: .036 seconds, answer 6332189866718 ok"
]
},
- "execution_count": 757,
+ "execution_count": 1275,
"metadata": {},
"output_type": "execute_result"
}
@@ -1603,7 +1618,7 @@
},
{
"cell_type": "code",
- "execution_count": 759,
+ "execution_count": 1277,
"id": "fcf4d832-3d7d-4987-aa57-e6e0f1df16bf",
"metadata": {},
"outputs": [],
@@ -1645,17 +1660,17 @@
},
{
"cell_type": "code",
- "execution_count": 760,
+ "execution_count": 1279,
"id": "e3036875-88d0-496e-9d2f-facd0e80a5b2",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
- "Puzzle 9.2: 2.748 seconds, answer 6353648390778 ok"
+ "Puzzle 9.2: 2.424 seconds, answer 6353648390778 ok"
]
},
- "execution_count": 760,
+ "execution_count": 1279,
"metadata": {},
"output_type": "execute_result"
}
@@ -1670,7 +1685,7 @@
"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."
+ "I got the right answer, but I confess I had an off-by-one **bug** in `find_freespace` on the first try, and another bug where I was moving a large file to the right (thereby uncompressing the disk) rather than leaving it in place."
]
},
{
@@ -1680,12 +1695,12 @@
"source": [
"# [Day 10](https://adventofcode.com/2024/day/10): Hoof It\n",
"\n",
- "Today's input is a topological map, with digits indicating the elevation of each terrain position."
+ "Today's input is a topological map of hiking paths on Lava Island, with digits indicating the elevation of each terrain position."
]
},
{
"cell_type": "code",
- "execution_count": 763,
+ "execution_count": 1283,
"id": "5804fb03-05f3-402f-b6cc-6804c5d22512",
"metadata": {},
"outputs": [
@@ -1731,7 +1746,7 @@
"source": [
"### Part 1: What is the sum of the scores of all trailheads on your topographic map?\n",
"\n",
- "A **trailhead** is any position with elevation 0, and a **peak** is any position with elevation 9. The **score** of a trailhead is the number of peaks that can be reached by following a path where each step increases the elevation by exactly 1. All moves are in one of the four cardinal directions (north/south/east/west).\n",
+ "A **trailhead** is any position with elevation 0, and a **peak** is any position with elevation 9. The **score** of a trailhead is the number of peaks that can be reached by following a path where each step increases the elevation by exactly 1. All steps are in one of the four cardinal directions (north/south/east/west).\n",
"\n",
"I'll keep a set of points on the frontier of possible paths, updating this set on each iteratation from 1 to 9, by looking at each point on the frontier and seeing which of the neighboring points `p` have the right elevation:"
]
@@ -1799,30 +1814,39 @@
" for elevation in range(1, 10):\n",
" frontier = accumulate((p, frontier[f]) \n",
" for f in frontier\n",
- " for p in topo.neighbors(f) if topo[p] == elevation)\n",
+ " for p in topo.neighbors(f) \n",
+ " if topo[p] == elevation)\n",
" return sum(frontier.values())"
]
},
+ {
+ "cell_type": "markdown",
+ "id": "bfb4ae54-db59-44df-a76d-12e155fd1e5e",
+ "metadata": {},
+ "source": [
+ "(Note my [utility function](AdventUtils.ipynb) `accumulate` takes as input an iterable of (key, count) pairs, and returns a Counter of the total count for each key.)\n"
+ ]
+ },
{
"cell_type": "code",
- "execution_count": 769,
+ "execution_count": 1294,
"id": "f8a87032-6556-4fc9-9bb8-573611aee8dc",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
- "Puzzle 10.2: .006 seconds, answer 1651 ok"
+ "Puzzle 10.2: .014 seconds, answer 1651 ok"
]
},
- "execution_count": 769,
+ "execution_count": 1294,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(10.2, 1651, lambda:\n",
- " sum(rating(topo, head) for head in topo.findall([0])))"
+ " sum(rating(topo, trailhead) for trailhead in topo.findall([0])))"
]
},
{
@@ -1837,8 +1861,8 @@
},
{
"cell_type": "code",
- "execution_count": 771,
- "id": "4b35defa-a19e-46c5-bd04-3af55bea14e4",
+ "execution_count": 1309,
+ "id": "eea7e277-4687-48a9-85b5-a031a9d4b6e1",
"metadata": {},
"outputs": [],
"source": [
@@ -1866,7 +1890,7 @@
},
{
"cell_type": "code",
- "execution_count": 773,
+ "execution_count": 1292,
"id": "76b68cef-d8de-4145-b65c-b254fedf1671",
"metadata": {},
"outputs": [
@@ -1966,9 +1990,8 @@
"It looks like the number of stones is roughly doubling every 1 or 2 blinks, so for 75 blinks we could have trillions of stones. I'd like something more efficient. I note that:\n",
"- Although the puzzle makes it clear that the stones are in a line, it turns out their position in the line is irrelevant.\n",
"- Because all the even-digit numbers get split in half, it seems like many small numbers will appear multiple times.\n",
- "- (In the given example, after 6 blinks the number 2 appears 4 times.)\n",
- "- Therefore, I'll keep a `Counter` of stones rather than a `list` of stones.\n",
- "- (Note my [utility function](AdventUtils.ipynb) `accumulate` takes as input an iterable of (key, count) pairs, and returns a Counter of the total count for each key.)"
+ " - (In the given example, after 6 blinks the number 2 appears 4 times.)\n",
+ "- Therefore, I'll keep a `Counter` of stones rather than a `list` of stones."
]
},
{
@@ -2045,6 +2068,8 @@
"id": "ce377749-b3e2-4ca4-b50d-e7c3d2e7201a",
"metadata": {},
"source": [
+ "I'm glad I used the `Counter`; it would have taken a petabyte of storage (and a long, long time) to represent that many stones as a list.\n",
+ "\n",
"Again, I did pretty well, with no errors, and moving at what I thought was a good pace, but I didn't even crack the top 2000 on the leaderboard. "
]
},
@@ -2055,7 +2080,7 @@
"source": [
"# [Day 12](https://adventofcode.com/2024/day/12): Garden Groups\n",
"\n",
- "Today's input is yet another 2D map. This one depicts different garden plots on a farm, each plot planted with a crop, indicated by a letter (maybe \"I\" is iceberg lettuce and \"O\" is okra, and so on):"
+ "Today's input is yet another 2D map. This one depicts different garden plots on a farm, each plot planted with a crop, indicated by a letter. Perhaps \"I\" is iceberg lettuce and \"O\" is okra, and so on."
]
},
{
@@ -2094,12 +2119,12 @@
"source": [
"### Part 1: What is the total price of fencing all regions on your map?\n",
"\n",
- "We are asked to calculate the cost of putting fences around each **region** (a region is a set of plots with the same crop that abut each other horizontally or vertically). The price of the fence for a region is defined as the product of the region's area and its perimeter. If we represent a region as a set of (x, y) points, then the area is easy: it is just the number of points. The perimeter length can be computed by, for each plot point in the region, looking at each of the four directions and counting cases where the adjacent plot in that direction is *not* in the region. (Initially I had a **bug** in that I looked at the `farm.neighbors` of each plot. That doesn't work because a plot on the edge of the grid should count as part of the perimeter.)"
+ "We are asked to calculate the cost of putting fences around each **region** (a region is a set of garden plots with the same crop that abut each other horizontally or vertically). The price of the fence for a region is defined as the product of the region's area and its perimeter. If we represent a region as a set of (x, y) points, then the area is easy: it is just the number of points. The perimeter length can be computed by, for each plot point in the region, looking at each of the four directions and counting cases where the adjacent plot in that direction is *not* in the region. (Initially I had a **bug** in that I looked at the `farm.neighbors` of each plot. That doesn't work because a plot on the edge of the grid should count as part of the perimeter, so we have to explicitly check all four directions.)"
]
},
{
"cell_type": "code",
- "execution_count": 786,
+ "execution_count": 1316,
"id": "79f91f38-e325-44f2-9e53-b64ce12d9d35",
"metadata": {},
"outputs": [],
@@ -2109,13 +2134,15 @@
"\n",
"def fence_price(farm: Grid) -> int:\n",
" \"\"\"Total price of fences for all the regions in the farm.\"\"\"\n",
- " return sum(map(region_price, regions(farm)))\n",
+ " return sum(map(region_price, all_regions(farm)))\n",
"\n",
"def region_price(region) -> int: return region_area(region) * perimeter_length(region)\n",
"\n",
"def perimeter_length(region: Region) -> int:\n",
- " \"\"\"The number of sides on the perimeter of the region.\"\"\"\n",
- " return quantify(add2(plot, d) not in region for plot in region for d in directions4)"
+ " \"\"\"The number of sides on the perimeter of the region: \n",
+ " ones where the adjacent position in some direction is not in the region.\"\"\"\n",
+ " return quantify(add2(plot, d) not in region \n",
+ " for plot in region for d in directions4)"
]
},
{
@@ -2123,17 +2150,17 @@
"id": "9524ee15-c378-4cd4-a79b-14fb99b17cb3",
"metadata": {},
"source": [
- "To find all the regions I'll start at a point and do a [flood fill](https://en.wikipedia.org/wiki/Flood_fill) to neighboring points with the same region letter, keeping track of points that have already been found so as to not duplicate them. The function `regions` iterates over all points to make sure it finds every region, and `flood_fill` recursively expands to all points that neighbor `p` and have the same crop (letter). `flood_fill` mutates the set `already_found` as it goes (and also mutates the `region` it is building up)."
+ "To find all the regions I'll start at a point and do a [flood fill](https://en.wikipedia.org/wiki/Flood_fill) to neighboring points with the same region letter, keeping track of points that have already been found so as to not duplicate them. The function `all_regions` iterates over all points to make sure it finds every region, and `flood_fill` recursively expands to all points that neighbor `p` and have the same crop (letter). `flood_fill` mutates the set `already_found` as it goes (and also mutates the `region` it is building up)."
]
},
{
"cell_type": "code",
- "execution_count": 788,
+ "execution_count": 1319,
"id": "1fbabbfb-50c8-4197-8517-e7cee9582765",
"metadata": {},
"outputs": [],
"source": [
- "def regions(farm: Grid) -> List[Region]:\n",
+ "def all_regions(farm: Grid) -> List[Region]:\n",
" \"\"\"Find all the regions in the farm.\"\"\"\n",
" already_found = set() # Set of plots already accounted for\n",
" return [flood_fill(p, farm, set(), already_found) \n",
@@ -2152,17 +2179,17 @@
},
{
"cell_type": "code",
- "execution_count": 789,
+ "execution_count": 1321,
"id": "cdaf655b-d12c-4973-b19b-3132e5e691c6",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
- "Puzzle 12.1: .031 seconds, answer 1402544 ok"
+ "Puzzle 12.1: .053 seconds, answer 1402544 ok"
]
},
- "execution_count": 789,
+ "execution_count": 1321,
"metadata": {},
"output_type": "execute_result"
}
@@ -2202,14 +2229,14 @@
},
{
"cell_type": "code",
- "execution_count": 791,
+ "execution_count": 1323,
"id": "38c30e15-3a33-40c2-b734-163a15af7a8a",
"metadata": {},
"outputs": [],
"source": [
"def fence_price(farm: Grid, region_price=region_price) -> int:\n",
" \"\"\"Total price of fences for all the regions in the farm, given the price function for a region.\"\"\"\n",
- " return sum(map(region_price, regions(farm)))\n",
+ " return sum(map(region_price, all_regions(farm)))\n",
"\n",
"def discount_region_price(region) -> int: return region_area(region) * region_sides(region)\n",
" \n",
@@ -2225,17 +2252,17 @@
},
{
"cell_type": "code",
- "execution_count": 792,
+ "execution_count": 1325,
"id": "72175812-dcd0-4f1b-9efa-0dceeeafa609",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
- "Puzzle 12.1: .030 seconds, answer 1402544 ok"
+ "Puzzle 12.1: .054 seconds, answer 1402544 ok"
]
},
- "execution_count": 792,
+ "execution_count": 1325,
"metadata": {},
"output_type": "execute_result"
}
@@ -2247,17 +2274,17 @@
},
{
"cell_type": "code",
- "execution_count": 793,
+ "execution_count": 1327,
"id": "9defcd35-91bc-41d4-a16f-bb7a4ede75e7",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
- "Puzzle 12.2: .042 seconds, answer 862486 ok"
+ "Puzzle 12.2: .069 seconds, answer 862486 ok"
]
},
- "execution_count": 793,
+ "execution_count": 1327,
"metadata": {},
"output_type": "execute_result"
}