The problem description in Problem 15 of Project Eulercontains a figure, which I wont copy, so go ahead an read the full description at theProject Euler site. The problem can be understood without it though. The problem reads
Starting in the top left corner of a 2×2 grid, there are 6 routes (without backtracking) to the bottom right corner.
How many routes are there through a 20×20 grid?
My first question for many of the problems has been – Can it be brute forced? And my best answer to that is “probably”, but I cannot figure out how to generate all the routes. So instead I will give you two other approaches, which are both efficient. One is inspired by dynamic programming and the other gives an analytic solution using combinatorics.
Dynamic Programming
Dynamic programming is an optimization technique where you can minimize or maximize some function. Dynamic programming relies on an important property; the problem must have anoptimal substructure. What it means is that you need to be generate a solution based on solving smaller problems. One example, if you want to find the shortest route , you can use dynamic programming, if you can find the shortest route of a part of the problem independent of how you got to the start point of the shorter distance you are trying to find.
Confused? Don’t worry too much, we will cover it all again in a later problem and you will hopefully get the hang of what I mean through this problem. The problem we are currently trying to solve is not about optimization, but rather counting. However, the solution is still heavily inspired by the techniques in dynamic programming.
Instead of solving the whole problem, lets solve the problem for the scenario where we stand in the point just to the left or above the end point as marked in the figure to the right, no matter how we got to that point, we only have only one possible path to follow to reach the goal.
Now that we have solved one subproblem, we can expand that by solving the problem where we stand two grid points to the left. That point gives us one option to continue, no matter how we got to that grid point. From there we only have one option to get to the goal. Using that argument we can solve the problem for all bottom and all the rightmost points in the grid.
So far it has been rather trivial with only one path to follow, but it gives a nice boundary condition for the algorithm as we shall see later on. But let us move inside the grid. If we stand in the green grid point on the figure below, we can move right – from where we already know that we have 1 option to complete the path, or we can move down where we also have 1 option to complete the path. So that gives us a total of 2 different paths to take from the green point.
Now that we know we can make 2 paths from the green point, we can figure out how many paths we can make from the blue grid point. If we move down, we have 1 path, and if we move right we have 2 options to complete the path. So in total we can choose between 3 paths to get from the blue grid point to the end.
Iteratively we can solve larger and larger sub problems once we have solve a smaller sub problem, since we can calculate the number of paths by adding the number of paths from the point below with the number of paths from the point to the right. Under the assumptions that we know the number of paths in these points. The deducted boundary conditions ensures us that we can always find such a point, and by going through the whole grid, we can calculate routes all the way until we reach the top most point.
If we continue these calculations until we have filled the 2×2 grid as I have done in the figure to the left, we end up with 6 paths, just as the example in the problem formulation.
That should be rather easy to make an algorithm that does this for us. There is only one little note I want to make, even if it is a 20×20 grid, we have 21×21 grid points, so the algorithm looks like
1
2
3
4
5
6
7
8
9
10
11
12
13
|
const
int
gridSize = 20;
long
[,] grid =
new
long
[gridSize+1, gridSize+1];
//Initialise the grid with boundary conditions
for
(
int
i = 0; i < gridSize; i++) {
grid[i, gridSize] = 1;
grid[gridSize,i] = 1;
}
for
(
int
i = gridSize - 1; i >= 0; i--) {
for
(
int
j = gridSize - 1; j >= 0; j--) {
grid[i, j] = grid[i+1, j] + grid[i, j+1];
}
}
|
And the execution gives us
1
2
|
In a 20x20 grid there are 137846528820 possible paths.
Solution took 0 ms
|
It scales squared with side length of the grid, so it scales really well, however the number of paths explodes with large grids, so we will rather quickly face problems with using longs for storage. However that is another problem which can be circumvented using BigInteger as described in the answer to Problem 13.
Combinatorics
The kind of problem we solve here is treated in this book Notes on Introductory Combinatorics by George Polya et al. I haven’t read it but it has been recommended to me by a friend, when I said I wanted to know a bit more about combinatorics.
In order to pose the question as a combinatorics question, we must realise a few things. I have generalised the observations to an NxN grid.
- All paths can be described as a series of directions. And since we can only go down and right, we could describe the paths as a series of Ds and Rs. In a 2×2 grid all paths are 1) DDRR 2) DRDR 3) DRRD 4) RDRD 5) RDDR 6) RRDD.
- Based on the example we can see that all paths have exactly size 2N of which there are N Rs and N Ds.
- If we have 2N empty spaces and place all Rs, then the placement of the Ds are given
Once we have made these realisations, we can repose the question as
In how many ways can we choose N out of 2N possible places if the order does not matter?
And combinatorics gives us an easy answer to that. The Binomial Coefficient gives us exactly the tool we need to answer the above question. The question is usually posed as
And using the multiplicative formula we can express it as
We could also express it as a factorial expression, but that usually gives problems with very large numbers when we try to make the calculations.
Wikipedia has a suggested implementation for the multiplicative formula that I have used to get the result so the code looks like
1
2
3
4
5
6
7
|
const
int
gridSize = 20;
long
paths = 1;
for
(
int
i = 0; i < gridSize; i++) {
paths *= (2 * gridSize) - i;
paths /= i + 1;
}
|
and the result is
1
2
|
In a 20x20 grid there are 137846528820 possible paths.
Solution took 0 ms
|
This solution has a smaller implementation and will be significantly smaller in memory usage than the first approach. However both works really fast.