diff --git a/ipynb/Advent-2024.ipynb b/ipynb/Advent-2024.ipynb index 0f6b1d7..accdf1a 100644 --- a/ipynb/Advent-2024.ipynb +++ b/ipynb/Advent-2024.ipynb @@ -262,7 +262,7 @@ { "data": { "text/plain": [ - "Puzzle 2.1: .0012 seconds, answer 257 ok" + "Puzzle 2.1: .0011 seconds, answer 257 ok" ] }, "execution_count": 7, @@ -312,7 +312,7 @@ { "data": { "text/plain": [ - "Puzzle 2.2: .0071 seconds, answer 328 ok" + "Puzzle 2.2: .0077 seconds, answer 328 ok" ] }, "execution_count": 9, @@ -400,7 +400,7 @@ { "data": { "text/plain": [ - "Puzzle 3.1: .0013 seconds, answer 156388521 ok" + "Puzzle 3.1: .0014 seconds, answer 156388521 ok" ] }, "execution_count": 12, @@ -449,7 +449,7 @@ { "data": { "text/plain": [ - "Puzzle 3.2: .0008 seconds, answer 75920122 ok" + "Puzzle 3.2: .0009 seconds, answer 75920122 ok" ] }, "execution_count": 14, @@ -539,7 +539,7 @@ { "data": { "text/plain": [ - "Puzzle 4.1: .0704 seconds, answer 2401 ok" + "Puzzle 4.1: .0742 seconds, answer 2401 ok" ] }, "execution_count": 17, @@ -577,7 +577,7 @@ { "data": { "text/plain": [ - "Puzzle 4.2: .0602 seconds, answer 1822 ok" + "Puzzle 4.2: .0605 seconds, answer 1822 ok" ] }, "execution_count": 18, @@ -708,7 +708,7 @@ { "data": { "text/plain": [ - "Puzzle 5.1: .0014 seconds, answer 5762 ok" + "Puzzle 5.1: .0011 seconds, answer 5762 ok" ] }, "execution_count": 21, @@ -793,7 +793,7 @@ { "data": { "text/plain": [ - "Puzzle 5.2: .0020 seconds, answer 4130 ok" + "Puzzle 5.2: .0016 seconds, answer 4130 ok" ] }, "execution_count": 24, @@ -875,7 +875,7 @@ { "data": { "text/plain": [ - "Puzzle 6.1: .0047 seconds, answer 5329 ok" + "Puzzle 6.1: .0043 seconds, answer 5329 ok" ] }, "execution_count": 26, @@ -912,9 +912,9 @@ "- 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, any path that has taken a number of steps equal to the number of empty spaces in the grid must be a loop. That seems simpler. \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", - "- 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 20 seconds or so. I'll try it, and if it seems too slow, I'll try to think of something else." + "- 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." ] }, { @@ -926,16 +926,18 @@ "source": [ "def is_loopy_path(grid: Grid, guard_pos, facing=North) -> bool:\n", " \"\"\"Does the path followed by the guard form a loop?\"\"\"\n", - " N = len(lab_grid.findall('.')) # Number of empty spaces\n", - " for step in range(N):\n", + " path = {(guard_pos, facing)}\n", + " while True:\n", " ahead = add2(guard_pos, facing)\n", " if ahead not in grid:\n", " return False # Walked off the grid; not a loop\n", " elif grid[ahead] == '#':\n", " facing = make_turn(facing, 'R')\n", + " if (guard_pos, facing) in path:\n", + " return True\n", + " path.add((guard_pos, facing))\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) -> Iterable[Point]:\n", " \"\"\"All positions in which placing an obstacle would result in a loopy path for the guard.\"\"\"\n", @@ -956,7 +958,7 @@ { "data": { "text/plain": [ - "Puzzle 6.2: 18.6630 seconds, answer 2162 ok" + "Puzzle 6.2: 5.3153 seconds, answer 2162 ok" ] }, "execution_count": 28, @@ -984,7 +986,7 @@ "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: " + "The narrative for today involves fixing a bridge, and each line of our input represents a calibration equation for the bridge. Unfortunately, some nearby elephants stole all the operators from the equations, so all that is left are the integers:" ] }, { @@ -1025,7 +1027,15 @@ } ], "source": [ - "calibrations = parse(7, ints)" + "equations = parse(7, ints)" + ] + }, + { + "cell_type": "markdown", + "id": "be207b67-a970-4f79-85be-5d62b7cedd9f", + "metadata": {}, + "source": [ + " " ] }, { @@ -1035,9 +1045,9 @@ "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", + "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 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*n* 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 will be 2*n*-2 possible equations; is that going to be a problem?" ] }, { @@ -1058,7 +1068,7 @@ } ], "source": [ - "max(map(len, calibrations))" + "max(map(len, equations))" ] }, { @@ -1066,7 +1076,7 @@ "id": "e0d9b0b2-fe1e-434e-b84e-c044da3d3673", "metadata": {}, "source": [ - "No problem! With 13 numbers on a line there are 11 places to choose an operator, and 211 = 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." + "No problem! With 13 numbers on a line there are 211 = 2048 equations; 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. Although the instructions were a bit vague, it appears that when they talk about \"numbers\" in the equations they mean \"positive integers\". That means that neither addition nor multiplication can cause a number to decrease, so once a result exceeds the target, we'll drop it." ] }, { @@ -1079,9 +1089,9 @@ "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", + " results = {first} # A set of all possible results of the partial computation\n", " for y in rest:\n", - " results = {op(x, y) for x in results for op in operators}\n", + " results = {op(x, y) for x in results if x <= target for op in operators}\n", " return target in results" ] }, @@ -1094,7 +1104,7 @@ { "data": { "text/plain": [ - "Puzzle 7.1: .0471 seconds, answer 1985268524462 ok" + "Puzzle 7.1: .0406 seconds, answer 1985268524462 ok" ] }, "execution_count": 32, @@ -1104,7 +1114,7 @@ ], "source": [ "answer(7.1, 1985268524462, lambda:\n", - " sum(numbers[0] for numbers in calibrations if can_be_calibrated(numbers)))" + " sum(numbers[0] for numbers in equations if can_be_calibrated(numbers)))" ] }, { @@ -1114,7 +1124,7 @@ "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 311 = 177,147, almost 100 times more than Part 1, so this will take a few seconds:" + "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 a few seconds:" ] }, { @@ -1126,7 +1136,7 @@ { "data": { "text/plain": [ - "Puzzle 7.2: 5.1590 seconds, answer 150077710195188 ok" + "Puzzle 7.2: 2.6449 seconds, answer 150077710195188 ok" ] }, "execution_count": 33, @@ -1138,7 +1148,7 @@ "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)))" + " sum(numbers[0] for numbers in equations if can_be_calibrated(numbers, operators3)))" ] }, { @@ -1201,7 +1211,7 @@ { "data": { "text/plain": [ - "Puzzle 8.1: .0009 seconds, answer 220 ok" + "Puzzle 8.1: .0006 seconds, answer 220 ok" ] }, "execution_count": 35, @@ -1398,7 +1408,7 @@ { "data": { "text/plain": [ - "Puzzle 9.1: .0463 seconds, answer 6332189866718 ok" + "Puzzle 9.1: .0433 seconds, answer 6332189866718 ok" ] }, "execution_count": 40, @@ -1479,7 +1489,7 @@ { "data": { "text/plain": [ - "Puzzle 9.2: 6.3337 seconds, answer 6353648390778 ok" + "Puzzle 9.2: 6.2098 seconds, answer 6353648390778 ok" ] }, "execution_count": 42, @@ -1588,7 +1598,7 @@ { "data": { "text/plain": [ - "Puzzle 10.1: .0153 seconds, answer 744 ok" + "Puzzle 10.1: .0141 seconds, answer 744 ok" ] }, "execution_count": 45, @@ -1610,7 +1620,7 @@ "\n", "The **rating** of a trailhead is the number of distinct paths from the trailhead to a peak.\n", "\n", - "As in Part 1, I'll keep a frontier and update it on each iteration from 1 to 9, but this time the frontier will be a counter of `{position: count}` where the count indicates the number of paths to that position. On each iteration I'll look at each point `f` on the frontier and see which of the neighboring points `p` have the right elevation, and increment the counts for those points by the count for `f`:" + "As in Part 1, I'll keep a frontier and update it on each iteration from 1 to 9, but this time the frontier will be a counter of `{position: count}` where the count indicates the number of paths to that position. On each iteration I'll look at each point `f` on the frontier and see which of the neighboring points `p` have the right elevation, and increment the counts for those points by the count for `f`. This approach is linear in the number of positions, whereas if I followed all possible paths depth-first there could be an exponential number of paths." ] }, { @@ -1639,7 +1649,7 @@ { "data": { "text/plain": [ - "Puzzle 10.2: .0175 seconds, answer 1651 ok" + "Puzzle 10.2: .0161 seconds, answer 1651 ok" ] }, "execution_count": 47, @@ -1657,7 +1667,195 @@ "id": "af410d30-7096-4be6-bb20-904b3c8e2f59", "metadata": {}, "source": [ - "Today I went pretty fast (for me); I started a few minutes late and finished in 15 minutes. From the point of view of a competitive coder I did foolish things like write docstrings and use variables of more than one letter, so while this time was fast for me, it placed out of the top 1000." + "Today I went pretty fast (for me); I started a few minutes late and finished in 15 minutes. From the point of view of a competitive coder I did foolish things like write docstrings and use variables of more than one letter, so while this time was fast for me, it placed well out of the top 1000." + ] + }, + { + "cell_type": "markdown", + "id": "3e01d7f5-d0f0-4e7b-8cab-eef2afc02f6b", + "metadata": {}, + "source": [ + "# [Day 11](https://adventofcode.com/2024/day/11): Plutonian Pebbles\n", + "\n", + "Today's input is a single line consisting of a list of integers, representing numbers enscribed on some stones, which are arranged in a straight line." + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "id": "76b68cef-d8de-4145-b65c-b254fedf1671", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Puzzle input ➜ 1 str:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "0 27 5409930 828979 4471 3 68524 170\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "Parsed representation ➜ 1 tuple:\n", + "────────────────────────────────────────────────────────────────────────────────────────────────────\n", + "(0, 27, 5409930, 828979, 4471, 3, 68524, 170)\n" + ] + } + ], + "source": [ + "stones = the(parse(11, ints))" + ] + }, + { + "cell_type": "markdown", + "id": "a7302dc5-5163-4f0b-bdcc-8c00e367391c", + "metadata": {}, + "source": [ + "### Part 1: How many stones will you have after blinking 25 times?\n", + "\n", + "Every time you blink, the stones appear to change, according to these rules:\n", + "- A stone marked 0 changes to 1.\n", + "- Otherwise, a stone with an even number of digits splits into two stones, with the first and second halves of those digits.\n", + "- Otherwise, the stone's number is multiplied by 2024.\n", + "\n", + "I'll define `blink` to simulate the effect of a given number of blinks, and `change_stone` to change a single stone, returning a list of wither one or two stones (the two stones computed by `split_stone`):" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "id": "1513df56-3d6f-42cf-8aec-1bdbeb991d90", + "metadata": {}, + "outputs": [], + "source": [ + "def blink(stones: Ints, blinks=25) -> List[int]:\n", + " \"\"\"Simulate the changes in the list of stones after blinking `blinks` times.\"\"\"\n", + " for _ in range(blinks):\n", + " stones = append(map(change_stone, stones))\n", + " return stones\n", + " \n", + "def change_stone(stone: int) -> List[int]:\n", + " \"\"\"Change a single stone into one or two, according to the rules.\"\"\"\n", + " digits = str(stone)\n", + " return ([1] if stone == 0 else\n", + " split_stone(digits) if len(digits) % 2 == 0 else\n", + " [stone * 2024])\n", + "\n", + "def split_stone(digits: str) -> List[int]:\n", + " \"\"\"Split a stone into two halves.\"\"\"\n", + " half = len(digits) // 2\n", + " return [int(digits[:half]), int(digits[half:])]" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "id": "eff17cd0-a2c7-4d69-bc55-c0ef97917915", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 11.1: .1570 seconds, answer 194482 ok" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "answer(11.1, 194482, lambda:\n", + " len(blink(stones)))" + ] + }, + { + "cell_type": "markdown", + "id": "2f65e94f-43e8-4f08-85df-827928c57e0b", + "metadata": {}, + "source": [ + "### Part 2: How many stones would you have after blinking a total of 75 times?\n", + "\n", + "It looks like the number of stones is roughly doubling every 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. In the 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." + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "id": "707b5a97-0296-48df-bdab-e34064cc67c2", + "metadata": {}, + "outputs": [], + "source": [ + "def blink2(stones: Ints], blinks=25) -> Counter:\n", + " \"\"\"Simulate the changes after blinking `blinks` times and return a Counter of stones.\"\"\"\n", + " counts = Counter(stones)\n", + " for _ in range(blinks):\n", + " counts = accumulate((s, counts[stone]) \n", + " for stone in counts \n", + " for s in change_stone(stone))\n", + " return counts" + ] + }, + { + "cell_type": "markdown", + "id": "f5bf07ce-b48e-40db-8992-b9b571e66554", + "metadata": {}, + "source": [ + "Now we can re-run Part 1 (it should be slightly faster), and run Part 2 without fear of having trillion-element lists:" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "id": "efdcdbf8-e8ec-4a85-9d09-90a20e08c66a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 11.1: .0037 seconds, answer 194482 ok" + ] + }, + "execution_count": 52, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "answer(11.1, 194482, lambda:\n", + " total(blink2(stones, 25)))" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "id": "657b1f13-ffcc-44c6-84f1-398fa2fcdac7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Puzzle 11.2: .1285 seconds, answer 232454623677743 ok" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "answer(11.2, 232454623677743, lambda:\n", + " total(blink2(stones, 75)))" + ] + }, + { + "cell_type": "markdown", + "id": "ce377749-b3e2-4ca4-b50d-e7c3d2e7201a", + "metadata": {}, + "source": [ + "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. I guess I spent too much time writing docstrings and type hints, and refactoring as I go." ] }, { @@ -1672,7 +1870,7 @@ }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 54, "id": "34813fc9-a000-4cd8-88ae-692851b3242c", "metadata": {}, "outputs": [ @@ -1682,24 +1880,26 @@ "text": [ "Puzzle 1.1: .0002 seconds, answer 1830467 ok\n", "Puzzle 1.2: .0002 seconds, answer 26674158 ok\n", - "Puzzle 2.1: .0012 seconds, answer 257 ok\n", - "Puzzle 2.2: .0071 seconds, answer 328 ok\n", - "Puzzle 3.1: .0013 seconds, answer 156388521 ok\n", - "Puzzle 3.2: .0008 seconds, answer 75920122 ok\n", - "Puzzle 4.1: .0704 seconds, answer 2401 ok\n", - "Puzzle 4.2: .0602 seconds, answer 1822 ok\n", - "Puzzle 5.1: .0014 seconds, answer 5762 ok\n", - "Puzzle 5.2: .0020 seconds, answer 4130 ok\n", - "Puzzle 6.1: .0047 seconds, answer 5329 ok\n", - "Puzzle 6.2: 18.6630 seconds, answer 2162 ok\n", - "Puzzle 7.1: .0471 seconds, answer 1985268524462 ok\n", - "Puzzle 7.2: 5.1590 seconds, answer 150077710195188 ok\n", - "Puzzle 8.1: .0009 seconds, answer 220 ok\n", + "Puzzle 2.1: .0011 seconds, answer 257 ok\n", + "Puzzle 2.2: .0077 seconds, answer 328 ok\n", + "Puzzle 3.1: .0014 seconds, answer 156388521 ok\n", + "Puzzle 3.2: .0009 seconds, answer 75920122 ok\n", + "Puzzle 4.1: .0742 seconds, answer 2401 ok\n", + "Puzzle 4.2: .0605 seconds, answer 1822 ok\n", + "Puzzle 5.1: .0011 seconds, answer 5762 ok\n", + "Puzzle 5.2: .0016 seconds, answer 4130 ok\n", + "Puzzle 6.1: .0043 seconds, answer 5329 ok\n", + "Puzzle 6.2: 5.3153 seconds, answer 2162 ok\n", + "Puzzle 7.1: .0406 seconds, answer 1985268524462 ok\n", + "Puzzle 7.2: 2.6449 seconds, answer 150077710195188 ok\n", + "Puzzle 8.1: .0006 seconds, answer 220 ok\n", "Puzzle 8.2: .0021 seconds, answer 813 ok\n", - "Puzzle 9.1: .0463 seconds, answer 6332189866718 ok\n", - "Puzzle 9.2: 6.3337 seconds, answer 6353648390778 ok\n", - "Puzzle 10.1: .0153 seconds, answer 744 ok\n", - "Puzzle 10.2: .0175 seconds, answer 1651 ok\n" + "Puzzle 9.1: .0433 seconds, answer 6332189866718 ok\n", + "Puzzle 9.2: 6.2098 seconds, answer 6353648390778 ok\n", + "Puzzle 10.1: .0141 seconds, answer 744 ok\n", + "Puzzle 10.2: .0161 seconds, answer 1651 ok\n", + "Puzzle 11.1: .0037 seconds, answer 194482 ok\n", + "Puzzle 11.2: .1285 seconds, answer 232454623677743 ok\n" ] } ],