Lately I’ve slowly been trying to grok the fullness of dynamic programming. It is an algorithmic technique that the vast majority of developers never master, which is unfortunate since it can help you come up with viable solutions for seemingly intractable problems. The issue with dynamic programming (besides the totally misleading name), is that it can be very difficult to see how to apply it to a particular problem and even when you do, it is a real pain to get it right. Anyway, I don’t want to expound on this, I have something more interesting in mind.
The Dropbox Challenges
I was surfing the web the other day and in the course of my random wanderings I ended up at the Dropbox programming challenges page. Apparently, the Dropbox guys have posted up some coding challenges for people who want to apply to work there (and everyone else, I guess, since it’s on the internet and all :)). Challenge 3 (The Dropbox Diet) immediately caught my eye since it looked like one of those problems that should have a dynamic programming solution, so I decided to use it as an opportunity to practice. The full description of the problem is on the challenge page, but here is the gist of it.
We get a list of up to 50 activities and an associated calorie value for each (either positive or negative), we need to find a subset of activities where the sum of all the calorie values is zero.
It sounded easy enough until ****I thought about it and realised it was more complex than it first appeared. So, I went for a walk :) and when I came back I settled in to analyse it for real. The first part of solving any problem is to really understand what problem you’re trying to solve (that one sentence really deserves its own article). In this case the activities list is just extraneous information, what we really have is a list of numbers and we need to find a subset of these numbers where the sum of the subset is equal to a particular value. It took me quite a while to come up with that definition, but once you have something like that, you can do some research and see if it is a known problem.
Of course, I did nothing of the kind, I had already decided that there must be a dynamic programming solution so I went ahead and tried to come up with it myself. This wasted about an hour at the end of which I had absolutely nothing; I guess my dynamic programming chops are still lamb-chops as opposed to nice meaty beef-chops :). Having failed I decided to do what I should have done in the first place – some research. Since I had taken the time to come up with a decent understanding of the problem, it only took 5 minutes of Googling to realise that I was dealing with the subset sum problem.
The Subset Sum Problem
The unfortunate thing about the subset sum problem is the fact that it’s NP-complete. This means that if our input is big enough we may be in trouble. Wikipedia does give some algorithmic approaches to the problem (no code though), but just to cross our t’s I also cracked open Cormen et al (have you ever noticed how that book has everything when it comes to algorithms :)). In this case the book agreed with Wikipedia, but once again, no code (there are only two things I don’t like about Intro To Algorithms, the lack of real code and the lack of examples). I browsed the web some more, in case it would give me further insight into the problem, but there wasn’t much more to know – it was time to get my code on.
The Exponential Time Algorithm
The problem with the exponential time algorithm is its runtime complexity (obviously), but our maximum input size was only 50 and even if that turned out to be too big, perhaps there were some easy optimizations to be made. Regardless I decided to tackle this one first, if nothing else it would immerse me in the problem. I’ll demonstrate how it works via example. Let’s say our input looks like this:
[1, -3, 2, 4]
We need to iterate through the values and on every iteration produce all the possible subsets that can be made with all the numbers we’ve looked at up until now. Here is how it looks:
Iteration 1:
[(http://feeds.feedburner.com/softwaretechandmore)]Iteration 2:
[(http://feeds.feedburner.com/softwaretechandmore), [-3], [1, -3]]Iteration 3:
[(http://feeds.feedburner.com/softwaretechandmore), [-3], [1, -3], [2], [1, 2], [-3, 2], [1, -3, 2]]Iteration 4:
[(http://feeds.feedburner.com/softwaretechandmore), [-3], [1, -3], [2], [1, 2], [-3, 2], [1, -3, 2], [4], [1, 4], [-3, 4], [1, -3, 4], [2, 4], [1, 2, 4], [-3, 2, 4], [1, -3, 2, 4]]
On every iteration we simply take the number we’re currently looking at as well as a clone of the list of all the subsets we have seen so far, we append the new number to all the subsets (we also add the number itself to the list since it can also be a subset) and then we concatenate this new list to the list of subsets that we generated on the previous iteration. Here is the previous example again, but demonstrating this approach:
Iteration 1:
[] + (http://feeds.feedburner.com/softwaretechandmore)Iteration 2:
(http://feeds.feedburner.com/softwaretechandmore) + [-3], [1, -3]Iteration 3:
(http://feeds.feedburner.com/softwaretechandmore), [-3], [1, -3] + [2], [1, 2], [-3, 2], [1, -3, 2]Iteration 4:
(http://feeds.feedburner.com/softwaretechandmore), [-3], [1, -3], [2], [1, 2], [-3, 2], [1, -3, 2] + [4], [1, 4], [-3, 4], [1, -3, 4], [2, 4], [1, 2, 4], [-3, 2, 4], [1, -3, 2, 4]
This allows us to generate all the possible subsets of our input, all we have to do then is pick out the subsets that sum up to the value we’re looking for (e.g. 0).
The list of subsets grows exponentially (it being an exponential time algorithm and all :)), but since we know what sum we’re looking for, there is one small optimization we can make. We can sort our input list before trying to generate the subsets, this way all the negative values will be first in the list. The implication here is this, once the sum of any subset exceeds the value we’re looking for, we can instantly discard it since all subsequent values we can append to it will only make it bigger. Here is some code:
|
|
If we execute that (with our reference value being 0 and our array being [1, -3, 2, 4]), we get the following output:
[[-3], [-3, 1], [-3, 2], [-3, 1, 2]]
All the subsets in that list sum up to less than or equal to our reference value (__). All we need to do now is pick out the ones that we’re after.
|
|
This function calls the previous one and then picks out the subset we’re after:
[[-3, 1, 2]]
It’s simple and works very well for any input array with less than 20 values or so, and if you try it with more than 25 – good luck waiting for it to finish :). Exponential time is no good if we want it to work with an input size of 50 (or more) numbers.
The Dynamic Programming Algorithm
Both Wikipedia and Cormen tell us that there is a polynomial time approximate algorithm, but that’s no good for us since we want the subsets that add up to exactly zero, not approximately zero. Fortunately, just like I suspected, there is a dynamic programming solution, Wikipedia even explains how it works, which is only marginally helpful when it comes to implementing it. I know because that was the solution I tackled next. Here is how it works, using the same input as before:
[1, -3, 2, 4]
Just like with any dynamic programming problem, we need to produce a matrix, the key is to figure out what it’s a matrix of (how do we label the rows and how do we label the columns). In this case the rows are simply the indexes of our input array; the columns are labelled with every possible sum that can be made out of the input numbers. In our case, the smallest sum we can make from our input is -3 since that’s the only negative number we have, the biggest sum is seven (1 + 2 + 4). So, our uninitialized matrix looks like this:
+---+----+----+----+---+---+---+---+---+---+---+---+ | | -3 | -2 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +---+----+----+----+---+---+---+---+---+---+---+---+ | 0 | | | | | | | | | | | | | 1 | | | | | | | | | | | | | 2 | | | | | | | | | | | | | 3 | | | | | | | | | | | | +---+----+----+----+---+---+---+---+---+---+---+---+
So far so good, but what should we put in every cell of our matrix. In this case every cell will contain either T (true) or F (false).
A T value in a cell means that the sum that the column is labelled with can be constructed using the input array numbers that are indexed by the current row label and the labels of all the previous rows we have already looked at. An F in a cell means the sum of the column label cannot be constructed. Let’s try to fill in our matrix to see how this works.
We start with the first row, the number indexed by the row label is 1, there is only one sum that can be made using that number – 1. So only one cell gets a T in it, all the rest get an F.
+---+----+----+----+---+---+---+---+---+---+---+---+ | | -3 | -2 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +---+----+----+----+---+---+---+---+---+---+---+---+ | 0 | F | F | F | F | T | F | F | F | F | F | F | | 1 | | | | | | | | | | | | | 2 | | | | | | | | | | | | | 3 | | | | | | | | | | | | +---+----+----+----+---+---+---+---+---+---+---+---+
The number indexed by the second row label is -3, so in the second row, the column labelled by -3 will get a T in it. However, we’re considering the numbers indexed by the current row and all previous rows, which means any sum that can be made using the numbers 1 and -3 will get a T in its column. This means that the column labelled with 1 gets a T and the column labelled with -2 gets a T since
1 + -3 = -2
+---+----+----+----+---+---+---+---+---+---+---+---+ | | -3 | -2 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +---+----+----+----+---+---+---+---+---+---+---+---+ | 0 | F | F | F | F | T | F | F | F | F | F | F | | 1 | T | T | F | F | T | F | F | F | F | F | F | | 2 | | | | | | | | | | | | | 3 | | | | | | | | | | | | +---+----+----+----+---+---+---+---+---+---+---+---+
We continue in the same vein for the next row, we’re now looking at number 2 since it’s indexed by the third row in our matrix. So, the column labelled by 2 will get a T, all the columns labelled by T in the previous row propagate their T value down, since all those sums are still valid. But we can produce a few other sums given the numbers at our disposal:
2 + -3 = -1 1 + 2 + -3 = 0 1 + 2 = 3
All those sums get a T in their column for this row.
+---+----+----+----+---+---+---+---+---+---+---+---+ | | -3 | -2 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +---+----+----+----+---+---+---+---+---+---+---+---+ | 0 | F | F | F | F | T | F | F | F | F | F | F | | 1 | T | T | F | F | T | F | F | F | F | F | F | | 2 | T | T | T | T | T | T | T | F | F | F | F | | 3 | | | | | | | | | | | | +---+----+----+----+---+---+---+---+---+---+---+---+
There are three patterns that are starting to emerge.
- For every row, the column which is equivalent to the number indexed by the row get a T in it (e.g. row zero represents the number 1, so the column labelled by 1 gets a T in row 0, row one represents the number -3 so the column labelled by -3 get a T in row 1 etc.), every row will get one T in one of the columns via this pattern. This is because a single number by itself is a valid subset sum.
- If a column already has a T in the previous row, this T propagates down to the current row (e.g. when looking at the second row, the column labelled by 1 has a T in the first row and will therefore have a T in the second row also, when looking at the third row columns labelled by -3, -2 and 1 all had a T in the second row and will therefore contain a T in the third row). This is due to the fact that once it is possible to construct a certain sum using a subset of our input numbers, looking at more of the input numbers does not invalidate the existing subsets.
- Looking at any column label X in the current row which still has a value of F, if we subtract the number indexed by the current row from this column label we get a new number Y, we then check the row above the current row in the column labelled by Y, if we see a T, this T is propagated into the column X in the current row (e.g. if we’re looking at the second row, column labelled with -2, we subtract the number of the current row -3 from the column label, -2 – -3 = -2 + 3 = 1, this new number is the column label in the first row, we can see that in the first row in the column labelled with 1 there is a T, therefore this T gets propagated to the second row into the column labelled with -2). This is due to the fact that if we take a sum that is already possible and add another number to it, this obviously creates a new sum which is now possible.
Those three patterns are the algorithm that we use to fill in our matrix one row at a time. We can now use them to fill in the last row. The number indexed by the last row is 4. Therefore in the last row, the column labelled by 4 will get a T (via the first pattern). All the columns that already have a T will have that T propagate to the last row (via the second pattern). This means the only columns with an F will be those labelled by 5, 6 and 7. However using pattern 3, if we subtract 4 from 5, 6 and 7 we get:
5 - 4 = 1 6 - 4 = 2 7 - 4 = 3
If we now look at the previous row in the columns labelled by those numbers we can see a T for all three cases, therefore, even the columns labelled with 5, 6 and 7 in the last row will pick up a T via the third pattern. Our final matrix is:
+---+----+----+----+---+---+---+---+---+---+---+---+ | | -3 | -2 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +---+----+----+----+---+---+---+---+---+---+---+---+ | 0 | F | F | F | F | T | F | F | F | F | F | F | | 1 | T | T | F | F | T | F | F | F | F | F | F | | 2 | T | T | T | T | T | T | T | F | F | F | F | | 3 | T | T | T | T | T | T | T | T | T | T | T | +---+----+----+----+---+---+---+---+---+---+---+---+
One final problem remains, how can we use this matrix to get the subset that adds up to the value we want (i.e. 0). This is also reasonably simple. We start in the column labelled by the sum we’re after, in our case we start in the column labelled by zero. If this column does not contain a T then our sum is not possible and the input does not have a solution. In our case, the column does have a T so we’re in business.
- We start at the last row in this column; if it has a T and the row above has a T we go to the row above.
- If the row above has an F then we take the number which is indexed by the current row and write it into our final output.
- We then subtract this number from the column label to get the next column label. We jump to the new column label and go up a row.
- Once again if there is a T there and there is an F above, then we write the number indexed by the row into our output and subtract it from the current column label to get the next column label.
- We then jump to that column and go up a row again.
- We keep doing this until we get to the top of the matrix, at this point the numbers we have written to the output will be our solution.
Let’s do this for our matrix. We start at the column labelled by 0 since that’s the sum we’re looking for. We look at the last row and see a T, but there is also a T in the row above so we go up to that row. Now there is an F in the row above, so we write the number indexed by this row into our output:
output = [2]
We now subtract this number from our column label to get the new column label:
0 - 2 = -2
We jump to the column labelled by -2 and go up a row, there is another T there with an F in the row above, so we write the number indexed by the row to our output:
output = [2, -3]
We perform our subtraction step again:
-2 - -3 = -2 + 3 = 1
We now jump to the column labelled by 1 in the first row in the matrix. There is also a T there, so we need to write one last number to our output:
output = [2, -3, 1]
Since we’re at the top of the matrix, we’re done. As you can see the procedure we perform to reconstruct the output subset is actually a variant of the third pattern we used to construct the matrix. And that’s all there is to it.
Oh yeah, I almost forgot the code :), since it is not tiny, I put it in a gist, you can find it here. But, here are the guts of it:
|
|
You can recognise the 3 patterns being applied in the ‘populate’ method. We’re, of course, missing the code for instantiating the matrix in the first place. Grab the whole thing from the gist and give it a run, it generates random inputs of size 50 with values between -1000 and 1000. And if you think that would produce quite a large matrix, you would be right :) (50 rows and about 25000 columns give or take a few thousand). But even with input size 100 it only takes a couple of seconds to get an answer, which is MUCH better than the exponential time algorithm; in my book that equals success. Dropbox Challenge 3 – solved (more or less :))!
By the way if you want to print out a few more matrices, grab the code and uncomment the relevant line (102) and you’ll get a matrix similar to those above along with the rest of the output. Obviously, if you’re doing that, make sure your input size is small enough for the matrix to actually fit on the screen. I used the great terminal-table gem to produce the nice ASCII tables.
Lastly, if you’re wondering what framework this is:
|
|
That would be me eating my own dog food, I took the time to write it, might as well use it :).
By the way, it took me hours (pretty much the better part of a day) to get all of this stuff working properly, dynamic programming algorithms really are fiddly little beasts. But, I had some fun, and got some good practice and learning out of it – time well spent (and now there is some decent subset sum code on the internet :P). Of course once I finished with this I had to look at the other challenges, number 2 didn’t really catch my attention, but I couldn’t walk away from number 1 with its ASCII boxes and bin packing goodness – I’ll write that one up some other time.
Images by johntrainor, infinitewhite and familymwr