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 for
con 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
Có 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:
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.
(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:
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)
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 .
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);)