I have a task for uni, the requirements of which are as follows:
- There is a collection of coins. For each non-negative integer k, there are two coins with the value 2k, i.e. the collection of coins is {1, 1, 2, 2, 4, 4, 8, 8, ... }
- For a given number, I need to write a method that returns the unique number of ways to make change for that amount, given the collection of coins.
- For example, if the number passed to the algorithm is 6, the relevant collection of coins would be {1, 1, 2, 2, 4, 4}, the subsets that add up to 6 are {1, 1, 2, 2}, {1, 1, 4}, {2, 4}, {2, 4}, {2, 4} and {2, 4}, the unique subsets are {1, 1, 2, 2}, {1, 1, 4} and {2, 4}, and therefore the total unique ways is 3.
- The numbers (and potential combinations) can be very large: the largest number in the tester class is 999,999,999,999,999,999 (1018 - 1), for which the expected result is 29,665,503.
It's apparent to me that the approach should involve dynamic programming. I've used DP once before (for another task where we had to maximise our returns in a 'coin game'), and I've watched lots of videos (such as MIT OCW) on dynamic programming to try and understand how we could solve this particular problem, but I'm quite stuck, with my current confusion as follows:
- I'm struggling to understand how we can frame this problem in terms of minimising or maximising something, and therefore how to structure the recurrence relationship. As opposed to trying to determine the minimum number of coins, we're interested in all combinations that work.
- There's also the issue that we (I think?) need to keep track of the solutions themselves, otherwise we won't be able to filter out duplicates.
- Although it may become apparent as I work out the recurrence relation and how it should be memoized, I feel like space will be an issue: wouldn't we need something like a Z*|C| (where |C| is the size of the array of coins) sized array to store our memoized results? For a Z of 1018, that array would be huge.
At the risk of making this post too long, I've tried to sketch out a few approaches, but always come down to the problem that the recurrence seems like an OR relationship. Something like:
Let z = desired amount
Let A be array of coins, and i be the index in that array
Recurrence relation: DP(i, z) = OR ( DP(i + 1, z), DP(i + 1, z - A[i]) )
// Unsure how to deal with this OR in actual code. We're not saying,
// "Return one of these, whichever is smaller/bigger". We're saying,
// "We want to know if either case works."
Or another approach where you don't actually have an array of coins, but just start at the largest power of 2 less than Z and work down:
Let z = desired amount
Let largestCoin = largestCoinLessThanZ(z) // e.g. for z = 6, largestCoin = 4
findChange(desiredSum, runningTotal, coin):
if runningTotal + coin = desiredSum:
[add path to pile of valid paths]
return ( findChange(desiredSum, runningTotal + coin, coin / 2) // using coin of this denomination once
or findChange(desiredSum, runningTotal + coin + coin, coin / 2) // using coin of this denomination twice
or findChange(desiredSum, runningTotal, coin / 2) // not using coin of this denomination at all
)
Main:
findchange(z, 0, largestCoin)
Sorry for the janky pseudocode - just trying to convey how I've approached it in my head.
In summary, I'm hoping for help understanding the recurrence relationship to solve this problem, and how to deal with potential space constraints. I'm working with C#, but I don't expect code - any math or pseudocode would be greatly appreciated.