¿Por qué rand () + rand () produce números negativos?


304

Observé que la rand()función de biblioteca cuando se llama solo una vez dentro de un ciclo, casi siempre produce números positivos.

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

Pero cuando agrego dos rand()llamadas, los números generados ahora tienen más números negativos.

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

¿Alguien puede explicar por qué estoy viendo números negativos en el segundo caso?

PD: inicializo la semilla antes del ciclo como srand(time(NULL)).


11
rand()no puede ser negativo ...
twentylemon

293
rand () + rand () puede owerflow
maskacovnik

13
¿Qué es RAND_MAXpara tu compilador? Por lo general, puedes encontrarlo en stdlib.h. (Divertido: comprobación man 3 rand, lleva la descripción de una línea "generador de números aleatorios incorrectos".)
usr2564301

66
haz lo que haría cualquier programador cuerdo abs(rand()+rand()). ¡Prefiero tener un UB positivo que uno negativo! ;)
Vinicius Kamakura

11
@hexa: eso no es solución para la UB, ya que ocurre para la adición ya. No puede hacer que UB se convierta en un comportamiento definido . Un programador sano evitaría a UB como el infierno.
demasiado honesto para este sitio

Respuestas:


542

rand()se define para devolver un número entero entre 0y RAND_MAX.

rand() + rand()

podría desbordarse. Lo que observa es probablemente el resultado de un comportamiento indefinido causado por un desbordamiento de enteros.


44
@JakubArnold: ¿Cómo se especifica el comportamiento de desbordamiento en cada idioma de manera diferente? Python, por ejemplo, no tiene ninguno (bueno, hasta memoria disponible), ya que int solo crece.
demasiado honesto para este sitio

2
@Olaf Depende de cómo un idioma decida representar enteros con signo. Java no tenía ningún mecanismo para detectar el desbordamiento de enteros (hasta java 8) y lo definió para ajustarse y Go usa solo la representación del complemento 2 y lo define como legal para desbordamientos de enteros con signo. C obviamente es compatible con más de 2 de complemento.
PP

2
@EvanCarslake No, ese no es un comportamiento universal. Lo que dices es sobre la representación del complemento de 2. Pero el lenguaje C también permite otras representaciones. La especificación del lenguaje C dice que el desbordamiento de enteros con signo no está definido . Por lo tanto, en general, ningún programa debe confiar en ese comportamiento y debe codificarse cuidadosamente para no causar un desbordamiento de enteros con signo. Pero esto no es aplicable para enteros sin signo, ya que se "envolverían" de una manera bien definida (módulo de reducción 2). [continuación] ...
PP

12
Esta es la cita del estándar C relacionada con el desbordamiento de enteros con signo: si se produce una condición excepcional durante la evaluación de una expresión (es decir, si el resultado no está matemáticamente definido o no está en el rango de valores representables para su tipo), el comportamiento es indefinido.
PP

3
@EvanCarslake alejándose un poco de la pregunta, los compiladores de C usan el estándar y para los enteros con signo pueden suponer que a + b > asi lo saben b > 0. También pueden suponer que si hay una declaración ejecutada más tarde, el a + 5valor actual es menor INT_MAX - 5. Por lo tanto, incluso en el procesador / intérprete complementario de 2 sin programa de trampas podría no comportarse como si intel complemento de 2 fuera sin trampas.
Maciej Piechotka

90

El problema es la adición. rand()devuelve un intvalor de 0...RAND_MAX. Entonces, si agrega dos de ellos, podrá hacerlo RAND_MAX * 2. Si eso excede INT_MAX, el resultado de la suma desborda el rango válido que intpuede contener. El desbordamiento de valores firmados es un comportamiento indefinido y puede hacer que su teclado le hable en lenguas extranjeras.

Como no hay ganancia aquí al agregar dos resultados aleatorios, la idea simple es simplemente no hacerlo. Alternativamente, puede emitir cada resultado unsigned intantes de la suma si eso puede contener la suma. O use un tipo más grande. Tenga en cuenta que longno es necesariamente más ancho que int, ¡lo mismo se aplica a long longif intes de al menos 64 bits!

Conclusión: solo evite la adición. No proporciona más "aleatoriedad". Si necesita más bits, puede concatenar los valores sum = a + b * (RAND_MAX + 1), pero eso probablemente también requiera un tipo de datos mayor que int.

Como su razón declarada es evitar un resultado cero: eso no puede evitarse agregando los resultados de dos rand()llamadas, ya que ambas pueden ser cero. En cambio, puede simplemente incrementar. Si RAND_MAX == INT_MAX, esto no se puede hacer en int. Sin embargo, (unsigned int)rand() + 1lo hará muy, muy probable. Probable (no definitivamente), porque requiere UINT_MAX > INT_MAX, lo cual es cierto en todas las implementaciones que conozco (que cubren algunas arquitecturas integradas, DSP y todas las plataformas de escritorio, móviles y servidores de los últimos 30 años).

Advertencia:

Aunque ya aparece en los comentarios aquí, tenga en cuenta que agregar dos valores aleatorios no obtiene una distribución uniforme, sino una distribución triangular como lanzar dos dados: para obtener 12(dos dados) ambos dados tienen que mostrar 6. porque 11ya hay dos posibles variantes: 6 + 5o 5 + 6, etc.

Entonces, la adición también es mala desde este aspecto.

También tenga en cuenta que los resultados rand()generados no son independientes entre sí, ya que son generados por un generador de números pseudoaleatorios . Tenga en cuenta también que el estándar no especifica la calidad o la distribución uniforme de los valores calculados.


14
@badmad: ¿Y qué pasa si ambas llamadas devuelven 0?
demasiado honesto para este sitio

3
@badmad: Me pregunto si UINT_MAX > INT_MAX != falseestá garantizado por el estándar. (Suena probable, pero no estoy seguro si es necesario). Si es así, puede lanzar un solo resultado e incremento (¡en ese orden!).
demasiado honesto para este sitio

3
Hay ganancia al agregar múltiples números aleatorios cuando desea una distribución no uniforme: stackoverflow.com/questions/30492259/…
Cœur

66
para evitar 0, un simple "mientras el resultado es 0, volver a tirar"?
Olivier Dulac

2
Agregarlos no solo es una mala forma de evitar 0, sino que también da como resultado una distribución no uniforme. Obtiene una distribución como los resultados de lanzar dados: 7 es 6 veces más probable que 2 o 12.
Barmar

36

Esta es una respuesta a una aclaración de la pregunta hecha en comentario a esta respuesta ,

La razón por la que estaba agregando era para evitar '0' como el número aleatorio en mi código. rand () + rand () fue la solución rápida y sucia que me vino a la mente.

El problema era evitar 0. Hay (al menos) dos problemas con la solución propuesta. Una es, como indican las otras respuestas, que rand()+rand()puede invocar un comportamiento indefinido. El mejor consejo es nunca invocar un comportamiento indefinido. Otro problema es que no hay garantía de que rand()no produzca 0 dos veces seguidas.

Lo siguiente rechaza cero, evita un comportamiento indefinido y, en la gran mayoría de los casos, será más rápido que dos llamadas a rand():

int rnum;
for (rnum = rand(); rnum == 0; rnum = rand()) {}
// or do rnum = rand(); while (rnum == 0);

99
¿Qué hay de rand() + 1?
askvictor

3
@askvictor Eso podría desbordarse (aunque es poco probable).
gerrit

3
@gerrit - depende de MAX_INT y RAND_MAX
askvictor

3
@gerrit, me sorprendería que no fueran lo mismo, pero supongo que este es un lugar para pedantes :)
askvictor

10
Si RAND_MAX == MAX_INT, rand () + 1 se desbordará con exactamente la misma probabilidad que el valor de rand () sea 0, lo que hace que esta solución sea completamente inútil. Si está dispuesto a arriesgarse e ignorar la posibilidad de un desbordamiento, también puede usar rand () como está e ignorar la posibilidad de que regrese 0.
Emil Jeřábek

3

Básicamente rand()produzca números entre 0y RAND_MAX, y 2 RAND_MAX > INT_MAXen su caso.

Puede modular con el valor máximo de su tipo de datos para evitar el desbordamiento. Por supuesto, esto interrumpirá la distribución de los números aleatorios, pero randes solo una forma de obtener números aleatorios 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;
}

2

Es posible que pueda intentar un enfoque complicado al asegurarse de que el valor devuelto por la suma de 2 rand () nunca exceda el valor de RAND_MAX. Un posible enfoque podría ser sum = rand () / 2 + rand () / 2; Esto garantizaría que para un compilador de 16 bits con un valor RAND_MAX de 32767, incluso si ambos rands devuelven 32767, incluso entonces (32767/2 = 16383) 16383 + 16383 = 32766, por lo tanto, no daría como resultado una suma negativa.


1
El OP quería excluir 0 de los resultados. La adición tampoco proporciona una distribución uniforme de valores aleatorios.
demasiado honesto para este sitio

@Olaf: No hay garantía de que dos llamadas consecutivas rand()no den cero, por lo que el deseo de evitar cero no es una buena razón para agregar dos valores. Por otro lado, un deseo de tener una distribución no uniforme sería una buena razón para agregar dos valores aleatorios si uno asegura que no se produzca un desbordamiento.
supercat

1

La razón por la que estaba agregando era para evitar '0' como el número aleatorio en mi código. rand () + rand () fue la solución rápida y sucia que me vino a la mente.

Una solución simple (bueno, llámalo "Hack") que nunca produce un resultado cero y nunca se desbordará:

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

Esto limitará su valor máximo, pero si no le importa, entonces esto debería funcionar bien para usted.


1
Nota al margen: cuidado con los desplazamientos a la derecha de las variables con signo. Solo está bien definido para valores no negativos, para negativos, está definido para la implementación. (Afortunadamente, rand()siempre devuelve un valor no negativo). Sin embargo, dejaría la optimización al compilador aquí.
demasiado honesto para este sitio el

@Olaf: En general, la división firmada por dos será menos eficiente que un turno. A menos que un escritor del compilador haya invertido un esfuerzo en decirle al compilador que randno será negativo, el cambio será más eficiente que la división por un entero con signo 2. La división por 2upodría funcionar, pero si xes intposible, puede haber advertencias sobre la conversión implícita de sin signo a firmado.
supercat

@supercat: Por favor lea mi comentario car3efully nuevamente. Debes saber que cualquier compilador razonable usará un cambio de / 2todos modos (lo he visto incluso para algo así -O0, es decir, sin optimizaciones solicitadas explícitamente). Es posiblemente la optimización más trivial y más establecida del código C. El punto es que la división está bien definida por el estándar para todo el rango entero, no solo los valores no negativos. Nuevamente: deje las optimizaciones al compilador, escriba el código correcto y claro en primer lugar. Esto es aún más importante para los principiantes.
demasiado honesto para este sitio

@Olaf: Todos los compiladores que he probado generan un código más eficiente cuando se desplaza a la rand()derecha por uno o se divide por 2uque cuando se divide por 2, incluso cuando se usa -O3. Uno podría decir razonablemente que tal optimización no es importante, pero decir "dejar tales optimizaciones al compilador" implicaría que los compiladores probablemente las realizarían. ¿Conoces algún compilador que realmente lo haga?
supercat

@supercat: Entonces deberías usar compiladores más modernos. gcc acaba de generar un código fino la última vez que revisé el ensamblador generado. Sin embargo, por mucho que aprecie tener un groopie, preferiría no ser acosado tanto como usted presente la última vez. Estas publicaciones tienen años, mis comentarios son perfectamente válidos. Gracias.
demasiado honesto para este sitio

1

Para evitar 0, intente esto:

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

Necesitas incluir limits.h.


44
Eso duplicará la probabilidad de obtener 1. Es básicamente lo mismo (pero posiblemente más lento) que agregar condicionalmente 1 si rand()produce 0.
demasiado honesto para este sitio

Sí, tienes razón, Olaf. Si rand () = 0 o INT_MAX -1 el número será 1.
Doni

Peor aún, cuando lo pienso. En realidad duplicará la capacidad de propagación para 1y 2(todo asumido RAND_MAX == INT_MAX). Me olvidé de la - 1.
demasiado honesto para este sitio

1
El -1aquí no tiene ningún valor. rand()%INT_MAX+1; aún generaría valores en el rango [1 ... INT_MAX].
chux - Restablece a Monica

-2

Si bien lo que todos los demás han dicho sobre el probable desbordamiento podría ser la causa de lo negativo, incluso si usa enteros sin signo. El verdadero problema es usar la funcionalidad de hora / fecha como semilla. Si realmente te has familiarizado con esta funcionalidad, sabrás exactamente por qué digo esto. Como lo que realmente hace es dar una distancia (tiempo transcurrido) desde una fecha / hora dada. Si bien el uso de la funcionalidad de fecha / hora como la semilla de un rand (), es una práctica muy común, realmente no es la mejor opción. Debes buscar mejores alternativas, ya que hay muchas teorías sobre el tema y no podría entrar en todas ellas. Agrega a esta ecuación la posibilidad de desbordamiento y este enfoque estuvo condenado desde el principio.

Aquellos que publicaron el rand () + 1 están utilizando la solución que la mayoría usa para garantizar que no obtengan un número negativo. Pero, ese enfoque tampoco es la mejor manera.

Lo mejor que puede hacer es tomarse el tiempo extra para escribir y usar el manejo de excepciones adecuado, y solo agregar al número rand () si y / o cuando termina con un resultado cero. Y, para lidiar con los números negativos correctamente. La funcionalidad rand () no es perfecta y, por lo tanto, debe usarse junto con el manejo de excepciones para garantizar que termine con el resultado deseado.

Merece la pena el tiempo y el esfuerzo extra para investigar, estudiar e implementar adecuadamente la funcionalidad rand (). Solo mis dos centavos. Buena suerte en tus esfuerzos ...


2
rand()no especifica qué semilla usar. El estándar lo especifica para usar un generador pseudoaleatorio, no una relación con ningún momento. Tampoco establece la calidad del generador. El problema actual es claramente el desbordamiento. Tenga en cuenta que rand()+1se utiliza para evitar 0; rand()no devuelve un valor negativo Lo siento, pero perdiste el punto aquí. No se trata de la calidad del PRNG. ...
demasiado honesto para este sitio

... Una buena práctica bajo GNU / Linux es iniciar /dev/randomy usar un buen PRNG después (no estoy seguro de la calidad de rand()glibc) o continuar usando el dispositivo, arriesgando su aplicación para bloquear si no hay suficiente entropía disponible. Tratar de obtener su entropía en la aplicación podría ser una vulnerabilidad, ya que posiblemente sea más fácil de atacar. Y ahora se trata de endurecer, no aquí
demasiado honesto para este sitio
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.