Mengapa rand () + rand () menghasilkan angka negatif?

304

Saya mengamati bahwa rand()fungsi perpustakaan ketika dipanggil hanya sekali dalam satu lingkaran, hampir selalu menghasilkan angka positif.

for (i = 0; i < 100; i++) {
    printf("%d\n", rand());
}

Tetapi ketika saya menambahkan dua rand()panggilan, nomor yang dihasilkan sekarang memiliki lebih banyak angka negatif.

for (i = 0; i < 100; i++) {
    printf("%d = %d\n", rand(), (rand() + rand()));
}

Dapatkah seseorang menjelaskan mengapa saya melihat angka negatif dalam kasus kedua?

PS: Saya menginisialisasi benih sebelum loop sebagai srand(time(NULL)).

badmad
sumber
11
rand()tidak boleh negatif ...
twentylemon
293
rand () + rand () dapat owerflow
maskacovnik
13
Apa RAND_MAXuntuk kompiler Anda? Anda biasanya dapat menemukannya di stdlib.h. (Lucu: memeriksa man 3 rand, ini berisi deskripsi satu baris "generator nomor acak buruk".)
usr2564301
6
lakukan apa yang dilakukan oleh setiap programmer waras abs(rand()+rand()). Saya lebih suka memiliki UB positif daripada yang negatif! ;)
Vinicius Kamakura
11
@hexa: itu bukan sotution untuk UB, karena sudah terjadi penambahan. Anda tidak dapat membuat UB menjadi perilaku yang didefinisikan . Sebuah waras progrtammer akan menghindari UB seperti neraka.
terlalu jujur ​​untuk situs ini

Jawaban:

542

rand()didefinisikan untuk mengembalikan integer antara 0dan RAND_MAX.

rand() + rand()

bisa meluap. Apa yang Anda amati kemungkinan merupakan hasil dari perilaku tidak terdefinisi yang disebabkan oleh integer overflow.

PP
sumber
4
@JakubArnold: Bagaimana perilaku melimpah ditentukan oleh setiap bahasa secara berbeda? Python misalnya tidak memiliki (well, hingga memori yang tersedia), ketika int tumbuh.
terlalu jujur ​​untuk situs ini
2
@ Elaf Tergantung bagaimana bahasa memutuskan untuk mewakili bilangan bulat yang ditandatangani. Java tidak memiliki mekanisme untuk mendeteksi integer overflow (sampai java 8) dan mendefinisikannya untuk membungkus dan Go menggunakan representasi komplemen 2's saja dan mendefinisikannya legal untuk integer overflow yang ditandatangani. C jelas mendukung lebih dari 2 pelengkap.
PP
2
@ EvanCarslake Tidak, itu bukan perilaku universal. Apa yang Anda katakan adalah tentang representasi komplemen 2's. Tetapi bahasa C memungkinkan untuk representasi lain juga. Spesifikasi bahasa C mengatakan integer overflow yang ditandatangani tidak terdefinisi . Jadi secara umum, tidak ada program yang harus bergantung pada perilaku seperti itu dan perlu kode dengan hati-hati untuk tidak menyebabkan overflow integer yang ditandatangani. Tetapi ini tidak berlaku untuk bilangan bulat tak bertanda karena mereka akan "membungkus" dengan cara yang didefinisikan dengan baik (pengurangan modulo 2). [lanjutan] ...
PP
12
Ini adalah kutipan dari standar C terkait dengan integer overflow yang ditandatangani: Jika kondisi luar biasa terjadi selama evaluasi ekspresi (yaitu, jika hasilnya tidak didefinisikan secara matematis atau tidak dalam kisaran nilai yang dapat diwakili untuk jenisnya), perilaku tidak terdefinisi.
PP
3
@ EvanCarslake bergerak sedikit menjauh dari pertanyaan bahwa kompiler C memang menggunakan standar dan untuk bilangan bulat yang ditandatangani mereka dapat berasumsi bahwa a + b > ajika mereka tahu itu b > 0. Mereka juga dapat mengasumsikan bahwa jika ada pernyataan yang dieksekusi a + 5kemudian maka nilai saat ini lebih rendah dari itu INT_MAX - 5. Jadi, bahkan pada pelengkap 2 prosesor / juru bahasa tanpa program perangkap mungkin tidak berperilaku seolah-olah intitu adalah pelengkap 2 tanpa perangkap.
Maciej Piechotka
90

Masalahnya adalah penambahan. rand()mengembalikan intnilai 0...RAND_MAX. Jadi, jika Anda menambahkan dua dari mereka, Anda akan bangun RAND_MAX * 2. Jika itu melebihi INT_MAX, hasil penambahan melebihi rentang yang valid yang intdapat ditampung. Melimpahnya nilai yang ditandatangani adalah perilaku yang tidak terdefinisi dan dapat menyebabkan papan ketik Anda berbicara kepada Anda dalam bahasa asing.

Karena tidak ada keuntungan di sini dalam menambahkan dua hasil acak, ide sederhana adalah tidak melakukannya. Atau Anda dapat memberikan setiap hasil unsigned intsebelum penambahan jika itu dapat menahan jumlah. Atau gunakan tipe yang lebih besar. Catatan yang longbelum tentu lebih luas dari int, hal yang sama berlaku long longjika intsetidaknya 64 bit!

Kesimpulan: Hindari saja penambahan. Itu tidak memberikan lebih banyak "keacakan". Jika Anda membutuhkan lebih banyak bit, Anda bisa menggabungkan nilainya sum = a + b * (RAND_MAX + 1), tetapi itu juga kemungkinan membutuhkan tipe data yang lebih besar daripada int.

Karena alasan yang Anda nyatakan adalah untuk menghindari hasil nol: Itu tidak dapat dihindari dengan menambahkan hasil dari dua rand()panggilan, karena keduanya bisa nol. Sebagai gantinya, Anda hanya bisa menambah. Jika RAND_MAX == INT_MAX, ini tidak dapat dilakukan di int. Namun, (unsigned int)rand() + 1akan melakukan sangat, sangat mungkin. Kemungkinan (tidak pasti), karena memang membutuhkan UINT_MAX > INT_MAX, yang benar pada semua implementasi yang saya ketahui (yang mencakup beberapa arsitektur tertanam, DSP, dan semua platform desktop, seluler, dan server selama 30 tahun terakhir).

Peringatan:

Meskipun sudah ditaburkan di komentar di sini, harap dicatat bahwa menambahkan dua nilai acak tidak mendapatkan distribusi yang seragam, tetapi distribusi segitiga seperti menggulung dua dadu: untuk mendapatkan 12(dua dadu) kedua dadu harus ditampilkan 6. karena 11sudah ada dua varian yang mungkin: 6 + 5atau 5 + 6, dll.

Jadi, penambahannya juga buruk dari aspek ini.

Juga catat bahwa hasil yang rand()dihasilkan tidak independen satu sama lain, karena dihasilkan oleh generator nomor pseudorandom . Perhatikan juga bahwa standar tidak menentukan kualitas atau distribusi seragam dari nilai yang dihitung.

terlalu jujur ​​untuk situs ini
sumber
14
@badmad: Jadi bagaimana jika kedua panggilan mengembalikan 0?
terlalu jujur ​​untuk situs ini
3
@badmad: Saya hanya ingin tahu apakah UINT_MAX > INT_MAX != falsedijamin oleh standar. (Mungkin terdengar, tetapi tidak yakin jika diperlukan). Jika demikian, Anda hanya dapat memberikan satu hasil dan penambahan (dalam urutan itu!).
terlalu jujur ​​untuk situs ini
3
Ada keuntungan dalam menambahkan beberapa angka acak ketika Anda menginginkan distribusi yang tidak seragam: stackoverflow.com/questions/30492259/…
Cœur
6
untuk menghindari 0, "sementara hasilnya adalah 0, putar ulang" sederhana?
Olivier Dulac
2
Tidak hanya menambahkannya cara yang buruk untuk menghindari 0, tetapi juga menghasilkan distribusi yang tidak seragam. Anda mendapatkan distribusi seperti hasil dadu bergulir: 7 adalah 6 kali lebih mungkin dari 2 atau 12.
Barmar
36

Ini adalah jawaban untuk klarifikasi dari pertanyaan yang dibuat dalam komentar untuk jawaban ini ,

alasan saya menambahkan adalah untuk menghindari '0' sebagai angka acak dalam kode saya. rand () + rand () adalah solusi kotor cepat yang siap muncul di benakku.

Masalahnya adalah untuk menghindari 0. Ada (setidaknya) dua masalah dengan solusi yang diusulkan. Salah satunya adalah, seperti yang ditunjukkan oleh jawaban lainnya, yang rand()+rand()dapat memicu perilaku tidak terdefinisi. Saran terbaik adalah jangan pernah memaksakan perilaku yang tidak terdefinisi. Masalah lainnya adalah tidak ada jaminan yang rand()tidak akan menghasilkan 0 dua kali berturut-turut.

Berikut ini menolak nol, menghindari perilaku tidak terdefinisi, dan dalam sebagian besar kasus akan lebih cepat dari dua panggilan ke rand():

int rnum;
for (rnum = rand(); rnum == 0; rnum = rand()) {}
// or do rnum = rand(); while (rnum == 0);
David Hammen
sumber
9
Bagaimana dengan rand() + 1?
askvictor
3
@askvictor Itu bisa meluap (meskipun tidak mungkin).
gerrit
3
@gerrit - tergantung pada MAX_INT dan RAND_MAX
askvictor
3
@gerrit, saya akan terkejut jika mereka tidak sama, tapi saya kira ini adalah tempat untuk orang
mengayuh
10
Jika RAND_MAX == MAX_INT, rand () + 1 akan meluap dengan probabilitas yang sama persis dengan nilai rand () menjadi 0, yang membuat solusi ini sama sekali tidak ada gunanya. Jika Anda mau mengambil risiko dan mengabaikan kemungkinan overflow, Anda juga dapat menggunakan rand () apa adanya dan mengabaikan kemungkinan pengembaliannya 0.
Emil Jeřábek
3

Pada dasarnya rand()menghasilkan angka antara 0dan RAND_MAX, dan 2 RAND_MAX > INT_MAXdalam kasus Anda.

Anda dapat modulus dengan nilai maksimal tipe data Anda untuk mencegah overflow. Kursus ini akan mengganggu distribusi angka acak, tetapi randhanya cara untuk mendapatkan angka acak cepat.

#include <stdio.h>
#include <limits.h>

int main(void)
{
    int i=0;

    for (i=0; i<100; i++)
        printf(" %d : %d \n", rand(), ((rand() % (INT_MAX/2))+(rand() % (INT_MAX/2))));

    for (i=0; i<100; i++)
        printf(" %d : %ld \n", rand(), ((rand() % (LONG_MAX/2))+(rand() % (LONG_MAX/2))));

    return 0;
}
Khaled.K
sumber
2

Mungkin Anda bisa mencoba pendekatan yang agak rumit dengan memastikan bahwa nilai yang dikembalikan dengan jumlah 2 rand () tidak pernah melebihi nilai RAND_MAX. Suatu pendekatan yang mungkin bisa berupa penjumlahan = rand () / 2 + rand () / 2; Ini akan memastikan bahwa untuk kompiler 16 bit dengan nilai RAND_MAX dari 32767 bahkan jika kedua rand terjadi untuk mengembalikan 32767, bahkan kemudian (32767/2 = 16383) 16383 + 16383 = 32766, dengan demikian tidak akan menghasilkan jumlah negatif.

Jibin Mathew
sumber
1
OP ingin mengecualikan 0 dari hasil. Penambahan juga tidak menyediakan distribusi seragam dari nilai acak.
terlalu jujur ​​untuk situs ini
@ Elaf: Tidak ada jaminan bahwa dua panggilan berurutan rand()tidak akan menghasilkan nol, jadi keinginan untuk menghindari nol bukanlah alasan yang baik untuk menambahkan dua nilai. Di sisi lain, keinginan untuk memiliki distribusi yang tidak seragam akan menjadi alasan yang baik untuk menambahkan dua nilai acak jika satu memastikan bahwa tidak terjadi overflow.
supercat
1

alasan saya menambahkan adalah untuk menghindari '0' sebagai angka acak dalam kode saya. rand () + rand () adalah solusi kotor cepat yang siap muncul di benakku.

Solusi sederhana (oke, sebut saja "Retas") yang tidak pernah menghasilkan hasil nol dan tidak akan pernah meluap adalah:

x=(rand()/2)+1    // using divide  -or-
x=(rand()>>1)+1   // using shift which may be faster
                  // compiler optimization may use shift in both cases

Ini akan membatasi nilai maksimum Anda, tetapi jika Anda tidak peduli tentang itu, maka ini akan berfungsi dengan baik untuk Anda.

Kevin Fegan
sumber
1
Sidenote: Hati-hati dengan pergeseran kanan variabel yang ditandatangani. Ini hanya didefinisikan dengan baik untuk nilai-nilai non-negatif, untuk negatif, itu adalah implementasi yang ditentukan. (Untungnya, rand()selalu mengembalikan nilai tidak negatif). Namun, saya akan meninggalkan optimasi ke kompiler di sini.
terlalu jujur ​​untuk situs ini
@ Elaf: Secara umum, pembagian yang ditandatangani oleh dua akan kurang efisien daripada pergeseran. Kecuali seorang penulis kompiler telah menginvestasikan upaya dalam memberitahu kompiler bahwa randakan menjadi non-negatif, perubahan akan lebih efisien daripada pembagian oleh bilangan bulat yang ditandatangani 2. Divisi dengan 2udapat bekerja, tetapi jika xsuatu intdapat menghasilkan peringatan tentang konversi implisit dari yang tidak ditandatangani. untuk ditandatangani.
supercat
@supercat: Silakan baca komentar saya lagi car3efully. Anda harus tahu bahwa kompiler yang masuk akal akan menggunakan shift / 2(saya telah melihat ini bahkan untuk sesuatu seperti -O0, yaitu tanpa optimisasi diminta secara eksplisit). Ini mungkin optimasi kode C yang paling sepele dan paling mapan. Poinnya adalah pembagian didefinisikan dengan baik oleh standar untuk seluruh rentang bilangan bulat, bukan hanya nilai-nilai non-negatif. Sekali lagi: serahkan OPT ke compiler, tulis kode yang benar dan jelas di tempat pertama. Ini bahkan lebih penting bagi pemula.
terlalu jujur ​​untuk situs ini
@ Elaf: Setiap kompiler yang saya uji menghasilkan kode yang lebih efisien saat menggeser yang rand()benar dengan satu atau membaginya dengan 2usaat membaginya dengan 2, bahkan ketika menggunakan -O3. Orang mungkin mengatakan bahwa pengoptimalan semacam itu tidak akan menjadi masalah, tetapi mengatakan "serahkan pengoptimalan semacam itu ke kompiler" akan menyiratkan bahwa penyusun kemungkinan akan melakukan hal itu. Apakah Anda tahu ada kompiler yang benar-benar mau?
supercat
@supercat: Anda harus menggunakan kompiler yang lebih modern. gcc baru saja menghasilkan kode baik terakhir kali saya memeriksa Assembler yang dihasilkan. Namun demikian, sebanyak yang saya kira memiliki groopie, saya lebih suka untuk tidak dilecehkan selama Anda hadir terakhir kali. Posting ini sudah tua, komentar saya benar-benar valid. Terima kasih.
terlalu jujur ​​untuk situs ini
1

Untuk menghindari 0, coba ini:

int rnumb = rand()%(INT_MAX-1)+1;

Anda harus memasukkan limits.h.

Doni
sumber
4
Itu akan menggandakan probabilitas untuk mendapatkan 1. Ini pada dasarnya sama (tapi mungkin lebih lambat) dengan menambahkan 1 jika secara kondisional rand()menghasilkan 0
terlalu jujur ​​untuk situs ini
Ya, Anda benar Olaf. Jika rand () = 0 atau INT_MAX -1 rnumb akan menjadi 1.
Doni
Lebih buruk lagi, ketika saya mulai memikirkannya. Ini sebenarnya akan menggandakan propabilitas untuk 1dan 2(semua diasumsikan RAND_MAX == INT_MAX). Saya memang lupa tentang - 1.
terlalu jujur ​​untuk situs ini
1
Di -1sini tidak ada nilainya. rand()%INT_MAX+1; hanya akan menghasilkan nilai dalam kisaran [1 ... INT_MAX].
chux - Reinstate Monica
-2

Sementara apa yang orang lain katakan tentang kemungkinan overflow bisa jadi penyebab negatifnya, bahkan ketika Anda menggunakan bilangan bulat yang tidak ditandatangani. Masalah sebenarnya sebenarnya menggunakan fungsi waktu / tanggal sebagai unggulan. Jika Anda benar-benar menjadi terbiasa dengan fungsi ini, Anda akan tahu persis mengapa saya mengatakan ini. Seperti apa yang sebenarnya dilakukannya adalah memberi jarak (waktu berlalu) sejak tanggal / waktu tertentu. Meskipun penggunaan fungsi tanggal / waktu sebagai seed to rand (), adalah praktik yang sangat umum, itu sebenarnya bukan pilihan terbaik. Anda harus mencari alternatif yang lebih baik, karena ada banyak teori tentang topik ini dan saya tidak mungkin membahas semuanya. Anda menambahkan ke dalam persamaan ini kemungkinan overflow dan pendekatan ini sudah ditakdirkan sejak awal.

Mereka yang memposting rand () +1 menggunakan solusi yang paling banyak digunakan untuk menjamin bahwa mereka tidak mendapatkan angka negatif. Tapi, pendekatan itu juga bukan cara terbaik.

Hal terbaik yang dapat Anda lakukan adalah meluangkan waktu ekstra untuk menulis dan menggunakan penanganan pengecualian yang tepat, dan hanya menambah angka rand () jika dan / atau ketika Anda berakhir dengan hasil nol. Dan, untuk menangani angka negatif dengan benar. Fungsionalitas rand () tidak sempurna, dan karena itu perlu digunakan bersamaan dengan penanganan pengecualian untuk memastikan bahwa Anda mendapatkan hasil yang diinginkan.

Meluangkan waktu dan upaya ekstra untuk menyelidiki, mempelajari, dan mengimplementasikan fungsionalitas rand () dengan baik sepadan dengan waktu dan upaya. Hanya dua sen saya. Semoga beruntung dengan usahamu...

Mark Krug
sumber
2
rand()tidak menentukan benih apa yang akan digunakan. Standar tidak menetapkannya untuk menggunakan generator pseudorandom, bukan hubungan dengan waktu kapan saja. Itu juga tidak menyatakan tentang kualitas generator. Masalah yang sebenarnya jelas meluap. Catatan yang rand()+1digunakan untuk menghindari 0; rand()tidak mengembalikan nilai negatif. Maaf, tapi Anda tidak mengerti intinya di sini. Ini bukan tentang kualitas PRNG. ...
terlalu jujur ​​untuk situs ini
... Praktek yang baik di bawah GNU / Linux untuk memulai dari /dev/randomdan menggunakan PRNG yang baik sesudahnya (tidak yakin tentang kualitas rand()dari glibc) atau terus menggunakan perangkat - mempertaruhkan aplikasi Anda untuk diblokir jika tidak tersedia cukup entropi. Mencoba memasukkan entropi Anda ke dalam aplikasi mungkin sangat rentan karena itu mungkin lebih mudah diserang. Dan sekarang sampai pada pengerasan - tidak di sini
terlalu jujur ​​untuk situs ini