Add files via upload

This commit is contained in:
Peter Norvig
2025-12-11 10:07:36 -08:00
committed by GitHub
parent 38cc88deda
commit a23ede1a52

View File

@@ -184,7 +184,7 @@
{
"data": {
"text/plain": [
"Puzzle 1.2: .1396 seconds, answer 6907 correct"
"Puzzle 1.2: .1417 seconds, answer 6907 correct"
]
},
"execution_count": 6,
@@ -234,7 +234,7 @@
{
"data": {
"text/plain": [
"Puzzle 1.2: .0010 seconds, answer 6907 correct"
"Puzzle 1.2: .0009 seconds, answer 6907 correct"
]
},
"execution_count": 8,
@@ -379,7 +379,7 @@
{
"data": {
"text/plain": [
"Puzzle 2.1: .0037 seconds, answer 23560874270 correct"
"Puzzle 2.1: .0033 seconds, answer 23560874270 correct"
]
},
"execution_count": 12,
@@ -485,7 +485,7 @@
{
"data": {
"text/plain": [
"Puzzle 2.2: .0040 seconds, answer 44143124633 correct"
"Puzzle 2.2: .0036 seconds, answer 44143124633 correct"
]
},
"execution_count": 16,
@@ -671,7 +671,7 @@
{
"data": {
"text/plain": [
"Puzzle 3.1: .0007 seconds, answer 17085 correct"
"Puzzle 3.1: .0006 seconds, answer 17085 correct"
]
},
"execution_count": 23,
@@ -792,7 +792,7 @@
{
"data": {
"text/plain": [
"Puzzle 4.1: .0534 seconds, answer 1569 correct"
"Puzzle 4.1: .0538 seconds, answer 1569 correct"
]
},
"execution_count": 27,
@@ -843,7 +843,7 @@
{
"data": {
"text/plain": [
"Puzzle 4.2: 1.2283 seconds, answer 9280 correct"
"Puzzle 4.2: 1.2313 seconds, answer 9280 correct"
]
},
"execution_count": 29,
@@ -892,7 +892,7 @@
{
"data": {
"text/plain": [
"Puzzle 4.2: .1393 seconds, answer 9280 correct"
"Puzzle 4.2: .1409 seconds, answer 9280 correct"
]
},
"execution_count": 31,
@@ -987,7 +987,7 @@
{
"data": {
"text/plain": [
"Puzzle 5.1: .0075 seconds, answer 635 correct"
"Puzzle 5.1: .0073 seconds, answer 635 correct"
]
},
"execution_count": 34,
@@ -1152,7 +1152,7 @@
{
"data": {
"text/plain": [
"Puzzle 6.1: .0013 seconds, answer 5877594983578 correct"
"Puzzle 6.1: .0015 seconds, answer 5877594983578 correct"
]
},
"execution_count": 40,
@@ -1235,7 +1235,7 @@
{
"data": {
"text/plain": [
"Puzzle 6.2: .0043 seconds, answer 11159825706149 correct"
"Puzzle 6.2: .0039 seconds, answer 11159825706149 correct"
]
},
"execution_count": 42,
@@ -1481,7 +1481,7 @@
{
"data": {
"text/plain": [
"Puzzle 7.2: .0013 seconds, answer 422102272495018 correct"
"Puzzle 7.2: .0014 seconds, answer 422102272495018 correct"
]
},
"execution_count": 51,
@@ -1615,7 +1615,7 @@
{
"data": {
"text/plain": [
"Puzzle 8.1: .5880 seconds, answer 24360 correct"
"Puzzle 8.1: .6008 seconds, answer 24360 correct"
]
},
"execution_count": 54,
@@ -1667,7 +1667,7 @@
{
"data": {
"text/plain": [
"Puzzle 8.2: .6032 seconds, answer 2185817796 correct"
"Puzzle 8.2: .6149 seconds, answer 2185817796 correct"
]
},
"execution_count": 56,
@@ -1783,7 +1783,7 @@
{
"data": {
"text/plain": [
"Puzzle 9.1: .0266 seconds, answer 4772103936 correct"
"Puzzle 9.1: .0264 seconds, answer 4772103936 correct"
]
},
"execution_count": 59,
@@ -1803,11 +1803,11 @@
"source": [
"### Part 2: What is the largest area of any rectangle that has only red and green tiles?\n",
"\n",
"In Part 2 we pay attention to the **green** tiles on the floor. Every red tile is connected to the red tile before and after it (in the input list order) by a straight line of green tiles. (It is guaranteed this will always be a straight line.) The first red tile is also connected to the last red tile. This forms a closed figure, and the interior of the figure is also all green. (The color of the outside of the figure is not stated, but I'm calling it white.) The elves want to know: What is the largest area of any rectangle that consists of only red and green tiles?\n",
"In Part 2 we pay attention to the **green** tiles on the floor. Every red tile is connected to the red tile before and after it (in the input list order) by a straight line of green tiles. (It is guaranteed this will always be a straight horizontal or vertical line.) The first red tile is also connected to the last red tile. This forms a closed polygon, and the interior of the polygon is also all green. (The color of the tiles outside of the polygon is not stated, but I'm saying white.) The elves want to know: What is the largest area of any rectangle that consists of only red and green tiles?\n",
"\n",
"**This is a tough one!** More difficult than all the previous puzzles. There are only 496 red tiles, so enumerating all pairs of them in Part 1 was easy. But there are roughly 100,000<sup>2</sup>or 10 billion total tiles, so filling in all the green tiles and checking them for each pair of corners would be too slow. \n",
"\n",
"To get some ideas for what to try, I really want to see what the red and green tiles look like. I'll show the border tiles, but not the interior tiles:"
"To get some ideas for what to try, I really want to see what the red and green tiles look like. I'll plot the border tiles, but not the interior tiles:"
]
},
{
@@ -1845,10 +1845,11 @@
"**Very Interesting!** Here's what I'm thinking:\n",
"- Most of the lines of green tiles are very short, except for the two long lines across the \"equator.\"\n",
"- A red-and-green rectangle can't cross the two equator lines, because there are white tiles between them.\n",
"- Therefore it seems clear that one of the corners of the maximal rectangle has to be one of the two points on the east end of the equator lines, and the other corner has to be somewhere on the left side of the circle.\n",
"- Therefore it seems that one of the corners of the maximal rectangle has to be one of the two points on the east end of the equator lines, and the other corner has to be somewhere on the left side of the circle, in the same semi-circle as the first corner.\n",
"- The points are all roughly in a circle, so we're looking for a rectangle roughly inscribed in the circle.\n",
"- A roughly correct way to check if a candidate inscribed rectangle is all red-and-green is to see if it contains a red tile in the interior. If there is a red tile in the interior, then some neighbor of the red tile must be white, and thus the candidate is no good. (Note that a red tile on the border is fine.)\n",
"- This red-tile-in-interior heuristic by itself isn't enough to stop a candidate rectangle from crossing over the long equator lines. We can fix that by inserting some more red tiles on any long lines, so that any rectangle of sufficient size that crosses a long line will contain a red tile. \n",
"- A roughly correct way to check if a candidate inscribed rectangle is all red-and-green is to see if it contains a red tile in the interior.\n",
"- To be more precise, it would be ok if a red tile is on the border of the rectangle. It would also be ok if the red tile is part of a line that only goes one square in from the border. That would just mean more green on the inside. But if the red tile is two squares in from the border, then there must be a white square on at least one side of it, and thus a white square insiude the rectange. \n",
"- This red-tile-in-interior heuristic by itself isn't enough to stop a candidate rectangle from crossing over the long equator lines. Or detect a line that starts on the border or one square in and then crosses all the way across the rectangle. We can fix that by estimating the side-length of the rectangle we are looking for and then altering the set of red tiles by inserting some more red tiles on any lines that are longer than that. Then, any rectangle of sufficient size that crosses a long line will contain a red tile in the interior. \n",
"\n",
"I'm ready to start coding.\n",
"- `find_possible_corners` will return a list of the two candidate corner points at the east end of the equator lines.\n",
@@ -1917,7 +1918,7 @@
"source": [
"Now I'll define `biggest_rectangle` to find the largest possible all-red-and-green rectangle. I'll do that by considering pairs of corner points, where one of the corners can be any red tile, and the other corner by default will be one of the two `find_possible_corners` points. We then sort the possible rectangles by area, biggest first, and go through them one at a time. When we find one that does not have `any_intrusions`, we return it; it must be the biggest.\n",
"\n",
"The function `any_intrusions` checks to see if a red tile is completely inside the rectangle defined by the corners. It is ok for a red tile to be on the border; but if it is inside, then the rectangle is not all red-and-green."
"The function `any_intrusions` checks to see if a red tile is completely inside the rectangle defined by the corners. "
]
},
{
@@ -1946,8 +1947,9 @@
"source": [
"def biggest_rectangle(red_tiles, corner1_list=None, d=10000) -> Corners:\n",
" \"\"\"Find the biggest rectangle that stays within the interior-and-border of the tiles.\n",
" If no list of candidates for `corner1_list` are given, use find_possible_corners(red_tiles).\"\"\"\n",
" By default, try `find_possible_corners(red_tiles)` to narrow down choices for first corner.\"\"\"\n",
" tiles = breadcrumbs(red_tiles, d)\n",
" # Use a given list for first corner, or try to find 2 on equator, or just use all the red tiles\n",
" corner1_list = corner1_list or find_possible_corners(red_tiles) or red_tiles\n",
" corners_list = list(cross_product(red_tiles, corner1_list))\n",
" for corners in sorted(corners_list, key=tile_area, reverse=True):\n",
@@ -1956,9 +1958,9 @@
"\n",
"def any_intrusions(red_tiles: List[Point], corners: Corners) -> bool:\n",
" \"\"\"Does any point p in tiles intrude inside the rectangle defined by the corners?\"\"\"\n",
" # This doesn't handle all possibilities, but it does handle what I see in my input\n",
" xrange = range(min(Xs(corners)) + 1, max(Xs(corners)))\n",
" yrange = range(min(Ys(corners)) + 1, max(Ys(corners)))\n",
" # OK for a red tile to be on border or just one square in, but not 2 squares in\n",
" xrange = range(min(Xs(corners)) + 2, max(Xs(corners)) - 2 + 1)\n",
" yrange = range(min(Ys(corners)) + 2, max(Ys(corners)) - 2 + 1)\n",
" return any(X_(p) in xrange and Y_(p) in yrange\n",
" for p in red_tiles)"
]
@@ -1980,7 +1982,7 @@
{
"data": {
"text/plain": [
"Puzzle 9.2: .0309 seconds, answer 1529675217 correct"
"Puzzle 9.2: .0296 seconds, answer 1529675217 correct"
]
},
"execution_count": 65,
@@ -2032,7 +2034,7 @@
"source": [
"We see that if the upper-left corner of the blue rectangle were any higher, then there would be red (and white) tiles in the upper-right corner of the rectangle. If the upper-left corner of the blue rectangle were any further west that would be ok, but would result in a slightly smaller area. You'll just have to take it for granted that all the possible rectangles formed below the equater lines are also a little bit smaller in area.\n",
"\n",
"Here's one thing that bothers me: suppose the two candidate corners on the east end of the equator were just one space apart from each other. Then there would be *no* white space between them, and a rectangle would be free to cross them. (If points were real-valued there would always be some space between them, but on a grid, it is possible to have no space between two lines.) So if it is possible for two red tiles to be adjacent, then my `any_intrusions` algorithm could reject a valid rectangle. Now, it turns out there are no instances of adjacent red tiles:"
"Here's **one thing that bothers me**: suppose the two candidate corners on the east end of the equator were just one space apart from each other. Then there would be *no* white space between them, and a rectangle would be free to cross the equator. (If points were real-valued there would always be some space between them, but on a grid, it is possible to have no empty squares between two lines.) So if it is possible for two red tiles to be adjacent, then my `any_intrusions` algorithm could reject a valid rectangle. Now, it turns out there are no instances of adjacent red tiles:"
]
},
{
@@ -2042,7 +2044,7 @@
"metadata": {},
"outputs": [],
"source": [
"assert all(distance(p, q) > 1 for (p, q) in sliding_window(red_tiles, 2))"
"assert not any(distance(p, q) == 1 for (p, q) in sliding_window(red_tiles, 2))"
]
},
{
@@ -2052,7 +2054,7 @@
"source": [
"But the instructions do not explicitly state that this is impossible, so my algorithm might fail on some inputs.\n",
"\n",
"Another thing that bothers me: it is not guaranteed that `find_possible_corners` will find corners for every possible input. What if it doesn't? If it has to check all pairs of corners (which we get by calling `biggest_rectangle(red_tiles, red_tiles)` it will be a lot slower, but at least on my input, it gets the same answer:"
"Another thing that bothers me: if `find_possible_corners` doesn't find corners, will it still work? Let's check, by passing in all red tiles as the possible corners:"
]
},
{
@@ -2065,8 +2067,8 @@
"name": "stdout",
"output_type": "stream",
"text": [
"CPU times: user 3.47 s, sys: 82.4 ms, total: 3.55 s\n",
"Wall time: 2.94 s\n"
"CPU times: user 3.69 s, sys: 83.8 ms, total: 3.77 s\n",
"Wall time: 3.18 s\n"
]
}
],
@@ -2079,9 +2081,9 @@
"id": "b4e35203-687a-434e-88f5-0afea9f31c57",
"metadata": {},
"source": [
"Two remarks to be made here. One, this was the first puzzle of the year that was **difficult**. Two, my solution is **unsatisfying** in that it works for *my* input, and I strongly suspect that it would work for *your* input, because Eric Wastl probably creating them all to be similar. But it does not work on every possible input allowed by the rules. \n",
"Yes, it finds the same maximal rectangle, but it takes a lot longer to find it.\n",
"\n",
"To cover every possible input I couldn't rely on `find_possible_corners`. I would need to be careful in choosing the value of `d` for `breadcrumbs`; perhaps iterating on different values. The `any_intrusions` function would have to be more complex. See [ChatGPT's solution to 9.2](Advent-2025-AI.ipynb) for example; `any_intrusions` would have to recognize that if two points that are adjacent are inside the rectangle, that is not an intrusion of a non-red tile; that's just a two-wide strip of red."
"**Two final remarks**: One, this was the first puzzle of the year that was **difficult**. Two, my solution is **unsatisfying** in that it works for *my* input, and I strongly suspect that it would work for *your* input, because Eric Wastl probably created them all to be similar. But it may not work on every possible input allowed by the rules. "
]
},
{
@@ -2151,7 +2153,7 @@
"source": [
"### Part 1: What is the fewest button presses to configure the lights on all the machines?\n",
"\n",
"The lights are initially all off, and we want to get them to the goal configuration with the minimum number of button presses. It makes no sense to press any button twice; that just toggles lights on and off and we end up where we started. So we want to find the smallest subset of buttons that when pressed gives the goal light configuration. The utility function `powerset` enumerates subsets in smallest first order, so just look for the first subset of the buttons that toggles each light the proper odd/even number of times. "
"The lights are initially all off, and we want to get them to the goal configuration with the minimum number of button presses. It makes no sense to press any button twice; that just toggles lights on and off and we end up where we started. So we want to find the smallest subset of buttons that when pressed gives the goal light configuration. The function `powerset` (from the [itertools recipes](https://docs.python.org/3/library/itertools.html#itertools-recipes)) yields subsets in smallest first order, so just look for the first subset of the buttons that toggles every light the proper odd/even number of times. "
]
},
{
@@ -2184,7 +2186,7 @@
{
"data": {
"text/plain": [
"Puzzle 10.1: .0531 seconds, answer 441 correct"
"Puzzle 10.1: .0533 seconds, answer 441 correct"
]
},
"execution_count": 71,
@@ -2204,11 +2206,11 @@
"source": [
"### Part 2: What is the fewest button presses to configure the joltage levels on all the machines?\n",
"\n",
"In Part 2 we move a lever, and now the buttons control the joltage levels of the lights, not the lights themselves. The joltage levels all start at zero. Pressing the button `(2, 3)` increments the joltage level of lights numbered 2 and 3 by one unit each. Our task is to get the joltage levels all exactly to the target levels in the minimum number of presses.\n",
"In Part 2 we move a lever, and now the function of the buttons changes: they control the joltage levels of the lights, not the lights themselves. The joltage levels all start at zero. Pressing the button `(2, 3)` increments the joltage level of lights numbered 2 and 3 by one unit each. Our task is to get the joltage levels all exactly to the target levels in the minimum number of presses.\n",
"\n",
"My first thought when reading the puzzle description was \"*This is an [integer linear programming](https://en.wikipedia.org/wiki/Integer_programming) problem.*\" My thought was confirmed by the instructions that said \"*You have to push each button an integer number of times; there's no such thing as 0.5 presses (nor can you push a button a negative number of times).*\" because having fractional or negative results is exactly what you might get from linear programming; you have to take extra steps to constrain the results to be non-negative and to be integers.\n",
"\n",
"Still, I was reluctant to use an integer linear programming package; that would mean that someone else is writing most of the code for the solution. I started programming an A* search solution, but it was way too slow. Why is it slow? Let's investigate. First, the number of buttons per machine is not too bad:"
"Still, I was reluctant to use an integer linear programming package; that would mean that someone else is writing most of the code for the solution. This could also be seen as a search problem; I started programming an A* search solution, but it was way too slow. Why is it slow? Let's investigate. First, the number of buttons per machine is not too bad:"
]
},
{
@@ -2266,22 +2268,35 @@
"id": "fcbf4426-ce35-4270-aa51-7fcacdf86e24",
"metadata": {},
"source": [
"That means a completely naive search would have to consider about 7<sup>115</sup> possible button press sequences. Of course we can make some optimizations:\n",
"- Many joltage states will be repeated; we can cache them.\n",
"- Button presses are commutative; we can impose a canonical ordering.\n",
"- We can keep track of the best solution found so far and eliminate states that can't reach the goal in that number of presses.\n",
"- Some actions will be forced: if there is only one button that increments a given light, we *must* press it until the light hits its goal.\n",
"That means a completely naive search would have to consider about 7<sup>115</sup> possible button press sequences. Of course we can take advantage of the structure of the problem to make some optimizations to the search:\n",
"1) Many joltage states will be repeated; we can cache them.\n",
"2) Button presses are commutative; we can impose a canonical ordering.\n",
"3) We can keep track of the best solution found so far and eliminate states that can't reach the goal in that number of presses.\n",
"4) Some actions will be forced: if there is only one button that increments a given light, we *must* press it until the light hits its goal.\n",
"5) If there are only two buttons that increment a given light, then the total presses of those two buttons must equal the joltage requirement for that light.\n",
"6) And if there are three buttons that increment a given light, maybe we can somehow eliminate one button to get to two, and then to one.\n",
"7) That process of button elimination is called [Gaussian elimination](https://en.wikipedia.org/wiki/Gaussian_elimination). Reluctantly, I will give in to the power of linear programming.\n",
"\n",
"Unfortunately, I couldn't come up with enough ideas to make the search fast, so I gave in and accepted the dark side of integer programming.\n",
"A linear programming solver finds a solution **x** to the equation **A** **x** = **b** that minimizes **c** **x**, where **A** is a two-dimensional matrix and the other variables are one-dimensional vectors.\n",
"\n",
"I started researching integer programming packages that run in Python. [Z3](https://github.com/Z3Prover/z3) seems to be the most popular, but it is a separate step to install it. I know that I (and many other people) already have **scipy** installed, and the [**scipy.optimize**](https://docs.scipy.org/doc/scipy/tutorial/optimize.html) package contains the function [**milp**](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.milp.html#scipy.optimize.milp), for \"mixed integer linear programming.\" The \"linear\" part is appropriate: we have variables (i.e., the buttons) which have coefficients (the number of presses) that must add up to the right total (the joltage requirements). The \"mixed\" part means that we can declare that some of the variables must be integers, while others can be continuous. (For our problem they will all be integers.)\n",
"For our problem we have:\n",
"- **b** is the vector of joltage requirements for each light,\n",
"- **c** says how much it costs to press each button, which is one press each so it is a vector of all ones,\n",
"- **A** is a matrix where **A**<sub>*i,j*</sub> says how much button *i* increments joltage *j* (each is either 0 or 1),\n",
"- **x** will be the solution: a vector of number-of-pushes for each button.\n",
"\n",
"\n",
"\n",
"\n",
"\n",
"I started researching integer programming packages that run in Python. [Z3](https://github.com/Z3Prover/z3) seems to be the most popular, but it is a separate step to install it. I know that I (and many other people) already have **scipy** installed, and the [**scipy.optimize**](https://docs.scipy.org/doc/scipy/tutorial/optimize.html) package contains the function [**milp**](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.milp.html#scipy.optimize.milp), for \"mixed integer linear programming.\" The \"mixed\" part means that we can declare that some of the variables must be integers, while others can be continuous. (For our problem they will all be integers.)\n",
"\n",
"The arguments to **milp** are:\n",
"- **c**: a cost function to be minimized. Represented by coefficients, one per variable. For our problem, each button press counts one, so this will be an array of ones.\n",
"- **integrality**: indicates which variables must be integers. For our problem they all are, so this is also an array of ones (`1` means `True`).\n",
"- **constraints**: a linear constraint, which says that we want to find values for the array **x** of variables such that *lb* ≤ **A** **x** ≤ *ub*, where *lb* and *ub* are the lower bounds and upper bounds. In our case **x** is the count of button presses for each button, and **A** is a matrix such that **A**<sub>*i,j*</sub> says how much button *i* increments joltage *j* (this will be either 0 or 1). Both upper and lower bounds are the joltage requirements (so the dot product **A** **x** must be exactly equal to the joltage requirements)\n",
"- **c**: the cost vector (a 1 for every button in our problem).\n",
"- **integrality**: indicates which variables must be integers; a 1 (True) for every button.\n",
"- **constraints**: a linear constraint. I want to say **A** **x** = **b**, but in this package I have to say **lb** ≤ **A** **x** ≤ **ub**, where **lb** and **ub** are the lower and upper bounds on **b**. \n",
"\n",
"If we give it the right inputs, **milp** will magically return an optimal result for **x** (an array of counts of number of button presses for each button). Here's how we get the data out of a `machine` and feed it to **milp**:"
"If we give it the right inputs, **milp** will magically return an optimal result for **x**. Here's how we get the data out of a `machine` and feed it to **milp**:"
]
},
{
@@ -2323,7 +2338,7 @@
{
"data": {
"text/plain": [
"Puzzle 10.2: .1154 seconds, answer 18559 correct"
"Puzzle 10.2: .1198 seconds, answer 18559 correct"
]
},
"execution_count": 75,
@@ -2338,17 +2353,88 @@
},
{
"cell_type": "markdown",
"id": "7f597ab2-1569-478e-be09-b62c60c979d8",
"id": "6be27520-1a83-42c9-841c-deb3f6422b4e",
"metadata": {},
"source": [
"**This felt like cheating!** I only wrote a few lines of code, and **milp** did the rest. My main contribution was just recognizing that this was an integer programming problem.\n",
"\n",
"Here are some tests on smaller machines:"
"At least I can do a bit of analysis. One thing I was interested in: a system of linear equations can be determined, underdetermined, or overdetermined:\n",
"- **Determined**: Same number of equations as variables (same number of buttons as lights); one unique solution.\n",
"- **Underdetermined**: Fewer equations than variables (fewer buttons than lights); usually multiple solutions; we want the minimal one.\n",
"- **Overdetermined**: More equations than variables (more buttons than lights); no exact solutions unless you are lucky (but for this puzzle Eric Wastl can make sure we are lucky every time).\n",
"\n",
"How under- or over-determined are the equations for our machines?"
]
},
{
"cell_type": "code",
"execution_count": 76,
"id": "b9f82fef-612b-48a5-9de1-6ff0b137f7fb",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Counter({-1: 68, 1: 65, 0: 32})"
]
},
"execution_count": 76,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"Counter(sign(len(lights) - len(buttons)) for (lights, buttons, joltage) in machines)"
]
},
{
"cell_type": "markdown",
"id": "a094063d-cd1d-446a-88c4-748d87506d92",
"metadata": {},
"source": [
"This says that 32 machines are determined, 68 are underdetermined and 65 are overdetermined. By how much?"
]
},
{
"cell_type": "code",
"execution_count": 77,
"id": "e536441d-64d4-410d-bb40-2343f6e01d88",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Counter({-1: 32, -2: 31, 1: 34, 2: 31, 0: 32, -3: 5})"
]
},
"execution_count": 77,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"Counter(len(lights) - len(buttons) for (lights, buttons, joltage) in machines)"
]
},
{
"cell_type": "markdown",
"id": "541e9ab9-fdcd-4a06-be5a-3f6bac052652",
"metadata": {},
"source": [
"Most by only 1 or two variables, but `(-3, 5)` means that 5 of the machines are underdetermined by 3 variables. Most of the time taken by **milp** must have been in searching for a minimal solution to these underdetermined cases."
]
},
{
"cell_type": "markdown",
"id": "6ff8655e-d1cc-4524-b207-c1e425b2a408",
"metadata": {},
"source": [
"Here are some tests on smaller machines that I used when I was unsure where my mistakes with **milp** were:"
]
},
{
"cell_type": "code",
"execution_count": 78,
"id": "b72544c8-6069-4310-a6dc-b4acd77981b4",
"metadata": {},
"outputs": [],
@@ -2370,12 +2456,12 @@
"source": [
"# [Day 11](https://adventofcode.com/2025/day/11): Reactor\n",
"\n",
"There are a bunch of servers here in the reactor room. Today's input consists of a list of device names and for each one the devices it outputs to. (Probably this will be converted to a `dict`, but for now in the input phase I'll just parse each line as a tuple of names.)"
"There are a bunch of servers here in the reactor room. Today's input consists of a list of device connectivity specifications; the line \"`wfc: ond mpw vsz`\" means that device `wfc` outputs to 3 devices, `ond`, `mpw`, and `vsz`. I'll capture the data like this:"
]
},
{
"cell_type": "code",
"execution_count": 77,
"execution_count": 79,
"id": "11e17f6a-acba-44c4-b704-7e4ff7471e7e",
"metadata": {},
"outputs": [
@@ -2421,12 +2507,14 @@
"source": [
"### Part 1: How many different paths lead from you to out?\n",
"\n",
"We are asked how many distinct output paths there are from the device named `'you'` to the device named `'out'`. It is a simple recursive count, but it is likely that multiple paths will lead to the same intermediate devices, so it will save time to cache intermediate results. The dict `lookup_table` is not hashable and thus can't be in a `@cache`, so I make an inner function, `count`, and decorate that:"
"We are asked how many distinct output paths there are from the device named `'you'` to the device named `'out'`. It is a simple recursive count, but I'll make two optimizations:\n",
"- I'll convert the list of devices into a `dict`, for *O*(1) lookup instead of *O*(*n*).\n",
"- It is likely that multiple paths will lead to the same intermediate devices, so I should memoize the counting function. (Note that a `dict` is not hashable and thus can't be in a `@cache`, so I make an inner function, `count`, and decorate that.)"
]
},
{
"cell_type": "code",
"execution_count": 78,
"execution_count": 80,
"id": "7540a982-988a-4822-af0d-6581f6f848c6",
"metadata": {},
"outputs": [],
@@ -2453,7 +2541,7 @@
},
{
"cell_type": "code",
"execution_count": 79,
"execution_count": 81,
"id": "0c2d68a5-843b-49d6-aff6-23045968207f",
"metadata": {},
"outputs": [
@@ -2463,7 +2551,7 @@
"Puzzle 11.1: .0003 seconds, answer 574 correct"
]
},
"execution_count": 79,
"execution_count": 81,
"metadata": {},
"output_type": "execute_result"
}
@@ -2480,42 +2568,42 @@
"source": [
"### Part 2: How many paths from svr to out visit both dac and fft?\n",
"\n",
"Now we are asked for a count of the paths from a different start device, `svr`, to `out`, but the paths are constrained to visit some other devices along the way. That's easy; copy the structure from Part 1, but keep track of what other devices we still need to visit:"
"Now we are asked for a count of the paths from a different start device, `svr`, to the end device `out`, but the paths are constrained to visit some other devices along the way. That's easy; copy the structure from Part 1, but keep track of what other devices we still need to visit:"
]
},
{
"cell_type": "code",
"execution_count": 80,
"execution_count": 82,
"id": "0294e044-c7ef-418a-9c02-71615a453002",
"metadata": {},
"outputs": [],
"source": [
"def count_constrained_paths(devices, start='svr', end='out', needed=frozenset({'dac', 'fft'})):\n",
" \"\"\"How many distinct paths are there from start to end, visiting all of needed?\"\"\"\n",
"def count_constrained_paths(devices, start='svr', end='out', others=frozenset({'dac', 'fft'})):\n",
" \"\"\"How many distinct paths are there from start to end, visiting all the others?\"\"\"\n",
" lookup_table = {device: outputs for (device, *outputs) in devices}\n",
" @cache\n",
" def count(here, there, needed) -> int:\n",
" needed -= {here}\n",
" def count(here, there, others) -> int:\n",
" others -= {here}\n",
" if here == there:\n",
" return (1 if not needed else 0)\n",
" return (1 if not others else 0)\n",
" else:\n",
" return sum(count(mid, there, needed) for mid in lookup_table[here])\n",
" return count(start, end, needed)"
" return sum(count(mid, there, others) for mid in lookup_table[here])\n",
" return count(start, end, others)"
]
},
{
"cell_type": "code",
"execution_count": 81,
"execution_count": 83,
"id": "677a97b3-183b-474e-87ba-7db0d6b763d8",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 11.2: .0022 seconds, answer 306594217920240 correct"
"Puzzle 11.2: .0031 seconds, answer 306594217920240 correct"
]
},
"execution_count": 81,
"execution_count": 83,
"metadata": {},
"output_type": "execute_result"
}
@@ -2537,7 +2625,7 @@
},
{
"cell_type": "code",
"execution_count": 82,
"execution_count": 84,
"id": "4d512a50-c6ae-4803-a787-b8f6e0103e31",
"metadata": {},
"outputs": [
@@ -2546,29 +2634,29 @@
"output_type": "stream",
"text": [
"Puzzle 1.1: .0005 seconds, answer 1182 correct\n",
"Puzzle 1.2: .0010 seconds, answer 6907 correct\n",
"Puzzle 1.2: .0009 seconds, answer 6907 correct\n",
"Puzzle 2.1: .0027 seconds, answer 23560874270 correct\n",
"Puzzle 2.2: .0040 seconds, answer 44143124633 correct\n",
"Puzzle 3.1: .0007 seconds, answer 17085 correct\n",
"Puzzle 2.2: .0036 seconds, answer 44143124633 correct\n",
"Puzzle 3.1: .0006 seconds, answer 17085 correct\n",
"Puzzle 3.2: .0019 seconds, answer 169408143086082 correct\n",
"Puzzle 4.1: .0534 seconds, answer 1569 correct\n",
"Puzzle 4.2: .1393 seconds, answer 9280 correct\n",
"Puzzle 5.1: .0075 seconds, answer 635 correct\n",
"Puzzle 4.1: .0538 seconds, answer 1569 correct\n",
"Puzzle 4.2: .1409 seconds, answer 9280 correct\n",
"Puzzle 5.1: .0073 seconds, answer 635 correct\n",
"Puzzle 5.2: .0001 seconds, answer 369761800782619 correct\n",
"Puzzle 6.1: .0013 seconds, answer 5877594983578 correct\n",
"Puzzle 6.2: .0043 seconds, answer 11159825706149 correct\n",
"Puzzle 6.1: .0015 seconds, answer 5877594983578 correct\n",
"Puzzle 6.2: .0039 seconds, answer 11159825706149 correct\n",
"Puzzle 7.1: .0007 seconds, answer 1681 correct\n",
"Puzzle 7.2: .0013 seconds, answer 422102272495018 correct\n",
"Puzzle 8.1: .5880 seconds, answer 24360 correct\n",
"Puzzle 8.2: .6032 seconds, answer 2185817796 correct\n",
"Puzzle 9.1: .0266 seconds, answer 4772103936 correct\n",
"Puzzle 9.2: .0309 seconds, answer 1529675217 correct\n",
"Puzzle 10.1: .0531 seconds, answer 441 correct\n",
"Puzzle 10.2: .1154 seconds, answer 18559 correct\n",
"Puzzle 7.2: .0014 seconds, answer 422102272495018 correct\n",
"Puzzle 8.1: .6008 seconds, answer 24360 correct\n",
"Puzzle 8.2: .6149 seconds, answer 2185817796 correct\n",
"Puzzle 9.1: .0264 seconds, answer 4772103936 correct\n",
"Puzzle 9.2: .0296 seconds, answer 1529675217 correct\n",
"Puzzle 10.1: .0533 seconds, answer 441 correct\n",
"Puzzle 10.2: .1198 seconds, answer 18559 correct\n",
"Puzzle 11.1: .0003 seconds, answer 574 correct\n",
"Puzzle 11.2: .0022 seconds, answer 306594217920240 correct\n",
"Puzzle 11.2: .0031 seconds, answer 306594217920240 correct\n",
"\n",
"Time in seconds: sum = 1.638, mean = .074, median = .003, max = .603\n"
"Time in seconds: sum = 1.668, mean = .076, median = .003, max = .615\n"
]
}
],