I'm working on a very small blackjack game in PHP.
I'm currently writing the function to count the cards, but the aces are kicking my butt.
My cards are all in an array like this:
$card_array = array( "ca", "c10", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9",
"cj", "ck", "cq", "da", "d10", "d2", "d3", "d4", "d5", "d6", "d7", "d8",
"d9", "dj", "dk", "dq", "ha", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "h9",
"hj", "hk", "hq", "sa", "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s9",
"s10", "sj", "sk", "sq");`
Where the first character is the suit and everything after that is the card (j for jack, etc)
Here's what I have for counting the values up so far:
function bj_calculate_values( $cards ) {
$card_values = 0;
foreach ($cards as $card) {
if (substr($card, 1) == "k" || substr($card, 1) == "q" || substr($card, 1) == "j") {
$card_values += 10;
}
else if (substr($card, 1) != "a") {
$card_values += substr($card, 1);
}
}
}
Originally, I also had the ace in there valued at 11, but obviously that would cause problems. I feel like I need to keep relooping to make sure the aces don't put us over 21, but I'm not entirely sure of the best way to go about doing that.
I'd appreciate some input, guys.
EDIT:
The best I can think of is adding this to the function
foreach ($cards as $card) {
if (substr($card, 1) == "a") {
if ($card_values + 11 <= 21) {
$card_values += 11;
}
else {
$card_values += 1;
}
}
}
Actually I think this might work. Give me a few minutes to test.
EDIT:
Nope, didn't work. 4, ace, ace, 6 came out to 22 with this.
I do not know much about BlackJack, but this is my try.
function bj_calculate_values( $cards ) {
$card_values = 0;
$ace = 0;
foreach ($cards as $card) {
$val = substr($card, 1);
if (!is_numeric($val))
{
if ('a' == $val)
{
$val = 0;
$ace ++;
}
else
$val = 10;
}
$card_values += $val;
}
while ($ace)
$card_values += ($card_values + 11*$ace-- <= 21) ? 11: 1;
return $card_values;
}
UPDATE
If I understand your other comment, you want 6 4 A A A to come out 13, 6 4 A A to come out 12, and 6 4 A to come out 21. So the number of remaining aces count. Modified the source accordingly.
Explanation
First of all we calculate the value of non-ace cards. These are constant, so we get it over with. And this total is the (preliminary) $card_values.
Then we may have (or not) some aces. It makes sense to have a while($aces) so that if we have no aces, we do nothing.
And now the question is, what do we do with those aces? Here is where your requisite comes in: the sum must fit into 21. This means that I can't just say "I'm at 10, I have an ace, so it fits into 21" because there might be another ace after that. So I have to calculate what would the worst case be if I were to add in all remaining aces; and that's of course the number of aces, times 11. While the worst case is still good (i.e., $card_values + 11*$ace is less than 21) I can add 11. While it is not, I have to add 1.
It is a form of "greedy" algorithm: I fit in all the 1's we can, while ensuring enough 11's are left to reach the closest approach to 21 that does not exceed.
I seem to remember (I might be wrong, heh) that there was a nasty bug in this implementation which will come out whenever NumberOfItems * LowestValue >= HighestValue (I'm not too sure about that "equals" though). But in this case, it would require NumberOfItems to be above 11/1 = 11 aces, and there are not so many aces in the deck, so we're cool. And to be sure, the possible cases are five - from "no" to "four" aces in a hand - and easily tested for all sums of other cards:
// J Q K are worth 10, so we can use 10 instead.
// And a fake card with value of 0 stands for "nothing".
// We use the suit of Klingon, with up to four identical aces :-D
for ($first = 0; $first <= 21; $first++)
{
$hand = array("k$first");
for ($aces = 1; $aces < 5; $aces++)
{
$hand[] = "ka";
$value = bj_calculate_values($hand);
// Let us ponder the possible values of $value.
if ($value <= 11)
{
// This is an error: we could have gotten closer to 21
// by upvaluing an ace. A correct implementation will
// never enter here except in the case of a LONE ACE.
if (($first != 0) || ($aces != 1))
print "ERROR: " . implode(" ", $hand) . " = $value\n";
}
// We have a value from 12 to 21.
// Could we have done better? What cards do we have?
// If no aces, of course no. All card values are then fixed.
// If at least one ace, interpreted as 11, again not:
// cannot interpret it above 11, and interpret as 1 would LESSEN the score.
// If at least one ace, interpreted as 1, were we to interpret it
// as 11, we would be summing 10. And 12 would become 22, and blow.
// So, from 12 to 21, the result MUST be correct.
// Finally, $value more than 21.
if ($value > 21)
{
// This is admissible ONLY if we could have done no better, which
// means $value is given by the fixed cards' value, $first, plus
// ALL ACES COUNTED AS ONES.
if ($value != ($first + $aces))
print "ERROR: " . implode(" ", $hand) . " = $value\n";
}
}
}
The output (version 1)
VERIFY: k0 ka = 11 Correct, we had a lone ace and gave it 11.
VERIFY: k18 ka ka ka ka = 22 Correct, we had 18 in other cards and all A's to 1's.
VERIFY: k19 ka ka ka = 22 Ditto, 19 in cards.
VERIFY: k19 ka ka ka ka = 23 Ditto, 19 in cards.
... ...
Current output (added code to only print errors):
-- nothing :-)
Related
I really thought it would be easier to evaluate a straight but alas I am here after a couple days trying.
I have a deck that is dealt, each player gets two cards, when it comes to evaluation I combine the player cards with the community cards and sort them highest to lowest.
The array would look like ['14d', '13d', '12d', '1d', '10d', '9d', '8d']; eg. Ace Diamonds, King Diamonds, Queen Diamonds, etc..
I've tried multiple methods such as loops and manually evaluating each card, but it needs to be evaluated with a minimum of 5 cards (2 player cards + 3 community cards) up to 7 cards (2 player cards + 5 community cards)
Here is one of my attempts:
$hand = ['14d', '13d', '12d', '11d', '10d', '9d', '8d']; //Debug hand variable
$hand_len = count($hand);
$l_card = (int)$hand[0];
$set = array();
rsort($hand, SORT_NUMERIC);
for ($i=0; $i < $hand_len; $i++) {
//Which values to compare
if ( $i+1 != $hand_len && $i+2 != $hand_len) {
$c1 = (int)$hand[$i]; //14
$c2 = (int)$hand[$i+1]; //13
if ( $c1 == $c2+1 ) { //if 14 == 13(+1)
array_push($set, $hand[$i]);
}
} elseif ($i+1 == $hand_len && $i+2 == $hand_len) {
$c1 = (int)$hand[$i]; //8
$c2 = (int)$hand[$i-1];//9
if ( $c1 == $c2-1 ) { //if 8 == 9(-1)
array_push($set, $hand[$i]);
}
}
}
//Still to add evaluation for 14 (Ace) being used for low straight (eg. 14d, 2d, 3d...).
For each card I have to convert to an integer to rid the suit character then do the calculations. I can get some level of success until it has to evaluate the final card in the set.
For example. ['14d', '13d', '12d', '11d', '10d', '9d', '8d']; <- '8d' would not be evaluated and added to the straight set.
Does anyone have suggestions how to make this operate efficiently and reliably?
Thanks!
Your approach with a "set" is good, but at some point you have to check if you have reached 5 cards. You also have to check if you hit a gap in the straight, where you must start over again. And your code doesn't work for duplicate cards like 12d and 12h.
For sake of simplicity I removed the suit from the input and work on numbers only. First you remove any duplicates and sort them like you did with array_unique() and rsort(). Then you keep a list/set of the cards which are part of the straight. Add new cards to that chain when it fits. This is the case when the last card is only different by the value 1. When the difference is greater than 1, then you know it can't be a straight anymore. At that point you have to check for a new straight from that location with the remaining cards. See the following example source code:
$cards = array(6, 9, 5, 12, 7, 8, 11);
echo "Input cards: ".implode(',', $cards)."\n";
$cards = array_unique($cards);
rsort($cards);
$set = array(array_shift($cards)); // start with the first card
foreach ($cards as $card) {
$lastCard = $set[count($set)-1];
if ($lastCard - 1 != $card) {
// not a chain anymore, "restart" from here
$set = array($card);
} else {
$set[] = $card;
}
if (count($set) == 5) {
break;
}
}
if (count($set) == 5) {
echo "Found a straight with ".implode(',', $set)."\n";
} else {
echo "No straight\n";
}
This will generate the following output:
Input cards: 6,9,5,12,7,8,11
Found a straight with 9,8,7,6,5
The loop iterates as follow over the sorted cards 12, 11, 9, 8, 7, 6, 5:
sorted cards set content
--------------------------------------
12, 11, 9, 8, 7, 6, 5 [12] <-- start set
^ [12, 11]
^ [9] <-- break, restart set from here
^ [9,8]
^ [9,8,7]
^ [9,8,7,6]
^ [9,8,7,6,5]
Example Data
For this question, let's assume the following items:
Items: Apple, Banana, Carrot, Steak, Onion
Values: 2, 2, 4, 5, 3
Weights: 3, 1, 3, 4, 2
Max Weight: 7
Objective:
The MCKP is a type of Knapsack Problem with the additional constraint that "[T]he items are subdivided into k classes... and exactly one item must be taken from each class"
I have written the code to solve the 0/1 KS problem with dynamic programming using recursive calls and memoization. My question is whether it is possible to add this constraint to my current solution? Say my classes are Fruit, Vegetables, Meat (from the example), I would need to include 1 of each type. The classes could just as well be type 1, 2, 3.
Also, I think this can be solved with linear programming and a solver, but if possible, I'd like to understand the answer here.
Current Code:
<?php
$value = array(2, 2, 4, 5, 3);
$weight = array(3, 1, 3, 4, 2);
$maxWeight = 7;
$maxItems = 5;
$seen = array(array()); //2D array for memoization
$picked = array();
//Put a dummy zero at the front to make things easier later.
array_unshift($value, 0);
array_unshift($weight, 0);
//Call our Knapsack Solver and return the sum value of optimal set
$KSResult = KSTest($maxItems, $maxWeight, $value, $weight);
$maxValue = $KSResult; //copy the result so we can recreate the table
//Recreate the decision table from our memo array to determine what items were picked
//Here I am building the table backwards because I know the optimal value will be at the end
for($i=$maxItems; $i > 0; $i--) {
for($j=$maxWeight; $j > 0; $j--) {
if($seen[$i][$j] != $seen[$i-1][$j]
&& $maxValue == $seen[$i][$j]) {
array_push($picked, $i);
$maxValue -= $value[$i];
break;
}
}
}
//Print out picked items and max value
print("<pre>".print_r($picked,true)."</pre>");
echo $KSResult;
// Recursive formula to solve the KS Problem
// $n = number of items to check
// $c = total capacity of bag
function KSTest($n, $c, &$value, &$weight) {
global $seen;
if(isset($seen[$n][$c])) {
//We've seen this subproblem before
return $seen[$n][$c];
}
if($n === 0 || $c === 0){
//No more items to check or no more capacity
$result = 0;
}
elseif($weight[$n] > $c) {
//This item is too heavy, check next item without this one
$result = KSTest($n-1, $c, $value, $weight);
}
else {
//Take the higher result of keeping or not keeping the item
$tempVal1 = KSTest($n-1, $c, $value, $weight);
$tempVal2 = $value[$n] + KSTest($n-1, $c-$weight[$n], $value, $weight);
if($tempVal2 >= $tempVal1) {
$result = $tempVal2;
//some conditions could go here? otherwise use max()
}
else {
$result = $tempVal1;
}
}
//memo the results and return
$seen[$n][$c] = $result;
return $result;
}
?>
What I've Tried:
My first thought was to add a class (k) array, sort the items via class (k), and when we choose to select an item that is the same as the next item, check if it's better to keep the current item or the item without the next item. Seemed promising, but fell apart after a couple of items being checked. Something like this:
$tempVal3 = $value[$n] + KSTest($n-2, $c-$weight[$n]);
max( $tempVal2, $tempVal3);
Another thought is that at the function call, I could call a loop for each class type and solve the KS with only 1 item at a time of that type + the rest of the values. This will definitely be making some assumptions thought because the results of set 1 might still be assuming multiples of set 2, for example.
This looks to be the equation (If you are good at reading all those symbols?) :) and a C++ implementation? but I can't really see where the class constraint is happening?
The c++ implementation looks ok.
Your values and weights which are 1 dimensional array in your current PHP implementation will become 2 dimensional.
So for example,
values[i][j] will be value of j th item in class i. Similarly in case of weights[i][j]. You will be taking only one item for each class i and move forward while maximizing the condition.
The c++ implementation also does an optimization in memo. It only keeps 2 arrays of size respecting the max_weight condition, which are current and previous states. This is because you only need these 2 states at a time to compute present state.
Answers to your doubts:
1)
My first thought was to add a class (k) array, sort the items via
class (k), and when we choose to select an item that is the same as
the next item, check if it's better to keep the current item or the
item without the next item. Seemed promising, but fell apart after a
couple of items being checked. Something like this: $tempVal3 =
$value[$n] + KSTest($n-2, $c-$weight[$n]); max( $tempVal2, $tempVal3);
This won't work because there could be some item in class k+1 where you take a optimal value and to respect constraint you need to take a suboptimal value for class k. So sorting and picking the best won't work when the constraint is hit. If the constraint is not hit you can always pick the best value with best weight.
2)
Another thought is that at the function call, I could call a loop for
each class type and solve the KS with only 1 item at a time of that
type + the rest of the values.
Yes you are on the right track here. You will assume that you had already solved for first k classes. Now you will try extending using the values of k+1 class respecting the weight constraint.
3)
... but I can't really see where the class constraint is happening?
for (int i = 1; i < weight.size(); ++i) {
fill(current.begin(), current.end(), -1);
for (int j = 0; j < weight[i].size(); ++j) {
for (int k = weight[i][j]; k <= max_weight; ++k) {
if (last[k - weight[i][j]] > 0)
current[k] = max(current[k],
last[k - weight[i][j]] + value[i][j]);
}
}
swap(current, last);
}
In the above c++ snippet, the first loop iterates on class, the second loop iterates on values of class and the third loop extends the current state current using the previous state last and only 1 item j with class i at a time. Since you are only using previous state last and 1 item of the current class to extend and maximize, you are following the constraint.
Time complexity:
O( total_items x max_weight) which is equivalent to O( class x max_number_of_items_in_a_class x max_weight)
So I am not a php programmer but I will try to write a pseudocode with good explanation.
In the original problem each cell i, j meaning was: "Value of filling the knapsack with items 1 to i until it reach capacity j", the solution in the link you have provided defines each cell as "Value of filling the knapsack with items from buckets 1 to i until it reach capacity j". Notice that in this variation there is not such this as not taking an element from a class.
So on each step (each call for KSTest with $n, $c), we need to find which element to pick from the n'th class such that the weight of this element is less than c and it's value + KSTest(n - 1, c - w) is the greatest.
So I think you should only change the else if and else statements to something like:
else {
$result = 0
for($i=0; $i < $number_of_items_in_nth_class; $i++) {
if ($weight[$n][$i] > $c) {
//This item is too heavy, check next item
continue;
}
$result = max($result, KSTest($n-1, $c - $weight[$n][$i], $value, $weight));
}
}
Now two disclaimers:
I do not code in php so this code will not run :)
This is not the implementation given in the link you provided, TBH I didn't understood why the time complexity of their algorithm is so small (and what is C) but this implementation should work since it is following the definition of the recursive formula given.
The time complexity of this should be O(max_weight * number_of_classes * size_of_largerst_class).
This is my PHP solution. I've tried to comment the code in a way that it's easy to follow.
Update:
I updated the code because the old script was giving unreliable results. This is cleaner and has been thoroughly tested. Key takeaways are that I use two memo arrays, one at the group level to speed up execution and one at the item level to reconstruct the results. I found any attempts to track which items are being chosen as you go are unreliable and much less efficient. Also, isset() instead of if($var) is essential for checking the memo array because the previous results might have been 0 ;)
<?php
/**
* Multiple Choice Knapsack Solver
*
* #author Michael Cruz
* #version 1.0 - 03/27/2020
**/
class KS_Solve {
public $KS_Items;
public $maxValue;
public $maxWeight;
public $maxItems;
public $finalValue;
public $finalWeight;
public $finalItems;
public $finalGroups;
public $memo1 = array(); //Group memo
public $memo2 = array(); //Item memo for results rebuild
public function __construct() {
//some default variables as an example.
//KS_Items = array(Value, Weight, Group, Item #)
$this->KS_Items = array(
array(2, 3, 1, 1),
array(2, 1, 1, 2),
array(4, 3, 2, 3),
array(5, 4, 2, 4),
array(3, 2, 3, 5)
);
$this->maxWeight = 7;
$this->maxItems = 5;
$this->KS_Wrapper();
}
public function KS_Wrapper() {
$start_time = microtime(true);
//Put a dummy zero at the front to make things easier later.
array_unshift($this->KS_Items, array(0, 0, 0, 0));
//Call our Knapsack Solver
$this->maxValue = $this->KS_Solver($this->maxItems, $this->maxWeight);
//Recreate the decision table from our memo array to determine what items were picked
//ksort($this->memo2); //for debug
for($i=$this->maxItems; $i > 0; $i--) {
//ksort($this->memo2[$i]); //for debug
for($j=$this->maxWeight; $j > 0; $j--) {
if($this->maxValue == 0) {
break 2;
}
if($this->memo2[$i][$j] == $this->maxValue
&& $j == $this->maxWeight) {
$this->maxValue -= $this->KS_Items[$i][0];
$this->maxWeight -= $this->KS_Items[$i][1];
$this->finalValue += $this->KS_Items[$i][0];
$this->finalWeight += $this->KS_Items[$i][1];
$this->finalItems .= " " . $this->KS_Items[$i][3];
$this->finalGroups .= " " . $this->KS_Items[$i][2];
break;
}
}
}
//Print out the picked items and value. (IMPLEMENT Proper View or Return!)
echo "<pre>";
echo "RESULTS: <br>";
echo "Value: " . $this->finalValue . "<br>";
echo "Weight: " . $this->finalWeight . "<br>";
echo "Item's in KS:" . $this->finalItems . "<br>";
echo "Selected Groups:" . $this->finalGroups . "<br><br>";
$end_time = microtime(true);
$execution_time = ($end_time - $start_time);
echo "Results took " . sprintf('%f', $execution_time) . " seconds to execute<br>";
}
/**
* Recursive function to solve the MCKS Problem
* $n = number of items to check
* $c = total capacity of KS
**/
public function KS_Solver($n, $c) {
$group = $this->KS_Items[$n][2];
$groupItems = array();
$count = 0;
$result = 0;
$bestVal = 0;
if(isset($this->memo1[$group][$c])) {
$result = $this->memo1[$group][$c];
}
else {
//Sort out the items for this group
foreach($this->KS_Items as $item) {
if($item[2] == $group) {
$groupItems[] = $item;
$count++;
}
}
//$k adjusts the index for item memoization
$k = $count - 1;
//Find the results of each item + items of other groups
foreach($groupItems as $item) {
if($item[1] > $c) {
//too heavy
$result = 0;
}
elseif($item[1] >= $c && $group != 1) {
//too heavy for next group
$result = 0;
}
elseif($group == 1) {
//Just take the highest value
$result = $item[0];
}
else {
//check this item with following groups
$result = $item[0] + $this->KS_Solver($n - $count, $c - $item[1]);
}
if($result == $item[0] && $group != 1) {
//No solution with the following sets, so don't use this item.
$result = 0;
}
if($result > $bestVal) {
//Best item so far
$bestVal = $result;
}
//memo the results
$this->memo2[$n-$k][$c] = $result;
$k--;
}
$result = $bestVal;
}
//memo and return
$this->memo1[$group][$c] = $result;
return $result;
}
}
new KS_Solve();
?>
Let's say I've got three products:
Product A
Will deliver 5 power. Costs 50.
Product B Will deliver 9 power. Costs 80.
Product C Will deliver 15 power. Costs 140.
I want to know what combination of products I could buy when I need 7 power. I could buy two of A but one of B is cheaper.
When I'd need 65 power. I would need 4 times C and 1 time A (costs 680). But I could also go for seven B products and one A (costs 610).
I am looking for a way to calculate the possible combinations of products for the given amount of power I need.
The way I tried doing this doesn't give me what I want:
// $products are sorted DESC on their $power
$power = 65
while( $power > 0 ) {
foreach( $products as $productPower ) {
if( ( $productPower > $power && $power - $productPower > 0 ) || $productPower == end( $products ) ) {
// Add product to list
$power -= $productPower;
break;
}
}
}
This sample code will only give me 4 times C and one time A. How should I go about it?
EDIT The number of products is variable. Also, the specific cost and power is variable. So there may be 10 products with cheeper and more expensive price tags.
EDIT 2 As I said above, I want to calculate the possible combinations (plural). Some people seem to have missed that in my description.
Introduction
This would have been a Knapsack problem but because you are not not just looking for the optimal solution you also want to find all possible combination
Then you can solve this Subset sum problem + Coin Change to get :
List all possible Combination and not just total combination
Get Best Combination
For example, for N = 4,S = {1,2,3}, there are four solutions: {1,1,1,1},{1,1,2},{2,2},{1,3}.
Example 1
echo "<pre>";
$start = microtime(true);
// Start Finder
$finder = new CombinationFinder(65);
// Add Produts
$finder->addProduct(new Product("A", 5, 50));
$finder->addProduct(new Product("B", 9, 80));
$finder->addProduct(new Product("C", 15, 140));
// Output All Found Combinations
foreach ( $finder as $key => $sales ) {
echo $sales->getName(), "\t\t\t", $sales->getCombinationCost(), PHP_EOL;
}
// Get Best Combination
echo "Combination: ", $finder->getBestCombination()->getName(), PHP_EOL;
echo "Cost: ", number_format($finder->getBestCombination()->getCombinationCost(), 2), PHP_EOL;
// Total Time
echo PHP_EOL, microtime(true) - $start;
Output
Top Combinations
["A",1],["C",4] 610
["A",1],["B",5],["C",1] 590
["A",4],["C",3] 620
["A",4],["B",5] 600
["A",7],["C",2] 630
["A",10],["C",1] 640
["A",13] 650
Best Combination
Combination: ["A",1],["B",5],["C",1]
Cost: 590.00
Total Time
0.2533269405365
Best Combination
You can see the best Combination is A*1 ,B*5 ,C*1 .. Break down
A B C
Power : 5 * 1 + 9 * 5 + 15 * 1 = 65
Cost : 50 * 1 + 80 * 5 + 140 * 1 = 590 <---- Better than 610.00
Example 2
The class can be use for 2, 3 , 4 or more product combination and yet sill very fast
echo "<pre>";
$start = microtime(true);
// Start Finder
$finder = new CombinationFinder(65);
// Add Produts
$finder->addProduct(new Product("A", 5, 50));
$finder->addProduct(new Product("B", 9, 80));
$finder->addProduct(new Product("C", 15, 140));
$finder->addProduct(new Product("D", 20, 120)); // more product class
$finder->run(); // just run
// Get Best Combination
echo "Combination: ", $finder->getBestCombination()->getName(), PHP_EOL;
echo "Cost: ", number_format($finder->getBestCombination()->getCombinationCost(), 2), PHP_EOL;
// Total Time
echo PHP_EOL, microtime(true) - $start;
Output
Combination: ["A",1],["D",3] //<---------------------- Best Combination
Cost: 410.00
Time Taken
1.1627659797668 // less than 2 sec
Class Used
class Product {
public $name;
public $power;
public $cost;
public $unit;
function __construct($name, $power, $cost) {
$this->name = $name;
$this->power = $power;
$this->cost = $cost;
$this->unit = floor($cost / $power);
}
}
class Sales {
/**
*
* #var Product
*/
public $product;
public $count;
public $salePower;
public $saleCost;
function __construct(Product $product, $count) {
$this->product = $product;
$this->count = $count;
$this->salePower = $product->power * $count;
$this->saleCost = $product->cost * $count;
}
}
class SalesCombination {
private $combinationPower;
private $combinationCost;
private $combinationName;
private $combinationItems;
private $args;
function __construct(array $args) {
list($this->combinationPower, $this->combinationCost, $this->combinationItems) = array_reduce($args, function ($a, $b) {
$a[0] += $b->salePower;
$a[1] += $b->saleCost;
$a[2] = array_merge($a[2], array_fill(0, $b->count, $b->product->name));
return $a;
}, array(0,0,array()));
$this->args = $args;
}
function getName() {
$values = array_count_values($this->combinationItems);
$final = array();
foreach ( $values as $name => $amount ) {
$final[] = array($name,$amount);
}
return substr(json_encode($final), 1, -1);
}
function getCombinationPower() {
return $this->combinationPower;
}
function getCombinationCost() {
return $this->combinationCost;
}
}
class CombinationFinder implements IteratorAggregate, Countable {
private $sales;
private $products = array();
private $power;
private $found = array();
private $bestCombination = null;
private $run = false;
function __construct($power) {
$this->power = $power;
}
function addProduct(Product $product) {
$this->products[] = $product;
}
function getBestCombination() {
return $this->bestCombination;
}
function getFound() {
return $this->found ? : array();
}
public function getIterator() {
if ($this->run === false) {
$this->run();
}
return new ArrayIterator($this->found);
}
public function count() {
return count($this->found);
}
function run() {
$this->run = true;
$this->buildSales();
$u = new UniqueCombination($this->sales);
$u->setCallback(array($this,"find"));
$u->expand();
}
function find() {
$salesCombination = new SalesCombination(func_get_args());
if ($salesCombination->getCombinationPower() == $this->power) {
isset($this->bestCombination) or $this->bestCombination = $salesCombination;
$salesCombination->getCombinationCost() < $this->bestCombination->getCombinationCost() and $this->bestCombination = $salesCombination;
$this->found[sha1($salesCombination->getName())] = $salesCombination;
}
}
function buildSales() {
$total = count($this->products);
foreach ( $this->products as $product ) {
$max = floor($this->power / $product->power);
for($i = 1; $i <= $max; $i ++) {
$this->sales[$product->name][] = new Sales($product, $i);
}
}
}
}
class UniqueCombination {
private $items;
private $result = array();
private $callback = null;
function __construct($items) {
$this->items = array_values($items);
}
function getResult() {
return $this->result;
}
function setCallback($callback) {
$this->callback = $callback;
}
function expand($set = array(), $index = 0) {
if ($index == count($this->items)) {
if (! empty($set)) {
$this->result[] = $set;
if (is_callable($this->callback)) {
call_user_func_array($this->callback, $set);
}
}
return;
}
$this->expand($set, $index + 1);
foreach ( $this->items[$index] as $item ) {
$this->expand(array_merge($set, array($item)), $index + 1);
}
}
}
Updated answer
I stand with my original answer, but have since derived an explicit solution. Unfortunately, I am not versed in PHP, so the implementation I'll present is in (poorly written) F#.
The point which makes your question interesting is that you are not looking for THE best solution, but for all feasible solutions. As I pointed out in my original answer, this is tricky, because the set of feasible solutions is infinite. As an illustration, if you want to produce 65 units, you can use 13xA, which yields a power of 5x13 = 65. But then, obviously, any solution which contains more than 13 units of A will also be a solution.
You can't return an infinite set from a function. What you need here is the set of all "boundary" cases:
if a solution contains as many units as a boundary case for all products, it is valid
if a unit can be removed from a boundary case and it is still feasible, it is not feasible anymore.
For instance, the solution S = { A = 13; B = 0; C = 0 } is a boundary case. Remove one unit from any product, and it is not feasible - and if a combination is such that for every product, it contains more units than S, it is a valid solution, but "dominated" by S.
In other words, we cannot return all possible solutions, but we can return the "limit" that separates feasible and unfeasible solutions.
Note also that the cost of the Products is irrelevant here - once you have the set of boundary cases, computing the cost of a solution is trivial.
Given that you specify that the number of products could be arbitrary, this sounds like a clear case for recursion.
If you have no product, the solution is trivially empty - there is no solution.
If you have 1 product, the solution is ceiling (target / product.Power)
If you have 2 products, say A:5 and B:2, with a target of 10, you could
use 0 of A -> remaining target is 10, use 5 B (or more)
use 1 of A -> remaining target is 10 - 5, use 3 B (or more)
use 2 of A -> remaining target is 10 - 10, use 0 B (or more)
A is maxed out, so we are done.
Note that I sorted A and B by decreasing Power. An unsorted list would work, too, but you would produced "useless" boundary points. For instance, we would get [1 B; 2 A], and [2 B; 2 A].
The idea can be extended to a full recursion, along the lines of
Given a list of Products and a remaining Target power to achieve,
If the Product is the last one in the list, use ceiling of Target/product Power,
Else take every possible combination of the head product from 0 to max, and
Search deeper, decreasing Target Power by the units supplied by the Product selected.
Below is a simple F# implementation, which could easily be improved upon, and will hopefully convey the idea. The units function returns the minimum number of units of a product with Power value required to supply target Power, and the recursive function solve builds up the combinations into a List of solutions, tuples with a Product Id and the number of units to use:
type Product = { Id: string; Power: int }
let A = { Id = "A"; Power = 5 }
let B = { Id = "B"; Power = 9 }
let C = { Id = "C"; Power = 15 }
let products = [ A; B; C ] |> List.sortBy(fun e -> - e.Power)
let units (target: int) (value: int) =
if target < 0
then 0
else
(float)target / (float)value |> ceil |> (int)
let rec solve (products: Product list)
(current: (string * int) list)
(solutions: (string * int) list list)
(target: int) =
match products with
| [ ] -> [ ]
| [ one ] -> ((one.Id, (units target one.Power)) :: current) :: solutions
| hd :: tl ->
let max = units target hd.Power
[ 0 .. max ]
|> List.fold (fun s u ->
solve tl ((hd.Id, u) :: current) s (target - u * hd.Power)) solutions
I would run it this way:
> solve [B;A] [] [] 65;;
Real: 00:00:00.001, CPU: 00:00:00.000, GC gen0: 0, gen1: 0, gen2: 0
val it : (string * int) list list =
[[("A", 0); ("B", 8)]; [("A", 1); ("B", 7)]; [("A", 3); ("B", 6)];
[("A", 4); ("B", 5)]; [("A", 6); ("B", 4)]; [("A", 8); ("B", 3)];
[("A", 10); ("B", 2)]; [("A", 12); ("B", 1)]; [("A", 13); ("B", 0)]]
Note that the number of solutions will increase pretty fast. I ran your example, which yielded 28 solutions. As the number of products and the target power increases, the number of boundary solutions will expand quite a bit.
I can't code in PHP at all, but I assume it supports recursion - maybe someone will show a recursive solution in PHP? In any case, I hope this helps.
An interesting side question would be how different the problem would be, if the products could be purchased in non-integer quantities. In that case, the boundary would really be a surface (a polyhedron I believe); how to describe it adequately would be an interesting problem!
Original answer
Unless I am misunderstanding your question, what you describe is what is known in optimization as an Integer Linear Programming problem, with well established algorithms to resolve them. Your problem sounds like a variation of the Diet problem (given ingredients, find the cheapest way to get enough calories to survive), one of the archetypes of Linear Programming, with integer variable constraints.
First, the solution to your problem as stated has an infinite numbers of solutions; suppose that 5 x A is a solution to your problem, then any combination with more than 5 units of A will also satisfy your requirements.
Edit: I realize I might have misunderstood your problem - I assumed you could buy any quantity of each product. If you can buy only 1 unit of each, this is an easier problem: it is still an integer programming problem, but a simpler one, the Knapsack problem.
Note also that if you can by non-integer quantities of the products (doesn't seem to be the case for you), your problem is significantly easier to solve.
The most obvious way to restate your problem, which make it a standard optimization problem that can be solved fairly easily:
Find the combination of n Products which has the minimum total cost, subject to constraint that the total energy delivered is above desired threshold. (I assume that both the total cost and total energy delivered are linear functions of the quantity of A, B, C... purchased).
I assume this is actually what you really want - the best possible solution to your problem. If you are really interested in enumerating all the solution, one way to go about it is to identify the boundaries which define the feasible set (i.e. the geometric boundary such that if you are on one side you know it's not a solution, otherwise it is). This is much easier if you are working with numbers that don't have to be integers.
Hope this helps!
A simple observation on this specific problem may help people in solving this question. The way the power and costs are distributed here. You get the most value for your money with Product B. In fact, the only time you would ever use Product C, is when you need exactly 15 power, or 28-30 power.
So for any power needed above 30, just use integer division to get the # of Product B's you need by:
int num_productB = power_needed/9;
Then find out how much more power you need by:
int leftover = power_needed % 9;
If the leftover is greater than 5, just add one more Product B, Else use 1 Product A:
if(leftover > 5)
num_productB++;
else
productA = 1;
The full function would look something like this:
function computeBestCombination($power_needed){
$power_results = array();
//index 0 = Product A
//index 1 = Product B
//index 2 = Product C
if($power_needed == 15){
$power_results[0] = 0;
$power_results[1] = 0;
$power_results[2] = 1;
}
else if($power_needed >= 28 && $power_needed <= 30)
$power_results[0] = 0;
$power_results[1] = 0;
$power_results[2] = 2;
else{
$power_results[1] = $power_needed / 9;
$left_over = $power_needed % 9;
if($left_over > 5){
$power_results[1]++;
}
else{
$power_results[0] = 1;
}
$power_results[2] = 0;
}
return $power_results;
}
Check this code:
<?php
$products = array(5 => 50, 9 => 80, 15 => 140);
$power = 65;
$output = array();
function calculate_best_relation($products, $power, &$output) {
$aux = array_keys($products);
sort($aux);
$min = $aux[0];
if ($power <= $min) {
$output[] = $min;
return $output;
}
else {
//Calculate best relation
$relations = array();
foreach ($products as $p => $c) {
$relations[$p] = $c / $p;
}
asort($relations);
foreach($relations as $p => $c) {
if ($power > $c) {
$output[] = $p;
$power -= $c;
calculate_best_relation($products, $power, $output);
break;
}
}
}
}
calculate_best_relation($products, $power, $output);
print_r($output);
?>
This will print:
Array
(
[0] => 9
[1] => 9
[2] => 9
[3] => 9
[4] => 9
[5] => 9
[6] => 9
[7] => 5
)
Which is the correct solution.
P.D: Surely you can optimize the function.
An integer programming package such as pulp will make this easy as pie.
Here is a beautiful example that will guide you through the process.
Install python and then easy_install pulp and this will work.
The code should be easy to read and follow too.
__author__ = 'Robert'
import pulp
def get_lp_problem(products, required_power):
prob = pulp.LpProblem("MyProblem", pulp.LpMinimize)
total_cost = []
total_power = []
for product in products:
var = pulp.LpVariable(product.name,
lowBound=0,
upBound=None,
cat=pulp.LpInteger)
total_cost.append(var * product.cost)
total_power.append(var * product.power)
prob += sum(total_power) >= required_power #ensure we have required power
prob += sum(total_cost) #minimize total cost!
return prob
def solve(products, required_power):
lp_prob = get_lp_problem(products, required_power)
lp_prob.solve()
print lp_prob.solutionTime #0.01 seconds
for var in lp_prob.variables():
print var.name, var.varValue
from collections import namedtuple
Product = namedtuple("Product", "name, power, cost")
products = [
Product('A', 5, 50),
Product('B', 9, 80),
Product('C', 15, 140)
]
solve(products, 7)
"""
A 0.0
B 1.0
C 0.0
cost = 0*50 + 1*80 + 0*140 = 80
power = 0*5 + 1*9 + 0*15 = 9
"""
solve(products, 65)
"""
A 1.0
B 5.0
C 1.0
cost = 1*50 + 5*80 + 1*140 = 590
power = 1*5 + 5*9 + 1*15 = 65
"""
more products:
products = [Product(i, i, i-i/100) for i in range(1000)]
solve(products, 12345)
"""
solution time: 0.0922736688601
1 45.0
100 123.0
power = 123*100 + 45*1 =12345
"""
This is pretty nicely solved using dynamic programming. The trick is in finding the mathematical relationship between increasingly large values and previous, smaller values.
So let C(p) be the cost for p power. Then we know the following from your base cases:
Let's say I've got three products:
Product A Will deliver 5 power. Costs 50.
Product B Will deliver 9 power. Costs 80.
Product C Will deliver 15 power. Costs 140.
C(5) = 50
C(9) = 80
C(15) = 140
You can define the base cases however you want. Presumably C(0) = 0, but that is not given.
Then the trick is to find the recursion to solve this. Using the given values, we get
C(p) = Min(C(p-5) + 50, C(p-9) + 80, C(p-15) + 140)
More generally, you have to iterate over each of the base cases and see which way is cheaper.
So now you're left with two ways to build your solution: recursively or using dynamic programming. The former is easier given the recursive function, but is obviously quite inefficient. The other way to do this, then, is to start at the bottom and build your solution iteratively.
Lets say you want to find the cost for p power. Then the following pseudocode will work:
// Create an array big enough to hold elements 0 through p inclusive.
var solution = new Array(p+1);
// Initialize the array with the base cases.
for each base case b:
solution[power(b)] = cost(b);
// Now we build the array moving forward
for i from 0 to p:
// Start with a really big number
solution[i] = +Infinity;
// Iterate over base case to see what the cheapest way to get i power is.
for each base case b:
solution[i] = min(solution[i], solution[i - power(b)] + cost(b);
// The final answer is the last element in the array, but you get everything
// else for free. You can even work backwards and figure out the cheapest
// combination!
return solution[p]
Analysis left as an exercise to the reader :-)
You want to optimize the following function
$cost = $amountOfProductA * $costOfProductA + $amountOfProductB * $costOfProductB + $amountOfProductC * $costOfProductC
With the following restriction
$powerDeliveredByA * $amountOfProductA + $powerDeliveredByB * $amountOfProductB + $powerDeliveredByC * $amountOfProductC = 65
So these lines find solutions that yield 65 (or close to 65, using an acceptable threshold you'd have to set), then sort the solutions array by the cost, and get the first element of the solutions array:
$requiredPower = 65;
$productA = array('amount' => 0, 'cost' => 50, 'powerDelivered' => 5);
$productB = array('amount' => 0, 'cost' => 80, 'powerDelivered' => 9);
$productC = array('amount' => 0, 'cost' => 140, 'powerDelivered' => 15);
$increment = 0.01;
$threshold = 0.01;
$solutions = array();
while($productA['amount'] * $productA['powerDelivered'] < $requiredPower)
{
$productC['amount'] = 0;
while($productB['amount'] * $productB['powerDelivered'] < $requiredPower)
{
$productC['amount'] = 0;
while($productC['amount'] * $productC['powerDelivered'] < $requiredPower)
{
if($productA['amount'] * $productA['powerDelivered'] + $productB['amount'] * $productB['powerDelivered'] + $productC['amount'] * $productC['powerDelivered'] > $requiredPower + $threshold)
{
break;
}
if(isWithinThreshold($productA['powerDelivered'] * $productA['amount'] + $productB['powerDelivered'] * $productB['amount'] + $productC['powerDelivered'] * $productC['amount'], $requiredPower, $threshold))
{
//var_dump($productA['powerDelivered'] * $productA['amount'] + $productB['powerDelivered'] * $productB['amount'] + $productC['powerDelivered'] * $productC['amount']);
$cost = $productA['amount'] * $productA['cost'] + $productB['amount'] * $productB['cost'] + $productC['amount'] * $productC['cost'];
$solutions[number_format($cost,10,'.','')] = array('cost' => $cost, 'qA' => $productA['amount'], 'qB' => $productB['amount'], 'qC' => $productC['amount']);
}
$productC['amount'] = $productC['amount'] + $increment;
}
$productB['amount'] = $productB['amount'] + $increment;
}
$productA['amount'] = $productA['amount'] + $increment;
}
ksort($solutions, SORT_NUMERIC);
$minimumCost = array_shift($solutions);
var_dump($minimumCost);
//checks if $value1 is within $value2 +- $threshold
function isWithinThreshold($value1, $value2, $threshold)
{
if($value1 >= $value2 - $threshold && $value1 <= $value2 + $threshold)
{
return true;
}
}
The way to optimize a function is described here: Function Optimization
I have this logic written that loops out an array into <li>
and gives #1 and every 5th a class of "alpha".
$count = 0;
foreach($gallery->data as $row){
if ($count==0 || $count%4==0) {
echo '<li class="alpha"></li>'.PHP_EOL;
} else {
echo '<li></li>'.PHP_EOL;
}
$count++;
}
I need to add to this and get the code adding a class of "omega" to every 4th <li>
You do realize that as you describe it, there will be some overlap right? (example - item 30 is both a '5th' and a '6th') Brian gave you an answer for exactly what you described, but I'm not sure if its what you want. You want ALPHA, x, x, x, OMEGA, ALPHA, x, x, x, OMEGA, ALPHA.....
You seem to want Alpha on the 5*k + 1, and Omega on 5*k
conditions:
alpha - ($count + 1) % 5 == 1
omega - ($count + 1) % 5 == 0
I think grouping in the addition makes this easier to understand, since you're count starts at 0 but you seem to be thinking in terms of beginning at 1. If you don't like that, lose the addition and change the equivalences to 0 and 4, respectively - $count % 5 == 0 and $count % 5 == 4
i know this is better suited for a comment under the last answer, but I don't see how. Am i not allowed until my reputation is higher or am i just missing something?>
right now as it stands your code adds the "alpha" class to every fourth item beginning with the first, not every fifth. In other words, items 1, 5, 9, 13, etc. will have a class of "alpha" since your counter begins at 0.
I assume that you want to add the "omega" class then to items 4, 8, 12, etc. Here's what you need to do that:
$count = 0;
foreach($gallery->data as $row){
if ($count%4==0) {
echo '<li class="alpha"></li>'.PHP_EOL;
}
else if ($count%4==3) {
echo '<li class="omega"></li>'.PHP_EOL;
}
else {
echo '<li></li>'.PHP_EOL;
}
$count++;
}
I am building a list of "agent id's" in my database with the following requirements:
The ID must be 9 digits long (numeric only)
The ID may not contain more than 3 of the same number.
The ID may not contain more than 2 of the same number consecutively (i.e. 887766551; cannot have 888..)
So far I have part 1 down solid but am struggling with 2 and 3 above. My code is below.
function createRandomAGTNO() {
srand ((double) microtime( )*1000000);
$random_agtno = rand(100000000,900000000);
return $random_agtno;
}
// Usage
$NEWAGTNO = createRandomAGTNO();
Any ideas?
Do not re-seed the RNG on every call like that, unless you want to completely blow the security of your random numbers.
Unless your PHP is very old, you probably don't need to re-seed the RNG at all, as PHP seeds it for you on startup and there are very few cases where you need to replace the seed with one of your own choosing.
If it's available to you, use mt_rand instead of rand. My example will use mt_rand.
As for the rest -- you could possibly come up with a very clever mapping of numbers from a linear range onto numbers of the form you want, but let's brute-force it instead. This is one of those things where yes, the theoretical upper bound on running time is infinite, but the expected running time is bounded and quite small, so don't worry too hard.
function createRandomAGTNO() {
do {
$agt_no = mt_rand(100000000,900000000);
$valid = true;
if (preg_match('/(\d)\1\1/', $agt_no))
$valid = false; // Same digit three times consecutively
elseif (preg_match('/(\d).*?\1.*?\1.*?\1/', $agt_no))
$valid = false; // Same digit four times in string
} while ($valid === false);
return $agt_no;
}
For second condition, you can create an array like this
$a = array( 0,0,1,1,2,2,3,3.....,9,9 );
and get random elements: array_rand() (see manual) to get digit, append it to your ID and remove value from source array by unsetting at index.
Generally, this solving also third condition, but this solution excludes all ID's with possible and acceptable three digits
The first solution that comes to mind is a recursive function that simply tests your three requirements and restarts if any three of them fail. Not the most efficient solution but it would work. I wrote an untested version of this below. May not run without errors but you should get the basic idea from it.
function createRandomAGTNO(){
srand ((double) microtime( )*1000000);
$random_agtno = rand(100000000,900000000);
$random_agtno_array = explode('', $random_agtno);
foreach($random_agtno_array as $raa_index => $raa){
if($raa == $random_agtno_array[$raa_index + 1] && raa == $random_agtno_array[$raa_index + 2]) createRandomAGTNO();
$dup_match = array_search($raa, $random_agtno_array);
if($dup_match){
unset($random_agtno_array[$dup_match]);
if(array_search($raa, $random_agtno_array)) createRandomAGTNO();
};
}
return $random_agtno;
}
Try this code:
<?php
function createRandomAGTNO() {
//srand ((double) microtime( )*1000000);
$digits = array( 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 ,1, 2, 3, 4, 5, 6, 7, 8, 9, 0 );
shuffle($digits);
$random_agtno = 0;
for($i = 0; $i < 9; $i++)
{
if($i == 0)
{
while($digits[0] == 0)
shuffle($digits);
}
/*if($i >= 2)
{
while(($random_agtno % 100) == $digits[0])
shuffle($digits);
}*/
$random_agtno *= 10;
$random_agtno += $digits[0];
array_splice($digits, 0, 1);
}
return $random_agtno;
}
for($i = 0; $i < 1000; $i++)
{
$NEWAGTNO = createRandomAGTNO();
echo "<p>";
echo $NEWAGTNO;
echo "</p>";
}
?>
Good luck!
Edit:
Removed the call to srand() and commented-out the "if($i >= 2)" code, which is impossible anyway, here.