Por que esse código usando seqüências aleatórias imprime "hello world"?

1769

A seguinte declaração de impressão imprimiria "olá mundo". Alguém poderia explicar isso?

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

E randomString()fica assim:

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();
}
0x56794E
fonte
158
Bem, essas sementes em particular simplesmente funcionam perfeitamente. Aleatório não é verdadeiramente aleatório, é pseudo-aleatório.
fácil
341
Funciona, como outros já disseram, porque o aleatório não é. Para mim, uma pergunta mais interessante seria a pessoa que escreveu isso, força bruta ou há uma maneira fácil de prever o que o aleatório geraria para os próximos N valores para uma dada semente. A força bruta é fácil e, com o hardware moderno, não deve demorar muito, de modo que essa é uma maneira viável de fazê-lo. Como é estático, você pode distribuir facilmente a pesquisa por uma rede.
jmoreno
78
Pergunto-me o propósito de nnos for (int n = 0; ; n++). Eles poderiam usar for(;;)ou em while(true)vez disso!
Eng.Fouad
13
Em uma sequência verdadeiramente aleatória, todas as seqüências possíveis aparecerão eventualmente. Em uma sequência pseudo-aleatória de alta qualidade, pode-se esperar razoavelmente todas as cadeias possíveis de comprimento (log_s (N) - n) bits (onde N é o número de bits no estado interno do PRNGs e n é um número pequeno, vamos escolher 8 por conveniência ) para aparecer no ciclo. Esse código recebe ajuda do uso de um ponto de início codificado livremente escolhido (o valor do backtick de caractere) que recupera quase todos os 8 bits inteiros.
dmckee --- ex-moderador gatinho
13
Isto é de uma postagem que escrevi há alguns anos atrás. Você está em
Home

Respostas:

917

Quando uma instância de java.util.Randomé construída com um parâmetro de semente específico (neste caso -229985452ou -147909649), segue o algoritmo de geração de número aleatório começando com esse valor de semente.

Todo Randomconstruído com a mesma semente gerará o mesmo padrão de números todas as vezes.

FThompson
fonte
8
@ Vulcan - o javadoc diz que a semente é de 48 bits. docs.oracle.com/javase/7/docs/api/java/util/Random.html . Além disso, as sementes reais são valores de 32 bits.
Stephen C
80
Cada elemento da sequência numérica aleatória é obtido no módulo 27 e existem 6 elementos em cada um de "hello\0"e "world\0". Se você assumisse um gerador verdadeiramente aleatório, as chances seriam de 1 em 27 ^ 6 (387.420.489) para obter a sequência que você estava procurando - portanto, é bastante impressionante, mas não é impressionante!
Russell Borogove
17
@RussellBorogove: Mas com essas probabilidades e 2 ^ 64 sementes possíveis, são esperados 47,6 bilhões de valores de sementes que dão essa sequência. É apenas uma questão de encontrar um.
dan04
8
@ dan04 - Eu não estava muito disposto a fazer essa estimativa; dependendo da implementação do PRNG, o tamanho da palavra inicial pode não ser igual ao tamanho do estado e os caminhos de sequência podem não ser distribuídos igualmente. Mas ainda assim, as chances são definitivamente bom, e se você não poderia encontrar um par você pode tentar novamente com caixa diferente ( "Hello" "World"), ou utilizando 122-k, em vez de 96+k, ou ...
Russell Borogove
7
@ ThorbjørnRavnAndersen O Javadoc especifica que "algoritmos específicos são especificados para a classe Random. As implementações de Java devem usar todos os algoritmos mostrados aqui para a classe Random, por uma questão de portabilidade absoluta do código Java".
precisa saber é o seguinte
1137

As outras respostas explicam o porquê, mas aqui está como.

Dada uma instância de Random:

Random r = new Random(-229985452)

Os 6 primeiros números r.nextInt(27)gerados são:

8
5
12
12
15
0

e os 6 primeiros números r.nextInt(27)gerados Random r = new Random(-147909649)são:

23
15
18
12
4
0

Em seguida, basta adicionar esses números à representação inteira do caractere `(que é 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
Eng.Fouad
fonte
48
Pedanticamente, new Random(-229985452).nextInt(27)sempre retorna 8.
user253751 12/12/2015
1
@immibis por quê? Quero dizer Random () deve retornar um número aleatório toda vez, não um conjunto de números ordenados por correção?
roottraveller
5
@rootTraveller Para começar, new Random()não retorna um número.
user253751
2
Existe uma maneira de calcular essas sementes? Deve haver alguma lógica ... ou é apenas força bruta.
Sohit Gore
2
@SohitGore Dado que o padrão do Java Randomnão é criptograficamente seguro (tenho certeza de que é um Mersenne Twister, mas não me cite nisso), provavelmente é possível trabalhar de trás para frente, de "Quero esses números" para "este é o semente que eu usaria ". Eu fiz algo semelhante com o gerador congruencial linear C padrão.
Fund Monica's Lawsuit
280

Vou deixar aqui. Quem tiver muito tempo (CPU) de sobra, sinta-se à vontade para experimentar :) Além disso, se você dominou algum fork-join-fu para fazer essa coisa queimar todos os núcleos da CPU (apenas threads são chatos, certo?), Compartilhe seu código. Eu apreciaria muito.

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();
}

Resultado:

-9223372036808280701
Using base: 'Z'
stack
-9223372036853943469
Using base: 'b'
over
-9223372036852834412
Using base: 'e'
flow
-9223372036838149518
Using base: 'd'
rulez
Took 7087 ms
Denis Tulskiy
fonte
24
@OneTwoThree nextInt(27)significa dentro do intervalo [0, 26].
Eng.Fouad
30
@Vulcan A maioria das sementes está muito próxima do valor máximo, assim como se você selecionar números aleatórios entre 1 e 1000, a maioria dos números que você escolher terá três dígitos. Não é surpreendente, quando você pensa sobre isso :)
Thomas
18
@ Vulcan De fato, se você fizer as contas, verá que elas são tão próximas do valor máximo quanto de zero (suponho que a semente esteja sendo interpretada como um sinal no código de geração). Mas como o número de dígitos cresce apenas logaritmicamente com o valor real, o número parece muito próximo quando realmente não é.
Thomas
10
Ótima resposta. E para pontos de bônus, você pode encontrar uma semente que inicializará um Random que produzirá a sequência de 4 sementes necessárias para a inicialização dos randoms finais?
Marek
13
@Marek: Eu não acho que deuses pseudo-aleatórios aprovariam esse comportamento.
Denis Tulskiy
254

Todo mundo aqui fez um ótimo trabalho explicando como o código funciona e mostrando como você pode construir seus próprios exemplos, mas aqui está uma resposta teórica da informação mostrando por que podemos razoavelmente esperar que exista uma solução que a pesquisa de força bruta acabará encontrando.

As 26 letras minúsculas diferentes formam nosso alfabeto Σ. Para permitir a geração de palavras de diferentes comprimentos, adicionamos ainda um símbolo terminador para gerar um alfabeto estendido Σ' := Σ ∪ {⊥}.

Let αSer um símbolo e X uma variável aleatória distribuída uniformemente sobre Σ'. A probabilidade de obter esse símbolo, P(X = α)e seu conteúdo informativo I(α), são dados por:

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

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

Para uma palavra ω ∈ Σ*e sua ⊥-contraparte finalizada ω' := ω · ⊥ ∈ (Σ')*, temos

I (ω): = I (ω ') = | ω' | * log₂ (27) = (| ω | + 1) * log₂ (27)

Como o gerador de números aleatórios pseudo-aleatórios (PRNG) é inicializado com uma semente de 32 bits, podemos esperar a maioria das palavras com comprimento de até

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

para ser gerado por pelo menos uma semente. Mesmo se procurássemos uma palavra de 6 caracteres, ainda teríamos sucesso em 41,06% do tempo. Não é muito pobre.

Em 7 letras, estamos olhando para mais perto de 1,52%, mas eu não tinha percebido isso antes de tentar:

#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;
    }
}

Veja a saída: http://ideone.com/JRGb3l

xDD
fonte
minha teoria da informação é meio fraca, mas eu amo essa prova. alguém pode me explicar a linha lambda, claramente estamos dividindo o conteúdo das informações de uma com a outra, mas por que isso nos dá o tamanho da palavra? como eu disse eu estou meio enferrujado então desculpas por perguntar o óbvio (NB é algo a ver com a saída limite de código -de Shannon)
Mike HR
1
@ MikeH-R A linha lambda é a I(⍵)equação reorganizada. I(⍵)é 32 (bits) e |⍵|acaba sendo 5 (símbolos).
iceman
67

Eu escrevi um programa rápido para encontrar essas sementes:

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);
                }
            }
        }
    }
}

Eu tenho isso rodando em segundo plano agora, mas ele já encontrou palavras suficientes para um pangram clássico:

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);
        }
    }
}

( Demonstração em ideone. )

Ps. -727295876, -128911, -1611659, -235516779.

Ilmari Karonen
fonte
35

Fiquei intrigado com isso, corri esse gerador de palavras aleatórias em uma lista de palavras do dicionário. Intervalo: Integer.MIN_VALUE a Integer.MAX_VALUE

Eu tenho 15131 hits.

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

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

Impressões

the quick browny fox jumps over a lazy dog 
Puru--
fonte
7
Você fez o meu dia homem: DI tentei com Long.Min / Max e procure os nomes dos meus colegas e só encontrou peter: (peter 4611686018451441623 peter 24053719 peter -4611686018403334185 peter -9223372036830722089 peter -4611686017906248127 peter 521139776836205eter-peter 4611686017645756173 peter 781631731 peter 4611686019209019635 peter -9223372036073144077 peter -4611686017420317288 peter 1007070616 peter -9223372035847705192)
Marcel
25

A maioria dos geradores de números aleatórios é, de fato, "pseudo-aleatória". Eles são geradores lineares congruentes ou LCGs ( http://en.wikipedia.org/wiki/Linear_congruential_generator )

LCGs são bastante previsíveis, dada uma semente fixa. Basicamente, use uma semente que lhe dê sua primeira letra e, em seguida, escreva um aplicativo que continue a gerar o próximo int (char) até você acertar a próxima letra na string de destino e anote quantas vezes você precisou chamar o LCG. Continue até gerar todas as letras.

Sinclair Schuller
fonte
3
o que é um exemplo de um gerador de números aleatórios não pseudo
chiliNUT
1
@chiliNUT Esses geradores são dispositivos externos. Alguma lâmpada eletrônica. Ou bit mal escrito que é lido como 0 ou 1. Você não pode fazer um gerador digital puro de números aleatórios, os algoritmos digitais NÃO são aleatórios, são absolutamente precisos.
Gangnus
@chiliNUT Muitos sistemas operacionais coletam entropia . Por exemplo, no Linux, você pode usar o /dev/urandomdispositivo para ler dados aleatórios. Este é um recurso escasso, no entanto. Portanto, esses dados aleatórios são normalmente usados ​​para propagar PRNGs.
Adrian W
@AdrianW Wikipedia diz urandomainda está pseudo-aleatório en.wikipedia.org/wiki//dev/random
chiliNUT
1
Sim, mas é criptograficamente seguro, o que significa que não é possível fazer ataques de força bruta (como encontrar a semente para a sequência "aleatória" "olá mundo") com sequências aleatórias criadas a partir de /dev/random. O artigo que citei acima diz o kernel do Linux gera entropia a partir de tempos do teclado, movimentos do mouse e tempos do IDE e disponibiliza os dados aleatórios de caracteres para outros processos do sistema operacional através dos arquivos especiais / dev / random e / dev / urandom. Isso me permite acreditar que é realmente aleatório. Pode ser que não esteja totalmente correto. Mas /dev/randompelo menos contém alguma entropia.
Adrian W
23

Como o multi-threading é muito fácil com Java, aqui está uma variante que procura uma semente usando todos os núcleos disponíveis: http://ideone.com/ROhmTA

import java.util.ArrayList;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

public class SeedFinder {

  static class SearchTask implements Callable<Long> {

    private final char[] goal;
    private final long start, step;

    public SearchTask(final String goal, final long offset, final long step) {
      final char[] goalAsArray = goal.toCharArray();
      this.goal = new char[goalAsArray.length + 1];
      System.arraycopy(goalAsArray, 0, this.goal, 0, goalAsArray.length);
      this.start = Long.MIN_VALUE + offset;
      this.step = step;
    }

    @Override
    public Long call() throws Exception {
      final long LIMIT = Long.MAX_VALUE - this.step;
      final Random random = new Random();
      int position, rnd;
      long seed = this.start;

      while ((Thread.interrupted() == false) && (seed < LIMIT)) {
        random.setSeed(seed);
        position = 0;
        rnd = random.nextInt(27);
        while (((rnd == 0) && (this.goal[position] == 0))
                || ((char) ('`' + rnd) == this.goal[position])) {
          ++position;
          if (position == this.goal.length) {
            return seed;
          }
          rnd = random.nextInt(27);
        }
        seed += this.step;
      }

      throw new Exception("No match found");
    }
  }

  public static void main(String[] args) {
    final String GOAL = "hello".toLowerCase();
    final int NUM_CORES = Runtime.getRuntime().availableProcessors();

    final ArrayList<SearchTask> tasks = new ArrayList<>(NUM_CORES);
    for (int i = 0; i < NUM_CORES; ++i) {
      tasks.add(new SearchTask(GOAL, i, NUM_CORES));
    }

    final ExecutorService executor = Executors.newFixedThreadPool(NUM_CORES, new ThreadFactory() {

      @Override
      public Thread newThread(Runnable r) {
        final Thread result = new Thread(r);
        result.setPriority(Thread.MIN_PRIORITY); // make sure we do not block more important tasks
        result.setDaemon(false);
        return result;
      }
    });
    try {
      final Long result = executor.invokeAny(tasks);
      System.out.println("Seed for \"" + GOAL + "\" found: " + result);
    } catch (Exception ex) {
      System.err.println("Calculation failed: " + ex);
    } finally {
      executor.shutdownNow();
    }
  }
}
TwoThe
fonte
Para java noob como eu, você precisa sufixar o número de saída Le alterar o tipo de argumento para long, ou seja randomString(long i), para brincar. :)
Fruit
21

Aleatório sempre retorna a mesma sequência. É usado para embaralhar matrizes e outras operações como permutações.

Para obter sequências diferentes, é necessário inicializar a sequência em alguma posição, chamada "semente".

O randomSting obtém o número aleatório na posição i (semente = -229985452) da sequência "aleatória". Então usa o ASCII código para o próximo caractere 27 na sequência após a posição inicial até que esse valor seja igual a 0. Isso retorna o "hello". A mesma operação é feita para "mundo".

Eu acho que o código não funcionou para outras palavras. O cara que programou que conhece muito bem a sequência aleatória.

É um ótimo código nerd!

Arnaldo Ignacio Gaspar Véjar
fonte
10
Duvido que ele "conheça muito bem a sequência aleatória". Provavelmente, ele apenas tentou bilhões de sementes possíveis até encontrar uma que funcionasse.
dan04
24
@ dan04 Programadores reais não apenas usam o PRNG, eles lembram todo o período de cor e enumeram valores conforme necessário.
Thomas
1
"Random sempre retorna a mesma sequência" - coloque () depois de Random ou mostre-o como um código. Caso contrário, a sentença é falsa.
Gangnus
14

O principal é que a Classe Aleatória construída com a mesma semente gerará o mesmo padrão de números toda vez.

tomj0101
fonte
12

Derivado da resposta de Denis Tulskiy , esse método gera a semente.

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) {
                for (int i = 0; i < input.length; i++) {
                    if (input[i] != pool[i])
                        continue label;
                }
                return seed;
            }

        }

    throw new NoSuchElementException("Sorry :/");
}
sulai
fonte
10

Nos documentos Java, esse é um recurso intencional ao especificar um valor inicial para a classe Random.

Se duas instâncias de Random forem criadas com a mesma semente e a mesma sequência de chamadas de método for feita para cada uma, elas gerarão e retornarão sequências idênticas de números. Para garantir essa propriedade, algoritmos específicos são especificados para a classe Random. As implementações de Java devem usar todos os algoritmos mostrados aqui para a classe Random, por uma questão de portabilidade absoluta do código Java.

http://docs.oracle.com/javase/1.4.2/docs/api/java/util/Random.html

Estranho, você pensaria que existem problemas implícitos de segurança em ter números 'aleatórios' previsíveis.

deed02392
fonte
3
É por isso que o construtor padrão de Random"define a semente do gerador de números aleatórios para um valor muito provável de ser distinto de qualquer outra invocação desse construtor" ( javadoc ). Na implementação atual, essa é uma combinação do horário atual e de um contador.
martin
De fato. Presumivelmente, existem casos de uso práticos para especificar o valor inicial da semente. Eu acho que é o princípio de funcionamento dos pseudo Keyfobs você pode obter (os RSA?)
deed02392
4
@ deed02392 É claro que existem casos de uso práticos para especificar um valor inicial. Se você estiver simulando dados para usar algum tipo de abordagem de Monte Carlo para resolver um problema, é bom poder reproduzir seus resultados. Definir uma semente inicial é a maneira mais fácil de fazer isso.
Dason
8

É sobre "semente". As mesmas sementes dão o mesmo resultado.

Burak Keceli
fonte
3

Aqui está uma pequena melhoria para a resposta de Denis Tulskiy . Corta o tempo pela metade

public static long[] generateSeed(String goal, long start, long finish) {
    char[] input = goal.toCharArray();

    int[] dif = new int[input.length - 1];
    for (int i = 1; i < input.length; i++) {
        dif[i - 1] = input[i] - input[i - 1];
    }

    mainLoop:
    for (long seed = start; seed < finish; seed++) {
        Random random = new Random(seed);
        int lastChar = random.nextInt(27);
        int base = input[0] - lastChar;
        for (int d : dif) {
            int nextChar = random.nextInt(27);
            if (nextChar - lastChar != d) {
                continue mainLoop;
            }
            lastChar = nextChar;
        }
        if(random.nextInt(27) == 0){
            return new long[]{seed, base};
        }
    }

    throw new NoSuchElementException("Sorry :/");
}
Ilya Gazman
fonte
1

É tudo sobre a semente de entrada . A mesma semente dá os mesmos resultados o tempo todo. Mesmo você reexecutando seu programa repetidamente, é a mesma saída.

public static void main(String[] args) {

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

}

private static void randomString(int i) {
    Random ran = new Random(i);
    System.out.println(ran.nextInt());
    System.out.println(ran.nextInt());
    System.out.println(ran.nextInt());
    System.out.println(ran.nextInt());
    System.out.println(ran.nextInt());

}

Resultado

-755142161
-1073255141
-369383326
1592674620
-1524828502
------------
-755142161
-1073255141
-369383326
1592674620
-1524828502
nagendra547
fonte