加权随机选择

时间:2015-04-27 03:40:21

标签: php

我有一套物品。我需要随机选一个。问题是每个人的体重都是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内存作为数组。我想要一个完全不同的算法。

此外,我在半夜使用手机问了这个问题。在手机上键入代码几乎是不可能的,因为那些愚蠢的虚拟键盘简直太糟糕了。它会自动纠正所有内容,破坏我输入的任何代码。

更进一步,我今天早上醒来时使用了一种全新的算法,该算法使用虚拟无内存,并且不需要检查数组中的每个项目。我在下面发布了它作为答案。

6 个答案:

答案 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;
    }
}*/

对于您的情况,重新构建阵列将提供很多好处。 用于生成新阵列的内存成本将越来越合理:

  1. 数组大小增加
  2. 选择次数增加。
  3. 新阵列需要更换&#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),然后只有四行:

    • 重组数组
    • 生成随机数
    • 找到正确的值,
    • 显示它。