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"
]
}
],