我有一套物品。我需要随机选一个。问题是每个人的体重都是1-10。重量为2意味着物品的拾取可能性是重量的1倍.3的重量是可能性的3倍。
我目前用每个项目填充一个数组。如果重量是3,我在项目中放置了三个项目副本。然后,我选择一个随机项目。
我的方法很快,但使用了大量内存。我想要一个更快的方法,但没有任何想法。任何人都有这个问题的技巧?
编辑:我的代码......
显然,我不清楚。我不想使用(或改进)我的代码。这就是我所做的。
//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)];
这要求我检查$ a中的每个项目,并使用max_weight / 2 *大小的$ a内存作为数组。我想要一个完全不同的算法。
此外,我在半夜使用手机问了这个问题。在手机上键入代码几乎是不可能的,因为那些愚蠢的虚拟键盘简直太糟糕了。它会自动纠正所有内容,破坏我输入的任何代码。
更进一步,我今天早上醒来时使用了一种全新的算法,该算法使用虚拟无内存,并且不需要检查数组中的每个项目。我在下面发布了它作为答案。
答案 0 :(得分:3)
这种方式需要两次随机计算,但它们应该更快并且需要大约1/4的内存但如果权重具有不成比例的计数则会降低一些精度。 (参见更新以提高准确性,代价是一些内存和处理)
存储一个多维数组,其中每个项目都根据其权重存储在数组中:
$array[$weight][] = $item;
// example: Item with a weight of 5 would be $array[5][] = 'Item'
生成一个新数组,权重(1-10)出现 n 次 n 权重:
foreach($array as $n=>$null) {
for ($i=1;$i<=$n;$i++) {
$weights[] = $n;
}
}
上面的数组类似于:[ 1, 2, 2, 3, 3, 3, 4, 4, 4, 4 ... ]
首先计算:从我们刚创建的加权数组中获取随机权重
$weight = $weights[mt_rand(0, count($weights)-1)];
第二次计算:从该权重数组中获取一个随机密钥
$value = $array[$weight][mt_rand(0, count($array[$weight])-1)];
为什么会这样:您可以使用我们创建的加权整数数组来解决加权问题。然后从该加权组中随机选择。
更新:由于每个重量的项目数量可能不成比例,您可以为计数添加另一个循环和数组以提高准确性。
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;
}
}
这样做的目的是,如果你有2000个10和100个,它会增加200个10(20 * 10,20因为它有20倍的计数,10个因为它加权10)而不是10个10来制作它与那里有多少人相对应的最小重量计数。所以准确一点,不是为每个可能的键添加一个,而是根据MINIMUM权重计算比例。
答案 1 :(得分:3)
这是你的哈克贝利。
$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";
}
}
无需为每个权重生成包含项目的数组,无需使用n个元素填充数组,权重为n。只需生成1到总重量之间的随机数,然后循环遍历数组,直到找到小于随机数的权重。如果它不小于该数字,则从随机数中减去该权重并继续。
示例输出:
# 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
这也应该支持分数权重。
修改后的版本,使用按权重键入的数组,而不是按项目
$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";
使用示例输出:
# php wr2.php
weighted sum is 2751550
random value is 345713
random element is: 681
total time is 0.017189025878906
测量几乎不科学 - 并且根据元素在数组中的位置(显然)而波动,但对于大型数据集来说似乎足够快。
答案 2 :(得分:1)
我非常感谢上面的答案。请考虑这个答案,它不需要检查原始数组中的每个项目。
// 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;
}
此算法仅需要存储两个变量($ i和$ item),因为在算法启动之前已经创建了$ a。它不需要大量重复项或一系列间隔。
在最佳情况下,此算法将触摸原始数组中的一个项目并完成。在最坏的情况下,它将触摸n个项目数组中的n个项目(不一定是数组中的每个项目,因为有些项目可能被触摸多次)。
如果没有保护措施,这可能会永远存在。如果算法根本不选择项目,则可以使用安全措施来停止算法。触发安全措施时,触摸的最后一项是选择的项目。但是,在使用随机数量为1到10的100,000个项目的随机数据集(在我的代码中将rand(1,100)更改为rand(1,10))的数百万次测试中,保护措施从未被击中。
我制作了直方图,比较了我原始算法中选择的项目频率,上面答案中的项目频率以及答案中的项目频率。频率的差异是微不足道的 - 很容易归因于随机数的差异。
编辑......很明显,我的算法可以与pala_贴的算法结合使用,无需安全保护。
在pala_算法中,需要一个列表,我将其称为间隔列表。为简化起见,首先要使用相当高的random_weight。您逐步降低项目列表并减去每个项目的权重,直到random_weight降至零(或更低)。然后,您结束的项目是您要返回的项目。我已经测试了这种间隔算法的变化,而pala_是非常好的。但是,我想避免列出清单。我只想使用给定的加权列表,从不触及所有项目。以下算法将我对随机跳转的使用与pala_的间隔列表合并。而不是列表,我随机跳转列表。我保证最终会达到零,所以不需要保护。
// 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.
我希望我可以选择pala_和这个答案作为正确的答案。
答案 3 :(得分:0)
我的样本只有3个阶段的重量 - 要清楚 - 在我模拟你的主循环时使用外部 - 我只计算到100。 - 数组必须是init,带有一组初始数字,如我的样本所示。 - 在主循环的每次传递中,我只得到一个随机值,而我一直保持着重量。
<?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++;
}
?>
答案 4 :(得分:0)
我不确定这是否更快&#34;但我认为可能更多&#34;在内存使用和速度之间取得平衡。
我们的想法是将您当前的实现(500000个项目数组)转换为等长数组(100000个项目),其中最低的&#34;来源&#34;位置为键,原点索引为值:
<?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];
然后随机选择0到最高&#34;来源&#34;之间的数字。位置:
// 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);
完整代码:
<?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对代码有一些时间限制,因此演示没有完成。在我的笔记本电脑上,上述演示在10秒内完成(i7-4700 HQ)
答案 5 :(得分:0)
我将使用此输入数组作为我的解释:
$values_and_weights=array(
"one"=>1,
"two"=>8,
"three"=>10,
"four"=>4,
"five"=>3,
"six"=>10
);
简单版本不适合您,因为您的阵列太大了。它不需要修改数组,但可能需要迭代整个数组,这是一个交易破坏者。
/*$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;
}
}*/
对于您的情况,重新构建阵列将提供很多好处。 用于生成新阵列的内存成本将越来越合理:
新阵列需要更换&#34; weight&#34;用&#34;限制&#34;通过将前一个元素的权重添加到当前元素的权重来为每个值。
然后翻转数组,使限制为数组键,值为数组值。
选择逻辑是:所选值的最低限度为&gt; = $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
看起来像这样:
array (
1 => 'one',
9 => 'two',
19 => 'three',
23 => 'four',
26 => 'five',
36 => 'six',
)
现在生成随机$ pick并选择值:
// $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
这种方法很棒,因为isset()
速度非常快,而while循环中isset()
次调用的最大数量只能是最大权重(不要与限制混淆)阵列。
对于您的情况,这种方法将在10次或更少的时间内找到价值!
这是我的Demo接受加权数组(如$values_and_weights
),然后只有四行: