Reputation: 31
I am running through the Project Euler coding archive and have reached problem 115 which reads:
"NOTE: This is a more difficult version of Problem 114.
A row measuring n units in length has red blocks with a minimum length of m units placed on it, such that any two red blocks (which are allowed to be different lengths) are separated by at least one black square.
Let the fill-count function, F(m, n), represent the number of ways that a row can be filled.
For example, F(3, 29) = 673135 and F(3, 30) = 1089155.
That is, for m = 3, it can be seen that n = 30 is the smallest value for which the fill-count function first exceeds one million.
In the same way, for m = 10, it can be verified that F(10, 56) = 880711 and F(10, 57) = 1148904, so n = 57 is the least value for which the fill-count function first exceeds one million.
For m = 50, find the least value of n for which the fill-count function first exceeds one million."
It was manageable for me to solve this problem using a brute force approach (using three nested for-loops and a wealth of while-loops in between, spanding approx. 50 lines of code). In contrast, I have found this small piece of code, utilizing dynamic programming:
m, n = 50, 168
ways = [1]*(m) + [0]*(n-m+1)
for k in range(m, n+1):
ways[k] = ways[k-1] + sum(ways[:k-m]) + 1
ways[n]
Now this looks quite elegant to me! I understand the technical part of the code, but I don't get how this code solves the problem. Hoping for explanatory help here.
Upvotes: 2
Views: 148
Reputation: 1295
Let ways[k]
be the required number of possibilities for a row of length k
. For k = 0
up to k = m - 1
we can't place any red blocks, so there's only 1 possibility: placing nothing. Thus we initialise the first m
values of ways
with 1
. For k = m
onwards, there are three things that we can do with that k'th unit. Firstly we can set it to black. The total number of ways of doing this is the same as the number of ways of assigning for k - 1
, as we're not making any choices about placement beyond the ones for we made for k - 1
. The second thing we can do is assign a giant red block for the whole k
length. There is exactly 1 way of doing this. The third choice is to assign a red block that doesn't take up the entire row. Let's say the black square which is before the start of this new block (there must always be one, because we've already covered the case of the block spanning the entire region) has index i
. We know that i
is bounded by i + m < k
because the block has to be of length at least m
, so by subtracting m
we have i < k - m
. So for this third case, we want to consider every valid i
(starting at i = 0
and up to but not including i = k - m
) and add up all the possible ways we can start a red block at i + 1
, which is calculated by sum(ways[:k-m])
. Adding up each case corresponds to the implemented recurrence: ways[k] = ways[k-1] + sum(ways[:k-m]) + 1
. For any n
the answer now lies in ways[n]
. As a final note, the complexity of this algorithm can be even further improved with a more sophisticated data structure to efficiently answer the prefix sum queries with updates.
Upvotes: 1