ดังที่ฉันได้กล่าวถึงในความคิดเห็นของฉันข้างต้นฉันแนะนำให้คุณทำโพรไฟล์นี้ก่อนที่จะเข้าใจรหัสของคุณมากเกินไป การรวมอย่างรวดเร็วของfor
ลูกเต๋าจะทำให้เข้าใจและแก้ไขได้ง่ายกว่าสูตรทางคณิตศาสตร์ที่ซับซ้อนและการสร้างตาราง / การค้นหา ทำรายละเอียดก่อนเสมอเพื่อให้แน่ใจว่าคุณกำลังแก้ปัญหาสำคัญ ;)
ที่กล่าวว่ามีสองวิธีหลักในการสุ่มตัวอย่างการแจกแจงความน่าจะเป็นที่ซับซ้อนในบัดดล:
1. การแจกแจงความน่าจะเป็นแบบสะสม
มีเคล็ดลับเรียบร้อยในการสุ่มตัวอย่างจากการแจกแจงความน่าจะเป็นอย่างต่อเนื่องโดยใช้อินพุตแบบสุ่มชุดเดียวเท่านั้น มันเกี่ยวกับการแจกแจงสะสมฟังก์ชันที่ตอบว่า "ความน่าจะเป็นที่จะได้ค่าไม่เกิน x คืออะไร"
ฟังก์ชันนี้ไม่ลดลงเริ่มต้นที่ 0 และเพิ่มขึ้นเป็น 1 เหนือโดเมน ตัวอย่างสำหรับผลรวมของลูกเต๋าสองด้านหกตัวแสดงอยู่ด้านล่าง:
หากฟังก์ชันการแจกแจงสะสมของคุณมีค่าผกผันที่สะดวกในการคำนวณ (หรือคุณสามารถประมาณค่าได้ด้วยฟังก์ชันที่เป็นชิ้น ๆ เช่นเส้นโค้งเบซิเยร์) คุณสามารถใช้ฟังก์ชันนี้เพื่อสุ่มตัวอย่างจากฟังก์ชันความน่าจะเป็นแบบดั้งเดิม
ฟังก์ชันผกผันจัดการการแยกโดเมนระหว่าง 0 และ 1 เข้ากับช่วงเวลาที่แมปกับแต่ละเอาต์พุตของกระบวนการสุ่มดั้งเดิมโดยมีพื้นที่เก็บกักของแต่ละการจับคู่ความน่าจะเป็นดั้งเดิม (นี่คือความจริงที่ไม่สิ้นสุดสำหรับการแจกแจงแบบต่อเนื่องสำหรับการแจกแจงแบบแยกเช่นม้วนลูกเต๋าเราจำเป็นต้องใช้การปัดเศษอย่างระมัดระวัง)
นี่คือตัวอย่างของการใช้สิ่งนี้เพื่อเลียนแบบ 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);
}
เปรียบเทียบสิ่งนี้กับ:
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;
}
ดูว่าฉันหมายถึงอะไรเกี่ยวกับความแตกต่างของความชัดเจนและความยืดหยุ่นของโค้ด? วิธีที่ไร้เดียงสาอาจไร้เดียงสากับลูป แต่สั้นและง่ายชัดเจนทันทีเกี่ยวกับสิ่งที่มันทำและง่ายต่อการปรับขนาดตามขนาดและตัวเลขตาย การเปลี่ยนแปลงรหัสการแจกจ่ายแบบสะสมต้องใช้คณิตศาสตร์ที่ไม่สำคัญและจะง่ายต่อการแตกและทำให้เกิดผลลัพธ์ที่ไม่คาดคิดโดยไม่มีข้อผิดพลาดที่ชัดเจน (ซึ่งฉันหวังว่าฉันไม่ได้ทำด้านบน)
ดังนั้นก่อนที่คุณจะออกไปด้วยการวนลูปที่ชัดเจนตรวจสอบให้แน่ใจว่าเป็นปัญหาด้านประสิทธิภาพที่คุ้มค่ากับการเสียสละแบบนี้
2. วิธีนามแฝง
วิธีการแจกแจงสะสมทำงานได้ดีเมื่อคุณสามารถแสดงค่าผกผันของฟังก์ชันการแจกแจงสะสมเป็นนิพจน์ทางคณิตศาสตร์อย่างง่าย แต่นั่นอาจไม่ใช่เรื่องง่ายหรือเป็นไปได้เสมอไป ทางเลือกที่เชื่อถือได้สำหรับการกระจายแบบไม่ต่อเนื่องเป็นสิ่งที่เรียกว่าวิธีนามแฝงวิธีนามแฝง
สิ่งนี้ช่วยให้คุณสามารถสุ่มตัวอย่างจากการแจกแจงความน่าจะเป็นแบบไม่ต่อเนื่องใด ๆ โดยใช้อินพุตสุ่มแบบอิสระที่กระจายกันสองชุด
มันทำงานได้โดยการแจกแจงแบบเดียวกับด้านล่างทางซ้าย (ไม่ต้องกังวลว่าพื้นที่ / น้ำหนักไม่ได้รวมเป็น 1 สำหรับวิธี Alias ที่เราสนใจเกี่ยวกับน้ำหนักสัมพัทธ์ ) และแปลงเป็นตารางเช่นเดียวกับ ด้านขวาที่:
- มีหนึ่งคอลัมน์สำหรับแต่ละผลลัพธ์
- แต่ละคอลัมน์จะแบ่งออกเป็นสองส่วนโดยมากแต่ละคอลัมน์จะเชื่อมโยงกับผลลัพธ์ต้นฉบับหนึ่งรายการ
- พื้นที่ / น้ำหนักสัมพัทธ์ของผลลัพธ์แต่ละรายการจะถูกรักษาไว้
(แผนภาพตามภาพจากบทความที่ยอดเยี่ยมเกี่ยวกับวิธีการสุ่มตัวอย่าง )
ในรหัสเราแสดงสิ่งนี้ด้วยสองตาราง (หรือตารางของวัตถุที่มีสองคุณสมบัติ) แสดงถึงความน่าจะเป็นในการเลือกผลลัพธ์ทางเลือกจากแต่ละคอลัมน์และตัวตน (หรือ "นามแฝง") ของผลลัพธ์ทางเลือกนั้น จากนั้นเราสามารถสุ่มตัวอย่างจากการแจกแจงดังนี้
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];
}
}
สิ่งนี้เกี่ยวข้องกับการตั้งค่าเล็กน้อย:
คำนวณความน่าจะเป็นสัมพัทธ์ของผลลัพธ์ที่เป็นไปได้ทุกอย่าง (ดังนั้นหากคุณหมุน 1,000d6 เราจำเป็นต้องคำนวณจำนวนวิธีที่จะได้รับผลรวมทุก ๆ 1,000 ถึง 6,000)
สร้างคู่ของตารางที่มีรายการสำหรับแต่ละผลลัพธ์ วิธีการเต็มไปเกินขอบเขตของคำตอบนี้ดังนั้นฉันขอแนะนำให้อ้างอิงถึงคำอธิบายของอัลกอริทึมวิธีนามแฝงนี้
จัดเก็บตารางเหล่านั้นและอ้างอิงกลับไปที่พวกเขาทุกครั้งที่คุณต้องการม้วนมวนแบบสุ่มใหม่จากการกระจายนี้
นี่คือข้อดีของพื้นที่เวลา ขั้นตอนการคำนวณล่วงหน้าค่อนข้างละเอียดถี่ถ้วนและเราจำเป็นต้องจัดสรรหน่วยความจำให้สอดคล้องกับจำนวนผลลัพธ์ที่เรามี (แม้ว่าจะถึง 1,000d6 เรากำลังพูดถึงตัวเลขกิโลไบต์เดียวดังนั้นไม่มีอะไรเสียเวลานอน) เป็นเวลาคงที่ไม่ว่าการกระจายของเราจะซับซ้อนแค่ไหน
ฉันหวังว่าวิธีการหนึ่งอย่างใดอย่างหนึ่งอาจเป็นประโยชน์บางอย่าง (หรือฉันเชื่อว่าคุณว่าความเรียบง่ายของวิธีการไร้เดียงสานั้นคุ้มค่ากับเวลาที่ใช้ในการวนซ้ำ);)