Ağırlıklı rastgele sayılar


104

Ağırlıklı rastgele sayılar uygulamaya çalışıyorum. Şu anda kafamı duvara vuruyorum ve bunu çözemiyorum.

Projemde (Hold'em el aralıkları, öznel hepsi bir arada öz sermaye analizi), Boost'un rastgele işlevlerini kullanıyorum. Diyelim ki 1 ile 3 arasında rastgele bir sayı seçmek istiyorum (yani 1, 2 veya 3). Boost'un mersenne twister jeneratörü bunun için bir cazibe gibi çalışıyor. Ancak, örneğin şu şekilde ağırlıklandırılmasını istiyorum:

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

Boost'un bunun için bir çeşit işlevselliği var mı?

Yanıtlar:


181

Öğelerin ayrı ağırlıklara sahip olduğu rastgele bir öğeyi seçmek için basit bir algoritma vardır:

1) tüm ağırlıkların toplamını hesaplayın

2) 0 veya daha büyük olan ve ağırlıkların toplamından daha küçük olan rastgele bir sayı seçin

3) rastgele sayının o öğenin ağırlığından daha az olduğu öğeyi elde edene kadar, ağırlıklarını rastgele sayınızdan çıkararak öğeleri birer birer gözden geçirin.

Bunu gösteren sözde kod:

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");

Bu, destek kaplarınıza ve benzerlerine uyum sağlamak için basit olmalıdır.


Ağırlıklarınız nadiren değiştiriliyorsa ancak sık sık rastgele bir ağırlık seçiyorsanız ve konteynırınız nesnelere işaretçileri depoladığı veya birkaç düzineden fazla öğe uzunluğunda olduğu sürece (temel olarak, bunun yardımcı olup olmadığını veya engelleyeceğini bilmek için profil yapmanız gerekir) , sonra bir optimizasyon var:

Her bir öğedeki kümülatif ağırlık toplamını depolayarak , toplama ağırlığına karşılık gelen öğeyi seçmek için bir ikili arama kullanabilirsiniz .


Listedeki öğelerin sayısını bilmiyorsanız, ağırlıklı olarak uyarlanabilen rezervuar örnekleme adı verilen çok düzgün bir algoritma vardır .


3
Bir optimizasyon olarak, kümülatif ağırlıkları kullanabilir ve bir ikili arama kullanabilirsiniz. Ancak yalnızca üç farklı değer için bu muhtemelen aşırıdır.
sellibitze

2
"Sırayla" dediğinizde, seçim_ağırlığı dizisinde kasıtlı olarak bir ön sıralama adımını atladığınızı varsayıyorum, evet?
SilentDirge

2
@Aureis, diziyi sıralamanıza gerek yok. Dilimi netleştirmeye çalıştım.
Will

1
@Will: Evet ama aynı isimde bir algoritma var. sirkan.iit.bme.hu/~szirmay/c29.pdf ve en.wikipedia.org/wiki/Photon_mapping A Monte Carlo method called Russian roulette is used to choose one of these actions , google'da arama yaparken kovalar içinde ortaya çıkıyor. "rus ruleti algoritması". Yine de bu insanların hepsinin isminin yanlış olduğunu iddia edebilirsiniz.
v.oddou

3
Gelecekteki okuyucular için not: Rastgele sayınızdan ağırlıklarını çıkaran kısmı gözden kaçırmak kolaydır, ancak algoritma için çok önemlidir (yorumlarında @kobik ile aynı tuzağa düştüm).
Frank Schmitt

48

Eski bir soruya verilen cevap güncellendi. Bunu C ++ 11'de sadece std :: lib ile kolayca yapabilirsiniz:

#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';
}

Sistemimdeki çıktı:

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

Yukarıdaki kodun çoğunun yalnızca çıktıyı görüntülemeye ve analiz etmeye ayrıldığını unutmayın. Gerçek nesil, sadece birkaç satır koddur. Çıktı, istenen "olasılıkların" elde edildiğini gösterir. İstenilen çıktıyı 1.5'e bölmeniz gerekir, çünkü isteklerin toplamı budur.


Bu örneğin derlenmesiyle ilgili bir hatırlatma notu: C ++ 11 yani. -std = c ++ 0x derleyici işaretini kullanın, gcc 4.6'dan itibaren mevcuttur.
Pete855217

3
Sorunu çözen gerekli parçaları seçmek ister misiniz?
Jonny

2
Bu en iyi cevap, ama bence std::discrete_distributionbunun yerine std::piecewise_constant_distributiondaha da iyi olurdu.
Dan

1
@Dan, Evet, bunu yapmanın başka bir mükemmel yolu olur. Eğer kodlarsanız ve onunla cevaplarsanız, ona oy vereceğim. Kodun yukarıda sahip olduğuma oldukça benzer olabileceğini düşünüyorum. Oluşturulan çıktıya bir tane eklemeniz yeterlidir. Ve dağıtımın girdisi daha basit olacaktır. Bu alandaki bir karşılaştırma / kontrast cevap seti okuyucular için değerli olabilir.
Howard Hinnant

15

Ağırlıklarınız çizildiğinden daha yavaş değişiyorsa, C ++ 11 discrete_distributionen kolayı olacaktır:

#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...

Bununla birlikte, c ++ 11'in discrete_distributionbaşlatma sırasında tüm kümülatif toplamları hesapladığını unutmayın. Genellikle bunu istersiniz çünkü örnekleme süresini bir kerelik O (N) maliyeti için hızlandırır. Ancak hızla değişen bir dağıtım için ağır bir hesaplama (ve hafıza) maliyetine yol açacaktır. Örneğin, ağırlıklar kaç tane öğe olduğunu gösteriyorsa ve her bir tane çizdiğinizde onu kaldırırsanız, muhtemelen özel bir algoritma isteyeceksiniz.

Will'in cevabı https://stackoverflow.com/a/1761646/837451 bu ek yükten kaçınır, ancak ikili aramayı kullanamadığı için C ++ 11'den daha yavaş çekilir.

Bunu yaptığını görmek için ilgili satırları görebilirsiniz ( /usr/include/c++/5/bits/random.tccUbuntu 16.04 + GCC 5.3 kurulumumda):

  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

Sayıları ağırlıklandırmam gerektiğinde yaptığım şey, ağırlık için rastgele bir sayı kullanmaktır.

Örneğin: 1'den 3'e kadar aşağıdaki ağırlıklarla rastgele sayılar oluşturmam gerekiyor:

  • Rastgele bir sayının% 10'u 1 olabilir
  • Rastgele bir sayının% 30'u 2 olabilir
  • Rastgele bir sayının% 60'ı 3 olabilir

Sonra kullanırım:

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;
}

Bununla rastgele olarak olasılıkların% 10'u 1,% 30'u 2 ve% 60'ı 3'e sahiptir.

İhtiyaçlarınız olarak onunla oynayabilirsiniz.

Umarım sana yardım edebilirim, İyi Şanslar!


Bu, dağıtımın dinamik olarak ayarlanmasını ortadan kaldırır.
Josh C

2
Hacky ama hoşuma gitti. Biraz kaba ağırlık almak istediğiniz hızlı bir prototip için güzel.
drewish

1
Yalnızca rasyonel ağırlıklar için işe yarar. 1 / pi ağırlığıyla yapmakta zorlanacaksınız;)
Joseph Budin

1
@JosephBudin O halde, asla mantıksız bir ağırlığa sahip olamazsınız. ~ 4,3 milyar kasa anahtarı, değişken ağırlıklarda gayet iyi çalışmalıdır. : D
Jason C

1
Doğru @JasonC, sorun şu anda son derece küçük ama hala bir sorun;)
Joseph Budin

3

Seçilebilecek tüm öğelerden bir çanta (veya std :: vektör) oluşturun.
Her bir öğenin sayısının ağırlığınızla orantılı olduğundan emin olun.

Misal:

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

Öyleyse 60 1, 35 2 ve 53'lü 100 parçalık bir çantanız olsun.
Şimdi çantayı rastgele sıralayın (std :: random_shuffle)

Boş olana kadar çantadan öğeleri sırayla seçin.
Boşaldığında çantayı yeniden rastgele seçin ve yeniden başlayın.


6
Eğer kırmızı ve mavi mermer bir çanta var ve ondan bir kırmızı mermer seçmek ve eğer yok yerine hala aynı başka kırmızı mermer seçme ihtimali nedir? Aynı şekilde, "Boş olana kadar çantadan öğeleri sırayla seçin" ifadeniz, amaçlanandan tamamen farklı bir dağıtım üretir.
ldog

@dog: İddianızı anlıyorum ama gerçek rastgelelik aramıyoruz, belirli bir dağılım arıyoruz. Bu teknik, doğru dağıtımı garanti eder.
Martin York

4
Demek istediğim, önceki argümanıma göre, tam olarak dağıtımı doğru şekilde üretmediğinizdir. Basit sayaç örneğini düşünün, diyelim ki 3'lük bir diziye sahip 1,2,2olduğunuzu, zamanın 1 / 3'ünü ve 2 2 / 3'ünü üreten olarak koyun . Diziyi rastgele seç, ilkini seç, bir 2 diyelim, şimdi seçeceğin sonraki eleman 1 1/2 zaman ve 2 1/2 zaman dağılımını takip ediyor. Kıvraklık?
ldog

0

Yükseltme RNG'si için varsayılan işleç () olması gereken [0,1) üzerinde rastgele bir sayı seçin. Kümülatif olasılık yoğunluğu işlevine sahip öğeyi seçin> = bu sayı:

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 () bir çift> = 0 ve <1 döndürür. Yukarıdakilerin olasılıkların toplamının 1 olmasını gerektirmediğini unutmayın; onları sizin için normalleştirir.

p, koleksiyondaki bir öğeye [başlama, bitiş) olasılık atayan bir işlevdir. Sadece bir olasılıklar dizisine sahipseniz, onu atlayabilirsiniz (veya bir kimlik kullanabilirsiniz).


Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.