【问题标题】:How to pick an item by its probability?如何通过概率选择一个项目?
【发布时间】:2012-03-08 23:30:17
【问题描述】:

我有一个物品清单。这些项目中的每一个都有自己的概率。

谁能推荐一种算法来根据概率挑选物品?

【问题讨论】:

    标签: java list random probability


    【解决方案1】:
    1. 生成一个均匀分布的随机数。
    2. 遍历您的列表,直到被访问元素的累积概率大于随机数

    示例代码:

    double p = Math.random();
    double cumulativeProbability = 0.0;
    for (Item item : items) {
        cumulativeProbability += item.probability();
        if (p <= cumulativeProbability) {
            return item;
        }
    }
    

    【讨论】:

    • 这里p 是任何随机数,所以我们怎么能说大多数概率项首先被选中.. 例如:[{A,10},{B,20}] 所以你怎么能说假设在第一次迭代p=2 所以2&lt;=10 为真,第一个项目 {A,10} 被首先选中,尽管第二个项目的概率更大
    • @U2Answer,该算法需要对概率进行归一化(除以所有概率的总和)。通过这样做,您可以确保 Σp_i=1,然后是 0 到 1 之间的随机数就可以了。
    • 列表中项目的顺序无关紧要,因为r的值是一个均匀分布的随机数,也就是说r是某个值的概率等于所有其他值r 可能是。因此,列表中的项目不是“受欢迎的”,它们在列表中的位置无关紧要。
    【解决方案2】:

    因此,每个项目都存储一个数字来标记其相对概率,例如,如果您有 3 个项目,其中一个项目被选中的可能性应该是其他两个项目的两倍,那么您的列表将包含:

     [{A,1},{B,1},{C,2}]
    

    然后对列表的数字求和(在我们的例子中是 4)。 现在生成一个随机数并选择该索引。 int index = rand.nextInt(4); 返回使索引在正确范围内的数字。

    Java 代码:

    class Item {
        int relativeProb;
        String name;
    
        //Getters Setters and Constructor
    }
    
    ...
    
    class RandomSelector {
        List<Item> items = new List();
        Random rand = new Random();
        int totalSum = 0;
    
        RandomSelector() {
            for(Item item : items) {
                totalSum = totalSum + item.relativeProb;
            }
        }
    
        public Item getRandom() {
    
            int index = rand.nextInt(totalSum);
            int sum = 0;
            int i=0;
            while(sum < index ) {
                 sum = sum + items.get(i++).relativeProb;
            }
            return items.get(Math.max(0,i-1));
        }
    }
    

    【讨论】:

    • 感谢乌斯曼。但我想知道我应该带第 i 个物品还是第 (i-1) 个物品?我的意思是 items.get(i-1) 而不是 items.get(i)
    • 这里p 是任何随机数,所以我们怎么能说大多数概率项首先被选中.. 例如:[{A,10},{B,20}] 那么我们怎么能说假设在第一次迭代p=2 所以2&lt;=10 为真,第一个项目 {A,10} 被首先选中,即使第二个项目的概率更大
    • p 是从 0 到“所有权重之和”的随机数,因此在您的示例中,p 有 10/30 = 1/3 的机会是 0-9 之间的数字,并且有20/30 = p 的 2/3 变化是从 10 到 29 的数字。因此,获得 B 的机会仍然是获得 A 的两倍
    • 嘿,很好的答案,但你必须小心!函数 nextInt(...) 返回一个从 0(包括)到 totalSum(不包括)的值。 (见docs.oracle.com/javase/7/docs/api/java/util/…)。因此,如果选择 0 (index = 0),您将收到 IndexOutOfBoundsException,因为您尝试访问 items.get(-1),因为您完全跳过了 while 循环并且 i = 0。因此您应该返回 items.get( Math.max(0, i-1)).
    • 根据一些实验,如果我用int index = rand.nextInt(totalSum) + 1 替换int index = rand.nextInt(totalSum) 然后当然取出多余的Math.max,这对我有用。没有这个+1,我得到的列表中“基本”(和第一个)元素的数量是预期的两倍,即默认情况下,列表中的第一个元素的相对可能性为 1,其他一切都与此相关。
    【解决方案3】:

    假设我们有以下列表

    Item A 25%
    Item B 15%
    Item C 35%
    Item D 5%
    Item E 20%
    

    假设所有概率都是整数,并为每个项目分配一个“范围”,计算如下。

    Start - Sum of probability of all items before
    End - Start + own probability
    

    新号码如下

    Item A 0 to 25
    Item B 26 to 40
    Item C 41 to 75
    Item D 76 to 80
    Item E 81 to 100
    

    现在从 0 到 100 中选择一个随机数。假设您选择 32。32 属于项目 B 的范围。

    mj

    【讨论】:

    • 这比@Brent 的选择答案要快,但是如果说范围从 0 到一百万,它会占用太多内存。
    • 概率不一定是百分比。我们可以选择一个介于 0 到数字总和之间的随机数。
    • A 项应该是 1 到 25 而不是 0 到 25?
    • 我认为应该也是 1 到 25,0 到 25 表示项目 A 的概率为 26%。
    【解决方案4】:

    你可以试试Roulette Wheel Selection

    首先,将所有概率相加,然后通过将每个概率除以总和,以 1 的比例缩放所有概率。假设概率是A(0.4), B(0.3), C(0.25) and D(0.05)。然后您可以在 [0, 1] 范围内生成一个随机浮点数。现在你可以这样决定:

    random number between 0.00 and 0.40 -> pick A
                  between 0.40 and 0.70 -> pick B
                  between 0.70 and 0.95 -> pick C
                  between 0.95 and 1.00 -> pick D
    

    你也可以用随机整数来做——比如你生成一个介于 0 到 99(含)之间的随机整数,然后你可以像上面一样做出决定。

    【讨论】:

    • (+1) 困扰我的是,这个算法几乎总是用 GA 来描述(你在 Wikipedia 上的链接,也见 here)。加权轮盘算法有各种与 GA 无关的用途(比如这个问题)。
    • 是的,这很奇怪。我在学习 GA 的时候也知道了它的名字,但我在很久以前就使用了这种技术,因为其他原因。
    【解决方案5】:

    Ushman'sBrent's 和@kaushaya 的答案中描述的算法在Apache commons-math 库中实现。

    看看EnumeratedDistribution类(groovy代码如下):

    def probabilities = [
       new Pair<String, Double>("one", 25),
       new Pair<String, Double>("two", 30),
       new Pair<String, Double>("three", 45)]
    def distribution = new EnumeratedDistribution<String>(probabilities)
    println distribution.sample() // here you get one of your values
    

    请注意,概率总和不必等于 1 或 100 - 它会自动归一化。

    【讨论】:

    • @kaushaya 是谁?可能是她/他改名了。所以最好将各自的答案超链接。
    【解决方案6】:

    我的方法很简单。生成一个随机数。现在由于您的项目的概率是已知的,只需遍历排序的概率列表并选择概率小于随机生成的数字的项目。

    更多详情,请阅读我的回答here

    【讨论】:

      【解决方案7】:

      一种缓慢但简单的方法是让每个成员根据其概率选择一个随机数,然后选择具有最高值的一个。

      类比:

      假设需要选择 3 个人中的 1 个,但他们的概率不同。你给他们死了不同数量的面孔。第一人的骰子有 4 面,第二人的骰子有 6,第三人的骰子有 8。他们掷骰子,数字最大的人获胜。

      假设我们有以下列表:

      [{A,50},{B,100},{C,200}]

      伪代码:

       A.value = random(0 to 50);
       B.value = random(0 to 100);
       C.value = random (0 to 200);
      

      我们选择价值最高的那个。

      上面的这个方法并不能准确地映射概率。例如,100 的机会不会是 50 的两倍。但是我们可以通过稍微调整方法来做到这一点。

      方法二

      我们可以将它们从前一个变量的上限限制到当前变量的加法,而不是从 0 到权重。

      [{A,50},{B,100},{C,200}]

      伪代码:

       A.lowLimit= 0; A.topLimit=50;
       B.lowLimit= A.topLimit+1; B.topLimit= B.lowLimit+100
       C.lowLimit= B.topLimit+1; C.topLimit= C.lowLimit+200 
      

      产生的限制

      A.limits = 0,50
      B.limits = 51,151
      C.limits = 152,352
      

      然后我们从 0 到 352 中选择一个随机数,并将其与每个变量的限制进行比较,以查看随机数是否在其限制范围内。

      我相信这个调整有更好的性能,因为只有 1 个随机生成。

      其他答案中也有类似的方法,但此方法不要求总数为 100 或 1.00。

      【讨论】:

      • 为什么会被否决?我将不胜感激。
      • 您的方法并不像最初看起来那样容易地映射到概率。假设我们想要两个选择,A 和 B。A 有 2/5 = 0 到 40,B 有 3/5 = 0 到 60。如果我们用你的方法模拟这个,1/3 的时间 B 将在 41 和60、保证成功。然后另外 2/3 的时间,B 将是 0 到 40,A 将是 0 到 40,给他们偶数赔率,所以 2/3 的时间中的 1/2,B 会赢。那是 1/3 + 1/3 = 2/3,这与 B 获胜的预期 3/5 不同。
      • @MauveRanger 从未打算完全复制这些可能性,但数学似乎具有误导性。我将编辑并添加另一种方法,希望在数学上是正确的。
      【解决方案8】:

      Brent's answer 很好,但它没有考虑在 p = 0 的情况下错误选择概率为 0 的项目的可能性。通过检查概率(或者可能不添加项目):

      double p = Math.random();
      double cumulativeProbability = 0.0;
      for (Item item : items) {
          cumulativeProbability += item.probability();
          if (p <= cumulativeProbability && item.probability() != 0) {
              return item;
          }
      }
      

      【讨论】:

      • 我认为你不需要担心项目概率为零的情况。您应该已经退出循环,或者您应该继续,因为累积概率不会改变。
      【解决方案9】:

      所有提到的解决方案都有线性努力。以下只有对数的努力,也处理非标准化的概率。我建议使用 TreeMap 而不是 List:

          import java.util.*;
          import java.util.stream.IntStream;
      
          public class ProbabilityMap<T> extends TreeMap<Double,T>{
              private static final long serialVersionUID = 1L;
              public static Random random = new Random();
              public double sumOfProbabilities;
              public Map.Entry<Double,T> next() {
                  return ceilingEntry(random.nextDouble()*sumOfProbabilities);
              }
              @Override public T put(Double key, T value) {
                  return super.put(sumOfProbabilities+=key, value);
              }
              public static void main(String[] args) {
                  ProbabilityMap<Integer> map = new ProbabilityMap<>();
                  map.put(0.1,1);  map.put(0.3,3);    map.put(0.2,2);
                  IntStream.range(0, 10).forEach(i->System.out.println(map.next()));
              }
          }
      

      【讨论】:

      • 对于实现来说,这是一个可怕的继承案例,但这是一个完全正确的观点。如果您要进行大量查找并有很多条目,那么 TreeMap 看起来是一种有趣的方法。 (您可能必须对其进行分析以确定它是否真的提供了任何运行时优势 - 我不是在争论 O log(n) 与 O(n) 的胜利,但在弄清楚 TreeMap 的工作原理和可能不值得。)我喜欢它又讨厌它:)
      • 是的,TreeMap 有缺点,例如用于迭代和构造。但是,如果您有很多条目,那么选择显然应该比线性方法更快。
      • 我粗略地测试了 TreeMap 拾取的性能。如果我在项目中间挑选,它的速度大约是公认解决方案的 5 倍。对于 100 个项目,TreeMap 快 5 倍,对于 10000 个项目,TreeMap 快 17 倍。
      【解决方案10】:

      https://stackoverflow.com/a/37228927/11257746的代码改编为通用扩展方法。这将允许您从具有 结构的字典中获取加权随机值,其中 int 是权重值。

      值为 50 的键被选择的可能性是值为 5 的键的 10 倍。

      使用 LINQ 的 C# 代码:

      /// <summary>
      /// Get a random key out of a dictionary which has integer values treated as weights. 
      /// A key in the dictionary with a weight of 50 is 10 times more likely to be chosen than an element with the weight of 5.
      /// 
      /// Example usage to get 1 item: 
      /// Dictionary<MyType, int> myTypes;
      /// MyType chosenType = myTypes.GetWeightedRandomKey<MyType, int>().First();
      /// 
      /// Adapted into a general extention method from https://stackoverflow.com/a/37228927/11257746
      /// </summary>
      public static IEnumerable<TKey> GetWeightedRandomKey<TKey, TValue>(this Dictionary<TKey, int> dictionaryWithWeights)
      {
          int totalWeights = 0;
          foreach (KeyValuePair<TKey, int> pair in dictionaryWithWeights)
          { 
              totalWeights += pair.Value;
          }
      
          System.Random random = new System.Random();
          while (true)
          {
              int randomWeight = random.Next(0, totalWeights);
              foreach (KeyValuePair<TKey, int> pair in dictionaryWithWeights)
              {
                  int weight = pair.Value;
      
                  if (randomWeight - weight > 0)
                      randomWeight -= weight;
                  else
                  {
                      yield return pair.Key;
                      break;
                  }
              }
          }
      }
      

      使用示例:

      public enum MyType { Thing1, Thing2, Thing3 }
      public Dictionary<MyType, int> MyWeightedDictionary = new Dictionary<MyType, int>();
      
      public void MyVoid()
      {
          MyWeightedDictionary.Add(MyType.Thing1, 50);
          MyWeightedDictionary.Add(MyType.Thing2, 25);
          MyWeightedDictionary.Add(MyType.Thing3, 5);
          
          // Get a single random key
          MyType myChosenType = MyWeightedDictionary.GetWeightedRandomKey<MyType, int>().First();
          
          // Get 20 random keys
          List<MyType> myChosenTypes = MyWeightedDictionary.GetWeightedRandomKey<MyType, int>().Take(20).ToList();
      }
      

      【讨论】:

        【解决方案11】:

        一种耗费空间的方法是将每个项目克隆其概率的次数。选择将在 O(1) 中完成。

        例如

        //input
        [{A,1},{B,1},{C,3}]
        
        // transform into
        [{A,1},{B,1},{C,1},{C,1},{C,1}]
        

        然后从这个转换后的列表中随机选择任何项目。

        【讨论】:

          【解决方案12】:

          如果您不介意在代码中添加第三方依赖项,可以使用MockNeat.probabilities() 方法。

          例如:

          String s = mockNeat.probabilites(String.class)
                          .add(0.1, "A") // 10% chance to pick A
                          .add(0.2, "B") // 20% chance to pick B
                          .add(0.5, "C") // 50% chance to pick C
                          .add(0.2, "D") // 20% chance to pick D
                          .val();
          

          免责声明:我是该库的作者,所以我在推荐它时可能会有偏见。

          【讨论】:

            【解决方案13】:

            您可以使用 Julia 代码:

            function selrnd(a::Vector{Int})
                c = a[:]
                sumc = c[1]
                for i=2:length(c)
                    sumc += c[i]
                    c[i] += c[i-1]
                end
                r = rand()*sumc
                for i=1:length(c)
                    if r <= c[i]
                        return i
                    end
                end
            end
            

            此函数有效地返回项目的索引。

            【讨论】:

              猜你喜欢
              • 2019-09-13
              • 1970-01-01
              • 2011-02-15
              • 1970-01-01
              • 2015-08-22
              • 1970-01-01
              • 1970-01-01
              • 2016-10-31
              • 1970-01-01
              相关资源
              最近更新 更多