Mô phỏng chính xác rất nhiều cuộn xúc xắc không có vòng lặp?


14

OK vì vậy nếu trò chơi của bạn tung ra nhiều xúc xắc, bạn có thể gọi một trình tạo số ngẫu nhiên trong một vòng lặp. Nhưng đối với bất kỳ bộ xúc xắc nào được cuộn thường xuyên, bạn sẽ có được đường cong / biểu đồ phân phối. Vì vậy, câu hỏi của tôi là có một phép tính đơn giản tốt đẹp mà tôi có thể chạy sẽ cho tôi một số phù hợp với phân phối đó?

Ví dụ: 2D6 - Điểm -% Xác suất

2 - 2,77%

3 - 5,55%

4 - 8,33%

5 - 11,11%

6 - 13,88%

7 - 16,66%

8 - 13,88%

9 - 11,11%

10 - 8,33%

11 - 5,55%

12 - 2,77%

Vì vậy, biết những điều trên bạn có thể cuộn một d100 duy nhất và tìm ra giá trị 2D6 chính xác. Nhưng một khi chúng ta bắt đầu với 10D6, 50D6, 100D6, 1000D6 thì điều này có thể tiết kiệm rất nhiều thời gian xử lý. Vì vậy, phải có một hướng dẫn / phương pháp / thuật toán có thể làm điều này nhanh chóng? Nó có thể hữu ích cho thị trường chứng khoán, sòng bạc, trò chơi chiến lược, pháo đài lùn, v.v ... Điều gì sẽ xảy ra nếu bạn có thể mô phỏng kết quả của trận chiến chiến lược hoàn chỉnh sẽ mất hàng giờ để chơi với một vài lời gọi đến chức năng này và một số phép toán cơ bản?


5
Ngay cả ở 1000 d6, vòng lặp sẽ đủ nhanh trên một PC hiện đại mà bạn khó có thể nhận thấy, vì vậy đây có thể là tối ưu hóa sớm. Luôn luôn thử hồ sơ trước khi thay thế một vòng lặp rõ ràng bằng một công thức mờ. Điều đó nói rằng, có các tùy chọn thuật toán. Bạn có quan tâm đến xác suất rời rạc như súc sắc trong các ví dụ của bạn không, hoặc có thể chấp nhận mô hình hóa chúng dưới dạng phân phối xác suất liên tục (vì vậy có thể có kết quả phân đoạn như 2,5)?
DMGregory

DMGregory chính xác, tính toán 1000d6 sẽ không giống với bộ xử lý. Tuy nhiên, có một thứ gọi là Phân phối nhị thức (với một số công việc thông minh) sẽ nhận được kết quả mà bạn quan tâm. Ngoài ra, nếu bạn muốn tìm xác suất cho quy tắc cuộn tùy ý, hãy thử TRoll có ngôn ngữ khiêm tốn được thiết lập để chỉ định cách tung một bộ xúc xắc và nó sẽ tính toán tất cả các xác suất cho mọi kết quả có thể.
Draco18 không còn tin tưởng SE

Sử dụng phân phối Poisson: p.
Luis Masuelli

1
Đối với bất kỳ bộ xúc xắc nào được cuộn thường xuyên, bạn có thể sẽ nhận được đường cong / biểu đồ phân phối. Đó là một sự khác biệt quan trọng. Một con xúc xắc có thể cuộn một triệu 6s liên tiếp, điều đó là không thể, nhưng nó có thể
Richard Tingle

@RichardTingle Bạn có thể giải thích? Một đường cong / biểu đồ phân phối cũng sẽ bao gồm các triệu triệu 6s trong một trường hợp liên tiếp.
amitp

Câu trả lời:


16

Như tôi đã đề cập trong nhận xét của tôi ở trên, tôi khuyên bạn nên lập hồ sơ này trước khi áp dụng mã của bạn. Một forcon súc sắc tóm tắt vòng lặp nhanh dễ hiểu và dễ sửa đổi hơn nhiều so với các công thức toán học phức tạp và xây dựng / tìm kiếm bảng. Luôn luôn hồ sơ đầu tiên để đảm bảo bạn đang giải quyết các vấn đề quan trọng. ;)

Điều đó nói rằng, có hai cách chính để lấy mẫu phân phối xác suất tinh vi trong một cú trượt:


1. Phân phối xác suất tích lũy

một mẹo gọn gàng để lấy mẫu từ các phân phối xác suất liên tục bằng cách chỉ sử dụng một đầu vào ngẫu nhiên thống nhất duy nhất . Nó có liên quan đến phân phối tích lũy , hàm trả lời "Xác suất nhận được giá trị không lớn hơn x là bao nhiêu?"

Hàm này không giảm, bắt đầu từ 0 và tăng lên 1 trên miền của nó. Một ví dụ cho tổng của hai con xúc xắc sáu mặt được hiển thị dưới đây:

Đồ thị xác suất, phân phối tích lũy và nghịch đảo cho 2d6

Nếu hàm phân phối tích lũy của bạn có nghịch đảo tính toán thuận tiện (hoặc bạn có thể tính gần đúng nó với các hàm piecewise như đường cong Bézier), bạn có thể sử dụng hàm này để lấy mẫu từ hàm xác suất ban đầu.

Hàm nghịch đảo xử lý phân chia miền giữa 0 và 1 thành các khoảng được ánh xạ tới từng đầu ra của quá trình ngẫu nhiên ban đầu, với diện tích lưu vực của mỗi khớp với xác suất ban đầu của nó. (Đây là thông tin thực sự cho các phân phối liên tục. Đối với các phân phối rời rạc như cuộn xúc xắc, chúng ta cần áp dụng làm tròn cẩn thận)

Đây là một ví dụ về việc sử dụng điều này để mô phỏng 2d6:

int SimRoll2d6()
{
    // Get a random input in the half-open interval [0, 1).
    float t = Random.Range(0f, 1f);
    float v;

    // Piecewise inverse calculated by hand. ;)
    if(t <= 0.5f)
    {
         v = (1f + sqrt(1f + 288f * t)) * 0.5f;
    }
    else
    {
         v = (25f - sqrt(289f - 288f * t)) * 0.5f;
    }

    return floor(v + 1);
}

So sánh điều này với:

int NaiveRollNd6(int n)
{
    int sum = 0;
    for(int i = 0; i < n; i++)
       sum += Random.Range(1, 7); // I'm used to Range never returning its max
    return sum;
}

Xem những gì tôi có ý nghĩa về sự khác biệt trong độ rõ ràng và tính linh hoạt của mã? Cách ngây thơ có thể là ngây thơ với các vòng lặp của nó, nhưng nó ngắn và đơn giản, ngay lập tức rõ ràng về những gì nó làm, và dễ dàng mở rộng theo các kích cỡ và số chết khác nhau. Việc thay đổi mã phân phối tích lũy đòi hỏi một số phép toán không tầm thường, và nó sẽ dễ dàng phá vỡ và gây ra kết quả bất ngờ mà không có bất kỳ sai lầm rõ ràng nào. (Mà tôi hy vọng tôi đã không làm ở trên)

Vì vậy, trước khi bạn tránh xa một vòng lặp rõ ràng, hãy chắc chắn rằng đó thực sự là một vấn đề hiệu suất đáng để hy sinh.


2. Phương pháp bí danh

Phương pháp phân phối tích lũy hoạt động tốt khi bạn có thể biểu thị nghịch đảo của hàm phân phối tích lũy dưới dạng một biểu thức toán học đơn giản, nhưng điều đó không phải lúc nào cũng dễ dàng hoặc thậm chí có thể. Một sự thay thế đáng tin cậy cho các phân phối rời rạc là một cái gì đó gọi là Phương pháp Bí danh .

Điều này cho phép bạn lấy mẫu từ bất kỳ phân phối xác suất rời rạc tùy ý bằng cách chỉ sử dụng hai đầu vào ngẫu nhiên độc lập, phân phối đồng đều.

Nó hoạt động bằng cách lấy một phân phối như bên dưới bên trái (đừng lo lắng rằng các khu vực / trọng lượng không tổng bằng 1, đối với Phương pháp bí danh, chúng tôi quan tâm đến trọng lượng tương đối ) và chuyển đổi nó thành một bảng như trên đúng nơi:

  • Có một cột cho mỗi kết quả.
  • Mỗi cột được chia thành tối đa hai phần, mỗi phần được liên kết với một trong các kết quả ban đầu.
  • Diện tích / trọng lượng tương đối của mỗi kết quả được bảo tồn.

Ví dụ về Phương pháp bí danh chuyển đổi phân phối sang bảng tra cứu

(Sơ đồ dựa trên hình ảnh từ bài viết xuất sắc này về phương pháp lấy mẫu )

Trong mã, chúng tôi biểu thị điều này bằng hai bảng (hoặc một bảng đối tượng có hai thuộc tính) biểu thị xác suất chọn kết quả thay thế từ mỗi cột và danh tính (hoặc "bí danh") của kết quả thay thế đó. Sau đó, chúng ta có thể lấy mẫu từ phân phối như vậy:

int SampleFromTables(float[] probabiltyTable, int[] aliasTable)
{
    int column = Random.Range(0, probabilityTable.Length);
    float p = Random.Range(0f, 1f);
    if(p < probabilityTable[column])
    {
        return column;
    }
    else
    {
        return aliasTable[column];
    }
}

Điều này liên quan đến một chút thiết lập:

  1. Tính xác suất tương đối của mọi kết quả có thể xảy ra (vì vậy nếu bạn đang lăn 1000d6, chúng tôi cần tính toán số cách để có được mọi khoản tiền từ 1000 đến 6000)

  2. Xây dựng một cặp bảng với một mục cho mỗi kết quả. Phương pháp đầy đủ vượt ra ngoài phạm vi của câu trả lời này, vì vậy tôi khuyên bạn nên tham khảo phần giải thích này về thuật toán Bí danh .

  3. Lưu trữ các bảng đó và tham khảo lại chúng mỗi khi bạn cần một cuộn chết ngẫu nhiên mới từ bản phân phối này.

Đây là một sự đánh đổi không-thời gian . Bước tiền mã hóa có phần mệt mỏi và chúng ta cần đặt bộ nhớ tỷ lệ thuận với số lượng kết quả chúng ta có (mặc dù cho 1000d6, chúng ta đang nói về kilobyte một chữ số, vì vậy không có gì để mất ngủ), nhưng đổi lại lấy mẫu là thời gian không đổi cho dù phân phối của chúng tôi có thể phức tạp đến mức nào.


Tôi hy vọng một hoặc các phương pháp khác có thể được sử dụng (hoặc tôi đã thuyết phục bạn rằng sự đơn giản của phương pháp ngây thơ này đáng để bạn dành thời gian lặp lại);)


1
Câu trả lời tuyệt vời. Tôi thích cách tiếp cận ngây thơ mặc dù. Ít chỗ hơn cho các lỗi và dễ hiểu.
bummzack

FYI câu hỏi này là một bản sao-dán từ một câu hỏi ngẫu nhiên trên reddit.
Vaillancourt

Đối với tính linh hoạt, tôi nghĩ rằng đây là chủ đề reddit mà @AlexandreVaillancourt đang nói đến. Các câu trả lời ở đó chủ yếu đề nghị giữ phiên bản lặp (với một số bằng chứng cho thấy chi phí thời gian của nó có thể hợp lý) hoặc xấp xỉ số lượng lớn xúc xắc bằng cách sử dụng phân phối Gaussian bình thường.
DMGregory

+1 cho phương pháp bí danh, có vẻ như rất ít người biết về nó và nó thực sự là giải pháp lý tưởng cho rất nhiều loại tình huống lựa chọn xác suất này và +1 để đề cập đến giải pháp Gaussian, có lẽ là "tốt hơn" Trả lời nếu chúng ta chỉ quan tâm đến hiệu suất và tiết kiệm không gian.
whn

0

Thật không may, câu trả lời là phương pháp này sẽ không làm tăng hiệu suất.

Tôi tin rằng có thể có một số hiểu lầm trong câu hỏi làm thế nào một số ngẫu nhiên được tạo ra. Lấy ví dụ dưới đây [Java]:

Random r = new Random();
int n = 20;
int min = 1; //arbitrary
int max = 6; //arbitrary
for(int i = 0; i < n; i++){
    int randomNumber = (r.nextInt(max - min + 1) + min)); //silly maths
    System.out.println("Here's a random number: " + randomNumber);
}

Mã này sẽ lặp 20 lần in ra các số ngẫu nhiên trong khoảng từ 1 đến 6 (đã bao gồm). Khi chúng ta nói về hiệu suất của mã này, sẽ mất một thời gian để tạo đối tượng Ngẫu nhiên (liên quan đến việc tạo một mảng các số nguyên giả ngẫu nhiên dựa trên đồng hồ bên trong của máy tính tại thời điểm nó được tạo), và sau đó 20 thời gian không đổi tra cứu trên mỗi cuộc gọi nextInt (). Vì mỗi 'cuộn' là một hoạt động thời gian liên tục, điều này làm cho việc cuộn thời gian rất rẻ. Cũng lưu ý rằng phạm vi từ tối thiểu đến tối đa không thành vấn đề (nói cách khác, máy tính có thể cuộn d6 dễ dàng như khi nó lăn d10000). Nói về độ phức tạp thời gian, hiệu suất của giải pháp chỉ đơn giản là O (n) trong đó n là số xúc xắc.

Ngoài ra, chúng tôi có thể tính gần đúng bất kỳ số lượng cuộn d6 nào với một cuộn d100 (hoặc d10000 cho vấn đề đó). Sử dụng phương pháp này, trước tiên chúng ta cần tính s [số mặt cho xúc xắc] * n [số xúc xắc] phần trăm trước khi chúng ta lăn (về mặt kỹ thuật là s * n - n + 1 phần trăm và chúng ta có thể chia khoảng đó trong một nửa vì nó đối xứng, lưu ý rằng trong ví dụ của bạn để mô phỏng cuộn 2d6, bạn đã tính 11 phần trăm và 6 là duy nhất). Sau khi lăn, chúng ta có thể sử dụng tìm kiếm nhị phân để tìm ra phạm vi mà cuộn của chúng ta rơi vào. Về độ phức tạp thời gian, giải pháp này ước tính cho một giải pháp O (s * n), trong đó s là số cạnh và n là số xúc xắc. Như chúng ta có thể thấy, điều này chậm hơn giải pháp O (n) được đề xuất trong đoạn trước.

Ngoại suy từ đó, giả sử bạn đã tạo cả hai chương trình này để mô phỏng cuộn 1000d20. Việc đầu tiên chỉ đơn giản là cuộn 1.000 lần. Chương trình thứ hai trước tiên cần xác định 19.001 phần trăm (cho phạm vi tiềm năng từ 1.000 đến 20.000) trước khi làm bất cứ điều gì khác. Vì vậy, trừ khi bạn đang ở trên một hệ thống kỳ lạ nơi việc tra cứu bộ nhớ đắt hơn nhiều so với các thao tác dấu phẩy động, sử dụng lệnh gọi nextInt () cho mỗi cuộn có vẻ như là cách để thực hiện.


2
Các phân tích ở trên không hoàn toàn chính xác. Nếu chúng ta dành thời gian trước để tạo bảng xác suất & bí danh theo Phương pháp bí danh , thì chúng ta có thể lấy mẫu từ phân phối xác suất rời rạc tùy ý trong thời gian không đổi (2 số ngẫu nhiên và tra cứu bảng). Vì vậy, mô phỏng một cuộn 5 con xúc xắc hoặc một cuộn 500 con xúc xắc sẽ có cùng số lượng công việc, một khi các bảng được chuẩn bị. Điều này nhanh hơn bất thường so với việc lặp lại một số lượng lớn xúc xắc cho mỗi mẫu, mặc dù điều đó không nhất thiết làm cho nó trở thành một giải pháp tốt hơn cho vấn đề. ;)
DMGregory

0

Nếu bạn muốn lưu trữ các tổ hợp xúc xắc, thì tin tốt là có một giải pháp, điều tồi tệ là máy tính của chúng ta bị hạn chế phần nào liên quan đến loại vấn đề này.

Tin tốt:

Có một cách tiếp cận xác định của vấn đề này:

1 / Tính toán tất cả các kết hợp của nhóm súc sắc của bạn

2 / Xác định xác suất cho mỗi kết hợp

3 / Tìm kiếm trong danh sách này để biết kết quả thay vì ném xúc xắc

Các tin xấu:

Số lượng kết hợp với sự lặp lại được cho bởi các công thức sau đây

Γnk= =(n+k-1k)= =(n+k-1)!k! (n-1)!

( từ wikipedia tiếng Pháp ):

Kết hợp với sự lặp lại

Điều đó có nghĩa là, ví dụ, với 150 con xúc xắc, bạn có 698'526'906 kết hợp. Giả sử bạn lưu trữ xác suất dưới dạng float 32 bit, bạn sẽ cần 2,6 GB bộ nhớ và bạn vẫn phải thêm yêu cầu bộ nhớ cho các chỉ mục ...

Trong thuật ngữ điện toán, số kết hợp có thể được tính bằng các kết quả, rất tiện lợi nhưng không giải quyết được các hạn chế về bộ nhớ.

Để kết luận, đối với số lượng xúc xắc cao, tôi sẽ khuyên bạn nên ném xúc xắc và quan sát kết quả thay vì tính toán trước các xác suất liên quan đến mỗi kết hợp.

Biên tập

Tuy nhiên, vì bạn chỉ quan tâm đến tổng số súc sắc, bạn có thể lưu trữ xác suất với ít tài nguyên hơn.

Bạn có thể tính toán xác suất chính xác cho mỗi tổng xúc xắc bằng cách sử dụng tích chập.

FTôi(m)= =ΣnF1(n)FTôi-1(m-n)

Sau đó bắt đầu từ 1/6 mẫu mỗi kết quả với 1 con xúc xắc, bạn có thể xây dựng tất cả các xác suất chính xác cho bất kỳ số lượng xúc xắc nào.

Đây là một mã java thô mà tôi đã viết để minh họa (không thực sự được tối ưu hóa):

public class DiceProba {

private float[][] probas;
private int currentCalc;

public int getCurrentCalc() {
    return currentCalc;
}

public float[][] getProbas() {
    return probas;
}

public void calcProb(int faces, int diceNr) {

    if (diceNr < 0) {
        currentCalc = 0;
        return;
    }

    // Initialize
    float baseProba = 1.0f / ((float) faces);
    probas = new float[diceNr][];
    probas[0] = new float[faces + 1];
    probas[0][0] = 0.0f;
    for (int i = 1; i <= faces; ++i)
        probas[0][i] = baseProba;

    for (int i = 1; i < diceNr; ++i) {

        int maxValue = (i + 1) * faces + 1;
        probas[i] = new float[maxValue];

        for (int j = 0; j < maxValue; ++j) {

            probas[i][j] = 0;
            for (int k = 0; k <= j; ++k) {
                probas[i][j] += probability(faces, k, 0) * probability(faces, j - k, i - 1);
            }

        }

    }

    currentCalc = diceNr;

}

private float probability(int faces, int number, int diceNr) {

    if (number < 0 || number > ((diceNr + 1) * faces))
        return 0.0f;

    return probas[diceNr][number];

}

}

Gọi calcProb () với các tham số bạn muốn và sau đó truy cập vào bảng proba để biết kết quả (chỉ số đầu tiên: 0 cho 1 con xúc xắc, 1 cho hai con xúc xắc ...).

Tôi đã kiểm tra nó với 1'000D6 trên máy tính xách tay của mình, phải mất 10 giây để tính toán tất cả các xác suất từ ​​1 đến 1'000 con xúc xắc và tất cả các tổng số có thể của xúc xắc.

Với lưu trữ tiền mã hóa và hiệu quả, bạn có thể có câu trả lời nhanh chóng cho số lượng súc sắc cao.

Hy vọng nó giúp.


3
Vì OP chỉ tìm giá trị của tổng xúc xắc, nên phép toán tổ hợp này không áp dụng và số lượng mục nhập bảng xác suất tăng tuyến tính với kích thước của xúc xắc và với số lượng xúc xắc.
DMGregory

Bạn đúng rồi ! Tôi đã chỉnh sửa câu trả lời của mình. Chúng tôi luôn thông minh khi nhiều người;)
elenfoiro78

Tôi nghĩ bạn có thể cải thiện hiệu quả một chút bằng cách sử dụng phương pháp phân chia & chinh phục. Chúng ta có thể tính toán bảng xác suất cho 20d6 bằng cách kết hợp bảng cho 10d6 với chính nó. 10d6 chúng ta có thể tìm thấy bằng cách kết hợp bảng 5d6 với chính nó. 5d6 chúng ta có thể tìm thấy bằng cách kết hợp các bảng 2d6 & 3d6. Tiếp tục bằng một nửa theo cách này cho phép chúng tôi bỏ qua việc tạo hầu hết các kích thước bảng từ 1-20 và tập trung nỗ lực của chúng tôi vào những cái thú vị.
DMGregory

1
Và sử dụng đối xứng!
elenfoiro78
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.