Related
I have a set of items. I need to randomly pick one. The problem is that they each have a weight of 1-10. A weight of 2 means that the item is twice as likely to be picked than a weight of 1. A weight of 3 is three times as likely.
I currently fill an array with each item. If the weight is 3, I put three copies of the item in the array. Then, I pick a random item.
My method is fast, but uses a lot of memory. I am trying to think of a faster method, but nothing comes to mind. Anyone have a trick for this problem?
EDIT: My Code...
Apparently, I wasn't clear. I do not want to use (or improve) my code. This is what I did.
//Given an array $a where $a[0] is an item name and $a[1] is the weight from 1 to 100.
$b = array();
foreach($a as $t)
$b = array_merge($b, array_fill(0,$t[1],$t));
$item = $b[array_rand($b)];
This required me to check every item in $a and uses max_weight/2*size of $a memory for the array. I wanted a COMPLETELY DIFFERENT algorithm.
Further, I asked this question in the middle of the night using a phone. Typing code on a phone is nearly impossible because those silly virtual keyboards simply suck. It auto-corrects everything, ruining any code I type.
An yet further, I woke up this morning with an entirely new algorithm that uses virtual no extra memory at all and does not require checking every item in the array. I posted it as an answer below.
This ones your huckleberry.
$arr = array(
array("val" => "one", "weight" => 1),
array("val" => "two", "weight" => 2),
array("val" => "three", "weight" => 3),
array("val" => "four", "weight" => 4)
);
$weight_sum = 0;
foreach($arr as $val)
{
$weight_sum += $val['weight'];
}
$r = rand(1, $weight_sum);
print "random value is $r\n";
for($i = 0; $i < count($arr); $i++)
{
if($r <= $arr[$i]['weight'])
{
print "$r <= {$arr[$i]['weight']}, this is our match\n";
print $arr[$i]['val'] . "\n";
break;
}
else
{
print "$r > {$arr[$i]['weight']}, subtracting weight\n";
$r -= $arr[$i]['weight'];
print "new \$r is $r\n";
}
}
No need to generate arrays containing an item for every weight, no need to fill an array with n elements for a weight of n. Just generate a random number between 1 and total weight, then loop through the array until you find a weight less than your random number. If it isn't less than the number, subtract that weight from the random and continue.
Sample output:
# php wr.php
random value is 8
8 > 1, subtracting weight
new $r is 7
7 > 2, subtracting weight
new $r is 5
5 > 3, subtracting weight
new $r is 2
2 <= 4, this is our match
four
This should also support fractional weights.
modified version to use array keyed by weight, rather than by item
$arr2 = array(
);
for($i = 0; $i <= 500000; $i++)
{
$weight = rand(1, 10);
$num = rand(1, 1000);
$arr2[$weight][] = $num;
}
$start = microtime(true);
$weight_sum = 0;
foreach($arr2 as $weight => $vals) {
$weight_sum += $weight * count($vals);
}
print "weighted sum is $weight_sum\n";
$r = rand(1, $weight_sum);
print "random value is $r\n";
$found = false;
$elem = null;
foreach($arr2 as $weight => $vals)
{
if($found) break;
for($j = 0; $j < count($vals); $j ++)
{
if($r < $weight)
{
$elem = $vals[$j];
$found = true;
break;
}
else
{
$r -= $weight;
}
}
}
$end = microtime(true);
print "random element is: $elem\n";
print "total time is " . ($end - $start) . "\n";
With sample output:
# php wr2.php
weighted sum is 2751550
random value is 345713
random element is: 681
total time is 0.017189025878906
measurement is hardly scientific - and fluctuates depending on where in the array the element falls (obviously) but it seems fast enough for huge datasets.
This way requires two random calculations but they should be faster and require about 1/4 of the memory but with some reduced accuracy if weights have disproportionate counts. (See Update for increased accuracy at the cost of some memory and processing)
Store a multidimensional array where each item is stored in the an array based on its weight:
$array[$weight][] = $item;
// example: Item with a weight of 5 would be $array[5][] = 'Item'
Generate a new array with the weights (1-10) appearing n times for n weight:
foreach($array as $n=>$null) {
for ($i=1;$i<=$n;$i++) {
$weights[] = $n;
}
}
The above array would be something like: [ 1, 2, 2, 3, 3, 3, 4, 4, 4, 4 ... ]
First calculation: Get a random weight from the weighted array we just created
$weight = $weights[mt_rand(0, count($weights)-1)];
Second calculation: Get a random key from that weight array
$value = $array[$weight][mt_rand(0, count($array[$weight])-1)];
Why this works: You solve the weighted issue by using the weighted array of integers we created. Then you select randomly from that weighted group.
Update: Because of the possibility of disproportionate counts of items per weight, you could add another loop and array for the counts to increase accuracy.
foreach($array as $n=>$null) {
$counts[$n] = count($array[$n]);
}
foreach($array as $n=>$null) {
// Calculate proportionate weight (number of items in this weight opposed to minimum counted weight)
$proportion = $n * ($counts[$n] / min($counts));
for ($i=1; $i<=$proportion; $i++) {
$weights[] = $n;
}
}
What this does is if you have 2000 10's and 100 1's, it'll add 200 10's (20 * 10, 20 because it has 20x the count, and 10 because it is weighted 10) instead of 10 10's to make it proportionate to how many are in there opposed the minimum weight count. So to be accurate, instead of adding one for EVERY possible key, you are just being proportionate based on the MINIMUM count of weights.
I greatly appreciate the answers above. Please consider this answer, which does not require checking every item in the original array.
// Given $a as an array of items
// where $a[0] is the item name and $a[1] is the item weight.
// It is known that weights are integers from 1 to 100.
for($i=0; $i<sizeof($a); $i++) // Safeguard described below
{
$item = $a[array_rand($a)];
if(rand(1,100)<=$item[1]) break;
}
This algorithm only requires storage for two variables ($i and $item) as $a was already created before the algorithm kicked in. It does not require a massive array of duplicate items or an array of intervals.
In a best-case scenario, this algorithm will touch one item in the original array and be done. In a worst-case scenario, it will touch n items in an array of n items (not necessarily every item in the array as some may be touched more than once).
If there was no safeguard, this could run forever. The safeguard is there to stop the algorithm if it simply never picks an item. When the safeguard is triggered, the last item touched is the one selected. However, in millions of tests using random data sets of 100,000 items with random weights of 1 to 10 (changing rand(1,100) to rand(1,10) in my code), the safeguard was never hit.
I made histograms comparing the frequency of items selected among my original algorithm, the ones from answers above, and the one in this answer. The differences in frequencies are trivial - easy to attribute to variances in the random numbers.
EDIT... It is apparent to me that my algorithm may be combined with the algorithm pala_ posted, removing the need for a safeguard.
In pala_'s algorithm, a list is required, which I call an interval list. To simplify, you begin with a random_weight that is rather high. You step down the list of items and subtract the weight of each one until your random_weight falls to zero (or less). Then, the item you ended on is your item to return. There are variations on this interval algorithm that I've tested and pala_'s is a very good one. But, I wanted to avoid making a list. I wanted to use only the given weighted list and never touch all the items. The following algorithm merges my use of random jumping with pala_'s interval list. Instead of a list, I randomly jump around the list. I am guaranteed to get to zero eventually, so no safeguard is needed.
// Given $a as the weighted array (described above)
$weight = rand(1,100); // The bigger this is, the slower the algorithm runs.
while($weight>0)
{
$item = $a[array_rand($a)];
$weight-= $item[1];
}
// $item is the random item you want.
I wish I could select both pala_ and this answer as the correct answers.
I'm not sure if this is "faster", but I think it may be more "balance"d between memory usage and speed.
The thought is to transform your current implementation (500000 items array) into an equal-length array (100000 items), with the lowest "origin" position as key, and origin index as value:
<?php
$set=[["a",3],["b",5]];
$current_implementation=["a","a","a","b","b","b","b","b"];
// 0=>0 means the lowest "position" 0
// points to 0 in the set;
// 3=>1 means the lowest "position" 3
// points to 1 in the set;
$my_implementation=[0=>0,3=>1];
And then randomly picks a number between 0 and highest "origin" position:
// 3 is the lowest position of the last element ("b")
// and 5 the weight of that last element
$my_implemention_pick=mt_rand(0,3+5-1);
Full code:
<?php
function randomPickByWeight(array $set)
{
$low=0;
$high=0;
$candidates=[];
foreach($set as $key=>$item)
{
$candidates[$high]=$key;
$high+=$item["weight"];
}
$pick=mt_rand($low,$high-1);
while(!array_key_exists($pick,$candidates))
{
$pick--;
}
return $set[$candidates[$pick]];
}
$cache=[];
for($i=0;$i<100000;$i++)
{
$cache[]=["item"=>"item {$i}","weight"=>mt_rand(1,10)];
}
$time=time();
for($i=0;$i<100;$i++)
{
print_r(randomPickByWeight($cache));
}
$time=time()-$time;
var_dump($time);
3v4l.org demo
3v4l.org have some time limitation on codes, so the demo didn't finished. On my laptop the above demo finished in 10 seconds (i7-4700 HQ)
ere is my offer in case I've understand you right. I offer you take a look and if there are some question I'll explain.
Some words in advance:
My sample is with only 3 stages of weight - to be clear
- With outer while I'm simulating your main loop - I count only to 100.
- The array must to be init with one set of initial numbers as shown in my sample.
- In every pass of main loop I get only one random value and I'm keeping the weight at all.
<?php
$array=array(
0=>array('item' => 'A', 'weight' => 1),
1=>array('item' => 'B', 'weight' => 2),
2=>array('item' => 'C', 'weight' => 3),
);
$etalon_weights=array(1,2,3);
$current_weights=array(0,0,0);
$ii=0;
while($ii<100){ // Simulates your main loop
// Randomisation cycle
if($current_weights==$etalon_weights){
$current_weights=array(0,0,0);
}
$ft=true;
while($ft){
$curindex=rand(0,(count($array)-1));
$cur=$array[$curindex];
if($current_weights[$cur['weight']-1]<$etalon_weights[$cur['weight']-1]){
echo $cur['item'];
$array[]=$cur;
$current_weights[$cur['weight']-1]++;
$ft=false;
}
}
$ii++;
}
?>
I'll use this input array for my explanation:
$values_and_weights=array(
"one"=>1,
"two"=>8,
"three"=>10,
"four"=>4,
"five"=>3,
"six"=>10
);
The simple version isn't going to work for you because your array is so large. It requires no array modification but may need to iterate the entire array, and that's a deal breaker.
/*$pick=mt_rand(1,array_sum($values_and_weights));
$x=0;
foreach($values_and_weights as $val=>$wgt){
if(($x+=$wgt)>=$pick){
echo "$val";
break;
}
}*/
For your case, re-structuring the array will offer great benefits.
The cost in memory for generating a new array will be increasingly justified as:
array size increases and
number of selections increases.
The new array requires the replacement of "weight" with a "limit" for each value by adding the previous element's weight to the current element's weight.
Then flip the array so that the limits are the array keys and the values are the array values.
The selection logic is: the selected value will have the lowest limit that is >= $pick.
// Declare new array using array_walk one-liner:
array_walk($values_and_weights,function($v,$k)use(&$limits_and_values,&$x){$limits_and_values[$x+=$v]=$k;});
//Alternative declaration method - 4-liner, foreach() loop:
/*$x=0;
foreach($values_and_weights as $val=>$wgt){
$limits_and_values[$x+=$wgt]=$val;
}*/
var_export($limits_and_values);
$limits_and_values looks like this:
array (
1 => 'one',
9 => 'two',
19 => 'three',
23 => 'four',
26 => 'five',
36 => 'six',
)
Now to generate the random $pick and select the value:
// $x (from walk/loop) is the same as writing: end($limits_and_values); $x=key($limits_and_values);
$pick=mt_rand(1,$x); // pull random integer between 1 and highest limit/key
while(!isset($limits_and_values[$pick])){++$pick;} // smallest possible loop to find key
echo $limits_and_values[$pick]; // this is your random (weighted) value
This approach is brilliant because isset() is very fast and the maximum number of isset() calls in the while loop can only be as many as the largest weight (not to be confused with limit) in the array.
FOR YOUR CASE, THIS APPROACH WILL FIND THE VALUE IN 10 ITERATIONS OR LESS!
Here is my Demo that will accept a weighted array (like $values_and_weights), then in just four lines:
Restructure the array,
Generate a random number,
Find the correct value, and
Display it.
I know how to generate a random number in PHP but lets say I want a random number between 1-10 but I want more 3,4,5's then 8,9,10's. How is this possible? I would post what I have tried but honestly, I don't even know where to start.
Based on #Allain's answer/link, I worked up this quick function in PHP. You will have to modify it if you want to use non-integer weighting.
/**
* getRandomWeightedElement()
* Utility function for getting random values with weighting.
* Pass in an associative array, such as array('A'=>5, 'B'=>45, 'C'=>50)
* An array like this means that "A" has a 5% chance of being selected, "B" 45%, and "C" 50%.
* The return value is the array key, A, B, or C in this case. Note that the values assigned
* do not have to be percentages. The values are simply relative to each other. If one value
* weight was 2, and the other weight of 1, the value with the weight of 2 has about a 66%
* chance of being selected. Also note that weights should be integers.
*
* #param array $weightedValues
*/
function getRandomWeightedElement(array $weightedValues) {
$rand = mt_rand(1, (int) array_sum($weightedValues));
foreach ($weightedValues as $key => $value) {
$rand -= $value;
if ($rand <= 0) {
return $key;
}
}
}
For an efficient random number skewed consistently towards one end of the scale:
Choose a continuous random number between 0..1
Raise to a power γ, to bias it. 1 is unweighted, lower gives more of the higher numbers and vice versa
Scale to desired range and round to integer
eg. in PHP (untested):
function weightedrand($min, $max, $gamma) {
$offset= $max-$min+1;
return floor($min+pow(lcg_value(), $gamma)*$offset);
}
echo(weightedrand(1, 10, 1.5));
There's a pretty good tutorial for you.
Basically:
Sum the weights of all the numbers.
Pick a random number less than that
subtract the weights in order until the result is negative and return that number if it is.
This tutorial walks you through it, in PHP, with multiple cut and paste solutions. Note that this routine is slightly modified from what you'll find on that page, as a result of the comment below.
A function taken from the post:
/**
* weighted_random_simple()
* Pick a random item based on weights.
*
* #param array $values Array of elements to choose from
* #param array $weights An array of weights. Weight must be a positive number.
* #return mixed Selected element.
*/
function weighted_random_simple($values, $weights){
$count = count($values);
$i = 0;
$n = 0;
$num = mt_rand(1, array_sum($weights));
while($i < $count){
$n += $weights[$i];
if($n >= $num){
break;
}
$i++;
}
return $values[$i];
}
/**
* #param array $weightedValues
* #return string
*/
function getRandomWeightedElement(array $weightedValues)
{
$array = array();
foreach ($weightedValues as $key => $weight) {
$array = array_merge(array_fill(0, $weight, $key), $array);
}
return $array[array_rand($array)];
}
getRandomWeightedElement(array('A'=>10, 'B'=>90));
This is very easy method. How get random weighted element. I fill array variable $key. I get $key to array $weight x. After that, use array_rand to array. And I have random value ;).
Plain and fair.
Just copy/paste and test it.
/**
* Return weighted probability
* #param (array) prob=>item
* #return key
*/
function weightedRand($stream) {
$pos = mt_rand(1,array_sum(array_keys($stream)));
$em = 0;
foreach ($stream as $k => $v) {
$em += $k;
if ($em >= $pos)
return $v;
}
}
$item['30'] = 'I have more chances than everybody :]';
$item['10'] = 'I have good chances';
$item['1'] = 'I\'m difficult to appear...';
for ($i = 1; $i <= 10; $i++) {
echo weightedRand($item).'<br />';
}
Edit: Added missing bracket at the end.
You can use weightedChoice from Non-standard PHP library. It accepts a list of pairs (item, weight) to have the possibility to work with items that can't be array keys. You can use pairs function to convert array(item => weight) to the needed format.
use function \nspl\a\pairs;
use function \nspl\rnd\weightedChoice;
$weights = pairs(array(
1 => 10,
2 => 15,
3 => 15,
4 => 15,
5 => 15,
6 => 10,
7 => 5,
8 => 5,
9 => 5,
10 => 5
));
$number = weightedChoice($weights);
In this example, 2-5 will appear 3 times more often than 7-10.
i used Brad's answar and changed it a little to fit my situation and add more flexibility
i have an array with array value
$products = [
['id'=>1,'name'=> 'product1' , 'chance'=>2] ,
['id'=>2,'name'=> 'product2' , 'chance'=>7]
]
first i shuffle the products array
shuffle($products );
then you can pass it to the function
function getRandomWeightedElement(array $products) {
$chancesSum = 0;
foreach ($products as $product){
$chancesSum += (int) $product['chance'];
}
$rand = mt_rand(1, $chancesSum);
$range = 0;
foreach ($products as $product) {
$range += (int) $product['chance'];
$compare = $rand - $range;
if ($compare <= 0){
return (int) $product['id'];
}
}}
Since I used IainMH's solution, I may as well share my PHP code:
<pre><?php
// Set total number of iterations
$total = 1716;
// Set array of random number
$arr = array(1, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5);
$arr2 = array(0, 0, 1, 1, 2, 2, 2, 3, 3, 4, 5);
// Print out random numbers
for ($i=0; $i<$total; $i++){
// Pick random array index
$rand = array_rand($arr);
$rand2 = array_rand($arr2);
// Print array values
print $arr[$rand] . "\t" . $arr2[$rand2] . "\r\n";
}
?></pre>
I just released a class to perform weighted sorting easily.
It's based on the same algorithm mentioned in Brad's and Allain's answers, and is optimized for speed, unit-tested for uniform distribution, and supports elements of any PHP type.
Using it is simple. Instantiate it:
$picker = new Brick\Random\RandomPicker();
Then add elements as an array of weighted values (only if your elements are strings or integers):
$picker->addElements([
'foo' => 25,
'bar' => 50,
'baz' => 100
]);
Or use individual calls to addElement(). This method supports any kind of PHP values as elements (strings, numbers, objects, ...), as opposed to the array approach:
$picker->addElement($object1, $weight1);
$picker->addElement($object2, $weight2);
Then get a random element:
$element = $picker->getRandomElement();
The probability of getting one of the elements depends on its associated weight. The only restriction is that weights must be integers.
Many of the answers on this page seem to use array bloating, excessive iteration, a library, or a hard-to-read process. Of course, everyone thinks their own baby is the cutest, but I honestly think my approach is lean, simple and easy to read/modify...
Per the OP, I will create an array of values (declared as keys) from 1 to 10, with 3, 4, and 5 having double the weight of the other values (declared as values).
$values_and_weights=array(
1=>1,
2=>1,
3=>2,
4=>2,
5=>2,
6=>1,
7=>1,
8=>1,
9=>1,
10=>1
);
If you are only going to make one random selection and/or your array is relatively small* (do your own benchmarking to be sure), this is probably your best bet:
$pick=mt_rand(1,array_sum($values_and_weights));
$x=0;
foreach($values_and_weights as $val=>$wgt){
if(($x+=$wgt)>=$pick){
echo "$val";
break;
}
}
This approach involves no array modification and probably won't need to iterate the entire array (but may).
On the other hand, if you are going to make more than one random selection on the array and/or your array is sufficiently large* (do your own benchmarking to be sure), restructuring the array may be better.
The cost in memory for generating a new array will be increasingly justified as:
array size increases and
number of random selections increases.
The new array requires the replacement of "weight" with a "limit" for each value by adding the previous element's weight to the current element's weight.
Then flip the array so that the limits are the array keys and the values are the array values.
The logic is: the selected value will have the lowest limit that is >= $pick.
// Declare new array using array_walk one-liner:
array_walk($values_and_weights,function($v,$k)use(&$limits_and_values,&$x){$limits_and_values[$x+=$v]=$k;});
//Alternative declaration method - 4-liner, foreach() loop:
/*$x=0;
foreach($values_and_weights as $val=>$wgt){
$limits_and_values[$x+=$wgt]=$val;
}*/
var_export($limits_and_values);
Creates this array:
array (
1 => 1,
2 => 2,
4 => 3,
6 => 4,
8 => 5,
9 => 6,
10 => 7,
11 => 8,
12 => 9,
13 => 10,
)
Now to generate the random $pick and select the value:
// $x (from walk/loop) is the same as writing: end($limits_and_values); $x=key($limits_and_values);
$pick=mt_rand(1,$x); // pull random integer between 1 and highest limit/key
while(!isset($limits_and_values[$pick])){++$pick;} // smallest possible loop to find key
echo $limits_and_values[$pick]; // this is your random (weighted) value
This approach is brilliant because isset() is very fast and the maximum number of isset() calls in the while loop can only be as many as the largest weight (not to be confused with limit) in the array. For this case, maximum iterations = 2!
THIS APPROACH NEVER NEEDS TO ITERATE THE ENTIRE ARRAY
I used this:
mt_rand($min, mt_rand($min, $max));
it give more lower values and less higher values, since the more the value is high the more is cutted out by one of the mt_rand
The probability is linearly increasing in the lower values, forming a square diagonal (see maths lower)
PRO: easy and strightforward
CON: maybe too simple so not enough weightable or balanceable for some use case
Maths:
let i index of i-nth value from min to max,
let P(i) the probability of obtaining the i-nth value,
let N=max-min:
P(i)=(1+N-i)/sum(1,N)
Since N is equals for all terms:
P(i) is proportional to N-i
so, in facts, the probability is linearly increasing in the lower values, forming a square diagonal
Variants:
you can write variants:
mt_rand($min, mt_rand(1, mt_rand(1, $max))); //value more given in low part
mt_rand(mt_rand($min, $max), $max); //mirrored, more upper values than lower
...
function getBucketFromWeights($values) {
$total = $currentTotal = $bucket = 0;
foreach ($values as $amount) {
$total += $amount;
}
$rand = mt_rand(0, $total-1);
foreach ($values as $amount) {
$currentTotal += $amount;
if ($rand => $currentTotal) {
$bucket++;
}
else {
break;
}
}
return $bucket;
}
I ugh modified this from an answer here Picking random element by user defined weights
After I wrote this I saw someone else had an even more elegant answer. He he he he.
In a browser game we have items that occur based on their probabilities.
P(i1) = 0.8
P(i2) = 0.45
P(i3) = 0.33
P(i4) = 0.01
How do we implement a function in php that returns a random item based on its probability chance?
edit
The items have a property called rarity which varies from 1 to 100 and represents the probability to occcur. The item that occurs is chosen from a set of all items of a certain type. (e.x the given example above represents all artifacts tier 1)
I don't know if its the best solution but when I had to solve this a while back this is what I found:
Function taken from this blog post:
// Given an array of values, and weights for those values (any positive int)
// it will select a value randomly as often as the given weight allows.
// for example:
// values(A, B, C, D)
// weights(30, 50, 100, 25)
// Given these values C should come out twice as often as B, and 4 times as often as D.
function weighted_random($values, $weights){
$count = count($values);
$i = 0;
$n = 0;
$num = mt_rand(0, array_sum($weights));
while($i < $count){
$n += $weights[$i];
if($n >= $num){
break;
}
$i++;
}
return $values[$i];
}
Example call:
$values = array('A','B','C');
$weights = array(1,50,100);
$weighted_value = weighted_random($values, $weights);
It's somewhat unwieldy as obviously the values and weights need to be supplied separately but this could probably be refactored to suit your needs.
Tried to understand how Bulk's function works, and here is how I understand based on Benjamin Kloster answer:
https://softwareengineering.stackexchange.com/questions/150616/return-random-list-item-by-its-weight
Generate a random number n in the range of 0 to sum(weights), in this case $num so lets say from this: weights(30, 50, 100, 25).
Sum is 205.
Now $num has to be 0-30 to get A,
30-80 to get B
80-180 to get C
and 180-205 to get D
While loop finds in which interval the $num falls.
Not a duplicate of-
optimal algorithm for finding unique divisors
I came across this problem. I am not able to find an optimal algorithm.
The problem is :
Given a list L of natural numbers(number can be really large) and a number N, what's the optimal algorithm to determine the number of divisors of N which doesn't not divide any of the numbers present in the list L. Numbers in the list can be repetitive ie, one number can occur more than once.
Observation:
Divisors of some divisor d of N are also divisors of N.
MY approach was :
Find the divisors of N.
Sort L in reverse order(largest element being 1st element).
foreach divisor d of N, I check whether it divides any element in the list or not.(stop when you come to check for an element less than d in the list, as the list is sorted)
If d divides some number in the list L, then I don't check for any divisor of d, that is, I skip this checking.
Ultimately, left divisors which were neither divided any number in the list nor skipped are counted. This count is the final answer.
But this algorithm is not optimal for this problem.
Any ideas for a better algorithm?
What you need to look into is : co-primes (or relatively primes)
In number theory, a branch of mathematics, two integers a and b are
said to be coprime (also spelled co-prime) or relatively prime if the
only positive integer that evenly divides both of them is 1.
So to "transcode" your problem :
You basically want to find the Number of coprimes of N from the L list.
When a and b are co-primes?
If two numbers are relatively prime then their greatest common divisor (GCD)
is 1
Example code (for GCD) in PHP :
<?php
$gcd = gmp_gcd("12", "21");
echo gmp_strval($gcd) . "\n";
?>
Simply put :
$count = 0
Foreach element e in list L : calculate the GCD(e,N)
Is their GCD=1? If yes, they are coprime (so N and e have no common divisors). Count it up. $count++
And that's all there is to it.
First, factorize n and represent it in the following way: p1:k1, p2:k2,..., pm:km such that p1,p2,... are all primes and n=p1^k1 * p2^k2 ....
Now, iterate over r1, r2, r3,..., rm such that r1<=k1, r2<=k2, ..., rm<=km and check if p1^r1*p2^r2...*pm^rm divides any number in L. If not increment count by 1.
Optimization: Pick a value for r1. See if p1^r1 divides any number in L. If yes, then pick a number for r2 and so on. If p1^r1 does not divide any number in L, then increment count by (k2+1)(k3+1)..*(km+1).
Example N=72, L=[4, 5, 9, 12, 15, 20]:
Writing N as a primal product: 2:3, 3:2 (2^3*3*2 = 72).
p1=2, p2=3, k1=3, k2=2
count=0
r1=0:
r2=0:
Divides 4
r1=0:
r2=1:
Divides 9
r1=0:
r2=2:
Divides 9
r1=1:
r2=0:
Divides 4
r1=1:
r2=1:
Divides 12
r1=1:
r2=2:
L not divisible by 18. Count+=1 = 1
r1=2:
r2=0:
Divides 4
r1=2:
r2=1:
Divides 12
r1=2:
r2=2:
L not divisible by 36. Count+=1 = 2
r1=3:
r2=0:
L not divisible by 8. Count+=(k2+1) +=(2+1) = 5
<?php
class Divisors {
public $factor = array();
public function __construct($num) {
$this->num = $num;
}
// count number of divisors of a number
public function countDivisors() {
if ($this->num == 1) return 1;
$this->_primefactors();
$array_primes = array_count_values($this->factor);
$divisors = 1;
foreach($array_primes as $power) {
$divisors *= ++$power;
}
return $divisors;
}
// prime factors decomposer
private function _primefactors() {
$this->factor = array();
$run = true;
while($run && #$this->factor[0] != $this->num) {
$run = $this->_getFactors();
}
}
// get all factors of the number
private function _getFactors() {
if($this->num == 1) {
return ;
}
$root = ceil(sqrt($this->num)) + 1;
$i = 2;
while($i <= $root) {
if($this->num % $i == 0) {
$this->factor[] = $i;
$this->num = $this->num / $i;
return true;
}
$i++;
}
$this->factor[] = $this->num;
return false;
}
} // our class ends here
$example = new Divisors(4567893421);
print $example->countDivisors();
?>
I know how to generate a random number in PHP but lets say I want a random number between 1-10 but I want more 3,4,5's then 8,9,10's. How is this possible? I would post what I have tried but honestly, I don't even know where to start.
Based on #Allain's answer/link, I worked up this quick function in PHP. You will have to modify it if you want to use non-integer weighting.
/**
* getRandomWeightedElement()
* Utility function for getting random values with weighting.
* Pass in an associative array, such as array('A'=>5, 'B'=>45, 'C'=>50)
* An array like this means that "A" has a 5% chance of being selected, "B" 45%, and "C" 50%.
* The return value is the array key, A, B, or C in this case. Note that the values assigned
* do not have to be percentages. The values are simply relative to each other. If one value
* weight was 2, and the other weight of 1, the value with the weight of 2 has about a 66%
* chance of being selected. Also note that weights should be integers.
*
* #param array $weightedValues
*/
function getRandomWeightedElement(array $weightedValues) {
$rand = mt_rand(1, (int) array_sum($weightedValues));
foreach ($weightedValues as $key => $value) {
$rand -= $value;
if ($rand <= 0) {
return $key;
}
}
}
For an efficient random number skewed consistently towards one end of the scale:
Choose a continuous random number between 0..1
Raise to a power γ, to bias it. 1 is unweighted, lower gives more of the higher numbers and vice versa
Scale to desired range and round to integer
eg. in PHP (untested):
function weightedrand($min, $max, $gamma) {
$offset= $max-$min+1;
return floor($min+pow(lcg_value(), $gamma)*$offset);
}
echo(weightedrand(1, 10, 1.5));
There's a pretty good tutorial for you.
Basically:
Sum the weights of all the numbers.
Pick a random number less than that
subtract the weights in order until the result is negative and return that number if it is.
This tutorial walks you through it, in PHP, with multiple cut and paste solutions. Note that this routine is slightly modified from what you'll find on that page, as a result of the comment below.
A function taken from the post:
/**
* weighted_random_simple()
* Pick a random item based on weights.
*
* #param array $values Array of elements to choose from
* #param array $weights An array of weights. Weight must be a positive number.
* #return mixed Selected element.
*/
function weighted_random_simple($values, $weights){
$count = count($values);
$i = 0;
$n = 0;
$num = mt_rand(1, array_sum($weights));
while($i < $count){
$n += $weights[$i];
if($n >= $num){
break;
}
$i++;
}
return $values[$i];
}
/**
* #param array $weightedValues
* #return string
*/
function getRandomWeightedElement(array $weightedValues)
{
$array = array();
foreach ($weightedValues as $key => $weight) {
$array = array_merge(array_fill(0, $weight, $key), $array);
}
return $array[array_rand($array)];
}
getRandomWeightedElement(array('A'=>10, 'B'=>90));
This is very easy method. How get random weighted element. I fill array variable $key. I get $key to array $weight x. After that, use array_rand to array. And I have random value ;).
Plain and fair.
Just copy/paste and test it.
/**
* Return weighted probability
* #param (array) prob=>item
* #return key
*/
function weightedRand($stream) {
$pos = mt_rand(1,array_sum(array_keys($stream)));
$em = 0;
foreach ($stream as $k => $v) {
$em += $k;
if ($em >= $pos)
return $v;
}
}
$item['30'] = 'I have more chances than everybody :]';
$item['10'] = 'I have good chances';
$item['1'] = 'I\'m difficult to appear...';
for ($i = 1; $i <= 10; $i++) {
echo weightedRand($item).'<br />';
}
Edit: Added missing bracket at the end.
You can use weightedChoice from Non-standard PHP library. It accepts a list of pairs (item, weight) to have the possibility to work with items that can't be array keys. You can use pairs function to convert array(item => weight) to the needed format.
use function \nspl\a\pairs;
use function \nspl\rnd\weightedChoice;
$weights = pairs(array(
1 => 10,
2 => 15,
3 => 15,
4 => 15,
5 => 15,
6 => 10,
7 => 5,
8 => 5,
9 => 5,
10 => 5
));
$number = weightedChoice($weights);
In this example, 2-5 will appear 3 times more often than 7-10.
i used Brad's answar and changed it a little to fit my situation and add more flexibility
i have an array with array value
$products = [
['id'=>1,'name'=> 'product1' , 'chance'=>2] ,
['id'=>2,'name'=> 'product2' , 'chance'=>7]
]
first i shuffle the products array
shuffle($products );
then you can pass it to the function
function getRandomWeightedElement(array $products) {
$chancesSum = 0;
foreach ($products as $product){
$chancesSum += (int) $product['chance'];
}
$rand = mt_rand(1, $chancesSum);
$range = 0;
foreach ($products as $product) {
$range += (int) $product['chance'];
$compare = $rand - $range;
if ($compare <= 0){
return (int) $product['id'];
}
}}
Since I used IainMH's solution, I may as well share my PHP code:
<pre><?php
// Set total number of iterations
$total = 1716;
// Set array of random number
$arr = array(1, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5);
$arr2 = array(0, 0, 1, 1, 2, 2, 2, 3, 3, 4, 5);
// Print out random numbers
for ($i=0; $i<$total; $i++){
// Pick random array index
$rand = array_rand($arr);
$rand2 = array_rand($arr2);
// Print array values
print $arr[$rand] . "\t" . $arr2[$rand2] . "\r\n";
}
?></pre>
I just released a class to perform weighted sorting easily.
It's based on the same algorithm mentioned in Brad's and Allain's answers, and is optimized for speed, unit-tested for uniform distribution, and supports elements of any PHP type.
Using it is simple. Instantiate it:
$picker = new Brick\Random\RandomPicker();
Then add elements as an array of weighted values (only if your elements are strings or integers):
$picker->addElements([
'foo' => 25,
'bar' => 50,
'baz' => 100
]);
Or use individual calls to addElement(). This method supports any kind of PHP values as elements (strings, numbers, objects, ...), as opposed to the array approach:
$picker->addElement($object1, $weight1);
$picker->addElement($object2, $weight2);
Then get a random element:
$element = $picker->getRandomElement();
The probability of getting one of the elements depends on its associated weight. The only restriction is that weights must be integers.
Many of the answers on this page seem to use array bloating, excessive iteration, a library, or a hard-to-read process. Of course, everyone thinks their own baby is the cutest, but I honestly think my approach is lean, simple and easy to read/modify...
Per the OP, I will create an array of values (declared as keys) from 1 to 10, with 3, 4, and 5 having double the weight of the other values (declared as values).
$values_and_weights=array(
1=>1,
2=>1,
3=>2,
4=>2,
5=>2,
6=>1,
7=>1,
8=>1,
9=>1,
10=>1
);
If you are only going to make one random selection and/or your array is relatively small* (do your own benchmarking to be sure), this is probably your best bet:
$pick=mt_rand(1,array_sum($values_and_weights));
$x=0;
foreach($values_and_weights as $val=>$wgt){
if(($x+=$wgt)>=$pick){
echo "$val";
break;
}
}
This approach involves no array modification and probably won't need to iterate the entire array (but may).
On the other hand, if you are going to make more than one random selection on the array and/or your array is sufficiently large* (do your own benchmarking to be sure), restructuring the array may be better.
The cost in memory for generating a new array will be increasingly justified as:
array size increases and
number of random selections increases.
The new array requires the replacement of "weight" with a "limit" for each value by adding the previous element's weight to the current element's weight.
Then flip the array so that the limits are the array keys and the values are the array values.
The logic is: the selected value will have the lowest limit that is >= $pick.
// Declare new array using array_walk one-liner:
array_walk($values_and_weights,function($v,$k)use(&$limits_and_values,&$x){$limits_and_values[$x+=$v]=$k;});
//Alternative declaration method - 4-liner, foreach() loop:
/*$x=0;
foreach($values_and_weights as $val=>$wgt){
$limits_and_values[$x+=$wgt]=$val;
}*/
var_export($limits_and_values);
Creates this array:
array (
1 => 1,
2 => 2,
4 => 3,
6 => 4,
8 => 5,
9 => 6,
10 => 7,
11 => 8,
12 => 9,
13 => 10,
)
Now to generate the random $pick and select the value:
// $x (from walk/loop) is the same as writing: end($limits_and_values); $x=key($limits_and_values);
$pick=mt_rand(1,$x); // pull random integer between 1 and highest limit/key
while(!isset($limits_and_values[$pick])){++$pick;} // smallest possible loop to find key
echo $limits_and_values[$pick]; // this is your random (weighted) value
This approach is brilliant because isset() is very fast and the maximum number of isset() calls in the while loop can only be as many as the largest weight (not to be confused with limit) in the array. For this case, maximum iterations = 2!
THIS APPROACH NEVER NEEDS TO ITERATE THE ENTIRE ARRAY
I used this:
mt_rand($min, mt_rand($min, $max));
it give more lower values and less higher values, since the more the value is high the more is cutted out by one of the mt_rand
The probability is linearly increasing in the lower values, forming a square diagonal (see maths lower)
PRO: easy and strightforward
CON: maybe too simple so not enough weightable or balanceable for some use case
Maths:
let i index of i-nth value from min to max,
let P(i) the probability of obtaining the i-nth value,
let N=max-min:
P(i)=(1+N-i)/sum(1,N)
Since N is equals for all terms:
P(i) is proportional to N-i
so, in facts, the probability is linearly increasing in the lower values, forming a square diagonal
Variants:
you can write variants:
mt_rand($min, mt_rand(1, mt_rand(1, $max))); //value more given in low part
mt_rand(mt_rand($min, $max), $max); //mirrored, more upper values than lower
...
function getBucketFromWeights($values) {
$total = $currentTotal = $bucket = 0;
foreach ($values as $amount) {
$total += $amount;
}
$rand = mt_rand(0, $total-1);
foreach ($values as $amount) {
$currentTotal += $amount;
if ($rand => $currentTotal) {
$bucket++;
}
else {
break;
}
}
return $bucket;
}
I ugh modified this from an answer here Picking random element by user defined weights
After I wrote this I saw someone else had an even more elegant answer. He he he he.