Here's my submission to the making change quiz. As Paolo mentioned,
this is the Knapsack problem which means that finding the absolute
optimal answer can be extremely expensive. One way of thinking of the
problem is as an N-dimensional search problem, where N is the number
of coins available. The key is to limit the variance in each dimension
to restrict the search space.
We start with a list of coins: [C0, C1, ... Cn) and must end up with a
count for each coin such that
V = X0C0 + X1C1 + ... + XnCn
The counts all have to be positive and V has to match the target value
supplied by the user. It's possible that there's no set of counts that
will produce a correct answer. The 'best' answer is the one that
contains the least number of coins.
My approach is to take each coin in turn and vary it between a lower
bound and an upper bound. For each count, I try to match the value
with the remaining coins. In order to minimise the search space as
much as possible, I compute the bounds as follows:
For the lower bound, first I compute the least common multiple of the
coin and the higher value coins (taking the largest one that won't put
be over the value), and then use the highest multiple of that which is
less than the value.
For example, if the problem is make_change(75, [30, 5, 1]) and I'm
varying '5', then the lcm of 5 and [30] is 30 and the lower bound is 60.
Then I compute the same thing for the lower value coins except that I
combine all of them (giving me the smallest number whereby changing
lower value coins can no longer improve the solution). The best of
these two bounds is used as the lower bound.
The upper bound is the highest count of the coin which is still below
the target value.
In each iteration, I recursively call make_change with the remaining
value and coins.
Here's a couple of runs with logging to show the effect. Notice that
in both cases, it doesn't waste time trying permutations of '2' and
'1' that could not produce a better result.
$ ruby -v make_change.rb 1000001 1000000 2 1
ruby 1.8.6 (2007-09-23 patchlevel 110) [powerpc-darwin9.0.0]
make_change 1000001, [1000000, 2, 1]
make_change 1, [2, 1]
make_change 1, [1]
make_change 1, [1000000, 1]
make_change 1, [1]
[1000000, 1]
$ ruby -v make_change.rb 1000001 1000000 999999 2
ruby 1.8.6 (2007-09-23 patchlevel 110) [powerpc-darwin9.0.0]
make_change 1000001, [1000000, 999999, 2]
make_change 1000001, [999999, 2]
make_change 1000001, [2]
make_change 2, [2]
make_change 1, [999999]
make_change 1, [999999, 2]
make_change 1, [2]
make_change 1, [999999]
make_change 1000001, [1000000, 2]
make_change 1, [2]
make_change 1, [1000000]
make_change 2, [1000000, 2]
make_change 2, [2]
make_change 1, [1000000, 999999]
make_change 1, [999999]
make_change 1, [1000000]
[999999, 2]
/dh
make_change.rb:
#!/usr/bin/env ruby
def greatest_common_divisor(a, b)
return a if b == 0
greatest_common_divisor(b, a % b)
end
def least_common_multiple(a, b)
return 0 if a == 0 or b == 0
(a * b) / greatest_common_divisor(a, b)
end
def make_change(value, coins=[])
return [] if value == 0
return nil if coins.empty?
puts "make_change #{value}, #{coins.inspect}" if $VERBOSE
best = nil
coins.each_with_index do |c, i|
lcm = coins[0,i].inject(0) { |memo, val| lcm =
least_common_multiple(c, val); lcm > memo && lcm <= value ? lcm : memo}
start = lcm == 0 ? 0 : (value - (value % lcm)) / c
lcm = coins[i+1,coins.size].inject(c) { |memo, val|
least_common_multiple(memo, val)}
if lcm != 0
try = (value - (value % lcm)) / c
start = try if try > start && try <= value
end
max = value / c
others = coins.reject {|v| v == c}
start = max if others.empty?
start.upto(max) do |n|
remaining = value - n * c
change = make_change(remaining, others)
next if change == nil
try = [c] * n + change
best = try if best.nil? or try.size < best.size
end
end
best.sort!.reverse! if best
best
end
if $0 == __FILE__
if ARGV.size == 0
puts "Usage: #{File.basename($PROGRAM_NAME)} <value> <coin>
<coin>..."
exit
end
value = ARGV.shift.to_i
coins = ARGV.collect {|c| c.to_i}
coins.sort!.reverse!
change = make_change value, coins
if change
puts change.inspect
else
puts "It's not possible to make #{value} with coins of value
#{coins.inspect}"
end
end