Số ngẫu nhiên có trọng số


101

Tôi đang cố gắng triển khai một số ngẫu nhiên có trọng số. Tôi hiện đang đập đầu vào tường và không thể hiểu được điều này.

Trong dự án của tôi (phạm vi nắm giữ, phân tích chủ quan toàn bộ vốn chủ sở hữu), tôi đang sử dụng các chức năng ngẫu nhiên của Boost. Vì vậy, giả sử tôi muốn chọn một số ngẫu nhiên từ 1 đến 3 (do đó, 1, 2 hoặc 3). Máy phát điện xoắn mersenne của Boost hoạt động giống như một sự quyến rũ cho điều này. Tuy nhiên, tôi muốn lựa chọn có trọng số, ví dụ như thế này:

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

Boost có một số loại chức năng cho việc này không?

Câu trả lời:


179

Có một thuật toán đơn giản để chọn một mặt hàng một cách ngẫu nhiên, trong đó các mặt hàng có trọng lượng riêng:

1) tính tổng của tất cả các trọng số

2) chọn một số ngẫu nhiên bằng 0 hoặc lớn hơn và nhỏ hơn tổng các trọng số

3) xem qua từng mục một, trừ đi trọng lượng của chúng từ số ngẫu nhiên của bạn, cho đến khi bạn nhận được mục có số ngẫu nhiên nhỏ hơn trọng lượng của mục đó

Mã giả minh họa điều này:

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

Điều này nên đơn giản để thích ứng với các vùng chứa boost của bạn và những thứ tương tự.


Nếu trọng lượng của bạn hiếm khi thay đổi nhưng bạn thường chọn một cách ngẫu nhiên và miễn là thùng chứa của bạn đang lưu trữ con trỏ đến các đối tượng hoặc dài hơn vài chục mục (về cơ bản, bạn phải lập hồ sơ để biết điều này có ích hay cản trở) , sau đó có một tối ưu hóa:

Bằng cách lưu trữ tổng trọng lượng tích lũy trong mỗi mục, bạn có thể sử dụng tìm kiếm nhị phân để chọn mục tương ứng với trọng lượng chọn.


Nếu bạn không biết số lượng các mục trong danh sách, thì có một thuật toán rất gọn gàng được gọi là lấy mẫu hồ chứa có thể được điều chỉnh để có trọng số.


3
Để tối ưu hóa, bạn có thể sử dụng trọng số tích lũy và sử dụng tìm kiếm nhị phân. Nhưng chỉ với ba giá trị khác nhau, điều này có thể là quá mức cần thiết.
sellibitze

2
Tôi giả sử khi bạn nói "theo thứ tự", bạn đang cố tình bỏ qua bước sắp xếp trước trên mảng choice_weight, đúng không?
SilentDirge

2
@Aureis, không cần phải sắp xếp mảng. Tôi đã cố gắng làm sáng tỏ ngôn ngữ của mình.
Sẽ

1
@Will: Có, nhưng có một thuật toán cùng tên. 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 nó xuất hiện trong thùng khi googling tìm nó. "thuật toán roulette Nga". Bạn có thể tranh luận rằng tất cả những người này đều sai tên.
v.oddou

3
Lưu ý cho người đọc trong tương lai: phần trừ trọng lượng của họ khỏi số ngẫu nhiên của bạn rất dễ bị bỏ qua, nhưng rất quan trọng đối với thuật toán (Tôi đã rơi vào bẫy giống như @kobik trong nhận xét của họ).
Frank Schmitt

48

Cập nhật câu trả lời cho một câu hỏi cũ. Bạn có thể dễ dàng làm điều này trong C ++ 11 chỉ với std :: lib:

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

Đầu ra trên hệ thống của tôi:

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

Lưu ý rằng hầu hết các đoạn mã trên được dành để chỉ hiển thị và phân tích đầu ra. Thế hệ thực sự chỉ là một vài dòng mã. Đầu ra chứng minh rằng các "xác suất" được yêu cầu đã đạt được. Bạn phải chia đầu ra được yêu cầu cho 1,5 vì đó là giá trị mà các yêu cầu cộng lại.


Chỉ cần một lưu ý nhắc nhở khi biên dịch ví dụ này: yêu cầu C ++ 11 tức là. cờ trình biên dịch use -std = c ++ 0x, có sẵn từ gcc 4.6 trở đi.
Pete855217

3
Bạn chỉ cần chọn ra những phần cần thiết để giải quyết vấn đề?
Jonny

2
Đây là câu trả lời tốt nhất, nhưng tôi nghĩ std::discrete_distributionthay vì std::piecewise_constant_distributionsẽ còn tốt hơn.
Dan

1
@Dan, Vâng, đó sẽ là một cách tuyệt vời khác để làm điều đó. Nếu bạn viết mã và trả lời với nó, tôi sẽ bỏ phiếu cho nó. Tôi nghĩ rằng mã có thể khá giống với những gì tôi có ở trên. Bạn chỉ cần thêm một vào đầu ra đã tạo. Và đầu vào cho phân phối sẽ đơn giản hơn. Một bộ câu trả lời so sánh / tương phản trong lĩnh vực này có thể có giá trị đối với độc giả.
Howard Hinnant

15

Nếu trọng số của bạn thay đổi chậm hơn so với khi được vẽ, thì C ++ 11 discrete_distributionsẽ là cách dễ nhất:

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

Tuy nhiên, lưu ý rằng c ++ 11 discrete_distributiontính toán tất cả các tổng tích lũy khi khởi tạo. Thông thường, bạn muốn điều đó vì nó tăng tốc thời gian lấy mẫu với chi phí O (N) một lần. Nhưng đối với một phân phối thay đổi nhanh chóng, nó sẽ phải chịu một chi phí tính toán (và bộ nhớ) lớn. Ví dụ: nếu các trọng số đại diện cho số lượng mục có và mỗi khi bạn vẽ một mục, bạn loại bỏ nó, bạn có thể sẽ muốn một thuật toán tùy chỉnh.

Câu trả lời của Will https://stackoverflow.com/a/1761646/837451 tránh được chi phí này nhưng sẽ chậm hơn so với C ++ 11 vì nó không thể sử dụng tìm kiếm nhị phân.

Để thấy rằng nó làm được điều này, bạn có thể xem các dòng liên quan ( /usr/include/c++/5/bits/random.tcctrên bản cài đặt Ubuntu 16.04 + GCC 5.3 của tôi):

  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

Những gì tôi làm khi tôi cần cân số lượng là sử dụng một số ngẫu nhiên cho trọng lượng.

Ví dụ: Tôi cần tạo các số ngẫu nhiên từ 1 đến 3 với các trọng số sau:

  • 10% của một số ngẫu nhiên có thể là 1
  • 30% của một số ngẫu nhiên có thể là 2
  • 60% của một số ngẫu nhiên có thể là 3

Sau đó, tôi sử dụng:

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

Với điều này, ngẫu nhiên nó có 10% xác suất là 1, 30% là 2 và 60% là 3.

Bạn có thể chơi với nó theo nhu cầu của bạn.

Hy vọng tôi có thể giúp bạn, Chúc may mắn!


Điều này quy định việc điều chỉnh động phân phối.
Josh C

2
Hacky nhưng tôi thích nó. Rất tốt cho một nguyên mẫu nhanh mà bạn muốn có một số trọng lượng thô.
lôi cuốn

1
Nó chỉ hoạt động đối với trọng lượng hợp lý. Bạn sẽ có một thời gian khó khăn làm việc đó với trọng lượng 1 / pi;)
Joseph Budin

1
@JosephBudin Một lần nữa, bạn sẽ không bao giờ có thể có một trọng lượng phi lý. Một bộ chuyển đổi ~ 4,3 tỷ trường hợp sẽ hoạt động tốt cho trọng lượng phao. : D
Jason C

1
Đúng @JasonC, vấn đề bây giờ nhỏ hơn vô cùng nhưng vẫn là một vấn đề;)
Joseph Budin

3

Xây dựng một túi (hoặc std :: vector) của tất cả các vật phẩm có thể nhặt được.
Đảm bảo rằng số lượng của mỗi mục tỷ lệ thuận với trọng lượng của bạn.

Thí dụ:

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

Vì vậy, có một túi có 100 món đồ với 60 1's, 35 2's và 5 3's.
Bây giờ sắp xếp ngẫu nhiên túi (std :: random_shuffle)

Chọn các phần tử từ túi một cách tuần tự cho đến khi nó rỗng.
Sau khi trống, chọn lại túi ngẫu nhiên và bắt đầu lại.


6
Nếu bạn có một túi các viên bi đỏ và xanh và bạn chọn một viên bi đỏ từ đó và không thay thế nó thì xác suất chọn được viên bi đỏ khác vẫn bằng nhau? Theo cách tương tự, câu lệnh "Chọn các phần tử từ túi một cách tuần tự cho đến khi nó trống" tạo ra một phân phối hoàn toàn khác với dự định.
ldog

@ldog: Tôi hiểu lập luận của bạn nhưng chúng tôi không tìm kiếm sự ngẫu nhiên thực sự mà chúng tôi đang tìm kiếm một phân phối cụ thể. Kỹ thuật này đảm bảo phân phối chính xác.
Martin York

4
quan điểm của tôi chính xác là bạn không sản xuất phân phối một cách chính xác, theo lập luận trước đây của tôi. hãy xem xét ví dụ về bộ đếm đơn giản, giả sử bạn đặt bạn có một mảng 3 như 1,2,2tạo ra 1 1/3 thời gian và 2 2/3. Chọn ngẫu nhiên mảng, chọn phần đầu tiên, giả sử là 2, bây giờ phần tử tiếp theo bạn chọn tuân theo phân phối 1 1/2 thời gian và 2 1/2 thời gian. Hiểu?
ldog

0

Chọn một số ngẫu nhiên trên [0,1), đây phải là toán tử mặc định () để tăng RNG. Chọn mục có hàm mật độ xác suất tích lũy> = số đó:

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
}

Trong đó random01 () trả về giá trị kép> = 0 và <1. Lưu ý rằng điều trên không yêu cầu các xác suất phải tổng bằng 1; nó bình thường hóa chúng cho bạn.

p chỉ là một hàm gán xác suất cho một mục trong tập hợp [bắt đầu, kết thúc). Bạn có thể bỏ qua nó (hoặc sử dụng danh tính) nếu bạn chỉ có một chuỗi xác suất.


Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.