Por que rand () + rand () produz números negativos?

304

Observei que a rand()função de biblioteca, quando é chamada apenas uma vez dentro de um loop, quase sempre produz números positivos.

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

Mas quando adiciono duas rand()chamadas, os números gerados agora têm mais números negativos.

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

Alguém pode explicar por que estou vendo números negativos no segundo caso?

PS: Inicializo a semente antes do loop como srand(time(NULL)).

badmad
fonte
11
rand()não pode ser negativo ...
twentylemon
293
rand () + rand () pode Owerflow
maskacovnik
13
O que é RAND_MAXpara o seu compilador? Você geralmente pode encontrá-lo stdlib.h. (Engraçado: verificação man 3 rand, ele tem a descrição de uma linha "mau gerador de números aleatórios".)
usr2564301
6
faça o que todo programador sensato faria abs(rand()+rand()). Eu prefiro ter um UB positivo do que negativo! ;)
Vinicius Kamakura
11
@hexa: isso não é garantia para o UB, como já ocorre na adição. Você não pode fazer com que o UB se torne um comportamento definido . Um programador evitaria UB como o inferno.
muito honesto para este site

Respostas:

542

rand()é definido para retornar um número inteiro entre 0e RAND_MAX.

rand() + rand()

poderia transbordar. O que você observa é provavelmente o resultado de um comportamento indefinido causado pelo excesso de números inteiros.

PP
fonte
4
@JakubArnold: Como esse comportamento de estouro é especificado por cada idioma de maneira diferente? O Python, por exemplo, não possui (bem, a memória disponível), pois o int cresce.
muito honesto para este site
2
@Olaf Depende de como um idioma decide representar números inteiros assinados. O Java não tinha mecanismo para detectar o excesso de número inteiro (até o java 8) e o definiu para contornar e o Go usa apenas a representação de complemento de 2 e o define como legal para estouros de número inteiro assinados. C, obviamente, suporta mais de 2 complemento.
PP
2
@EvanCarslake Não, esse não é um comportamento universal. O que você diz é sobre a representação do complemento do 2. Mas a linguagem C também permite outras representações. A especificação da linguagem C diz que o excesso de número inteiro assinado é indefinido . Portanto, em geral, nenhum programa deve confiar nesse comportamento e precisa codificar com cuidado para não causar estouro de número inteiro assinado. Mas isso não é aplicável a números inteiros não assinados, pois eles "contornariam" de maneira bem definida (módulo de redução 2). [continuação] ...
PP
12
Esta é a citação do padrão C relacionada ao estouro de número inteiro assinado: se uma condição excepcional ocorrer durante a avaliação de uma expressão (ou seja, se o resultado não estiver matematicamente definido ou não estiver no intervalo de valores representáveis ​​para seu tipo), o comportamento está indefinido.
PP
3
@EvanCarslake, afastando-se um pouco da questão, os compiladores C usam o padrão e, para números inteiros assinados, podem assumir que, a + b > ase souberem disso b > 0. Eles também podem assumir que, se houver uma declaração executada posteriormente a + 5, o valor atual será menor INT_MAX - 5. Portanto, mesmo no programa processador / interpretador de complemento 2 sem traps, pode não se comportar como se ints fosse o complemento 2 sem traps.
Maciej Piechotka
90

O problema é a adição. rand()retorna um intvalor de 0...RAND_MAX. Então, se você adicionar dois deles, você fará o que quiser RAND_MAX * 2. Se isso exceder INT_MAX, o resultado da adição excederá o intervalo válido que intpode ser mantido. O excesso de valores assinados é um comportamento indefinido e pode levar o teclado a falar com você em línguas estrangeiras.

Como não há ganho aqui em adicionar dois resultados aleatórios, a idéia simples é simplesmente não fazê-lo. Como alternativa, você pode converter cada resultado unsigned intantes da adição, se isso puder conter a soma. Ou use um tipo maior. Observe que longnão é necessariamente maior que int, o mesmo se aplica a long longse inttiver pelo menos 64 bits!

Conclusão: Apenas evite a adição. Não fornece mais "aleatoriedade". Se você precisar de mais bits, poderá concatenar os valores sum = a + b * (RAND_MAX + 1), mas isso provavelmente também requer um tipo de dados maior que int.

Como o motivo declarado é evitar um resultado zero: isso não pode ser evitado adicionando os resultados de duas rand()chamadas, pois ambas podem ser zero. Em vez disso, você pode apenas incrementar. Se RAND_MAX == INT_MAX, isso não pode ser feito em int. No entanto, (unsigned int)rand() + 1fará muito, muito provavelmente. Provavelmente (não definitivamente), porque exige UINT_MAX > INT_MAX, o que é verdade em todas as implementações que eu conheço (que abrange várias arquiteturas incorporadas, DSPs e todas as plataformas de desktop, dispositivos móveis e servidores dos últimos 30 anos).

Aviso:

Embora já tenha sido polvilhado nos comentários aqui, observe que a adição de dois valores aleatórios não obtém uma distribuição uniforme, mas uma distribuição triangular como rolar dois dados: para obter 12(dois dados), ambos os dados precisam ser mostrados 6. pois 11já existem duas variantes possíveis: 6 + 5ou 5 + 6, etc.

Portanto, a adição também é ruim nesse aspecto.

Observe também que os resultados rand()gerados não são independentes um do outro, pois são gerados por um gerador de números pseudo - aleatórios . Observe também que o padrão não especifica a qualidade ou a distribuição uniforme dos valores calculados.

honesto demais para este site
fonte
14
@badmad: E se as duas chamadas retornarem 0?
muito honesto para este site
3
@badmad: Eu me pergunto se UINT_MAX > INT_MAX != falseé garantido pelo padrão. (Parece provável, mas não tenho certeza se necessário). Nesse caso, você pode apenas transmitir um único resultado e incrementar (nessa ordem!).
muito honesto para este site
3
Há um ganho em adicionar vários números aleatórios quando você deseja uma distribuição não uniforme: stackoverflow.com/questions/30492259/…
Cœur
6
para evitar 0, um simples "enquanto o resultado é 0, re-roll"?
Olivier Dulac
2
Não apenas adicioná-los é uma maneira ruim de evitar 0, mas também resulta em uma distribuição não uniforme. Você começa uma distribuição como os resultados de rolamento dos dados: 7 é 6 vezes mais provável que os 2 ou 12.
Barmar
36

Esta é uma resposta a um esclarecimento da pergunta feita no comentário a esta resposta ,

a razão pela qual eu adicionei foi evitar '0' como o número aleatório no meu código. rand () + rand () foi a solução rápida e suja que prontamente me veio à mente.

O problema era evitar 0. Há (pelo menos) dois problemas com a solução proposta. Uma é, como as outras respostas indicam, que rand()+rand()pode invocar um comportamento indefinido. O melhor conselho é nunca chamar um comportamento indefinido. Outra questão é que não há garantia que rand()não produza 0 duas vezes seguidas.

O seguinte rejeita zero, evita comportamento indefinido e, na grande maioria dos casos, será mais rápido que duas chamadas para rand():

int rnum;
for (rnum = rand(); rnum == 0; rnum = rand()) {}
// or do rnum = rand(); while (rnum == 0);
David Hammen
fonte
9
Que tal rand() + 1?
askvictor
3
@askvictor Isso pode estourar (embora seja improvável).
gerrit
3
@gerrit - depende MAX_INT e RAND_MAX
askvictor
3
@gerrit, eu ficaria surpreso se eles são não o mesmo, mas acho que este é um lugar para pedantes :)
askvictor
10
Se RAND_MAX == MAX_INT, rand () + 1 excederá exatamente a mesma probabilidade do valor de rand () ser 0, o que torna essa solução completamente inútil. Se você está disposto a arriscar e ignorar a possibilidade de um estouro, você pode também usar rand () como está e ignorar a possibilidade de ele retornar 0.
Emil Jerabek
3

Produza basicamente rand()números entre 0e RAND_MAX, e 2 RAND_MAX > INT_MAXno seu caso.

Você pode modular com o valor máximo do seu tipo de dados para evitar o estouro. Esse curso interromperá a distribuição dos números aleatórios, mas randé apenas uma maneira de obter números aleatórios rápidos.

#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
fonte
2

Pode ser que você tente uma abordagem complicada, garantindo que o valor retornado pela soma de 2 rand () nunca exceda o valor de RAND_MAX. Uma abordagem possível poderia ser sum = rand () / 2 + rand () / 2; Isso garantiria que, para um compilador de 16 bits com o valor RAND_MAX de 32767, mesmo que ambos os rand retornassem 32767, mesmo assim (32767/2 = 16383) 16383 + 16383 = 32766, portanto, não resultaria em soma negativa.

Jibin Mathew
fonte
1
O OP queria excluir 0 dos resultados. A adição também não fornece uma distribuição uniforme de valores aleatórios.
muito honesto para este site
@Olaf: Não há garantia de que duas chamadas consecutivas rand()não produzam zero, portanto, o desejo de evitar zero não é uma boa razão para adicionar dois valores. Por outro lado, o desejo de ter uma distribuição não uniforme seria uma boa razão para adicionar dois valores aleatórios, se alguém garantir que o estouro não ocorra.
22718
1

a razão pela qual eu adicionei foi evitar '0' como o número aleatório no meu código. rand () + rand () foi a solução rápida e suja que prontamente me veio à mente.

Uma solução simples (ok, chame de "Hack") que nunca produz um resultado zero e nunca transborda é:

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

Isso limitará seu valor máximo, mas se você não se importa com isso, isso deve funcionar bem para você.

Kevin Fegan
fonte
1
Nota: Cuidado com as mudanças certas de variáveis ​​assinadas. É apenas bem definido para valores não negativos, para negativos, é definido como implementação. (Felizmente, rand()sempre retorna um valor não negativo). No entanto, deixaria a otimização para o compilador aqui.
muito honesto para este site
@Olaf: Em geral, a divisão assinada por dois será menos eficiente que um turno. A menos que um gravador de compilador tenha investido esforços em dizer ao compilador que randnão será negativo, a mudança será mais eficiente que a divisão por um número inteiro 2. assinado. A divisão por 2upoderia funcionar, mas se xfor um intpode resultar em avisos sobre a conversão implícita assinado.
Supercat
@ supercat: Por favor, leia meu comentário car3fully novamente. Você deve saber muito bem que qualquer compilador razoável usará uma mudança de / 2qualquer maneira (eu já vi isso mesmo para algo como -O0, ou seja, sem otimizações solicitadas explicitamente). É possivelmente a otimização mais trivial e mais estabelecida do código C. Point é que a divisão está bem definida pelo padrão para todo o intervalo inteiro, não apenas valores não negativos. Novamente: deixe otimizações para o compilador, escreva o código correto e claro em primeiro lugar. Isso é ainda mais importante para iniciantes.
muito honesto para este site
@Olaf: Todo compilador que testei gera um código mais eficiente ao mudar para a rand()direita por um ou dividir por do 2uque ao dividir por 2, mesmo ao usar -O3. Pode-se dizer razoavelmente que é improvável que essa otimização seja importante, mas dizer "deixe essas otimizações para o compilador" implicaria que os compiladores provavelmente as executariam. Você conhece algum compilador que realmente irá?
Supercat
@ supercat: Você deve usar compiladores mais modernos então. O gcc acabou de gerar um código fino na última vez em que verifiquei o Assembler gerado. No entanto, por mais que eu aprecie um groopie, prefiro não ser assediado na medida em que você apresentar pela última vez. Esses posts têm anos, meus comentários são perfeitamente válidos. Obrigado.
muito honesto para este site
1

Para evitar 0, tente o seguinte:

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

Você precisa incluir limits.h.

Doni
fonte
4
Isso vai dobrar a probabilidade de obter 1. É basicamente o mesmo (mas possiblly mais lento) como condicionalmente adicionando 1 se rand()rendimentos 0.
honesto demais para este site
Sim, você está certo, Olaf. Se rand () = 0 ou INT_MAX -1, o rnumb será 1.
Doni
Pior ainda, quando penso nisso. Na verdade, dobrará a propabilidade para 1e 2(todos assumidos RAND_MAX == INT_MAX). Eu esqueci o - 1.
muito honesto para este site
1
O -1aqui não tem valor. rand()%INT_MAX+1; ainda geraria apenas valores no intervalo [1 ... INT_MAX].
chux - Restabelece Monica 16/02
-2

Embora o que todo mundo tenha dito sobre o provável estouro possa muito bem ser a causa do negativo, mesmo quando você usa números inteiros não assinados. O verdadeiro problema é realmente usar a funcionalidade de hora / data como a semente. Se você realmente se familiarizou com essa funcionalidade, saberá exatamente por que digo isso. O que realmente faz é dar uma distância (tempo decorrido) desde uma determinada data / hora. Embora o uso da funcionalidade de data / hora como a semente de um rand () seja uma prática muito comum, realmente não é a melhor opção. Você deve procurar alternativas melhores, pois existem muitas teorias sobre o assunto e eu não poderia entrar em todas elas. Você adiciona a essa equação a possibilidade de transbordamento e essa abordagem estava condenada desde o início.

Aqueles que postaram o rand () + 1 estão usando a solução que mais usa para garantir que eles não obtenham um número negativo. Mas, essa abordagem também não é realmente a melhor.

A melhor coisa que você pode fazer é dedicar um tempo extra para escrever e usar o tratamento adequado de exceções, e adicionar apenas ao número rand () se e / ou quando você terminar com um resultado zero. E, para lidar com números negativos corretamente. A funcionalidade rand () não é perfeita e, portanto, precisa ser usada em conjunto com o tratamento de exceções para garantir que você obtenha o resultado desejado.

Dedicar tempo e esforço extras para investigar, estudar e implementar adequadamente a funcionalidade rand () vale bem o tempo e o esforço. Apenas meus dois centavos. Boa sorte em seus empreendimentos...

Mark Krug
fonte
2
rand()não especifica qual semente usar. O padrão o especifica para usar um gerador pseudo-aleatório, e não uma relação com nenhum momento. Também não afirma sobre a qualidade do gerador. O problema atual é claramente o excesso. Observe que rand()+1é usado para evitar 0; rand()não retorna um valor negativo. Desculpe, mas você perdeu o ponto aqui. Não se trata da qualidade do PRNG. ...
honesto demais para este site
... As boas práticas no GNU / Linux são para semear /dev/randome usar um bom PRNG posteriormente (não tenho certeza da qualidade da rand()glibc) ou continuar usando o dispositivo - arriscando o seu aplicativo bloquear, se não houver entropia suficiente disponível. Tentar obter sua entropia no aplicativo pode muito bem ser uma vulnerabilidade, pois isso é possivelmente mais fácil de atacar. E agora se trata de endurecer - não aqui
muito honesto para este site