簡體   English   中英

加權隨機選擇

[英]Weighted random pick

我有一套物品。 我需要隨機選一個。 問題是每個人的體重都是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內存作為數組。 我想要一個完全不同的算法。

此外,我在半夜用手機問了這個問題。 在手機上鍵入代碼幾乎是不可能的,因為那些愚蠢的虛擬鍵盤簡直太糟糕了。 它會自動糾正所有內容,破壞我輸入的任何代碼。

更進一步,我今天早上醒來時使用了一種全新的算法,它根本不使用虛擬內存,並且不需要檢查數組中的每個項目。 我在下面發布了它作為答案。

這種方式需要兩次隨機計算,但它們應該更快並且需要大約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 1,它會增加200 10(20 * 10,20因為它有20倍計數,10因為它加權10)而不是10 10來使它與如何成比例許多人反對最低重量計數。 所以准確地說,不是為每個可能的鍵添加一個,而是根據MINIMUM權重計算比例。

這是你的哈克貝利。

  $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

測量幾乎不科學 - 並且根據元素在陣列中的位置(顯然)而波動,但對於大型數據集來說似乎足夠快。

我非常感謝上面的答案。 請考慮這個答案,它不需要檢查原始數組中的每個項目。

// 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_ posted算法結合使用,無需安全保護。

在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個階段的權重 - 要清楚 - 在我模擬你的主循環時使用外部 - 我只計算到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++;
}
?>

我不確定這是否“更快”,但我認為它可能在內存使用和速度之間更“平衡”。

我們的想法是將您當前的實現(500000個項目數組)轉換為等長數組(100000個項目),最低“origin”位置為鍵,原點索引為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];

然后隨機選擇0到最高“原點”位置之間的數字:

// 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演示
3v4l.org對代碼有一些時間限制,因此演示沒有完成。 在我的筆記本電腦上,上述演示在10秒內完成(i7-4700 HQ)

我將使用此輸入數組作為我的解釋:

$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. 選擇的數量增加。

新數組需要通過將前一個元素的權重與當前元素的權重相加來替換每個值的“權重”“權重”。

然后翻轉數組,使限制是數組鍵,值是數組值。

選擇邏輯是:所選值將具有> = $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 ),然后只有四行:

  • 重組數組,
  • 生成一個隨機數,
  • 找到正確的值,然后
  • 顯示它。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM