Skip to content

Commit

Permalink
Improving solution for d21 pt2
Browse files Browse the repository at this point in the history
  • Loading branch information
derailed-dash committed Feb 25, 2024
1 parent 8570312 commit f63ffdc
Showing 1 changed file with 87 additions and 53 deletions.
140 changes: 87 additions & 53 deletions src/AoC_2023/Dazbo's_Advent_of_Code_2023.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -8941,17 +8941,26 @@
"\n",
"Crucially, it's important to note that **the relationship between the number of locations ($p$) and the number of steps ($n$) is quadratic.** (Which intuitively makes sense, since the area is given by a square.)\n",
"\n",
"Alas, we can't just use the formula above, because we can't ignore the configuration of our tiles. I.e. we can't ignore all the rocks!! Eliminating certain locations (i.e. the rocks) cannot introduce higher-order terms to our equsetion, so the most general form will be a standard quadratic, i.e.\n",
"Alas, we can't just use the formula above, because we can't ignore the configuration of our tiles. I.e. we can't ignore all the rocks!! Eliminating certain locations (i.e. the rocks) cannot introduce higher-order terms to our equation, so the most general form will be a standard quadratic, i.e.\n",
"\n",
"$$p = an^2 + bn + c$$\n",
"$$p = ax^2 + bx + c$$\n",
"\n",
"Here, $p$ will the total number of reachable positions, $n$ is our available steps, and $a$, $b$ and $c$ are the coefficients we need to determine.\n",
"Here, $p$ will the total number of reachable positions, and $x$ represents how many additional _diamonds_ we have, beyond our initial diamond. Recall that:\n",
"\n",
"We know we need a quadratic equation, but we don't yet know what the coefficients are. _What to do?_\n",
"- The 0th diamond is created with 65 steps from the centre.\n",
"- The 1st diamond is created by adding an additional 131 steps.\n",
"- The 2nd diamond is created by adding another 131 steps.\n",
"- And so on.\n",
"\n",
"We also know that we can't simply BFS for 26501365 steps, because that will never complete and our computer will blow up.\n",
"In order to solve for an arbitrary number of diamonds, we need to determine the coefficients $a$, $b$ and $c$.\n",
"\n",
"But, there is a cool way to determine the coefficients of a quadratic formula, if you have three points from a quadratic plot to work with. And we can get three such points!! The technique is called the _Three Point Formula_ and it is described at these links:\n",
"So, a quick recap:\n",
"\n",
"- We know we need a quadratic equation, but we don't yet know what the coefficients are.\n",
"- We also know that we can't simply BFS for 26501365 steps, because that will never complete and our computer will blow up.\n",
"- We know that 26501365 steps will allow us to move 202300 complete tile lengths away from the centre, plus one half of a tile length.\n",
"\n",
"There is a cool way to determine the coefficients of a quadratic formula, if you have three points from a quadratic plot to work with. And we can get three such points!! The technique is called the _Three Point Formula_ and it is described at these links:\n",
"\n",
"- [Determining Quadratic Functions](https://sites.math.washington.edu/~conroy/m120-general/quadraticFunctionAlgebra.pdf)\n",
"- [Equation of Parabola Given 3 Points](https://www.youtube.com/watch?v=ohc1futhFYM)\n",
Expand All @@ -8962,21 +8971,20 @@
"\n",
"![Three points on a quadratic curve](https://aoc.just2good.co.uk/assets/images/three-points_quadratic.png)\n",
"\n",
"\n",
"Three valid points will be the number of steps that represent our first, second and third diamonds. So, for our actual input data, this means:\n",
"\n",
"- $n$ = `65` steps\n",
"- $n$ = `65 + 131 = 196` steps\n",
"- $n$ = `65 + (2*131) = 327` steps\n",
"- $n$ = `65` steps, $x$ = `0`\n",
"- $n$ = `65 + 131 = 196` steps, $x$ = `1`\n",
"- $n$ = `65 + (2*131) = 327` steps, $x$ = `2`\n",
"\n",
"#### BFS to Get Quadratic Coefficients Using Three Point Formula\n",
"\n",
"We can do a BFS with this number of steps. But first, we need to modify our BFS to allow us to expand beyond a single tile. I.e. such that we can cross from one tile to an adjacent tile. The BFS can be modified to allow this as follows:\n",
"\n",
"- Instead of storing start position and steps available in our FIFO queue, now we store: tile coordinate, position in tile, and steps remaining. The first tile would have a tile coordinate of `(0,0)`. So the tile to the right would be `(1,0)`, the tile below would be `(0,1)`, and so on.\n",
"- When we retrieve the neighbours of the current point, if the neighbour's coordinates fall outside of the grid boundary, then we update the tile coordinate, _and_ then offset the current coordinate _within_ the tile. For example, if we move off the current tile to the tile on the left, then the `x` coordinate _within_ the tile will now be `-1` and outside of the grid boundaryies. So we add the tile width to this `x` coordinate such that the x coordinate is `width-1`, which is a valid coordinate within the grid boundaries.\n",
"- When we retrieve the neighbours of the current point, if the neighbour's coordinates fall outside of the grid boundary, then we update the tile coordinate, _and_ then offset the current coordinate _within_ the tile. For example, if we move off the current tile to the tile on the left, then the `x` coordinate _within_ the tile will now be `-1` and outside of the grid boundaries. So we add the tile width to this `x` coordinate such that the x coordinate is `width-1`, which is a valid coordinate within the grid boundaries.\n",
"\n",
"That's pretty much all we need to do to the BFS. My new function is called `multi_tile_bfs()` (This modified BFS will still work for Part 1.)"
"That's pretty much all we need to do to the BFS. My new function is called `multi_tile_bfs()`. And this modified version still works for Part 1."
]
},
{
Expand All @@ -8985,10 +8993,10 @@
"metadata": {},
"outputs": [],
"source": [
"def multi_tile_bfs(grid, start: tuple[int,int], steps_available: int) -> int:\n",
"def multi_tile_bfs(grid: list, start: tuple[int,int], steps_available: int) -> int:\n",
" \"\"\" Modified BFS that now also includes a tile coordinate as part of state. \n",
" Args:\n",
" grid (_type_): 2D grid of chars\n",
" grid (list): 2D grid of chars\n",
" start (tuple[int,int]): start location in the grid\n",
" steps_available (int): steps available\n",
" \n",
Expand Down Expand Up @@ -9054,20 +9062,29 @@
"outputs": [],
"source": [
"def reachable_plots(data, steps_available:int):\n",
" \"\"\" Return the number of plots that can be reached in the given number of steps.\n",
" We call multi_tile_bfs() to perform a BFS for a limited number of repeating tiles.\n",
"\n",
" Args:\n",
" data (list): List of strings representing the grid.\n",
" steps_available (int): Number of steps available.\n",
"\n",
" Returns:\n",
" int: Number of plots that can be reached.\n",
" \"\"\"\n",
" grid = [[char for char in row] for row in data]\n",
" grid_size = len(grid) \n",
" \n",
" logger.debug(f\"Grid width={grid_size}\") \n",
" assert grid_size == len(grid[0]), \"The grid should be square\"\n",
" assert grid_size % 2 == 1, \"The grid size is odd\"\n",
" assert grid_size % 2 == 1, \"The grid size should be odd\"\n",
"\n",
" # Retrieve the start position\n",
" (start, ) = [(ri, ci) for ri, row in enumerate(grid)\n",
" for ci, char in enumerate(row) if char == \"S\"]\n",
"\n",
" assert start[0] == start[1] == grid_size // 2, \"Start is in the middle\"\n",
" \n",
" # For each location in the original grid (tile 0,0), \n",
" # can we reach this same location in other tiles? \n",
" answer = multi_tile_bfs(grid, start, steps_available)\n",
" logger.debug(f\"We have {answer} final plots for {steps_available} steps.\")\n",
"\n",
Expand Down Expand Up @@ -9107,13 +9124,8 @@
"\n",
"for sample_step_count, sample_answer in zip(step_counts, sample_answers):\n",
" validate(reachable_plots(sample_input.splitlines(), sample_step_count), sample_answer)\n",
"\n",
"step_counts = [64, 65, 196, 327] # 64 is just to check it matches what we had before\n",
"\n",
"validate(reachable_plots(input_data, step_counts[0]), soln_64)\n",
"\n",
"plots = [(step_count, reachable_plots(input_data, step_count)) for step_count in step_counts[1:]]\n",
"logger.info(f\"{plots=}\")"
" \n",
"logger.info(\"Tests passed!\")"
]
},
{
Expand All @@ -9129,54 +9141,71 @@
"metadata": {},
"outputs": [],
"source": [
"# Steps to reach the edges of our three diamonds, for our actual input data\n",
"step_counts = [64, 65, 196, 327] # 64 is just to check it matches what we had before\n",
"\n",
"validate(reachable_plots(input_data, step_counts[0]), soln_64)\n",
"\n",
"plot_counts = [(step_count, reachable_plots(input_data, step_count)) for step_count in step_counts[1:]]\n",
"logger.info(f\"{plots=}\")"
"logger.info(f\"{plot_counts=}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's call my three numbers $P(0)$, $P(1)$ and $P(2)$. Each can be represented as a quadratic:\n",
"Great! So now we have the number of reachable plots $p$ for three values of $x$.\n",
"\n",
"Let's substitute in these values of $x$:\n",
"\n",
"$$\n",
"\\begin{align}\n",
"P(x) &= ax^2 + bx + c \\nonumber \\\\\n",
"\\nonumber \\\\\n",
"\\text{Therefore:} \\nonumber \\\\\n",
"P(0) &= a.(65)^2 + b.(65) + c \\nonumber \\\\\n",
"P(1) &= a.(196)^2 + b.(196) + c \\nonumber \\\\\n",
"P(2) &= a.(327)^2 + b.(327) + c \\nonumber \\\\\n",
"\\end{align}\n",
"\\begin{align*}\n",
"p(0) &= a(0)^2 + b(0) + c \\\\\n",
" &= c \\\\\n",
"\\end{align*}\n",
"$$\n",
"\n",
"We can rearrange these formulae to come up with a standard set of equations to determine the coefficients $a$, $b$ and $c$. (We have three unknowns, so we need three equations to combine.)\n",
"This gives us the value of $c$. Next, for $x=1$:\n",
"\n",
"$$\n",
"\\begin{align*}\n",
"p(1) &= a(1)^2 + b(1) + c \\\\\n",
" &= a + b + c \\\\\n",
" &= a + b + p(0) \\\\\n",
"\\end{align*}\n",
"$$\n",
"\n",
"To find $c$, we can subtitute 0 for $x$:\n",
"And for $x=2$:\n",
"\n",
"$$\n",
"\\begin{align}\n",
"P(0) &= a.(0)^2 + b.(0) + c \\\\\n",
"c &= P(0)\n",
"\\end{align}\n",
"\\begin{align*}\n",
"p(2) &= a(2)^2 + b(2) + c \\\\\n",
" &= 4a + 2b + c \\\\\n",
" &= 4a + 2b + p(0) \\\\\n",
"\\end{align*}\n",
"$$\n",
"\n",
"We can then substitute and combine the equations to solve for $b$ and $c$:\n",
"Now subtract $p(1)$ from $p(2)$ to solve for $a$:\n",
"\n",
"$$\n",
"\\begin{align}\n",
"b &= \\frac{(4.P(1) - 3.P(0) - P(2))}{2} \\\\\n",
"a &= (P(1) - P(0) - b) \\\\\n",
"\\end{align}\n",
"\\begin{align*}\n",
"p(2) - 2 \\cdot p(1) &= 4a + 2b + p(0) - 2 \\cdot (a + b + p(0)) \\\\\n",
" &= 2a - p(0) \\\\\n",
" 2a &= p(2) - 2 \\cdot p(1) + p(0) \\\\\n",
"\\end{align*}\n",
"$$\n",
"\n",
"So this will give us the actual coefficients. Note that these coefficients are only valid for our _real data_ grid, since these coefficients are calculated based on our specific grid sizes and resulting _diamond_ sizes.\n",
"Rearrange $p(1)$ again, to solve for $b$:\n",
"\n",
"But what value to use for $x$? Here, we want the number of whole tile lengths."
"$$\n",
"\\begin{align*}\n",
" b &= p(1) - a - p(0) \\\\\n",
"\\end{align*}\n",
"$$\n",
"\n",
"So now we can determine the values of $a$, $b$ and $c$ by plugging in the values of $p(0)$, $p(1)$, and $p(2)$, which we determined using BFS. Note that these coefficients are only valid for our _real data_ grid, since these coefficients are calculated based on our specific grid sizes and resulting _diamond_ sizes.\n",
"\n",
"Finally, we run the quadratic formula, but setting $x$ to `202300`."
]
},
{
Expand All @@ -9185,28 +9214,33 @@
"metadata": {},
"outputs": [],
"source": [
"def solve_quadratic(data, plot_counts: list[int], steps:int):\n",
"def solve_quadratic(data: list, plot_counts: list[int], steps:int):\n",
" \"\"\" Return the total number of reachable plots in a specified number of steps, \n",
" by calculating the answer to the quadratic formula. \n",
" Here we calculate the coefficients a, b and c by using three sample values,\n",
" obtained from a smaller grid.\n",
"\n",
" Args:\n",
" data (_type_): The original grid tile.\n",
" data (list): The original grid tile.\n",
" plot_counts (list[int]): The plot counts determined for small step counts.\n",
" steps (int): The number of steps we must take.\n",
" \"\"\"\n",
" grid = [[char for char in row] for row in data]\n",
" grid_size = len(grid)\n",
"\n",
" # determine coefficients\n",
" c = plot_counts[0]\n",
" b = (4*plot_counts[1] - 3*plot_counts[0] - plot_counts[2]) // 2\n",
" a = plot_counts[1] - plot_counts[0] - b\n",
" \n",
" c = plot_counts[0] # p(0)\n",
" a = (plot_counts[2] - 2*plot_counts[1] + plot_counts[0]) // 2 # (p(2) - 2*p(1) + p(0)) / 2\n",
" b = plot_counts[1] - a - c # p(1) - a - c\n",
"\n",
" logger.debug(f\"Coefficients: a={a}, b={b}, c={c}\")\n",
"\n",
" # determine the number of steps we can take in whole tile lengths\n",
" x = (steps - grid_size//2) // grid_size # number of whole tile lengths\n",
" logger.debug(f\"Solving for x={x}\")\n",
" return a*x**2 + b*x + c\n",
"\n",
"logger.setLevel(logging.DEBUG)\n",
"ans = solve_quadratic(input_data, plot_counts=[ct[1] for ct in plot_counts], steps=26501365)\n",
"logger.info(ans)"
]
Expand Down

0 comments on commit f63ffdc

Please sign in to comment.