ฉันได้เห็นคำถามนี้ถามมาก แต่ไม่เคยเห็นคำตอบที่เป็นรูปธรรมจริง ดังนั้นฉันจะโพสต์หนึ่งที่นี่ซึ่งหวังว่าจะช่วยให้ผู้คนเข้าใจว่าทำไมมี "modulo bias" เมื่อใช้ตัวสร้างตัวเลขสุ่มเช่นrand()
ใน C ++
ฉันได้เห็นคำถามนี้ถามมาก แต่ไม่เคยเห็นคำตอบที่เป็นรูปธรรมจริง ดังนั้นฉันจะโพสต์หนึ่งที่นี่ซึ่งหวังว่าจะช่วยให้ผู้คนเข้าใจว่าทำไมมี "modulo bias" เมื่อใช้ตัวสร้างตัวเลขสุ่มเช่นrand()
ใน C ++
คำตอบ:
ดังนั้นrand()
เป็นตัวสร้างตัวเลขสุ่มหลอกซึ่งเลือกจำนวนธรรมชาติระหว่าง 0 และRAND_MAX
ซึ่งเป็นค่าคงที่ที่กำหนดไว้ในcstdlib
(ดูบทความนี้สำหรับภาพรวมทั่วไปบนrand()
)
ตอนนี้จะเกิดอะไรขึ้นถ้าคุณต้องการสร้างตัวเลขสุ่มระหว่างพูด 0 ถึง 2 เพื่อประโยชน์ในการอธิบายให้พูดของRAND_MAX
10 และฉันตัดสินใจที่จะสร้างตัวเลขสุ่มระหว่าง 0 และ 2 rand()%3
โดยการเรียก อย่างไรก็ตามrand()%3
ไม่ได้สร้างตัวเลขระหว่าง 0 ถึง 2 ด้วยความน่าจะเป็นที่เท่ากัน!
เมื่อrand()
ผลตอบแทน 0, 3, 6, rand()%3 == 0
9, ดังนั้น P (0) = 4/11
เมื่อrand()
ผลตอบแทนที่ 1, 4, 7, rand()%3 == 1
10, ดังนั้น P (1) = 4/11
เมื่อrand()
ผลตอบแทนที่ 2, 5, rand()%3 == 2
8, ดังนั้น P (2) = 3/11
สิ่งนี้จะไม่สร้างตัวเลขระหว่าง 0 ถึง 2 ที่มีความน่าจะเป็นเท่ากัน แน่นอนว่าสำหรับช่วงขนาดเล็กนี่อาจไม่ใช่ปัญหาที่ใหญ่ที่สุด แต่สำหรับช่วงที่มีขนาดใหญ่กว่านี้อาจทำให้การกระจายกระจายไป
ดังนั้นเมื่อใดที่rand()%n
ส่งกลับช่วงของตัวเลขจาก 0 ถึง n-1 ที่มีความน่าจะเป็นที่เท่ากัน? RAND_MAX%n == n - 1
เมื่อ ในกรณีนี้พร้อมกับข้อสันนิษฐานก่อนหน้าของเราrand()
จะส่งคืนตัวเลขระหว่าง 0 และRAND_MAX
ด้วยความน่าจะเป็นที่เท่ากันคลาสโมดูโลของ n จะได้รับการแจกแจงเท่ากัน
แล้วเราจะแก้ปัญหานี้อย่างไร วิธีที่หยาบคือการสร้างตัวเลขสุ่มจนกว่าคุณจะได้ตัวเลขในช่วงที่คุณต้องการ:
int x;
do {
x = rand();
} while (x >= n);
แต่นั่นไม่มีประสิทธิภาพสำหรับค่าที่ต่ำn
เนื่องจากคุณมีn/RAND_MAX
โอกาสที่จะได้รับค่าในช่วงของคุณเท่านั้นดังนั้นคุณจะต้องทำการRAND_MAX/n
โทรrand()
โดยเฉลี่ย
วิธีการสูตรที่มีประสิทธิภาพมากขึ้นคือการใช้ช่วงขนาดใหญ่ที่มีความยาวหารด้วยn
เช่นRAND_MAX - RAND_MAX % n
สร้างตัวเลขสุ่มจนกว่าคุณจะได้รับหนึ่งที่อยู่ในช่วงและจากนั้นใช้โมดูลัส:
int x;
do {
x = rand();
} while (x >= (RAND_MAX - RAND_MAX % n));
x %= n;
สำหรับค่าขนาดเล็กn
สิ่งนี้จะไม่ค่อยต้องการการโทรมากกว่าหนึ่งrand()
ครั้ง
ทำงานอ้างถึงและอ่านเพิ่มเติม:
การเลือกแบบสุ่มเป็นวิธีที่ดีในการลบอคติ
ปรับปรุง
เราสามารถทำให้โค้ดได้อย่างรวดเร็วถ้าเราค้นหาที่อยู่ในช่วง x n
หารด้วย
// Assumptions
// rand() in [0, RAND_MAX]
// n in (0, RAND_MAX]
int x;
// Keep searching for an x in a range divisible by n
do {
x = rand();
} while (x >= RAND_MAX - (RAND_MAX % n))
x %= n;
การวนรอบด้านบนควรเร็วมากพูด 1 ซ้ำโดยเฉลี่ย
rand()
สามารถส่งคืนไม่ใช่จำนวนn
มากดังนั้นสิ่งที่คุณทำคุณจะได้รับ 'โมดูโล่อคติ' อย่างหลีกเลี่ยงไม่ได้เว้นแต่คุณจะละทิ้งค่าเหล่านั้น user1413793 อธิบายว่าเป็นอย่างดี (แม้ว่าวิธีการแก้ปัญหาที่เสนอในคำตอบนั้นช่างยากจริงๆ)
RAND_MAX+1 - (RAND_MAX+1) % n
ทำงานได้อย่างถูกต้อง แต่ฉันก็ยังคิดว่าควรเขียนRAND_MAX+1 - ((RAND_MAX+1) % n)
เพื่อความชัดเจน
RAND_MAX == INT_MAX
ดูความคิดเห็นที่สองของฉันต่อ @ user1413793 ด้านบน
@ user1413793 ถูกต้องเกี่ยวกับปัญหา ฉันจะไม่พูดคุยเรื่องนี้ต่อไปนอกจากจะทำให้ประเด็นหนึ่ง: ใช่สำหรับค่าขนาดเล็กn
และค่าขนาดใหญ่ของRAND_MAX
อคติโมดูโลอาจมีขนาดเล็กมาก แต่การใช้รูปแบบที่ทำให้เกิดอคติหมายความว่าคุณต้องพิจารณาความเอนเอียงทุกครั้งที่คุณคำนวณตัวเลขสุ่มและเลือกรูปแบบที่แตกต่างกันสำหรับกรณีที่แตกต่างกัน และถ้าคุณทำการเลือกผิดข้อบกพร่องที่มันนำเสนอนั้นบอบบางและแทบจะเป็นไปไม่ได้เลยที่จะทำการทดสอบหน่วย เมื่อเทียบกับการใช้เครื่องมือที่เหมาะสม (เช่นarc4random_uniform
) นั่นเป็นงานพิเศษไม่ทำงานน้อยลง การทำงานมากขึ้นและการแก้ปัญหาที่แย่กว่านั้นคือวิศวกรรมที่แย่มากโดยเฉพาะอย่างยิ่งเมื่อทำในสิ่งที่ถูกต้องทุกครั้งจะง่ายบนแพลตฟอร์มส่วนใหญ่
น่าเสียดายที่การใช้งานโซลูชันไม่ถูกต้องหรือมีประสิทธิภาพน้อยกว่าที่ควรจะเป็น (แต่ละวิธีมีความคิดเห็นที่อธิบายถึงปัญหาต่าง ๆ แต่ไม่มีการแก้ไขวิธีใดที่จะแก้ไขปัญหาเหล่านี้) มีแนวโน้มที่จะสร้างความสับสนให้กับผู้ตอบคำถามแบบไม่เป็นทางการ
อีกครั้งทางออกที่ดีที่สุดคือการใช้งานarc4random_uniform
บนแพลตฟอร์มที่ให้บริการหรือโซลูชันที่มีระยะคล้ายคลึงกันสำหรับแพลตฟอร์มของคุณ (เช่นRandom.nextInt
บน Java) มันจะทำสิ่งที่ถูกต้องโดยไม่มีค่าใช้จ่ายสำหรับคุณ นี่เป็นการโทรที่ถูกต้องเกือบทุกครั้ง
หากคุณไม่มีarc4random_uniform
คุณสามารถใช้พลังของ opensource เพื่อดูว่ามีการใช้งานอย่างไรบน RNG ar4random
ในวงกว้าง ( ในกรณีนี้ แต่วิธีการที่คล้ายกันอาจใช้งานได้กับRNG อื่น ๆ )
นี่คือการใช้งาน OpenBSD :
/*
* Calculate a uniformly distributed random number less than upper_bound
* avoiding "modulo bias".
*
* Uniformity is achieved by generating new random numbers until the one
* returned is outside the range [0, 2**32 % upper_bound). This
* guarantees the selected random number will be inside
* [2**32 % upper_bound, 2**32) which maps back to [0, upper_bound)
* after reduction modulo upper_bound.
*/
u_int32_t
arc4random_uniform(u_int32_t upper_bound)
{
u_int32_t r, min;
if (upper_bound < 2)
return 0;
/* 2**32 % x == (2**32 - x) % x */
min = -upper_bound % upper_bound;
/*
* This could theoretically loop forever but each retry has
* p > 0.5 (worst case, usually far better) of selecting a
* number inside the range we need, so it should rarely need
* to re-roll.
*/
for (;;) {
r = arc4random();
if (r >= min)
break;
}
return r % upper_bound;
}
เป็นสิ่งที่ควรสังเกตเมื่อคอมเม้นต์นี้มีไว้สำหรับผู้ที่ต้องการใช้สิ่งที่คล้ายกัน:
เปลี่ยน arc4random_uniform () เพื่อคำนวณเป็น
2**32 % upper_bound
-upper_bound % upper_bound
ลดความซับซ้อนของรหัสและทำให้มันเหมือนกันทั้งในสถาปัตยกรรม ILP32 และ LP64 และยังเร็วขึ้นเล็กน้อยในสถาปัตยกรรม LP64 โดยใช้ส่วนที่เหลือแบบ 32 บิตแทนที่จะเป็นส่วนที่เหลือ 64 บิตชี้ให้เห็นโดย Jorden Verwer บนเทคโนโลยี @ ok deraadt; ไม่มีการคัดค้านจาก djm หรือ otto
การใช้งานจาวาสามารถค้นหาได้ง่าย (ดูลิงค์ก่อนหน้า):
public int nextInt(int n) {
if (n <= 0)
throw new IllegalArgumentException("n must be positive");
if ((n & -n) == n) // i.e., n is a power of 2
return (int)((n * (long)next(31)) >> 31);
int bits, val;
do {
bits = next(31);
val = bits % n;
} while (bits - val + (n-1) < 0);
return val;
}
arcfour_random()
ใช้อัลกอริทึม RC4 จริงในการปรับใช้เอาต์พุตจะมีอคติอย่างแน่นอน หวังว่าผู้เขียนห้องสมุดของคุณจะเปลี่ยนไปใช้ CSPRNG ที่ดีกว่าในอินเทอร์เฟซเดียวกัน ฉันจำได้ว่าหนึ่งใน BSDs ตอนนี้ใช้อัลกอริทึม ChaCha20 เพื่อนำไปใช้arcfour_random()
จริง ข้อมูลเพิ่มเติมเกี่ยวกับอคติ RC4 ซึ่งทำให้ไร้ประโยชน์สำหรับความปลอดภัยหรือแอปพลิเคชั่นที่สำคัญอื่น ๆ เช่นวิดีโอโปกเกอร์
/dev/random
เคยใช้ RC4 ในบางแพลตฟอร์มในอดีต (Linux ใช้ SHA-1 ในโหมดตัวนับ) น่าเสียดายที่ man page ที่ฉันพบผ่านการค้นหาระบุว่า RC4 ยังคงใช้งานได้บนแพลตฟอร์มต่าง ๆ ที่มีให้arc4random
(แม้ว่ารหัสจริงอาจแตกต่างกัน)
-upper_bound % upper_bound == 0
เหรอ
-upper_bound % upper_bound
จะเป็น 0 จริงถ้าint
กว้างกว่า 32 บิต มันควรจะเป็น(u_int32_t)-upper_bound % upper_bound)
(สมมติว่าu_int32_t
เป็น BSD-ism สำหรับuint32_t
)
Modulo Biasคืออคติโดยธรรมชาติในการใช้ modulo arithmetic เพื่อลดชุดเอาต์พุตเป็นชุดย่อยของชุดอินพุต โดยทั่วไปแล้วอคตินั้นจะเกิดขึ้นทุกครั้งที่การแมประหว่างชุดอินพุตและเอาต์พุตไม่ได้กระจายเท่า ๆ กันเช่นในกรณีของการใช้เลขคณิตโมดูโลเมื่อขนาดของชุดเอาต์พุตไม่ได้เป็นตัวหารของขนาดของชุดอินพุต
อคตินี้ยากที่จะหลีกเลี่ยงในการคำนวณโดยที่ตัวเลขจะแสดงเป็นสตริงของบิต: 0s และ 1s การหาแหล่งที่มาของการสุ่มอย่างแท้จริงนั้นยากมากเช่นกัน แต่อยู่นอกเหนือขอบเขตของการสนทนานี้ สำหรับส่วนที่เหลือของคำตอบนี้สมมติว่ามีแหล่งสุ่มบิตอย่างไม่ จำกัด
ลองพิจารณาจำลองการหมุนตาย (0 ถึง 5) โดยใช้บิตสุ่มเหล่านี้ มีความเป็นไปได้ 6 อย่างดังนั้นเราต้องการบิตมากพอที่จะเป็นตัวแทนของหมายเลข 6 ซึ่งก็คือ 3 บิต น่าเสียดายที่ 3 บิตสุ่มมี 8 ผลลัพธ์ที่เป็นไปได้:
000 = 0, 001 = 1, 010 = 2, 011 = 3
100 = 4, 101 = 5, 110 = 6, 111 = 7
เราสามารถลดขนาดของผลลัพธ์ที่ตั้งค่าเป็น 6 อย่างแน่นอนโดยการใช้ค่าโมดูโล 6 แต่สิ่งนี้นำเสนอปัญหาแบบโมดูโลอคติ : 110
ให้ผลเป็น 0 และ111
ให้ผลเป็น 1 ตายนี้ถูกโหลด
แทนที่จะใช้บิตสุ่มในทางทฤษฎีเราสามารถจ้างกองทัพขนาดเล็กหมุนลูกเต๋าได้ตลอดวันและบันทึกผลลัพธ์ลงในฐานข้อมูลแล้วใช้ผลลัพธ์แต่ละรายการเพียงครั้งเดียว เรื่องนี้เกี่ยวกับการปฏิบัติเท่าที่ฟังและมากกว่าจะไม่ให้ผลลัพธ์ที่สุ่มอย่างแท้จริงอยู่แล้ว (ปุนตั้งใจ)
แทนที่จะใช้โมดูลัสโซลูชันที่ไร้เดียงสา แต่ถูกต้องทางคณิตศาสตร์คือการทิ้งผลลัพธ์ที่ให้ผล110
และ111
ลองอีกครั้งด้วย 3 บิตใหม่ แต่น่าเสียดายที่นี้หมายถึงว่ามีโอกาส 25% ในแต่ละม้วนที่ re-ม้วนจะต้องรวมถึงแต่ละม้วนอีกครั้งตัวเอง สิ่งนี้ทำไม่ได้อย่างเห็นได้ชัดสำหรับทุกคน แต่มีประโยชน์น้อยที่สุด
ใช้บิตเพิ่มเติม: แทน 3 บิตใช้ 4 ผลนี้ให้ผลลัพธ์ที่เป็นไปได้ 16 ข้อ แน่นอนว่าการกลิ้งซ้ำเมื่อใดก็ตามที่ผลลัพธ์มากกว่า 5 ทำให้สิ่งเลวร้ายลง (10/16 = 62.5%) เพื่อที่จะไม่ช่วยคนเดียว
ขอให้สังเกตว่า 2 * 6 = 12 <16 ดังนั้นเราสามารถรับผลได้อย่างปลอดภัยน้อยกว่า 12 และลดโมดูโล 6 นั้นเพื่อกระจายผลลัพธ์อย่างสม่ำเสมอ ผลลัพธ์อื่น ๆ อีก 4 รายการจะต้องถูกยกเลิกและนำไปใช้ใหม่ในแนวทางก่อนหน้านี้
ฟังดูดีในตอนแรก แต่มาตรวจสอบคณิตศาสตร์กันดีกว่า:
4 discarded results / 16 possibilities = 25%
ในกรณีนี้1 บิตพิเศษไม่ได้ช่วยอะไรเลย!
ผลลัพธ์นั้นโชคร้าย แต่ลองอีกครั้งด้วย 5 บิต:
32 % 6 = 2 discarded results; and
2 discarded results / 32 possibilities = 6.25%
การปรับปรุงที่ชัดเจน แต่ไม่ดีพอในหลายกรณี ข่าวดีก็คือการเพิ่มบิตมากขึ้นจะไม่เพิ่มโอกาสของการต้องทิ้งและอีกม้วน สิ่งนี้ไม่ได้มีไว้สำหรับลูกเต๋าเท่านั้น แต่ในทุกกรณี
ตามที่แสดงไว้อย่างไรก็ตามการเพิ่ม 1 บิตพิเศษอาจไม่เปลี่ยนแปลงอะไรเลย ในความเป็นจริงถ้าเราเพิ่มการหมุนของเราเป็น 6 บิตความน่าจะเป็นยังคงอยู่ที่ 6.25%
คำถามนี้มี 2 คำถามเพิ่มเติม:
โชคดีที่คำตอบสำหรับคำถามแรกคือใช่ ปัญหาเกี่ยวกับ 6 คือ 2 ^ x mod 6 พลิกระหว่าง 2 และ 4 ซึ่งบังเอิญเป็นทวีคูณของ 2 จากกันและกันดังนั้นสำหรับ x คู่> 1
[2^x mod 6] / 2^x == [2^(x+1) mod 6] / 2^(x+1)
ดังนั้น 6 เป็นข้อยกเว้นมากกว่ากฎ มีความเป็นไปได้ที่จะพบโมดูลัสที่ใหญ่กว่าซึ่งให้พลังต่อเนื่องเป็น 2 ในลักษณะเดียวกัน แต่ในที่สุดสิ่งนี้จะต้องล้อมรอบและความน่าจะเป็นของการละทิ้งจะลดลง
โดยไม่ต้องเสนอการพิสูจน์เพิ่มเติมโดยทั่วไปการใช้จำนวนบิตที่ต้องการเป็นสองเท่าจะทำให้มีโอกาสน้อยลงในการทิ้ง
นี่คือตัวอย่างโปรแกรมที่ใช้ libcrypo ของ OpenSSL ในการจัดหาไบต์แบบสุ่ม เมื่อทำการรวบรวมให้แน่ใจว่าได้ลิงค์ไปยังไลบรารี-lcrypto
ที่ทุกคนส่วนใหญ่ควรมีอยู่
#include <iostream>
#include <assert.h>
#include <limits>
#include <openssl/rand.h>
volatile uint32_t dummy;
uint64_t discardCount;
uint32_t uniformRandomUint32(uint32_t upperBound)
{
assert(RAND_status() == 1);
uint64_t discard = (std::numeric_limits<uint64_t>::max() - upperBound) % upperBound;
uint64_t randomPool = RAND_bytes((uint8_t*)(&randomPool), sizeof(randomPool));
while(randomPool > (std::numeric_limits<uint64_t>::max() - discard)) {
RAND_bytes((uint8_t*)(&randomPool), sizeof(randomPool));
++discardCount;
}
return randomPool % upperBound;
}
int main() {
discardCount = 0;
const uint32_t MODULUS = (1ul << 31)-1;
const uint32_t ROLLS = 10000000;
for(uint32_t i = 0; i < ROLLS; ++i) {
dummy = uniformRandomUint32(MODULUS);
}
std::cout << "Discard count = " << discardCount << std::endl;
}
ฉันขอแนะนำให้เล่นกับMODULUS
และROLLS
ค่านิยมเพื่อดูว่ามีม้วนซ้ำเกิดขึ้นจริงภายใต้เงื่อนไขส่วนใหญ่อย่างไร บุคคลที่สงสัยอาจต้องการบันทึกค่าที่คำนวณลงในไฟล์และตรวจสอบว่าการแจกแจงปรากฏเป็นปกติ
randomPool = RAND_bytes(...)
สายมักจะส่งผลให้เกิดrandomPool == 1
เนื่องจากการยืนยัน สิ่งนี้จะส่งผลให้เกิดการละทิ้งและการหมุนซ้ำ ฉันคิดว่าคุณต้องการประกาศแยกบรรทัด ดังนั้นสิ่งนี้ทำให้ RNG กลับมาพร้อมกับ1
การวนซ้ำทุกครั้ง
randomPool
เราจะประเมิน1
ตามเอกสารRAND_bytes()
OpenSSL เสมอเพราะจะประสบความสำเร็จเสมอเนื่องจากการRAND_status()
ยืนยัน
มีสองเรื่องร้องเรียนปกติด้วยการใช้โมดูโล
หนึ่งที่ถูกต้องสำหรับเครื่องกำเนิดไฟฟ้าทั้งหมด มันง่ายกว่าที่จะเห็นในกรณีขีด จำกัด หากเครื่องกำเนิดของคุณมี RAND_MAX ซึ่งเป็น 2 (ที่ไม่สอดคล้องกับมาตรฐาน C) และคุณต้องการเพียง 0 หรือ 1 เป็นค่าการใช้ modulo จะสร้าง 0 สองครั้งบ่อยครั้ง (เมื่อเครื่องกำเนิดสร้าง 0 และ 2) ตามที่มันจะ สร้าง 1 (เมื่อเครื่องกำเนิดสร้าง 1) โปรดทราบว่านี่เป็นความจริงทันทีที่คุณไม่ปล่อยค่าใด ๆ ที่การแมปที่คุณใช้จากค่าตัวกำเนิดไปยังค่าที่ต้องการหนึ่งจะเกิดขึ้นสองครั้งบ่อยเท่าที่อื่น ๆ
เครื่องกำเนิดไฟฟ้าบางชนิดมีบิตที่มีนัยสำคัญน้อยกว่าซึ่งสุ่มน้อยกว่าเครื่องอื่นอย่างน้อยสำหรับพารามิเตอร์บางตัว แต่น่าเสียดายที่พารามิเตอร์เหล่านั้นมีคุณลักษณะที่น่าสนใจอื่น ๆ (เช่นมีความสามารถในการมี RAND_MAX น้อยกว่าพลัง 2) ปัญหานี้เป็นที่รู้จักกันดีและการใช้งานห้องสมุดเป็นเวลานานอาจหลีกเลี่ยงปัญหาได้ (ตัวอย่างเช่น rand ตัวอย่าง () การใช้งานในมาตรฐาน C ใช้ตัวกำเนิดชนิดนี้ แต่ลดบิตที่สำคัญน้อยกว่า 16 บิต) แต่บางคนก็ชอบบ่น นั่นและคุณอาจโชคร้าย
ใช้สิ่งที่ชอบ
int alea(int n){
assert (0 < n && n <= RAND_MAX);
int partSize =
n == RAND_MAX ? 1 : 1 + (RAND_MAX-n)/(n+1);
int maxUsefull = partSize * n + (partSize-1);
int draw;
do {
draw = rand();
} while (draw > maxUsefull);
return draw/partSize;
}
เพื่อสร้างตัวเลขสุ่มระหว่าง 0 ถึง n จะหลีกเลี่ยงปัญหาทั้งสอง (และหลีกเลี่ยงการล้นด้วย RAND_MAX == INT_MAX)
BTW, C ++ 11 แนะนำวิธีมาตรฐานในการลดและตัวกำเนิดอื่นที่ไม่ใช่แรนด์ ()
โซลูชันของ Mark (โซลูชันที่ยอมรับ) นั้นสมบูรณ์แบบเกือบ
int x; do { x = rand(); } while (x >= (RAND_MAX - RAND_MAX % n)); x %= n;
แก้ไข Mar 25 '16 เวลา 23:16
Mark Amery 39k21170211
อย่างไรก็ตามมันมีข้อแม้ที่แยกผลลัพธ์ที่ถูกต้อง 1 ชุดในสถานการณ์ใด ๆ ที่RAND_MAX
( RM
) เป็น 1 น้อยกว่าผลคูณของN
(ที่ไหนN
= จำนวนผลลัพธ์ที่ใช้ได้ที่เป็นไปได้)
กล่าวคือเมื่อ 'จำนวนค่าที่ละทิ้ง' ( D
) เท่ากับN
แล้วพวกเขาจะเป็นชุดที่ถูกต้อง ( V)
ไม่ใช่ชุดที่ไม่ถูกต้องI
)
สิ่งที่ทำให้เกิดนี้อยู่ในบางจุดมาร์คสูญเสียการมองเห็นความแตกต่างระหว่างและN
Rand_Max
N
เป็นชุดที่สมาชิกที่ถูกต้องของจะประกอบด้วยเฉพาะจำนวนเต็มบวกเนื่องจากประกอบด้วยจำนวนการตอบสนองที่จะถูกต้อง (เช่น: Set N
= {1, 2, 3, ... n }
)
Rand_max
อย่างไรก็ตามเป็นชุดที่ (ตามที่กำหนดไว้สำหรับวัตถุประสงค์ของเรา) รวมถึงจำนวนเต็มที่ไม่เป็นลบ
ในรูปแบบทั่วไปที่สุดสิ่งที่กำหนดไว้ที่นี่เป็นRand Max
ชุดของผลลัพธ์ที่ถูกต้องทั้งหมดซึ่งในทางทฤษฎีอาจรวมถึงจำนวนลบหรือค่าที่ไม่ใช่ตัวเลข
ดังนั้นจึงRand_Max
ถูกกำหนดให้ดีขึ้นเป็นชุดของ "การตอบสนองที่เป็นไปได้"
อย่างไรก็ตามN
ดำเนินการกับการนับจำนวนของค่าภายในชุดของการตอบสนองที่ถูกต้องดังนั้นแม้ตามที่กำหนดไว้ในกรณีเฉพาะของเราRand_Max
จะเป็นค่าหนึ่งน้อยกว่าจำนวนทั้งหมดที่มี
การใช้โซลูชันของ Mark จะถูกยกเลิกค่าเมื่อ: X => RM - RM% N
EG:
Ran Max Value (RM) = 255
Valid Outcome (N) = 4
When X => 252, Discarded values for X are: 252, 253, 254, 255
So, if Random Value Selected (X) = {252, 253, 254, 255}
Number of discarded Values (I) = RM % N + 1 == N
IE:
I = RM % N + 1
I = 255 % 4 + 1
I = 3 + 1
I = 4
X => ( RM - RM % N )
255 => (255 - 255 % 4)
255 => (255 - 3)
255 => (252)
Discard Returns $True
ดังที่คุณเห็นในตัวอย่างด้านบนเมื่อค่าของ X (ตัวเลขสุ่มที่เราได้รับจากฟังก์ชั่นเริ่มต้น) คือ 252, 253, 254 หรือ 255 เราจะทิ้งมันแม้ว่าค่าสี่ค่านี้จะประกอบด้วยชุดค่าส่งคืนที่ถูกต้อง .
IE: เมื่อการนับจำนวนของค่าที่ถูกทิ้ง (I) = N (จำนวนผลลัพธ์ที่ถูกต้อง) ดังนั้นชุดของค่าที่ส่งคืนที่ถูกต้องจะถูกยกเลิกโดยฟังก์ชั่นดั้งเดิม
หากเราอธิบายความแตกต่างระหว่างค่า N และ RM เป็น D คือ:
D = (RM - N)
จากนั้นเมื่อค่า D น้อยลงเปอร์เซ็นต์ของการม้วนซ้ำที่ไม่จำเป็นเนื่องจากวิธีนี้เพิ่มขึ้นในการคูณแบบธรรมชาติแต่ละครั้ง (เมื่อ RAND_MAX ไม่เท่ากับจำนวนเฉพาะนี่เป็นข้อกังวลที่ถูกต้อง)
เช่น:
RM=255 , N=2 Then: D = 253, Lost percentage = 0.78125%
RM=255 , N=4 Then: D = 251, Lost percentage = 1.5625%
RM=255 , N=8 Then: D = 247, Lost percentage = 3.125%
RM=255 , N=16 Then: D = 239, Lost percentage = 6.25%
RM=255 , N=32 Then: D = 223, Lost percentage = 12.5%
RM=255 , N=64 Then: D = 191, Lost percentage = 25%
RM=255 , N= 128 Then D = 127, Lost percentage = 50%
เนื่องจากเปอร์เซ็นต์ของ Rerolls จำเป็นต้องเพิ่มจำนวน N ที่ใกล้เคียงกับ RM มากขึ้นสิ่งนี้อาจเป็นข้อกังวลที่ถูกต้องในหลาย ๆ ค่าขึ้นอยู่กับข้อ จำกัด ของระบบที่รันโค้ดและค่าที่ค้นหา
เพื่อลบล้างสิ่งนี้เราสามารถทำการแก้ไขง่าย ๆ ดังที่แสดงไว้ที่นี่:
int x;
do {
x = rand();
} while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) );
x %= n;
นี่เป็นสูตรทั่วไปที่จะอธิบายถึงลักษณะเพิ่มเติมของการใช้โมดูลัสเพื่อกำหนดค่าสูงสุดของคุณ
ตัวอย่างของการใช้ค่าน้อยสำหรับ RAND_MAX ซึ่งเป็นตัวคูณของ N
Mark'original Version:
RAND_MAX = 3, n = 2, Values in RAND_MAX = 0,1,2,3, Valid Sets = 0,1 and 2,3.
When X >= (RAND_MAX - ( RAND_MAX % n ) )
When X >= 2 the value will be discarded, even though the set is valid.
รุ่นทั่วไป 1:
RAND_MAX = 3, n = 2, Values in RAND_MAX = 0,1,2,3, Valid Sets = 0,1 and 2,3.
When X > (RAND_MAX - ( ( RAND_MAX % n ) + 1 ) % n )
When X > 3 the value would be discarded, but this is not a vlue in the set RAND_MAX so there will be no discard.
นอกจากนี้ในกรณีที่ N ควรเป็นจำนวนค่าใน RAND_MAX ในกรณีนี้คุณสามารถตั้งค่า N = RAND_MAX +1 ยกเว้นว่า RAND_MAX = INT_MAX
การวนรอบคุณสามารถใช้ N = 1 ได้และค่าใด ๆ ของ X จะได้รับการยอมรับอย่างไรก็ตามและใส่คำสั่ง IF สำหรับตัวคูณสุดท้ายของคุณ แต่บางทีคุณอาจมีรหัสที่อาจมีเหตุผลที่ถูกต้องในการส่งคืน 1 เมื่อเรียกใช้ฟังก์ชันด้วย n = 1 ...
ดังนั้นจึงควรใช้ 0 ซึ่งปกติจะให้ข้อผิดพลาด Div 0 เมื่อคุณต้องการมี n = RAND_MAX + 1
รุ่นทั่วไป 2:
int x;
if n != 0 {
do {
x = rand();
} while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) );
x %= n;
} else {
x = rand();
}
โซลูชันทั้งสองนี้แก้ปัญหาด้วยผลลัพธ์ที่ถูกต้องโดยไม่จำเป็นซึ่งจะเกิดขึ้นเมื่อ RM + 1 เป็นผลิตภัณฑ์ของ n
เวอร์ชันที่สองยังครอบคลุมสถานการณ์ของเคสขอบเมื่อคุณต้องการ n ให้เท่ากับชุดของค่าที่เป็นไปได้ทั้งหมดที่มีอยู่ใน RAND_MAX
วิธีการแก้ไขในทั้งสองนั้นเหมือนกันและอนุญาตให้มีวิธีแก้ปัญหาทั่วไปเพิ่มเติมเพื่อความต้องการในการให้ตัวเลขสุ่มที่ถูกต้องและลดค่าที่ถูกทิ้ง
หากต้องการย้ำ:
โซลูชันทั่วไปพื้นฐานซึ่งขยายตัวอย่างของเครื่องหมาย:
// Assumes:
// RAND_MAX is a globally defined constant, returned from the environment.
// int n; // User input, or externally defined, number of valid choices.
int x;
do {
x = rand();
} while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) ) );
x %= n;
โซลูชันทั่วไปเพิ่มเติมที่อนุญาตให้ใช้สถานการณ์เพิ่มเติมของ RAND_MAX + 1 = n:
// Assumes:
// RAND_MAX is a globally defined constant, returned from the environment.
// int n; // User input, or externally defined, number of valid choices.
int x;
if n != 0 {
do {
x = rand();
} while (x > (RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n) ) );
x %= n;
} else {
x = rand();
}
ในบางภาษา (โดยเฉพาะภาษาที่ตีความ) ที่ทำการคำนวณการเปรียบเทียบนอกเงื่อนไขขณะที่อาจนำไปสู่ผลลัพธ์ที่เร็วขึ้นเนื่องจากเป็นการคำนวณแบบครั้งเดียวไม่ว่าจะต้องลองใหม่อีกเท่าใด YMMV!
// Assumes:
// RAND_MAX is a globally defined constant, returned from the environment.
// int n; // User input, or externally defined, number of valid choices.
int x; // Resulting random number
int y; // One-time calculation of the compare value for x
if n != 0 {
y = RAND_MAX - ( ( ( RAND_MAX % n ) + 1 ) % n)
do {
x = rand();
} while (x > y);
x %= n;
} else {
x = rand();
}
RAND_MAX%n = n - 1
ด้วยRAND_MAX
ค่าของ3
(ในความเป็นจริงมันควรจะสูงกว่านั้นมาก แต่ความเอนเอียงจะยังคงอยู่) มันสมเหตุสมผลแล้วจากการคำนวณเหล่านี้ว่ามีอคติ:
1 % 2 = 1
2 % 2 = 0
3 % 2 = 1
random_between(1, 3) % 2 = more likely a 1
ในกรณีนี้% 2
คือสิ่งที่คุณไม่ควรทำเมื่อคุณต้องการจำนวนสุ่มระหว่างและ0
1
คุณอาจจะได้รับจำนวนสุ่มระหว่าง0
และ2
โดยการทำ% 3
แต่เนื่องจากในกรณีนี้มีหลายRAND_MAX
3
วิธีอื่น
นอกจากนี้ง่ายมาก แต่จะเพิ่มให้กับคำตอบอื่น ๆ นี่คือคำตอบของฉันที่จะได้รับจำนวนสุ่มระหว่าง0
และn - 1
ดังนั้นn
ความเป็นไปได้ที่แตกต่างกันโดยไม่มีอคติ
>= n
รีสตาร์ท (ไม่มีแบบโมดูโล)ข้อมูลสุ่มจริง ๆ ไม่ใช่เรื่องง่ายดังนั้นทำไมใช้บิตมากกว่าที่ต้องการ
ด้านล่างเป็นตัวอย่างใน Smalltalk โดยใช้แคชบิตจากเครื่องกำเนิดตัวเลขแบบหลอกเทียม ฉันไม่มีผู้เชี่ยวชาญด้านความปลอดภัยใช้ความเสี่ยงของคุณเอง
next: n
| bitSize r from to |
n < 0 ifTrue: [^0 - (self next: 0 - n)].
n = 0 ifTrue: [^nil].
n = 1 ifTrue: [^0].
cache isNil ifTrue: [cache := OrderedCollection new].
cache size < (self randmax highBit) ifTrue: [
Security.DSSRandom default next asByteArray do: [ :byte |
(1 to: 8) do: [ :i | cache add: (byte bitAt: i)]
]
].
r := 0.
bitSize := n highBit.
to := cache size.
from := to - bitSize + 1.
(from to: to) do: [ :i |
r := r bitAt: i - from + 1 put: (cache at: i)
].
cache removeFrom: from to: to.
r >= n ifTrue: [^self next: n].
^r
ในฐานะที่เป็นคำตอบที่ได้รับการยอมรับบ่งชี้ว่า "โมดูโลอคติ" RAND_MAX
มีรากในมูลค่าที่ต่ำของ เขาใช้ค่าที่น้อยมากที่RAND_MAX
(10) เพื่อแสดงว่าหาก RAND_MAX เท่ากับ 10 คุณจะพยายามสร้างตัวเลขระหว่าง 0 ถึง 2 โดยใช้% ผลลัพธ์ที่ตามมาจะเป็นดังนี้:
rand() % 3 // if RAND_MAX were only 10, gives
output of rand() | rand()%3
0 | 0
1 | 1
2 | 2
3 | 0
4 | 1
5 | 2
6 | 0
7 | 1
8 | 2
9 | 0
ดังนั้นจึงมี 4 เอาต์พุตของ 0 (โอกาส 4/10) และมีเพียง 3 เอาต์พุตของ 1 และ 2 (โอกาสแต่ละ 3/10)
ดังนั้นมันมีอคติ ตัวเลขที่ต่ำกว่ามีโอกาสที่ดีกว่าในการออกมา
แต่นั่นจะปรากฏให้เห็นอย่างชัดเจนเมื่อRAND_MAX
มีขนาดเล็กมีขนาดเล็กหรือมากขึ้นโดยเฉพาะเมื่อจำนวนของคุณจะ modding RAND_MAX
โดยมีขนาดใหญ่เมื่อเทียบกับ
ทางออกที่ดีกว่าการวนซ้ำ (ซึ่งไม่มีประสิทธิภาพอย่างไม่น่าเชื่อและไม่ควรแม้แต่จะแนะนำ) คือการใช้ PRNG ที่มีช่วงเอาต์พุตที่ใหญ่กว่ามาก Mersenne Twisterอัลกอริทึมที่มีการส่งออกสูงสุดของ 4294967295 การทำเช่นนี้MersenneTwister::genrand_int32() % 10
เพื่อจุดประสงค์และจุดประสงค์ทั้งหมดจะได้รับการเผยแพร่อย่างเท่าเทียมกันและผลของโมดูโลจะมี แต่จะหายไป
MT::genrand_int32()%2
เลือก 0 (50 + 2.3e-8)% ของเวลาและ 1 (50 - 2.3e-8)% ของเวลา นอกจากว่าคุณกำลังสร้าง RGN ของคาสิโน (ซึ่งคุณอาจจะใช้ช่วง RGN ที่ใหญ่กว่า) ผู้ใช้ทุกคนจะไม่สังเกตเห็นพิเศษ 2.3e-8% ของเวลา คุณกำลังพูดถึงตัวเลขน้อยเกินไปที่จะสำคัญที่นี่
RAND_MAX
ค่าที่สูงจะลดอคติแบบโมดูโล แต่จะไม่กำจัดมัน จะวนรอบ
RAND_MAX
มีจำนวนมากกว่าจำนวนที่คุณ modding อย่างเพียงพอจำนวนครั้งที่คุณจำเป็นต้องสร้างหมายเลขสุ่มจะหายไปเล็กน้อยและจะไม่มีผลกับประสิทธิภาพ ฉันบอกว่าให้วนซ้ำไปเรื่อย ๆ ตราบใดที่คุณกำลังทดสอบตัวคูณที่ใหญ่ที่สุดn
แทนที่จะn
ตอบตามคำตอบที่ยอมรับ
ฉันเพิ่งเขียนรหัสสำหรับวิธีการหยอดเหรียญแบบไม่เอนเอียงของ Von Neumann ซึ่งในทางทฤษฎีควรกำจัดอคติใด ๆ ในกระบวนการสร้างหมายเลขแบบสุ่ม ข้อมูลเพิ่มเติมสามารถดูได้ที่ ( http://en.wikipedia.org/wiki/Fair_coin )
int unbiased_random_bit() {
int x1, x2, prev;
prev = 2;
x1 = rand() % 2;
x2 = rand() % 2;
for (;; x1 = rand() % 2, x2 = rand() % 2)
{
if (x1 ^ x2) // 01 -> 1, or 10 -> 0.
{
return x2;
}
else if (x1 & x2)
{
if (!prev) // 0011
return 1;
else
prev = 1; // 1111 -> continue, bias unresolved
}
else
{
if (prev == 1)// 1100
return 0;
else // 0000 -> continue, bias unresolved
prev = 0;
}
}
}
rand() % 100
100 ครั้ง B) หากผลลัพธ์ทั้งหมดต่างกันให้จดผลลัพธ์แรกไว้ C) มิฉะนั้น GOTO A. สิ่งนี้จะได้ผล แต่ด้วยจำนวนการทำซ้ำประมาณ 10 ^ 42 คุณจะต้องอดทน และเป็นอมตะ
else if(prev==2) prev= x1; else { if(prev!=x1) return prev; prev=2;}
RAND_MAX%n == n - 1
_(RAND_MAX + 1) % n == 0
คือ เมื่ออ่านรหัสฉันมักจะเข้าใจ% something == 0
ว่า "หารอย่างสม่ำเสมอ" ได้ง่ายกว่าวิธีการคำนวณอื่น ๆ แน่นอนถ้า C ++ stdlib ของคุณมีRAND_MAX
มูลค่าเท่าINT_MAX
กัน(RAND_MAX + 1)
แน่นอนจะไม่ทำงาน ดังนั้นการคำนวณของ Mark ยังคงเป็นการใช้งานที่ปลอดภัยที่สุด