¿Por qué este código que usa cadenas aleatorias imprime “hola mundo”?

10 minutos de lectura

¿Por qué este código que usa cadenas aleatorias imprime "hola mundo"?
0x56794E

La siguiente declaración de impresión imprimiría “hola mundo”. ¿Alguien podría explicar esto?

System.out.println(randomString(-229985452) + " " + randomString(-147909649));

Y randomString() Se ve como esto:

public static String randomString(int i)
{
    Random ran = new Random(i);
    StringBuilder sb = new StringBuilder();
    while (true)
    {
        int k = ran.nextInt(27);
        if (k == 0)
            break;

        sb.append((char)('`' + k));
    }

    return sb.toString();
}

  • Bueno, esas semillas en particular funcionan perfectamente. Random no es verdaderamente aleatorio, es pseudoaleatorio.

    – tckmn

    03 mar.


  • Funciona, como han dicho otros, porque el azar no lo es. Para mí, una pregunta más interesante sería si la persona que escribió eso, lo hizo por fuerza bruta, o si hay una manera fácil de predecir qué generaría aleatoriamente para los próximos N valores para una semilla dada. La fuerza bruta es fácil y con el hardware moderno no debería tomar mucho tiempo, por lo que fue una forma viable de hacerlo. Dado que es estático, incluso podría distribuir fácilmente la búsqueda a través de una red.

    – jmoreno

    03 mar.

  • Me pregunto el propósito de n en for (int n = 0; ; n++). podrían usar for(;;) o while(true) ¡en lugar de!

    – Ing. Fouad

    03 mar. 13 a las 06:50

  • En una secuencia verdaderamente aleatoria, eventualmente aparecerán todas las cadenas posibles. En una secuencia pseudoaleatoria de alta calidad, se puede esperar razonablemente cada cadena posible de longitud (log_s(N) – n) bits (donde N es el número de bits en el estado interno de los PRNG y n es un número pequeño, elijamos 8 por conveniencia ) para aparecer en el ciclo. Este código recibe alguna ayuda del uso de un punto de inicio codificado libremente elegido (el valor de la tilde grave del carácter) que recupera casi la totalidad de los 8 bits.

    – dmckee — gatito ex-moderador

    04 mar. 13 a las 0:13


  • Si tuviera que refactorizar esto, además de refactorizar las llaves, solo cambiaría el nombre del método por uno más descriptivo: fixedAndNotSoRandomString o algo…

    – Emperador MC

    25 mar. 21 en 13:26

¿Por qué este código que usa cadenas aleatorias imprime "hola mundo"?
Ing. Fouad

Las otras respuestas explican por qué, pero aquí está cómo.

Dada una instancia de Random:

Random r = new Random(-229985452)

Los primeros 6 números que r.nextInt(27) genera son:

8
5
12
12
15
0

y los primeros 6 números que r.nextInt(27) genera dado Random r = new Random(-147909649) son:

23
15
18
12
4
0

Luego simplemente agregue esos números a la representación entera del carácter ` (que es 96):

8  + 96 = 104 --> h
5  + 96 = 101 --> e
12 + 96 = 108 --> l
12 + 96 = 108 --> l
15 + 96 = 111 --> o

23 + 96 = 119 --> w
15 + 96 = 111 --> o
18 + 96 = 114 --> r
12 + 96 = 108 --> l
4  + 96 = 100 --> d

  • Con pedantería, new Random(-229985452).nextInt(27) siempre devuelve 8.

    – usuario253751

    12 mar. 15 a las 2:16

  • @rootTraveller Para empezar, new Random() no devuelve un número en absoluto.

    – usuario253751

    07 mar. 17 en 07:25

  • @roottraveller “Random” es un generador de números pseudoaleatorios determinista. Si lo inicializa con una semilla fija, producirá una secuencia fija de números.

    – lavado de enchufe

    09 oct.

  • ¿Hay alguna forma de calcular estas semillas? Debe haber algo de lógica… o es solo fuerza bruta.

    – Sohit Gore

    25 ene.

  • @SohitGore Dado que el valor predeterminado de Java Random no es criptográficamente seguro (estoy bastante seguro de que es un Mersenne Twister, pero no me cites al respecto), probablemente sea posible retroceder desde “Quiero estos números” hasta “esta es la semilla que usaría”. He hecho algo similar con el generador congruencial lineal C estándar.

    – Nic

    27 jun.

Cuando una instancia de java.util.Random se construye con un parámetro semilla específico (en este caso -229985452 o -147909649), sigue el algoritmo de generación de números aleatorios comienzo con ese valor semilla.

Cada Random construido con la misma semilla generará el mismo patrón de números cada vez.

  • @Vulcan: el javadoc dice que la semilla es de 48 bits. docs.oracle.com/javase/7/docs/api/java/util/Random.html. Y además, las semillas reales son valores de 32 bits.

    – Esteban C.

    03 mar. 13 04:58


  • Cada elemento de la secuencia de números aleatorios se toma módulo 27, y hay 6 elementos en cada uno de "hello " y "world ". Si asumiera un generador verdaderamente aleatorio, las probabilidades serían de 1 en 27 ^ 6 (387 420 489) de obtener la secuencia que estaba buscando, ¡así que es bastante impresionante pero no del todo alucinante!

    –Russell Borogove

    03 mar. 13 en 07:48

  • @RussellBorogove: Pero con esas probabilidades y 2^64 semillas posibles, se esperan 47.600 millones de valores de semillas que dan esa secuencia. Es solo cuestión de encontrar uno.

    – dan04

    03 mar. 13 a las 17:54

  • @ dan04: no estaba dispuesto a hacer esa estimación; dependiendo de la implementación de PRNG, el tamaño de la palabra semilla puede no ser igual al tamaño del estado y las rutas de secuencia pueden no estar distribuidas uniformemente. Pero aún así, las probabilidades son definitivamente buenas, y si no puede encontrar un par, puede intentarlo nuevamente con una carcasa diferente ("Hello" "World"), o utilizando 122-k en vez de 96+k, o…

    –Russell Borogove

    03 mar. 13 a las 18:15

  • @ThorbjørnRavnAndersen El Javadoc especifica que “se especifican algoritmos particulares para la clase Random. Las implementaciones de Java deben usar todos los algoritmos que se muestran aquí para la clase Random, en aras de la portabilidad absoluta del código Java”.

    – F. Thompson

    27 ago. 13 en 3:05

¿Por qué este código que usa cadenas aleatorias imprime "hola mundo"?
Denis Tulsky

Lo dejaré aquí. Quien tenga mucho tiempo (de CPU) de sobra, siéntase libre de experimentar 🙂 Además, si ha dominado algunos fork-join-fu para hacer que esta cosa queme todos los núcleos de CPU (solo los hilos son aburridos, ¿verdad?), por favor comparta tu codigo. Me sería de gran aprecio.

public static void main(String[] args) {
    long time = System.currentTimeMillis();
    generate("stack");
    generate("over");
    generate("flow");
    generate("rulez");

    System.out.println("Took " + (System.currentTimeMillis() - time) + " ms");
}

private static void generate(String goal) {
    long[] seed = generateSeed(goal, Long.MIN_VALUE, Long.MAX_VALUE);
    System.out.println(seed[0]);
    System.out.println(randomString(seed[0], (char) seed[1]));
}

public static long[] generateSeed(String goal, long start, long finish) {
    char[] input = goal.toCharArray();
    char[] pool = new char[input.length];
    label:
    for (long seed = start; seed < finish; seed++) {
        Random random = new Random(seed);

        for (int i = 0; i < input.length; i++)
            pool[i] = (char) random.nextInt(27);

        if (random.nextInt(27) == 0) {
            int base = input[0] - pool[0];
            for (int i = 1; i < input.length; i++) {
                if (input[i] - pool[i] != base)
                    continue label;
            }
            return new long[]{seed, base};
        }

    }

    throw new NoSuchElementException("Sorry :/");
}

public static String randomString(long i, char base) {
    System.out.println("Using base: '" + base + "'");
    Random ran = new Random(i);
    StringBuilder sb = new StringBuilder();
    for (int n = 0; ; n++) {
        int k = ran.nextInt(27);
        if (k == 0)
            break;

        sb.append((char) (base + k));
    }

    return sb.toString();
}

Producción:

-9223372036808280701
Using base: 'Z'
stack
-9223372036853943469
Using base: 'b'
over
-9223372036852834412
Using base: 'e'
flow
-9223372036838149518
Using base: 'd'
rulez
Took 7087 ms

  • @Uno, dos, tres nextInt(27) significa dentro del rango [0, 26].

    – Ing. Fouad

    03 mar. 13 a las 21:48

  • @Vulcan La mayoría de las semillas están muy cerca del valor máximo, al igual que si selecciona números aleatorios entre 1 y 1000, la mayoría de los números que termine eligiendo tendrán tres dígitos. No es sorprendente, cuando lo piensas 🙂

    – Tomás

    03 mar. 13 a las 22:28

  • @Vulcan De hecho, si hace los cálculos, verá que están tan cerca del valor máximo como cero (supongo que la semilla se interpreta como sin firmar en el código de generación). Pero debido a que la cantidad de dígitos crece solo logarítmicamente con el valor real, el número parece muy cercano cuando en realidad no lo es.

    – Tomás

    03 mar. 13 a las 22:36

  • Gran respuesta. Y para obtener puntos de bonificación, ¿puedes encontrar una semilla que inicialice una aleatoria que produzca la secuencia de 4 semillas requerida para la inicialización de las aleatorias finales?

    –Marek

    11 mar. 13 en 08:39

  • @Marek: No creo que los dioses del pseudoaleatorio aprueben tal comportamiento.

    – Denis Tulsky

    11 mar. 13 en 08:43

¿Por qué este código que usa cadenas aleatorias imprime "hola mundo"?
xDD

Todos aquí hicieron un gran trabajo al explicar cómo funciona el código y mostrar cómo puede construir sus propios ejemplos, pero aquí hay una respuesta teórica de información que muestra por qué podemos esperar razonablemente que exista una solución que la búsqueda de fuerza bruta eventualmente encontrará.

Las 26 letras minúsculas diferentes forman nuestro alfabeto Σ. Para permitir la generación de palabras de diferentes longitudes, agregamos además un símbolo de terminador para producir un alfabeto extendido Σ' := Σ ∪ {⊥}.

Dejar α ser un símbolo y X una variable aleatoria uniformemente distribuida sobre Σ'. La probabilidad de obtener ese símbolo, P(X = α), y su contenido de información, I(α), están dadas por:

P(X = α) = 1/|Σ’| = 1/27

yo(α) = -log₂[P(X = α)] = -log₂(1/27) = log₂(27)

por una palabra ω ∈ Σ* y es ⊥-contraparte terminada ω' := ω · ⊥ ∈ (Σ')*, tenemos

yo(ω) := yo(ω’) = |ω’| * log₂(27) = (|ω| + 1) * log₂(27)

Dado que el generador de números pseudoaleatorios (PRNG) se inicializa con una semilla de 32 bits, podemos esperar que la mayoría de las palabras tengan una longitud de hasta

λ = piso[32/log₂(27)] – 1 = 5

ser generado por al menos una semilla. Incluso si tuviéramos que buscar una palabra de 6 caracteres, tendríamos éxito aproximadamente el 41,06 % de las veces. No está nada mal.

Para 7 letras estamos más cerca del 1,52 %, pero no me había dado cuenta de eso antes de intentarlo:

#include <iostream>
#include <random>
 
int main()
{
    std::mt19937 rng(631647094);
    std::uniform_int_distribution<char> dist('a', 'z' + 1);
 
    char alpha;
    while ((alpha = dist(rng)) != 'z' + 1)
    {
        std::cout << alpha;
    }
}

Ver la salida: http://ideone.com/JRGb3l

Escribí un programa rápido para encontrar estas semillas:

import java.lang.*;
import java.util.*;
import java.io.*;

public class RandomWords {
    public static void main (String[] args) {
        Set<String> wordSet = new HashSet<String>();
        String fileName = (args.length > 0 ? args[0] : "/usr/share/dict/words");
        readWordMap(wordSet, fileName);
        System.err.println(wordSet.size() + " words read.");
        findRandomWords(wordSet);
    }

    private static void readWordMap (Set<String> wordSet, String fileName) {
        try {
            BufferedReader reader = new BufferedReader(new FileReader(fileName));
            String line;
            while ((line = reader.readLine()) != null) {
                line = line.trim().toLowerCase();
                if (isLowerAlpha(line)) wordSet.add(line);
            }
        }
        catch (IOException e) {
            System.err.println("Error reading from " + fileName + ": " + e);
        }
    }

    private static boolean isLowerAlpha (String word) {
        char[] c = word.toCharArray();
        for (int i = 0; i < c.length; i++) {
            if (c[i] < 'a' || c[i] > 'z') return false;
        }
        return true;
    }

    private static void findRandomWords (Set<String> wordSet) {
        char[] c = new char[256];
        Random r = new Random();
        for (long seed0 = 0; seed0 >= 0; seed0++) {
            for (int sign = -1; sign <= 1; sign += 2) {
                long seed = seed0 * sign;
                r.setSeed(seed);
                int i;
                for (i = 0; i < c.length; i++) {
                    int n = r.nextInt(27);
                    if (n == 0) break;
                    c[i] = (char)((int)'a' + n - 1);
                }
                String s = new String(c, 0, i);
                if (wordSet.contains(s)) {
                    System.out.println(s + ": " + seed);
                    wordSet.remove(s);
                }
            }
        }
    }
}

Lo tengo ejecutándose en segundo plano ahora, pero ya ha encontrado suficientes palabras para un pangrama clásico:

import java.lang.*;
import java.util.*;

public class RandomWordsTest {
    public static void main (String[] args) {
        long[] a = {-73, -157512326, -112386651, 71425, -104434815,
                    -128911, -88019, -7691161, 1115727};
        for (int i = 0; i < a.length; i++) {
            Random r = new Random(a[i]);
            StringBuilder sb = new StringBuilder();
            int n;
            while ((n = r.nextInt(27)) > 0) sb.append((char)('`' + n));
            System.out.println(sb);
        }
    }
}

(Demostración en ideone.)

PD. -727295876, -128911, -1611659, -235516779.

Estaba intrigado por esto, ejecuté este generador de palabras aleatorias en una lista de palabras del diccionario. Rango: Entero.MIN_VALUE a Entero.MAX_VALUE

Obtuve 15131 visitas.

int[] arrInt = {-2146926310, -1885533740, -274140519, 
                -2145247212, -1845077092, -2143584283,
                -2147483454, -2138225126, -2147375969};

for(int seed : arrInt){
    System.out.print(randomString(seed) + " ");
}

Huellas dactilares

the quick browny fox jumps over a lazy dog 

¿Por qué este código que usa cadenas aleatorias imprime "hola mundo"?
Sinclair Schuller

La mayoría de los generadores de números aleatorios son, de hecho, “pseudoaleatorios”. Son Generadores Lineales Congruenciales, o LCGs (http://en.wikipedia.org/wiki/Linear_congruential_generator)

Los LCG son bastante predecibles dada una semilla fija. Básicamente, use una semilla que le dé su primera letra, luego escriba una aplicación que continúe generando el siguiente int (char) hasta que presione la siguiente letra en su cadena de destino y escriba cuántas veces tuvo que invocar el LCG. Continúe hasta que haya generado todas y cada una de las letras.

.

¿Ha sido útil esta solución?