加权随机数


101

我正在尝试实现加权随机数。我目前只是把头撞在墙上,无法解决这个问题。

在我的项目(Hold'em手范围,主观全能分析)中,我正在使用Boost的随机函数。因此,假设我要选择1到3之间的一个随机数(所以选择1、2或3)。Boost的mersenne扭曲生成器为此发挥了魅力。但是,我希望例如这样对选秀权进行加权:

1 (weight: 90)
2 (weight: 56)
3 (weight:  4)

Boost是否为此具有某种功能?

Answers:


179

有一个简单的算法可以随机选择一个项目,其中项目具有各自的权重:

1)计算所有权重之和

2)选择一个大于或等于0且小于权重总和的随机数

3)一次检查一个项目,从您的随机数中减去它们的权重,直到获得随机数小于该项目权重的项目

伪代码说明了这一点:

int sum_of_weight = 0;
for(int i=0; i<num_choices; i++) {
   sum_of_weight += choice_weight[i];
}
int rnd = random(sum_of_weight);
for(int i=0; i<num_choices; i++) {
  if(rnd < choice_weight[i])
    return i;
  rnd -= choice_weight[i];
}
assert(!"should never get here");

这应该很容易适应您的增压容器等。


如果您的权重很少改变,但是您经常随机选择一个,并且只要您的容器存储指向对象的指针或长度超过几十个项目(基本上,您就必须剖析以了解这是有帮助还是有障碍) ,那么有一个优化:

通过将累积重量总和存储在每个项目中,您可以使用二进制搜索来选择与拾取重量相对应的项目。


如果您不知道列表中的项目数,那么有一个非常简洁的算法称为储层采样,可以对其进行加权。


3
作为一种优化,您可以使用累积权重并使用二进制搜索。但是对于仅三个不同的值,这可能是过大的。
sellibitze

2
我假设当您说“按顺序”时,您故意在choice_weight数组上省略了预排序步骤,是吗?
SilentDirge,2011年

2
@Aureis,无需对数组进行排序。我试图澄清我的语言。
威尔

1
@Will:是的,但是有一个同名的算法。sirkan.iit.bme.hu/~szirmay/c29.pdfen.wikipedia.org/wiki/Photon_mapping A Monte Carlo method called Russian roulette is used to choose one of these actions在进行谷歌搜索时会出现在存储桶中。“俄罗斯轮盘赌算法”。您可能会争辩说,所有这些人的名字都错了。
v.oddou 2014年

3
敬请将来的读者注意:从您的随机数减去其权重的部分很容易忽略,但对于算法至关重要(我在评论中与@kobik陷入同一陷阱)。
Frank Schmitt

48

更新了对旧问题的答案。您只需使用std :: lib就可以在C ++ 11中轻松地做到这一点:

#include <iostream>
#include <random>
#include <iterator>
#include <ctime>
#include <type_traits>
#include <cassert>

int main()
{
    // Set up distribution
    double interval[] = {1,   2,   3,   4};
    double weights[] =  {  .90, .56, .04};
    std::piecewise_constant_distribution<> dist(std::begin(interval),
                                                std::end(interval),
                                                std::begin(weights));
    // Choose generator
    std::mt19937 gen(std::time(0));  // seed as wanted
    // Demonstrate with N randomly generated numbers
    const unsigned N = 1000000;
    // Collect number of times each random number is generated
    double avg[std::extent<decltype(weights)>::value] = {0};
    for (unsigned i = 0; i < N; ++i)
    {
        // Generate random number using gen, distributed according to dist
        unsigned r = static_cast<unsigned>(dist(gen));
        // Sanity check
        assert(interval[0] <= r && r <= *(std::end(interval)-2));
        // Save r for statistical test of distribution
        avg[r - 1]++;
    }
    // Compute averages for distribution
    for (double* i = std::begin(avg); i < std::end(avg); ++i)
        *i /= N;
    // Display distribution
    for (unsigned i = 1; i <= std::extent<decltype(avg)>::value; ++i)
        std::cout << "avg[" << i << "] = " << avg[i-1] << '\n';
}

我的系统上的输出:

avg[1] = 0.600115
avg[2] = 0.373341
avg[3] = 0.026544

请注意,上面的大多数代码仅用于显示和分析输出。实际的生成只是几行代码。输出表明已获得请求的“概率”。您必须将请求的输出除以1.5,因为这就是请求的总和。


只是关于此示例编译的提醒注意:需要C ++ 11,即。使用-std = c ++ 0x编译器标志,从gcc 4.6起可用。
Pete855217

3
是否只想挑选出解决问题的必要部件?
强尼

2
这是最好的答案,但我认为std::discrete_distribution不是std::piecewise_constant_distribution会是更好的。

1
@Dan,是的,那将是另一种出色的方法。如果您将其编码并回答,我会投票支持。我认为代码可能与我上面的代码非常相似。您只需要在生成的输出中添加一个即可。并且分配的输入将更简单。这方面的一组比较/对比答案可能对读者有价值。
霍华德·辛南特

15

如果您的权重变化比绘制的速度慢,则C ++ 11 discrete_distribution将是最简单的:

#include <random>
#include <vector>
std::vector<double> weights{90,56,4};
std::discrete_distribution<int> dist(std::begin(weights), std::end(weights));
std::mt19937 gen;
gen.seed(time(0));//if you want different results from different runs
int N = 100000;
std::vector<int> samples(N);
for(auto & i: samples)
    i = dist(gen);
//do something with your samples...

但是请注意,c ++ 11 discrete_distribution在初始化时会计算所有累积和。通常,您需要这样做是因为它可以将采样时间缩短为O(N)成本的一倍。但是对于快速变化的发行版,将导致沉重的计算(和内存)成本。例如,如果权重代表有多少个项目,并且每次绘制一个项目,都将其删除,则可能需要自定义算法。

Will的答案https://stackoverflow.com/a/1761646/837451避免了这种开销,但是从中提取比C ++ 11慢,因为它不能使用二进制搜索。

要看到它能做到这一点,您可以看到相关的行(/usr/include/c++/5/bits/random.tcc在我的Ubuntu 16.04 + GCC 5.3安装上):

  template<typename _IntType>
    void
    discrete_distribution<_IntType>::param_type::
    _M_initialize()
    {
      if (_M_prob.size() < 2)
        {
          _M_prob.clear();
          return;
        }

      const double __sum = std::accumulate(_M_prob.begin(),
                                           _M_prob.end(), 0.0);
      // Now normalize the probabilites.
      __detail::__normalize(_M_prob.begin(), _M_prob.end(), _M_prob.begin(),
                            __sum);
      // Accumulate partial sums.
      _M_cp.reserve(_M_prob.size());
      std::partial_sum(_M_prob.begin(), _M_prob.end(),
                       std::back_inserter(_M_cp));
      // Make sure the last cumulative probability is one.
      _M_cp[_M_cp.size() - 1] = 1.0;
    }

10

当我需要对数字进行加权时,我会使用一个随机数字作为加权。

例如:我需要生成具有以下权重的1到3的随机数:

  • 随机数的10%可能是1
  • 随机数的30%可能是2
  • 随机数的60%可能是3

然后我用:

weight = rand() % 10;

switch( weight ) {

    case 0:
        randomNumber = 1;
        break;
    case 1:
    case 2:
    case 3:
        randomNumber = 2;
        break;
    case 4:
    case 5:
    case 6:
    case 7:
    case 8:
    case 9:
        randomNumber = 3;
        break;
}

这样一来,它随机地有10%的概率是1、30%的概率是2和60%的概率是3。

您可以根据需要使用它。

希望我能帮助您,祝您好运!


这排除了动态调整分布的可能性。
2014年

2
哈克,但我喜欢。非常适合用于需要粗略加权的快速原型。
提请

1
它仅适用于合理的权重。您将很难以1 / pi的重量完成它;)
Joseph Budin

1
@JosephBudin然后,您再也无法拥有不合理的体重。一个约43亿个案例的开关应该对浮重很合适。:D
Jason C

1
对@JasonC来说,这个问题现在已经无限小了,但是仍然是一个问题;)
Joseph Budin

3

建立一个可以选择的所有物品的袋子(或std :: vector)。
确保每个项目的数量与您的权重成正比。

例:

  • 1 60%
  • 2 35%
  • 3 5%

因此,要有一个包含100个物品的袋子,其中包括60个1、35 2个和5 3个。
现在对袋子进行随机排序(std :: random_shuffle)

依次从袋子中拾取元素,直到袋子变空为止。
清空后,重新随机包装袋,然后重新开始。


6
如果您有一袋红色和蓝色的大理石,并且从中选择了一个红色大理石并且不进行替换,那么选择另一个红色大理石的可能性还是一样吗?同样,您的陈述“依次从袋子中取出元素直到袋子变空”将产生与预期完全不同的分布。
ldog

@ldog:我理解您的论点,但我们不是在寻找真正的随机性,而是在寻找特定的分布。这种技术保证了正确的分配。
马丁·约克2010年

4
我的意思是,按照我以前的观点,您不能正确地产生分布。考虑简单的计数器示例,假设您有3个数组,它们分别1,2,2产生1 1/3的时间和2 2/3的时间。随机化数组,选择第一个,假设为2,现在选择的下一个元素遵循1 1/2时间和2 1/2时间的分布。精明吗?
ldog

0

在[0,1)上选择一个随机数,它应该是增强RNG的默认operator()。选择具有累积概率密度函数> =该数字的项目:

template <class It,class P>
It choose_p(It begin,It end,P const& p)
{
    if (begin==end) return end;
    double sum=0.;
    for (It i=begin;i!=end;++i)
        sum+=p(*i);
    double choice=sum*random01();
    for (It i=begin;;) {
        choice -= p(*i);
        It r=i;
        ++i;
        if (choice<0 || i==end) return r;
    }
    return begin; //unreachable
}

其中random01()返回> = 0和<1的双精度数。请注意,以上并不要求总和为1;它为您规范化它们。

p只是一个为集合[begin,end)中的一个项目分配概率的函数。如果您只有一系列概率,则可以忽略它(或使用一个标识)。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.