I am making a game which consists of coin denominations of $10, $5, $3, and $1. The player may have 0 or more of each type of currency in his inventory with a maximum of 15 coin
This answer is based off of גלעד-ברקן's answer. I am posting it here as per his request. While none of the answers were quite the one that I was looking for I found that this was the best option posted. Here is the modified algorithm that I am currently using:
<?php
function leastChange($inventory, $price){
//NOTE: Hard coded these in the function for my purposes, but $coin value can be passed as a parameter for a more general-purpose algorithm
$num_coin_types = 4;
$coin_value = [10,5,3,1];
$have = 0;
for ($i=0; $i < $num_coin_types; $i++){
$have += $inventory[$i] * $coin_value[$i];
}
//NOTE: Check to see if you have enough money to make this purchase
if ($price > $have){
$error = ["error", "Insufficient Funds"];
return $error;
}
$stack = [[0,$price,$have,[]]];
$best = [-max($coin_value),[]];
while (!empty($stack)){
// each stack call traverses a different set of parameters
$parameters = array_pop($stack);
$i = $parameters[0];
$owed = $parameters[1];
$have = $parameters[2];
$result = $parameters[3];
if ($owed <= 0){
//NOTE: check for new option with least change OR if same as previous option check which uses the least coins paid
if ($owed > $best[0] || ($owed == $best[0] && (array_sum($result) < array_sum($best[1])))){
//NOTE: add extra zeros to end if needed
while (count($result) < 4){
$result[] = 0;
}
$best = [$owed,$result];
}
continue;
}
// skip if we have none of this coin
if ($inventory[$i] == 0){
$result[] = 0;
$stack[] = [$i + 1,$owed,$have,$result];
continue;
}
// minimum needed of this coin
$need = $owed - $have + $inventory[$i] * $coin_value[$i];
if ($need < 0){
$min = 0;
} else {
$min = ceil($need / $coin_value[$i]);
}
// add to stack
for ($j=$min; $j<=$inventory[$i]; $j++){
$stack[] = [$i + 1,$owed - $j * $coin_value[$i],$have - $inventory[$i] * $coin_value[$i],array_merge($result,[$j])];
if ($owed - $j * $coin_value[$i] < 0){
break;
}
}
}
return $best;
}
Here is my test code:
$start = microtime(true);
$inventory = [0,1,3,4];
$price = 12;
echo "\n";
echo json_encode(leastChange($inventory,$price));
echo "\n";
$inventory = [0,1,4,0];
$price = 12;
echo "\n";
echo json_encode(leastChange($inventory,$price));
echo "\n";
$inventory = [0,1,4,0];
$price = 6;
echo "\n";
echo json_encode(leastChange($inventory,$price));
echo "\n";
$inventory = [0,1,4,0];
$price = 7;
echo "\n";
echo json_encode(leastChange($inventory,$price));
echo "\n";
$inventory = [1,3,3,10];
$price=39;
echo "\n";
echo json_encode(leastChange($inventory,$price));
echo "\n";
$inventory = [1,3,3,10];
$price=45;
echo "\n";
echo json_encode(leastChange($inventory,$price));
echo "\n";
//stress test
$inventory = [25,25,25,1];
$price=449;
echo "\n";
echo json_encode(leastChange($inventory,$price));
echo "\n";
$time_elapsed = microtime(true) - $start;
echo "\n Time taken: $time_elapsed \n";
The result:
[0,[0,1,2,1]]
[0,[0,0,4,0]]
[0,[0,0,2,0]]
[-1,[0,1,1,0]]
[0,[1,3,3,5]]
["error","Insufficient Funds"]
[-1,[25,25,25,0]]
Time taken: 0.0046839714050293
Of course that time is in microseconds and therefore it executed in a fraction of a second!
I have come up with the following solution. If others can critique it for me I would appreciate it.
<?php
$coin_value = array(10,5,3,1);
$inventory = array(1,2,0,2);
$price = 17;
for ($i = 3; $i >= 0; $i--){
$btotal = 0;
$barray = array();
for ($j = 0; $j < 4; $j++){
$remaining = $price - $btotal;
$to_add = floor($remaining / $coin_value[$j]);
if ($i != 3 && $i == $j){
$to_add++;
}
if ($inventory[$j] < $to_add){
$to_add = $inventory[$j];
}
$btotal += $to_add * $coin_value[$j];
for ($k = 0; $k < $to_add; $k++){
$barray[] = $coin_value[$j];
}
if ($btotal >= $price)
break 2; //warning: breaks out of outer loop
}
}
$change_due = $btotal - $price;
print_r($barray);
echo "Change due: \$$change_due\n";
?>
It covers examples 1 and 2 in my original question, but does not cover example 3. However, I think it will do for now unless someone can come up with a better solution. I decided not to use recursion as it would seem to take too much time.