【问题标题】:How to pick 4 unique items from a weighted list?如何从加权列表中选择 4 个独特的项目?
【发布时间】:2011-09-21 11:30:04
【问题描述】:

所以我有一个加权项目列表,我想从这个列表中挑选 4 个不重复的项目。

Item     Weight
Apple     5
Banana    7
Cherry    12
...
Orange    8
Pineapple 50

最有效的方法是什么?我最初的尝试是,如果出现了已经选择的项目,则只为随后的选择重新滚动……但对于一个小列表,这可能会导致大量的重新滚动。

编辑澄清: 对于上面的示例,忽略 D 到 N 的水果,总重量为 82。因此,首先被采摘的机会是: 一个 ~6% 乙~8.5% C ~14.6% O ~9.8% 磷 ~61% 一旦选择了一个项目,概率就会(应该!)改变。

【问题讨论】:

  • 权重代表什么?必须选择元素的概率?你不说。从您的问题看来,权重只是无关紧要……是吗?
  • 重量对您的拣选算法有何影响?
  • 什么是“不重复”?我猜测具有不同重量或名称的物品,或两者兼而有之。是的?你想选择哪 4 个不重复的元素?也许是权重最高的元素?
  • 是的,重量可能是被挑选出来的。如果 A、B、C 和 O 的权重为 1,而 P 的权重为 4,那么将有 50% 的机会选择 P,其他四个选择中的每一个都有 12.5% 的机会。我的想法是我可以在列表中添加更多项目,或者即时增加/减少单个权重,而无需更改任何其他条目。
  • @Mihran:我不想两次选择同一个项目。所以在上面的例子中,除了一个项目之外,所有的项目都会被挑选出来(如果我们通过油桃忽略了火龙果)。

标签: php algorithm list random weighted


【解决方案1】:

在您的评论中,您说独特意味着:

我不想选择两次相同的项目。

.. 权重决定了被选中的可能性。

您需要做的就是确保您不会选择重复项,只需在选择下一个之前从列表中删除最后一个选择的项目。是的,这会稍微改变您的权重,但如果您确实想要独特的结果,那么这是正确的统计改变。


此外,我不确定您是如何使用权重来确定候选人的,但我想出了这个算法,它应该用最少的循环数来做到这一点(并且不需要根据权重,这可能导致非常大的数组,需要 int 权重等)

我在这里使用了 JavaScript,因此很容易在没有服务器的浏览器中查看输出。移植到 PHP 应该很简单,因为它没有做任何复杂的事情。

常量

var FRUITS = [
    {name : "Apple", weight: 8 },
    {name : "Orange", weight: 4 },
    {name : "Banana", weight: 4 },
    {name : "Nectarine", weight: 3 },
    {name : "Kiwi", weight: 1 }
];

var PICKS = 3;

function getNewFruitsAvailable(fruits, removeFruit) {
    var newFruits = [];
    for (var idx in fruits) {
        if (fruits[idx].name != removeFruit) {
            newFruits.push(fruits[idx]);
        }
    }
    return newFruits;
}

脚本

var results = [];
var candidateFruits = FRUITS;

for (var i=0; i < PICKS; i++) {
    // CALCULATE TOTAL WEIGHT OF AVAILABLE FRUITS
    var totalweight = 0;
    for (var idx in candidateFruits) {
        totalweight += candidateFruits[idx].weight;
    }
    console.log("Total weight: " + totalweight);

    var rand = Math.random();

    console.log("Random: " + rand);

    // ITERATE THROUGH FRUITS AND PICK THE ONE THAT MATCHES THE RANDOM
    var weightinc = 0;
    for (idx in candidateFruits) {
        // INCREMENT THE WEIGHT BY THE NEXT FRUIT'S WEIGHT
        var candidate = candidateFruits[idx];
        weightinc += candidate.weight;

        // IF rand IS BETWEEN LAST WEIGHT AND NEXT WEIGHT, PICK THIS FRUIT
        if (rand < weightinc/totalweight) {
            results.push(candidate.name);
            console.log("Pick: " + candidate.name);

            // GET NEXT SET OF FRUITS (REMOVING PICKED FRUIT)
            candidateFruits = getNewFruitsAvailable(candidateFruits, candidate.name);
            break;
        }
    }
    console.log("CandidateFruits: " + candidateFruits.length);
};

输出

for (var i=0; i < results.length; i++) {
    document.write(results[i] + "<br/>");
}

基本策略是为每个水果分配总范围[0,1) 的一部分。在第一个循环中,你会得到这个:

  • 苹果 — 8/20 = 0.0 到 0.4
  • 橙色 — 4/20 = 0.4 到 0.6
  • 香蕉 — 4/20 = 0.6 到 0.8
  • 油桃 — 3/20 = 0.8 到 0.95
  • 猕猴桃 — 8/20 = 0.95 到 1.0

脚本遍历列表中的每个项目,并进行重量计数器。当它到达包含第一个随机数的范围时,它会选择该项目,将其从列表中删除,然后根据新的总重量重新计算范围并再次运行。

【讨论】:

  • 好吧弗拉克。这太明显了。嘟嘟嘟。我把鞋子戴在鼻子上。
  • @aslum:也请参阅我的回答,提出了一个 我认为 会满足您的需求的 rand 函数。
  • @aslum - 我已经用 rand 脚本更新了我的答案(使用 Javascript)。由于您有唯一性要求,我认为避免仅针对权重用重复项填充数组很重要(从那时起您必须删除所有这些项目,这可能会很昂贵)。
  • @Renesis:我考虑过使用“更适合”的算法,但是当我设想从显式数组中拉出意味着(几乎可以肯定地)你从头重脚轻拉下时,我就被拉走了。也就是说,尽管(在 OP 示例中)菠萝的重量很重,但我不想总是因为它的重量太高而将它视为第一个选择。相反,我希望它仍然保留“随机性”。 (也许我的结论是错误的)。
  • @Brad 它确实似乎有一些不公平,但由于随机出现在范围内的任何地方的可能性相同,因此根据比例它仍然是 100% 随机的重量,这是所述的预期效果。也就是说,如果菠萝的重量是总重量的50%,那么第一个被选中的确切可能性是50%。
【解决方案2】:

Here I found the idea to following steps:

  1. 建立权重之和 --> SUM
  2. 构建一个介于 0 和 SUM 之间的随机数 --> RAND_NUMBER
  3. 遍历列表并从 RAND_NUMBER 中减去每个元素的权重。如果 RAND_NUMBER 为负数,那么您就有了第一个元素。
  4. 从列表中删除找到的元素并返回到第 1 步,直到有 4 个元素。

【讨论】:

    【解决方案3】:

    更新

    function array_rand2($ary,$n = 1)
    {
      // make sure we don't get in to an infinite loop
      // check we have enough options to select from
      $unique = count(array_unique(array_keys($ary)));
      if ($n > $unique) $n = count($unique);
    
      // First, explode the array and expand out all the weights
      // this means something with a weight of 5 will appear in
      // in the array 5 times
      $_ary = array();
      foreach ($ary as $item => $weight)
      {
        $_ary = array_merge($_ary, array_fill(0, $weight, $item));
      }
    
      // now look for $n unique entries
      $matches = array();
      while (count($matches) < $n)
      {
        $r = $_ary[array_rand($_ary)];
        if (!in_array($r,$matches))
        {
          $matches[] = $r;
        }
      }
    
      // and now grab those $n entries and return them
      $result = array();
      foreach ($matches as $match){
        $result[] = $match;
      }
      return $result;
    }
    

    看看这是否做得更好。

    【讨论】:

      【解决方案4】:

      也许您可以增加随机生成的列表元素索引而不是“重新滚动”:list.elementAt(rand_index++ % size(list))(类似的东西)。我想你会用这样的逻辑很快找到下一个随机独特的物品。

      我确信还有更好的解决方案,当然,通常有。

      编辑:看起来布拉德已经提供了一个.. :)

      【讨论】:

      • seanybob 也是。也许我应该删除我的建议。
      猜你喜欢
      • 2011-11-14
      • 1970-01-01
      • 2021-06-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-03-12
      • 2019-05-16
      • 1970-01-01
      相关资源
      最近更新 更多