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 coins in total. I am trying to figure out how to properly select coins so that the least amount of change is given in return. At first I thought this was going to be easy to solve, but now I'm having trouble wrapping my head around it.
Here are two examples that explain the situation further:
Example 1:
The user is carrying these coins: $5, $3, $3, $3, $1, $1, $1, $1 and want to buy an item for $12. The solution would be to pay with $5, $3, $3, $1 and give no change.
Example 2:
The user does not have any $1 coins, and is carrying $5, $3, $3, $3, $3. An item is bought for $12 so they pay with $5, $3, $3, and $3 and change of $2 is given back.
Since we select the larger coins first, what I can't figure out is how to know if there are enough lower valued coins ($1 in this case) in the player's inventory to accommodate example 1, and if there aren't enough to use more of the higher valued coins as in example 2.
A further issue is seen in the following example, though I'd be happy just getting the above two examples working:
Example 3:
The user is carrying these coins: $5, $3, $3, $3. The player buys something for $6. It would be better to use $3 and $3 and return no change rather than using $5 and $3 and give $2 in change.
I believe the first two examples can be solved using recursion and a variation of the greedy algorithm.
For the bounty award:
I have added my own answer below as a temporary solution for now. However, I like the approach of Mr. Llama's below (see the link he references) and would like to find a PHP example to satisfy this. I believe this approach does not need recursion and uses memoization.
If there are multiple options for the least amount of change then I would like the tie to be given to the one that pays with the least amount of coins.
The problem can be defined as:
Return a subset of items where the sum is closest to x, but >= x.
This problem is called the subset sum problem. It is NP-complete. You won't find a perfect algorithm that runs in pseudo-polynomial time, only imperfect heuristics.
However, if the number of coins is very small, then an exhaustive search of the solution space will certainly work.
If the number of coins is larger, then you should look at Wikipedia for an overview: https://en.wikipedia.org/wiki/Subset_sum_problem#Polynomial_time_approximate_algorithm
I had a similar problem except instead of being allowed to go over, the combination had to stay under the target amount. In the end, I used the dynamic approach presented in this answer. You should be able to use it too.
It goes something like this:
Start with a list consisting of a single empty element.
For each entry in the list...
Copy the entry and add to it the first coin (not coin value!) that it doesn't contain.
Store the new element in the original list if and only if* its new sum value doesn't already exist in the list.
Repeat step 2 until you make a pass where no new elements are added to the list
Iterate the result list and keep the best combination (using your criteria)
*: We can make this optimization because we don't particularly care which coins are used in the combination, only the sum value of the collection of coins.
The above algorithm can be optimized a bit if you use the sum value as the key.
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.
You can use a stack to enumerate valid combinations. The version below uses a small optimization, calculating if a minimum of the current denomination is needed. More than one least change combinations are returned if there are any, which could be restricted with memoization; one could also add an early exit if the current denomination could complete the combination with zero change. I hope the laconically commented code is self-explanatory (let me know if you'd like further explanation):
function leastChange($coin_value,$inventory,$price){
$n = count($inventory);
$have = 0;
for ($i=0; $i<$n; $i++){
$have += $inventory[$i] * $coin_value[$i];
}
$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];
// base case
if ($owed <= 0){
if ($owed > $best[0]){
$best = [$owed,$result];
} else if ($owed == $best[0]){
// here you can add a test for a smaller number of coins
$best[] = $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;
}
Output:
$coin_value = [10,5,3,1];
$inventory = [0,1,3,4];
$price = 12;
echo json_encode(leastChange($coin_value,$inventory,$price)); // [0,[0,1,2,1],[0,1,1,4],[0,0,3,3]]
$coin_value = [10,5,3,1];
$inventory = [0,1,4,0];
$price = 12;
echo json_encode(leastChange($coin_value,$inventory,$price)); // [0,[0,0,4]]
$coin_value = [10,5,3,1];
$inventory = [0,1,3,0];
$price = 6;
echo json_encode(leastChange($coin_value,$inventory,$price)); // [0,[0,0,2]]
$coin_value = [10,5,3,1];
$inventory = [0,1,3,0];
$price = 7;
echo json_encode(leastChange($coin_value,$inventory,$price)); // [-1,[0,1,1]]
Update:
Since you are also interested in the lowest number of coins, I think memoization could only work if we can guarantee that a better possibility won't be skipped. I think this can be done if we conduct our depth-first-search using the most large coins we can first. If we already achieved the same sum using larger coins, there's no point in continuing the current thread. Make sure the input inventory is presenting coins sorted in descending order of denomination size and add/change the following:
// maximum needed of this coin
$max = min($inventory[$i],ceil($owed / $inventory[$i]));
// add to stack
for ($j=$max; $j>=$min; $j--){
The solution I was able to made covers the 3 examples posted in your question. And always gives the change with as few coins as possible.
The tests I made seemed to be executed very fast.
Here I post the code:
<?php
//Example values
$coin_value = array(10,5,3,1);
$inventory = array(5,4,3,0);
$price = 29;
//Initialize counters
$btotal = 0;
$barray = array(0,0,0,0);
//Get the sum of coins
$total_coins = array_sum($inventory);
function check_availability($i) {
global $inventory, $barray;
$a = $inventory[$i];
$b = $barray[$i];
$the_diff = $a - $b;
return $the_diff != 0;
}
/*
* Checks the lower currency available
* Returns index for arrays, or -1 if none available
*/
function check_lower_available() {
for ($i = 3; $i >= 0; $i--) {
if (check_availability($i)) {
return $i;
}
}
return -1;
}
for($i=0;$i<4;$i++) {
while(check_availability($i) && ($btotal + $coin_value[$i]) <= $price) {
$btotal += $coin_value[$i];
$barray[$i]++;
}
}
if($price != $btotal) {
$buf = check_lower_available();
for ($i = $buf; $i >= 0; $i--) {
if (check_availability($i) && ($btotal + $coin_value[$i]) > $price) {
$btotal += $coin_value[$i];
$barray[$i]++;
break;
}
}
}
// Time to pay
$bchange = 0;
$barray_change = array(0,0,0,0);
if ($price > $btotal) {
echo "You have not enough money.";
}
else {
$pay_msg = "You paid $".$btotal."\n\n";
$pay_msg.= "You used ".$barray[0]." coins of $10\n";
$pay_msg.= "You used ".$barray[1]." coins of $5\n";
$pay_msg.= "You used ".$barray[2]." coins of $3\n";
$pay_msg.= "You used ".$barray[3]." coins of $1\n\n\n";
// Time to give change
$the_diff = $btotal - $price;
if (!empty($the_diff)) {
for ($i = 0; $i < 4; $i++) {
while($the_diff >= $coin_value[$i]) {
$bchange += $coin_value[$i];
$barray_change[$i]++;
$the_diff -= $coin_value[$i];
}
}
$check_sum = array_sum($inventory) - array_sum($barray);
$check_sum+= array_sum($barray_change);
$msg = "";
if ($check_sum < 15) {
$change_msg = "Your change: $".$bchange."\n\n";
$change_msg.= "You received ".$barray_change[0]." coins of $10\n";
$change_msg.= "You received ".$barray_change[1]." coins of $5\n";
$change_msg.= "You received ".$barray_change[2]." coins of $3\n";
$change_msg.= "You received ".$barray_change[3]." coins of $1\n\n";
$msg = $pay_msg.$change_msg;
}
else {
$msg = "You have not enough space to hold the change.\n";
$msg.= "Buy cancelled.\n";
}
}
else {
$msg = $pay_msg."You do not need change\n";
}
if ($check_sum < 15) {
for ($i = 0; $i < 4; $i++) {
$inventory[$i] -= $barray[$i];
$total_coins-= $barray[$i];
}
for ($i = 0; $i < 4; $i++) {
$inventory[$i] += $barray_change[$i];
$total_coins+= $barray[$i];
}
}
echo $msg;
echo "Now you have:\n";
echo $inventory[0]." coins of $10\n";
echo $inventory[1]." coins of $5\n";
echo $inventory[2]." coins of $3\n";
echo $inventory[3]." coins of $1\n";
}
I don't know PHP so I've tried it in Java. I hope that is ok as its the algorithm that is important.
My code is as follows:
package stackoverflow.changecalculator;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ChangeCalculator
{
List<Integer> coinsInTil = new ArrayList<>();
public void setCoinsInTil(List<Integer> coinsInTil)
{
this.coinsInTil = coinsInTil;
}
public Map<String, List> getPaymentDetailsFromCoinsAvailable(final int amountOwed, List<Integer> inPocketCoins)
{
List<Integer> paid = new ArrayList<>();
int remaining = amountOwed;
// Check starting with the largest coin.
for (Integer coin : inPocketCoins)
if (remaining > 0 && (remaining - coin) >= 0) {
paid.add(coin);
remaining = remaining - coin;
}
ProcessAlternative processAlternative = new ProcessAlternative(amountOwed, inPocketCoins, paid, remaining).invoke();
paid = processAlternative.getPaid();
remaining = processAlternative.getRemaining();
removeUsedCoinsFromPocket(inPocketCoins, paid);
int changeOwed = payTheRestWithNonExactAmount(inPocketCoins, paid, remaining);
List<Integer> change = calculateChangeOwed(changeOwed);
Map<String, List> result = new HashMap<>();
result.put("paid", paid);
result.put("change", change);
return result;
}
private void removeUsedCoinsFromPocket(List<Integer> inPocketCoins, List<Integer> paid)
{
for (int i = 0; i < inPocketCoins.size(); i++) {
Integer coin = inPocketCoins.get(i);
if (paid.contains(coin))
inPocketCoins.remove(i);
}
}
private List<Integer> calculateChangeOwed(int changeOwed)
{
List<Integer> change = new ArrayList<>();
if (changeOwed < 0) {
for (Integer coin : coinsInTil) {
if (coin + changeOwed == 0) {
change.add(coin);
changeOwed = changeOwed + coin;
}
}
}
return change;
}
private int payTheRestWithNonExactAmount(List<Integer> inPocketCoins, List<Integer> paid, int remaining)
{
if (remaining > 0) {
for (int coin : inPocketCoins) {
while (remaining > 0) {
paid.add(coin);
remaining = remaining - coin;
}
}
}
return remaining;
}
}
The ProcessAlternative class handles cases where the largest coin doesn't allow us to get a case where there is no change to be returned so we try an alternative.
package stackoverflow.changecalculator;
import java.util.ArrayList;
import java.util.List;
// if any remaining, check if we can pay with smaller coins first.
class ProcessAlternative
{
private int amountOwed;
private List<Integer> inPocketCoins;
private List<Integer> paid;
private int remaining;
public ProcessAlternative(int amountOwed, List<Integer> inPocketCoins, List<Integer> paid, int remaining)
{
this.amountOwed = amountOwed;
this.inPocketCoins = inPocketCoins;
this.paid = paid;
this.remaining = remaining;
}
public List<Integer> getPaid()
{
return paid;
}
public int getRemaining()
{
return remaining;
}
public ProcessAlternative invoke()
{
List<Integer> alternative = new ArrayList<>();
int altRemaining = amountOwed;
if (remaining > 0) {
for (Integer coin : inPocketCoins)
if (altRemaining > 0 && factorsOfAmountOwed(amountOwed).contains(coin)) {
alternative.add(coin);
altRemaining = altRemaining - coin;
}
// if alternative doesn't require change, use it.
if (altRemaining == 0) {
paid = alternative;
remaining = altRemaining;
}
}
return this;
}
private ArrayList<Integer> factorsOfAmountOwed(int num)
{
ArrayList<Integer> aux = new ArrayList<>();
for (int i = 1; i <= num / 2; i++)
if ((num % i) == 0)
aux.add(i);
return aux;
}
}
I worked in it by doing a test for example 1, then for example 2, and lastly moved on to example 3. The process alternative bit was added here and the alternative for the original test coins returned 0 change required so I updated to the amount input to 15 instead of 12 so it would calculate the change required.
Tests are as follows:
package stackoverflow.changecalculator;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class ChangeCalculatorTest
{
public static final int FIFTY_PENCE = 0;
public static final int TWENTY_PENCE = 1;
public static final int TEN_PENCE = 2;
public static final int FIVE_PENCE = 3;
public static final int TWO_PENCE = 4;
public static final int PENNY = 5;
public ChangeCalculator calculator;
#Before
public void setUp() throws Exception
{
calculator = new ChangeCalculator();
List<Integer> inTil = new ArrayList<>();
inTil.add(FIFTY_PENCE);
inTil.add(TWENTY_PENCE);
inTil.add(TEN_PENCE);
inTil.add(FIVE_PENCE);
inTil.add(TWO_PENCE);
inTil.add(PENNY);
calculator.setCoinsInTil(inTil);
}
#Test
public void whenHaveExactAmount_thenNoChange() throws Exception
{
// $5, $3, $3, $3, $1, $1, $1, $1
List<Integer> inPocket = new ArrayList<>();
inPocket.add(5);
inPocket.add(3);
inPocket.add(3);
inPocket.add(3);
inPocket.add(1);
inPocket.add(1);
inPocket.add(1);
inPocket.add(1);
Map<String, List> result = calculator.getPaymentDetailsFromCoinsAvailable(12, inPocket);
List change = result.get("change");
assertTrue(change.size() == 0);
List paid = result.get("paid");
List<Integer> expected = new ArrayList<>();
expected.add(5);
expected.add(3);
expected.add(3);
expected.add(1);
assertEquals(expected, paid);
}
#Test
public void whenDoNotHaveExactAmount_thenChangeReturned() throws Exception {
// $5, $3, $3, $3, $3
List<Integer> inPocket = new ArrayList<>();
inPocket.add(5);
inPocket.add(3);
inPocket.add(3);
inPocket.add(3);
inPocket.add(3);
Map<String, List> result = calculator.getPaymentDetailsFromCoinsAvailable(15, inPocket);
List change = result.get("change");
Object actual = change.get(0);
assertEquals(2, actual);
List paid = result.get("paid");
List<Integer> expected = new ArrayList<>();
expected.add(5);
expected.add(3);
expected.add(3);
expected.add(3);
expected.add(3);
assertEquals(expected, paid);
}
#Test
public void whenWeHaveExactAmountButItDoesNotIncludeBiggestCoin_thenPayWithSmallerCoins() throws Exception {
// $5, $3, $3, $3
List<Integer> inPocket = new ArrayList<>();
inPocket.add(5);
inPocket.add(3);
inPocket.add(3);
inPocket.add(3);
Map<String, List> result = calculator.getPaymentDetailsFromCoinsAvailable(6, inPocket);
List change = result.get("change");
assertTrue(change.size() == 0);
List paid = result.get("paid");
List<Integer> expected = new ArrayList<>();
expected.add(3);
expected.add(3);
assertEquals(expected, paid);
}
}
The tests are not the cleanest yet but they are all passing thus far. I may go back and add some more test cases later to see if I can break it but don't have time right now.
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!
This is my solution i do not know how efficient is it but it works,i am open for suggestion.
<?php
$player=array(0,3,1,0);//how much coins you have
$player_copy=$player;
$coin_count=array(0,0,0,0);//memorize which coins you gave
$coin_value=array(1,3,5,10);
$price=6; //price of item
$price_copy=$price;
$z=3;
$change=array(-1,-1,-1,-1,-1); //memorise possible changes you can get
$k=0;
$flag=0;
label1: for($j=3;$j>=0;$j--){
$coin_count[$j]=0;
$player[$j]=$player_copy[$j];
}
for($j=$z;$j>=0;$j--){
while(($price>0) && 1<=$player[$j]){
$price-=$coin_value[$j];
$player[$j]--;
$coin_count[$j]++;
}
}
$change[$k++]=$price;
if($price!=0){
for($j=$z;$j>=0;$j--)
if($price_copy>$coin_value[$j]){
$z=$j-1;
$price=$price_copy;
goto label1;
}
$flag=1;
}
//find minimum change
$minv=$change[0];
for($i=1;$change[$i]>=0 and $i<4;$i++)
if($change[$i]>$minv)
$minv=$change[$i];
$i;
//when you find minimum change find which coins you have used
for($i=0;$i<4;$i++)
if($change[$i]==$minv && $flag==1){
$flag=2;
for($j=3;$j>=0;$j--){//reset coin_count and player budget
$coin_count[$j]=0;
$player[$j]=$player_copy[$j];
}
for($j=3-($i%2)-1;$j>=0;$j--){
while(($price>0) && 1<=$player[$j]){
$price-=$coin_value[$j];
$player[$j]--;
$coin_count[$j]++;
}
}
}
//prints result
for($j=0;$j<4;$j++)
printf("%d x %d\n",$coin_count[$j],$coin_value[$j]);
printf("change: %d\n",$minv);
?>
I have a script I found on here that works well when looking for the Lowest Common Substring.
However, I need it to tolerate some incorrect/missing characters. I would like be able to either input a percentage of similarity required, or perhaps specify the number of missing/wrong characters allowable.
For example, I want to find this string:
big yellow school bus
inside of this string:
they rode the bigyellow schook bus that afternoon
This is the code i'm currently using:
function longest_common_substring($words) {
$words = array_map('strtolower', array_map('trim', $words));
$sort_by_strlen = create_function('$a, $b', 'if (strlen($a) == strlen($b)) { return strcmp($a, $b); } return (strlen($a) < strlen($b)) ? -1 : 1;');
usort($words, $sort_by_strlen);
// We have to assume that each string has something in common with the first
// string (post sort), we just need to figure out what the longest common
// string is. If any string DOES NOT have something in common with the first
// string, return false.
$longest_common_substring = array();
$shortest_string = str_split(array_shift($words));
while (sizeof($shortest_string)) {
array_unshift($longest_common_substring, '');
foreach ($shortest_string as $ci => $char) {
foreach ($words as $wi => $word) {
if (!strstr($word, $longest_common_substring[0] . $char)) {
// No match
break 2;
}
}
// we found the current char in each word, so add it to the first longest_common_substring element,
// then start checking again using the next char as well
$longest_common_substring[0].= $char;
}
// We've finished looping through the entire shortest_string.
// Remove the first char and start all over. Do this until there are no more
// chars to search on.
array_shift($shortest_string);
}
// If we made it here then we've run through everything
usort($longest_common_substring, $sort_by_strlen);
return array_pop($longest_common_substring);
}
Any help is much appreciated.
UPDATE
The PHP levenshtein function is limited to 255 characters, and some of the haystacks i'm searching are 1000+ characters.
Writing this as a second answer because it's not based on my previous (bad) one at all.
This code is based on http://en.wikipedia.org/wiki/Wagner%E2%80%93Fischer_algorithm and http://en.wikipedia.org/wiki/Approximate_string_matching#Problem_formulation_and_algorithms
It returns one (of potentially several) minimum-levenshtein substrings of $haystack, given $needle. Now, levenshtein distance is just one measure of edit distance and it may not actually suit your needs. 'hte' is closer on this metric to 'he' than it is to 'the'. Some of the examples I put in show the limitations of this technique. I believe this to be considerably more reliable than the previous answer I gave, but let me know how it works for you.
// utility function - returns the key of the array minimum
function array_min_key($arr)
{
$min_key = null;
$min = PHP_INT_MAX;
foreach($arr as $k => $v) {
if ($v < $min) {
$min = $v;
$min_key = $k;
}
}
return $min_key;
}
// Calculate the edit distance between two strings
function edit_distance($string1, $string2)
{
$m = strlen($string1);
$n = strlen($string2);
$d = array();
// the distance from '' to substr(string,$i)
for($i=0;$i<=$m;$i++) $d[$i][0] = $i;
for($i=0;$i<=$n;$i++) $d[0][$i] = $i;
// fill-in the edit distance matrix
for($j=1; $j<=$n; $j++)
{
for($i=1; $i<=$m; $i++)
{
// Using, for example, the levenshtein distance as edit distance
list($p_i,$p_j,$cost) = levenshtein_weighting($i,$j,$d,$string1,$string2);
$d[$i][$j] = $d[$p_i][$p_j]+$cost;
}
}
return $d[$m][$n];
}
// Helper function for edit_distance()
function levenshtein_weighting($i,$j,$d,$string1,$string2)
{
// if the two letters are equal, cost is 0
if($string1[$i-1] === $string2[$j-1]) {
return array($i-1,$j-1,0);
}
// cost we assign each operation
$cost['delete'] = 1;
$cost['insert'] = 1;
$cost['substitute'] = 1;
// cost of operation + cost to get to the substring we perform it on
$total_cost['delete'] = $d[$i-1][$j] + $cost['delete'];
$total_cost['insert'] = $d[$i][$j-1] + $cost['insert'];
$total_cost['substitute'] = $d[$i-1][$j-1] + $cost['substitute'];
// return the parent array keys of $d and the operation's cost
$min_key = array_min_key($total_cost);
if ($min_key == 'delete') {
return array($i-1,$j,$cost['delete']);
} elseif($min_key == 'insert') {
return array($i,$j-1,$cost['insert']);
} else {
return array($i-1,$j-1,$cost['substitute']);
}
}
// attempt to find the substring of $haystack most closely matching $needle
function shortest_edit_substring($needle, $haystack)
{
// initialize edit distance matrix
$m = strlen($needle);
$n = strlen($haystack);
$d = array();
for($i=0;$i<=$m;$i++) {
$d[$i][0] = $i;
$backtrace[$i][0] = null;
}
// instead of strlen, we initialize the top row to all 0's
for($i=0;$i<=$n;$i++) {
$d[0][$i] = 0;
$backtrace[0][$i] = null;
}
// same as the edit_distance calculation, but keep track of how we got there
for($j=1; $j<=$n; $j++)
{
for($i=1; $i<=$m; $i++)
{
list($p_i,$p_j,$cost) = levenshtein_weighting($i,$j,$d,$needle,$haystack);
$d[$i][$j] = $d[$p_i][$p_j]+$cost;
$backtrace[$i][$j] = array($p_i,$p_j);
}
}
// now find the minimum at the bottom row
$min_key = array_min_key($d[$m]);
$current = array($m,$min_key);
$parent = $backtrace[$m][$min_key];
// trace up path to the top row
while(! is_null($parent)) {
$current = $parent;
$parent = $backtrace[$current[0]][$current[1]];
}
// and take a substring based on those results
$start = $current[1];
$end = $min_key;
return substr($haystack,$start,$end-$start);
}
// some testing
$data = array( array('foo',' foo'), array('fat','far'), array('dat burn','rugburn'));
$data[] = array('big yellow school bus','they rode the bigyellow schook bus that afternoon');
$data[] = array('bus','they rode the bigyellow schook bus that afternoon');
$data[] = array('big','they rode the bigyellow schook bus that afternoon');
$data[] = array('nook','they rode the bigyellow schook bus that afternoon');
$data[] = array('they','console, controller and games are all in very good condition, only played occasionally. includes power cable, controller charge cable and audio cable. smoke free house. pes 2011 super street fighter');
$data[] = array('controker','console, controller and games are all in very good condition, only played occasionally. includes power cable, controller charge cable and audio cable. smoke free house. pes 2011 super street fighter');
foreach($data as $dat) {
$substring = shortest_edit_substring($dat[0],$dat[1]);
$dist = edit_distance($dat[0],$substring);
printf("Found |%s| in |%s|, matching |%s| with edit distance %d\n",$substring,$dat[1],$dat[0],$dist);
}
Earlier I wrote a code in Matlab for this sort of lottery function, just to test if it was possible. However, I actually needed it in PHP so I've just rewritten the code and it does seem to work, but as it involves a lot of looping I want to make sure I'm doing it as efficiently as possible.
What the code does:
You can call the function $lotto -> type($users,$difficulty) and it will return two numbers. Here's the explanation, $users is the number of users registered on the website, i.e the people who will potentially buy a ticket. $difficulty is a number between 1 and 10, where 5 is normal, 1 is easy and 10 is hard. Difficulty here means how hard it is to match all numbers on a lottery ticket.
So what are the numbers that the function returns? That would be $n and $r. $n is the amount of numbers there will be on the lottery ticket, and $r is the amount of numbers you can choose from the lottery ticket. For example, in the UK a national lottery ticket has 49 numbers if which you choose 6. I.e $n = 49 and $r = 6.
How does the function calculate these two numbers? In the UK national lottery there are 13,983,816 different possible ticket combinations. If I were to run $lotto -> type(13983816,1) it would return array(49,6). Basically it tried to make it so there are as many combinations of tickets as there are registered users.
tl;dr, here's the code:
<?php
class lotto {
public function type($users,$difficulty){
$current_r = $r = 2;
$current_n = 0;
$difficulty = ($difficulty + 5) / 10; // sliding scale from 1 - 10
$last_tickets_sold = 200; // tickets sold in last lotto
$last_users = 100; // how many users there were in the last lotto
$last_factor = $last_tickets_sold / $last_users; // tickets per user
$factor = $last_factor * $difficulty;
$users *= $factor;
while($r <= 10){
$u = 0;
$n = $r;
while($u < $users && $n < 50){
$u = $this -> nCr(++$n,$r);
}
if($r == 2){
$current_n = $n;
} elseif(abs($this -> nCr($n,$r) - $users) < abs($this -> nCr($current_n,$current_r) - $users)){
// this is a better match so update current n and r
$current_r = $r;
$current_n = $n;
}
$r++;
}
return array($current_n,$current_r);
}
private function nCr($n,$r){
return $this -> factorial($n) / (
$this -> factorial($r) * $this -> factorial($n - $r)
);
}
private function factorial($x){
$f = $x;
while(--$x){
$f *= $x;
}
return $f;
}
}
$lotto = new lotto;
print_r($lotto -> type(1000,5));
?>
I did a quick scan and spotted a few places that can be further optimized.
Combination
Your algorithm is a brute force one and can be further optimized
private function nCr($n,$r){
return $this -> factorial($n) / (
$this->factorial($r) * $this->factorial($n - $r)
);
}
to
function nCr($n,$r) {
$top = 1;
$sub = 1;
for($i = $r+1; $i <= $n; $i++)
$top *= $i;
$n -= $r;
for($i = 2; $i <= $n; $i++)
$sub *= $i;
return $top / $sub;
}
Too Much Combination Calculation
Calculate combination is expensive.
$u = 0;
$n = $r;
while($u < $users && $n < 50){
$u = $this -> nCr(++$n,$r);
}
to
$n = $r + 1;
$u = nCr($n, $r);
while ($u < $users && $n < 50) {
$n++;
$u *= $n;
$u /= ($n - $r);
}
An immediate observation is that you have the possibility of a divide by 0 error
$last_factor = $last_tickets_sold / $last_users;
Could be solved by putting a simple if statement around it
$last_factor = ($last_users == 0) ? 0 : $last_tickets_sold / $last_users;
Regardless detailed examination of your code, are you sure that your loops does not need continue or break?
The range of factorial() in your algo is [0,50], so why not just precompute this statically?
private static $factorial=array(1);
private static genFactorial($max) {
if( count( self::$factorial ) > $max ) return;
foreach ( range(count(self::$factorial), $max) as $n ) {
self::$factorial[$n] = $i*self::$factorial[$n-1];
}
}
Now add a self::genFactorial(50); to __construct() or to type() and replace references to $this -> factorial($n) by self::$factorial[$n].
This is just a quick code dump; not even compile checked so forgive any typos, etc. but what this does is to replace a function call (which includes a while loop) by an array element fetch.