My course is covering dynamic programming. I'm not sure if I really "get it".
I'm trying to find the minimum number of coins needed to make change for a given amount.
This does give me the correct answer. I've tried several different coin values and change amounts and it is correct. But looking around it seems like I'm supposed to be building up a 2D array? I'm not doing that. I'm just overwriting a 1D array as needed. Is this a violation of dynamic programming?
Also I can't for the life of me figure out how to modify my algorithm to also get which coins were used. I find the minimum, but idk how to get an array like: UsedCoins=[1,1,1,1] or UsedCoins=[0,0,0,1]. Where each value is how many of each denomination of the corresponding index.
Well, I have one hour to kill, and I'm feeling like discussing this dynamic programming a bit... Forgive me for the long post, I hope I'm not polluting the thread, and I post it in the remote chance it can be useful to someone...
I'm starting with basics, building up, catching some Python tricks along the way. The beginning is probably dead easy, I've written it partly as an attempt to lay some ideas for different kind of people, not as a direct reply, so you may jump a lot of things.
I agree with Somnid, dynamic programming is a child of recursion (the computation you're trying to do relies on similar, simpler computations) and memoization (remember the results you've already computed to avoid computing those several times).
Let's take for example fibonacci sequence, where
u_0 = 0
u_1 = 1
u_n = u_(n-1) + u(n-2) if n>1
The casual definition would be
Code:
def fibo(n) :
"""return the term u_n of Fibonacci sequence
where n is a positive integer"""
if n<2 :
return n
return fibo(n-1) + fibo(n-2)
The problem with this definition is that you'll compute a lot of time the same thing. For example, for Fibo(5), you need Fibo(4) and Fibo(3), but Fibo(4) will also call Fibo(3), so it'll be computed twice. It grows exponentially from there.
So the solution can be memoization, remember already computed results :
Code:
# A list to remember first 100 u_n values, filling the first ones
_memory = [ 0, 1 ] + [ None ] * 98
def fibo(n) :
"""return the term u_n of Fibonacci sequence
where n is a positive integer"""
# if value has already been computed, return it
if _memory[n] is not None :
return _memory[n]
# if not, compute and memorize it, and return it
_memory[n] = fibo(n-1) + fibo(n-2)
return _memory[n]
Now, _memory is a global variable, which is quite nasty. In C, you'd probably want something like a static variable, a variable in a function whose value is preserved between calls. No such thing in Python? Well, you could use algebraic closure, but there's a simpler solution: default parameters. Default parameters are "static" in Python, and evaluated at function definition (that's one of those sources of strange bugs). So you can write:
Code:
def fibo(n, *,
_memory = [ 0, 1 ] + [ None ] * 98) :
"""return the term u_n of Fibonacci sequence
where n is a positive integer"""
# if value has already been computed, return it
if _memory[n] is not None :
return _memory[n]
# if not, compute and memorize it, and return it
_memory[n] = fibo(n-1) + fibo(n-2)
return _memory[n]
That's a bit better (* in arguments is a kind of trick, it makes _memory a keyword-only argument, just to be on the safer side... I also used the _ at the beginning of _memory only as a warning that the variable has some internal meaning, that may not be the best idea, but well...).
Now, the previous codes fail for n>=100. You can extend the list if needed:
Code:
def fibo(n, *,
_memory = [ 0, 1 ] + [ None ] * 98) :
"""return the term u_n of Fibonacci sequence
where n is a positive integer"""
# make sure the list can hold n+1 values
if len(_memory) < n+1 :
_memory.extend([None]*((n+1)-len(_memory))
# if value has already been computed, return it
if _memory[n] is not None :
return _memory[n]
# if not, compute and memorize it, and return it
_memory[n] = fibo(n-1) + fibo(n-2)
return _memory[n]
Now, since you'll need to compute all u_p where 0<p<n to compute u_n, instead of a recursion, you can compute all of them, in a loop.
Code:
def fibo(n)
# Create a table just large enough,
# its content doesn't matter since you'll compute all u_p
# in order
memory = [ 0 ] * (n+1) :
# but we'll define initial values, u_0=0 and u_1=1
memory[1] = 1
# then we compute u_p where 2 <= p <= n
for p in range(2, n+1) :
memory[p] = memory[p-1] + memory[p-2]
# and return the expected result
return memory[n]
This last solution is a typical example of dynamic programming: solve problems of increasing difficulty (fibo(p) with increasing p) while memorizing the results and using the results of previous steps, till you solve the problem you wanted to solve.
Some remarks:
- this solution is better in Python (actually in most languages, but especially in Python) than recursive ones, because recursivity is inefficient (by design)... you especially won't be able to recursively compute u_1000 because there's a limit on recursive calls (well, you technically could change some Python parameters, but you probably shouldn't).
- if you call the function several times, you'll compute several times the same values, so making the memory list "static" would still be nice, but I'll avoid doing it here to keep things simple
- you'll quickly see you only need the last two values to compute any subsequent value, so this function would be more efficiently written
Code:
def fibo(n) :
if n<2 :
return n
u, v = 0, 1
for p in range(2, n+1) :
u, v = v, u+v
return v
which is, technically, also a kind of dynamic programming, but probably too simple to be seen this way.
Now, on your "money" problem... I'll change a little bit a couple of things of your program to make things simpler.
memory will be a list so that memory[p] contains minimum number of coins you need to get the amount p.
memory[0] is obviously 0
In a dynamic programming scheme, we'll now compute memory[p] for p=1, then p=2, 3... n
For each coin c <= p, you can get the amount p by adding the coin c to the amount p-c. If you need k coins to get the amount p-c, you can get the amount p with k+1 coins at most.
If k+1 must be the smallest possible value, you want k to be as small as possible, and look for the coin c that gives you the smallest k.
So, obviously, as Cpp_is_kind stated, you have
memory[p] = 1 + min( memory[p-c] for all coins c<=p )
The code could simply be
Code:
def GetMinimum(denoms, change) :
# All results are unknown at first, except memory[0] = 0
# The other values don't matter, since they'll be updated
memory = [ 0 ] * (change+1)
# Let's compute all memory[p] using the formula
for p in range(min(denoms), change+1) :
memory[p] = 1 + min(memory[p-c] for c in denoms if p>=c)
return memory[change]
It's the exact same idea as your code, but I think it's easier to read/understand (?)
Well... if I want to go into details, that will work 99% of the time. It'll work all the time with *real* sets of coins (obviously... you don't want a coin set that is unable to produce some amounts!).
But if you want to be cautious, there's a chance that the min() function try to work with an empty list (for example, when you try to compute memory[1] of all coins are >1...) Let's go further, and use None as a result for amounts you can't obtain with the set of coins.
Code:
def GetMini(denoms, change) :
# All results are unknown at first, except _memory[0] = 0
# The other values don't really matter, but let's assume it's impossible by
# default, so that we don't have to change the value if no solution is found
memory = [ None ] * (change+1)
memory[0] = 0
# Let's compute all _memory[p] with increasing p
for p in range(min(denoms), change+1) :
candidates = [ memory[p-c] for c in denoms if p>=c and memory[p-c] is not None ]
# If at least a solution exists, compute memory[p]
if len(candidates) > 0 :
memory[p] = 1 + min(candidates)
return memory[change]
It could also be done with exceptions, putting the min(...) in the previous code in a try ... except. Same result.
Let's assume now that you want the solution, not the number of coins. You can use memory[p] list to store the best solutions for an amount p, instead of the number of coins. Those solutions can be list of coins.
We still compute all memory[p] for increasing p, and for each p, we retrieve all solutions for p-c (assuming p-c can be done), keep the shortest one, and add the corresponding coin to the list.
It's barely harder (but getting the shortest list and the corresponding coin among all possibilites is a bit longer)
Code:
def GetMini(denoms, change) :
# All results are unknown at first, except _memory[0] = 0
# The other values don't really matter, but let's assume it's impossible by
# default, so that we don't have to change the value if no solution is found
memory = [ None ] * (change+1)
memory[0] = []
# Let's compute all memory[p] with increasing p
for p in range(min(denoms), change+1) :
# Look, for each coin c, the best solution for amount p-c (if possible)
candidates = [ (c, memory[p-c]) for c in denoms if p>=c and memory[p-c] is not None ]
# if there's at least a solution
if len(candidates) > 0 :
# Look for the shortest one
newcoin, bestsol = candidates[0]
for coin, sol in candidates :
if len(sol) < len(bestsol) :
newcoin, bestsol = coin, sol
# Keep, for the amount p, the shortest solution for p-c, and add the coin c
memory[p] = bestsol.copy()
memory[p].append(newcoin)
return memory[change]
Note that this algorithm gives you a set of coins, not the number of each coin needed. But it should be trivial to change (start with [0]*len(denoms), and +1 one of the values of the copied lists instead of appending newcoin...)
As a closing remark, this algorithm works with any set of coins, but for "normal" set of coins (=canonical sets*), a greedy algorithm will provide the best solution faster.
* most countries in the world use canonical sets of coins. One example of non-canonical set, where finding the best solution is harder, is for example the former british system...
Sorry again for this long post...