2

I'm practicing for programming interviews, and I stumbled upon this question on GlassDoor: "Find the number of ways we can sum the items in the set to get our target value. Order matters."

Apparently, the interviewer couldn't come up with an answer, but he left this comment on GlassDoor: "This was more of a math question than a programming question."

This problem seems to be different than this one: Finding all possible combinations of numbers to reach a given sum.

So my questions is: what is the correct approach to solve this problem, given that order matters? And also, what would be an efficient algorithm to solve the problem of finding all the ways to sum the items in the set to reach the target value, and order matters?

If you can provide working code, that would be awesome. Also, I'm practicing in Java, so I'd prefer a Java solution, but any language would also be fine.

Thanks so much.

Community
  • 1
  • 1
user224567893
  • 636
  • 4
  • 18
  • 28

2 Answers2

0

Use Dynamic Programming. Let's say we have items [1, 2, 3, 4, 5] and the goal is 10. To solve this problem we will calculate the number of ways to target any value from 0 to 10 with subsets [1], [1,2].. [1,2,3,4,5].

So what if we already knew the number of ways we can sum any value from 0 to 10 with [1,2,3]? How to get the number of ways to sum any value with one more item: [1,2,3,4]? It's pretty easy: we can simple not include this new item: it's all previous values. And for every targeted value x there is an addition number of ways = number of ways targeting value (x - 4) with previous set `[1,2,3]': Let's illustrate it with table:

| sum | 1 | 2 | 3 | 4     |
|   1 | 1 | 1 |(1)| 1     |
|   2 | 0 | 1 | 1 | 1     |
|   3 | 0 | 1 | 2 | 2     |
|   4 | 0 | 0 | 1 | 1     |
|   5 | 0 | 0 |(1)|(1)+(1)| - this means that we can target 5 with 1 way
|   6 | 0 | 0 | 0 | 0 + 1 | from set `[1,2,3]` and 1 way to achieve 5 - 4 
|   7 | 0 | 0 | 0 | 0 + 2 | with the same set `[1,2,3]
|   8 | 0 | 0 | 0 | 0 + 1 |

And it's easy to get initial value. With item 1 we can target only 1.

Nikita
  • 4,435
  • 3
  • 24
  • 44
  • In your table, it looks like 3 = 1 + 2 and 3 = 2 + 1 are considered the same, while the OP said they should be different in this problem: order matters. – Edward Doolittle Apr 19 '15 at 20:28
  • Ah, thx! There must be made additional calculation in backward propagation step. We will reconstruct our ordered solutions and produce all permutations of them. – Nikita Apr 19 '15 at 20:33
0

This is like a change-making problem, but you don't want the minimum number of "coins", you want to keep track of the number of ways. Furthermore, it is important to note that order matters. Use dynamic programming as follows. Suppose we want to show how to make 10 from sums of 1, 3, 5 where order doesn't matter (i.e. 1 + 3 + 1 + 5 is the same as 1 + 1 + 3 + 5). Make an array indexed to 10 (11 items). You can make 00 in 1 way. All negative indexes can be made in 0 ways.

00 01 02 03 04 05 06 07 08 09 10
 1

You then count the number of ways you can make 1. If you subtract coin value 1 from 1 you get 1. If you subtract any larger coin value from 1, you get a negative index which means zero ways. You sum up the results 1 way (subtracting 1) + 0 ways (subtracting 3) + 0 ways (subtracting 5). So we have

00 01 02 03 04 05 06 07 08 09 10
 1  1

Take a value of 2. We can subtract 1 cent coin to get 1 of which there is 1 way; 3 cent coin to get -1 of which there is 0 ways; 5 cent coin to get -3 of which there is 0 ways. Total 1 + 0 + 0 = 1 way to get 2 cents.

00 01 02 03 04 05 06 07 08 09 10
 1  1  1

For 3 cents, we can subtract 1 to access all the ways of making change for 2 cents, or subtract 3 cents to access all the ways of making 0 cents, or subtract 5 cents to access all the ways of making -2 cents; sum of ways is 1 + 1 + 0 = 2.

00 01 02 03 04 05 06 07 08 09 10
 1  1  1  2

For 4 cents, we can do it in 2 + 1 + 0 = 3 ways. However, note that the 3 ways include 1+1+1+1, 3+1, and 1+3. 1+3 and 3+1 are in different categories since they end in different numbers.

Continuing in this manner, we have

 00 01 02 03 04 05 06 07 08 09 10
  1  1  1  2  3  5  8 12 19 30 47

Now that we see how to construct the numbers, we can look for faster ways of doing so. Note that we have to deal with negative indices for the first few, which is a bit messy, so we might as well start with the first 5 values as given. What we need to calculate now is a recursive function: f(n) = f(n-1) + f(n-3) + f(n-5) with the condition that f(0),...,f(4) are given by the table above.

There are many ways to calculate that recursive function. This is where it becomes a real test of depth of knowledge. The naive approach would be to use recursion in the programming language. The first problem would be that your numbers will overflow because f has exponential growth. You can mitigate that situation for a time by using long instead of int. But long will quickly overflow too, so you'll have to use BigInteger to get exact results for n bigger than (I'm guessing) 20 or so.

Next, using recursive functions will be slow, because calculating f(n) might involve recalculating smaller f(x) over and over again. To mitigate that problem, use memoization to store f(x) for smaller values. Next, you'll experience stack overflow if you try to calculate from larger n to smaller. If you're calculating for a particular n, you should build up from lower values, instead of down from higher values of n.

Next, you'll experience out of memory. You can get around that by not remembering every value of f(x) as x increases, but only the last 5 values (in my example). Eventually you'll experience out of memory storing BigIntegers, but there's nothing you can do about that unless the question changes.

There are some more accelerations you can use if you're looking to improve the speed of the program. f(n) can be computed in log n operations rather than n operations in the following manner. Note that there is a matrix multiplication way of writing the recursion, where at each stage we need to remember 5 values to calculate the next one:

 |1 0 1 0 1| |f(n-1)|   | f(n) |
 |1 0 0 0 0| |f(n-2)|   |f(n-1)|
 |0 1 0 0 0| |f(n-3)| = |f(n-2)|
 |0 0 1 0 0| |f(n-4)|   |f(n-3)|
 |0 0 0 1 0| |f(n-5)|   |f(n-4)|

So we can calculate f(n) if we can calculate powers of the matrix:

 |1 0 1 0 1|^(n-4)
 |1 0 0 0 0|
 |0 1 0 0 0|
 |0 0 1 0 0|
 |0 0 0 1 0|

We can accelerate that process by using the binary expansion of n. Suppose n = 6, as it would be if we wanted f(10). Note that 6 = 4 + 2 in binary. Denote the matrix by M. We have M^1. We calculate M^2 = M^1 * M^1 and M^4 = M^2 * M^2, then M^6 = M^2 * M^4.

There may be other accelerations, depending on the particular example, but that should get you started.

Edward Doolittle
  • 4,002
  • 2
  • 14
  • 27
  • Why so complicated :( The dynamic programming is O(n) for computing time vs your claim about O(log n). And I didn't understand your last part about matrices. n is different from step to step - looks like matrix multiplication is not correct. – Nikita Apr 19 '15 at 20:40
  • The first part of my answer is simple and is all you need for an answer. The rest is a set of improvements or elaborations that you could use in a job interview, say, to show off your knowledge. I believe the matrix multiplication is correct, though it would have to be implemented to know for sure ... why don't you try it? – Edward Doolittle Apr 19 '15 at 21:16