【问题标题】:How to efficiently generate a set of unique random numbers with a predefined distribution?如何有效地生成一组具有预定义分布的唯一随机数?
【发布时间】:2014-03-15 08:29:29
【问题描述】:

我有一张带有某种概率分布的物品地图:

Map<SingleObjectiveItem, Double> itemsDistribution;

给定某个m,我必须从上述分布中抽取m 元素中的Set

到目前为止,我一直在使用幼稚的方式:

while(mySet.size < m)
   mySet.add(getNextSample(itemsDistribution));

getNextSample(...) 方法根据概率从分布中获取对象。现在,随着m 的增加,性能严重受损。对于m = 500itemsDistribution.size() = 1000 元素,抖动过多,函数在while 循环中停留的时间过长。生成 1000 个这样的集合,您就有了一个可以爬行的应用程序。

有没有更有效的方法来生成一组具有“预定义”分布的唯一随机数?大多数集合改组技术等都是一致随机的。解决这个问题的好方法是什么?

更新:循环将调用getNextSample(...)“至少”1 + 2 + 3 + ... + m = m(m+1)/2 次。那是在第一次运行中,我们肯定会得到该集合的样本。第二次迭代,它可能至少被调用两次,依此类推。如果getNextSample本质上是顺序的,即遍历整个累积分布来找到样本,那么循环的运行时间复杂度至少为:n*m(m+1)/2,'n'是分布中的元素数。如果m = cn; 0&lt;c&lt;=1 则循环至少为 Sigma(n^3)。这也是下限!

如果我们用二分查找代替顺序查找,复杂度至少为 Sigma(log n * n^2)。高效,但幅度可能不大。

此外,由于我调用上述循环k 次,生成k 这样的集合,因此无法从分发中删除。这些集合是项目随机“时间表”的一部分。因此是一组“项目”。

【问题讨论】:

  • 一个元素可以被多次选取吗?如果不是,地图中值的确切形式含义是什么?不能只是选择一个元素的概率,因为当我们已经选择了一些元素并且不能再次触摸它们时,这些值会失去某些概率属性。最明显的是,它们的总和不再等于 1。此外,挑选项目的顺序可能会干扰挑选一组的整体概率。例如,从 {1,2,3} 开始,选择 1 然后 2 可能与选择 2 然后 1 的概率不同 - 可能您希望在这方面保持一致。

标签: java algorithm random performance


【解决方案1】:

首先在二维中生成一些随机点。

然后应用您的分配

现在找到分布中的所有条目并选择 x 坐标,您就可以得到具有请求分布的随机数,如下所示:

【讨论】:

  • 不确定这会很快:给定输入元素的均匀分布,接受一个点的机会是 1/n,即平均而言,我们必须采样 m*n 个点才能得到一组大小为 m。这是相当多的。
  • 嗯 - 正态分布并涵盖 3 个标准差,大约 1/3 的点将在图表下方。
  • 但其他发行版远比这差。我只是想指出拒绝抽样需要在分布周围有一个合理的小边界框,并非所有分布都存在这种边界框。也就是说,您的答案仅适用于某些发行版。
【解决方案2】:

问题不太可能是您显示的循环:

设 n 为分布的大小,I 为调用 getNextSample 的次数。我们有 I = sum_i(C_i),其中 C_i 是调用 getNextSample 的次数,而集合的大小为 i。要找到 E[C_i],请观察 C_i 是 poisson process 的到达间隔时间,λ = 1 - i / n,因此 exponentially distributed 与 λ。因此,E[C_i] = 1 / λ = 因此 E[C_i] = 1 / (1 - i / n)

也就是说,对一组大小为 m = n/2 的样本进行抽样平均需要少于 2m = n 次 getNextSample 调用。如果那是“慢”和“爬行”,很可能是因为 getNextSample 很慢。考虑到将分布传递给方法的方式不合适(因为该方法必然会遍历整个分布以找到随机元素),这实际上并不令人惊讶。

以下应该更快(如果 m

class Distribution<T> {
    private double[] cummulativeWeight;
    private T[] item;
    private double totalWeight;

    Distribution(Map<T, Double> probabilityMap) {
        int i = 0;

        cummulativeWeight = new double[probabilityMap.size()];
        item = (T[]) new Object[probabilityMap.size()];

        for (Map.Entry<T, Double> entry : probabilityMap.entrySet()) {
            item[i] = entry.getKey();
            totalWeight += entry.getValue();
            cummulativeWeight[i] = totalWeight;
            i++;
        }
    }

    T randomItem() {
        double weight = Math.random() * totalWeight;
        int index = Arrays.binarySearch(cummulativeWeight, weight);
        if (index < 0) {
            index = -index - 1;
        }
        return item[index];
    }

    Set<T> randomSubset(int size) {
        Set<T> set = new HashSet<>();
        while(set.size() < size) {
            set.add(randomItem());
        }
        return set;
    }
}



public class Test {

    public static void main(String[] args) {
        int max = 1_000_000;
        HashMap<Integer, Double> probabilities = new HashMap<>();
        for (int i = 0; i < max; i++) {
            probabilities.put(i, (double) i);
        }

        Distribution<Integer> d = new Distribution<>(probabilities);
        Set<Integer> set = d.randomSubset(max / 2);
        //System.out.println(set);
    }
}

预期的运行时间为 O(m / (1 - m / n) * log n)。在我的计算机上,一组 1_000_000 的大小为 500_000 的子集在大约 3 秒内计算出来。

正如我们所见,当 m 接近 n 时,预期的运行时间接近无穷大。如果这是一个问题(即 m > 0.9 n),那么以下更复杂的方法应该会更好:

Set<T> randomSubset(int size) {
    Set<T> set = new HashSet<>();
    while(set.size() < size) {
        T randomItem = randomItem();
            remove(randomItem); // removes the item from the distribution
            set.add(randomItem);
    }
    return set;
}

要有效地实现删除,需要对分布使用不同的表示形式,例如一棵二叉树,其中每个节点都存储其根所在的子树的总权重。

但这相当复杂,所以如果已知 m 明显小于 n,我不会走那条路。

【讨论】:

  • 怎么样1/(1-c)?如果getNextSample(...)O(n) 中运行(不幸的是,是连续的),那么循环应该会在1 + 2 + 3 + ... + m = m(m+1)/2 中运行。如果m = cn 那很容易O(n^2)。二进制搜索肯定会提高效率。但我认为这不会是一个很大的差距。因为循环的期望值仍然是m(m+1)/2。我在这里错过了什么吗?
  • 我为该运行时分析添加了一个证明草图。此外,n 表示我们从中采样的集合的大小。因此,E[算法的执行时间] = O(E[I] * n) = O(m / (1 - c) * n)。也就是说,为什么您认为用二分搜索代替线性搜索不会显着改善执行时间,这超出了我的理解。
  • 哦,会的。我质疑加速是否真的很重要。我正在努力将其更改为 bSearch 以查看它的工作情况。一个快速的问题:你为什么假设一个指数分布?从概念上讲...
  • 好吧,如果 n = 1000, n / log(n) ~= 100 ...我将编辑为什么分布是指数的。
  • 你是对的。问题不仅在于循环,还在于捕获数据的方式。我接受了您的建议并创建了一个类似的课程来做到这一点。它确实大大加快了速度。正如我所猜测的那样,仅更改循环以进行二进制搜索并没有太大帮助。
【解决方案3】:

您应该实现自己的随机数生成器(使用 MonteCarlo 方法或任何好的统一生成器,如 mersen twister)并基于反演方法 (here)。

例如:指数定律:在[0,1] 中生成一个统一的随机数 u,那么你的指数定律的随机变量将是:ln(1-u)/(-lambda) lambda being the exponential law parameter and ln the natural logarithm

希望它会有所帮助;)。

【讨论】:

    【解决方案4】:

    我认为你有两个问题:

    1. 你的itemDistribution 不知道你需要一个集合,所以当你正在构建的集合得到 大你会选择很多已经在集合中的元素。如果你从 设置所有完整和删除元素,对于非常小的集合,您将遇到同样的问题。

      你有没有理由在你之后不从itemDistribution 中删除元素 捡到了吗?那么你不会选择两次相同的元素?

    2. itemDistribution 的数据结构选择在我看来很可疑。你想要 getNextSample 操作要快。从值到概率的映射不是强迫你吗 遍历每个getNextSample 的大部分地图。我不擅长 统计数据,但你不能用另一种方式表示itemDistribution,就像一张来自的地图 概率,或者可能是所有较小概率的总和 + 元素的概率 一组?

    【讨论】:

      【解决方案5】:

      您的性能取决于您的getNextSample 函数的工作方式。如果在选择下一个项目时必须遍历所有概率,那可能会很慢。

      从列表中挑选几个唯一随机项目的好方法是首先打乱列表,然后将项目从列表中弹出。您可以使用给定的分布对列表进行一次洗牌。从那时起,选择您的 m 项目就只是弹出列表。

      这是一个概率洗牌的实现:

      List<Item> prob_shuffle(Map<Item, int> dist)
      {
          int n = dist.length;
          List<Item> a = dist.keys();
          int psum = 0;
          int i, j;
      
          for (i in dist) psum += dist[i];
      
          for (i = 0; i < n; i++) {
              int ip = rand(psum);    // 0 <= ip < psum
              int jp = 0;
      
              for (j = i; j < n; j++) {
                  jp += dist[a[j]];
                  if (ip < jp) break;
              }
      
              psum -= dist[a[j]];
      
              Item tmp = a[i];
              a[i] = a[j];
              a[j] = tmp;
          }
          return a;
      }
      

      这不是 Java 中的,而是在 C 中实现后的伪代码,所以请持保留态度。这个想法是通过不断地从未洗牌区域中挑选项目来将项目附加到洗牌区域。

      在这里,我使用了整数概率。 (概率不必添加到特殊值,它只是“越大越好”。)您可以使用浮点数,但由于不准确,您最终可能会在选择项目时超出数组。然后你应该使用项目n - 1。如果你加上那个安全网,你甚至可以拥有总是最后被挑选的概率为零的物品。

      可能有一种方法可以加快拣货循环,但我真的不知道如何。交换使任何预先计算变得无用。

      【讨论】:

        【解决方案6】:

        在表格中累积您的概率

                       Probability
        Item       Actual  Accumulated
        Item1       0.10      0.10
        Item2       0.30      0.40
        Item3       0.15      0.55
        Item4       0.20      0.75
        Item5       0.25      1.00
        

        在 0.0 和 1.0 之间创建一个随机数,并对总和大于生成的数字的第一项进行二分搜索。该项目会以所需的概率被选中。

        【讨论】:

          【解决方案7】:

          Ebbe 的方法叫做rejection sampling

          我有时会使用一种简单的方法,使用inverse cumulative distribution function,这是一个将 0 到 1 之间的数字 X 映射到 Y 轴上的函数。 然后,您只需生成一个介于 0 和 1 之间的均匀分布的随机数,并将函数应用于它。 该函数也称为“分位数函数”。

          例如,假设您要生成一个正态分布的随机数。 它的累积分布函数称为Phi。 与之相反的是probit。 生成正态变量的方法有很多,这只是一个例子。

          您可以轻松地以表格的形式为您喜欢的任何单变量分布构建一个近似累积分布函数。 然后你可以通过查表和插值来反转它。

          【讨论】:

            【解决方案8】:

            如果您不太关心随机性属性,那么我会这样做:

            1. 为伪随机数创建缓冲区

              双增益[MAX]; // [edit1] 双伪随机数

              • MAX 大小应该足够大...例如 1024*128
              • 类型可以是任何类型 (float,int,DWORD...)
            2. 用数字填充缓冲区

              你有一个由你的概率分布定义的数字范围x = &lt; x0,x1 &gt; 和概率函数probability(x),所以这样做:

              for (i=0,x=x0;x<=x1;x+=stepx)
               for (j=0,n=probability(x)*MAX,q=0.1*stepx/n;j<n;j++,i++) // [edit1] unique pseudo-random numbers
                buff[i]=x+(double(i)*q);                                // [edit1] ...
              

              stepx 是您对项目的准确度(对于整数类型 = 1),现在 buff[] 数组与您需要的分布相同,但它不是伪随机的。您还应该添加检查j 是否不是&gt;= MAX 以避免数组溢出,并且最后buff[] 的实际大小是j(由于四舍五入可能小于MAX)

            3. 随机播放buff[]

              只做几个交换循环buff[i]buff[j],其中i是循环变量,j是伪随机&lt;0-MAX)

            4. 编写你的伪随机函数

              它只是从缓冲区返回数字。第一次调用返回buff[0],第二次返回buff[1],依此类推...对于标准生成器当您到达buff[] 的末尾时,再次随机播放buff[] 并再次从buff[0] 开始。但是由于您需要唯一的数字,因此您无法到达缓冲区的末尾,因此请将 MAX 设置为足够大以完成您的任务,否则将无法确保唯一性。

            [备注]

            MAX 应该足够大以存储您想要的整个发行版。如果它不够大,那么概率低的项目可能会完全丢失。

            [edit1] - 稍微调整答案以匹配问题需求(由 Meriton 指出,谢谢)

            PS.初始化复杂度为 O(N)获取次数为 O(1)

            【讨论】:

            • 这似乎不正确:您不能确保返回的数字是唯一的。此外,权重可能不是整数(问题使用 double 来表示它们)。
            • @meriton 哦,我之前忽略了独特的部分......但是我在那里写的数字可能是两倍。 buff 填充只需要稍微调整即可获得唯一的数字(在填充
            猜你喜欢
            • 1970-01-01
            • 2015-10-28
            • 1970-01-01
            • 1970-01-01
            • 2023-03-09
            • 2013-12-14
            • 2012-10-21
            • 2011-05-15
            相关资源
            最近更新 更多