Почему этот код с использованием случайных строк выводит «hello world»?

1619

Следующий оператор печати напечатает "hello world". Может ли кто-нибудь объяснить это?

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

И randomString() выглядит следующим образом:

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();
}
  • 144
    Ну, эти особые семена просто отлично работают. Случайное не является действительно случайным, это псевдослучайный.
  • 331
    Это работает, как говорили другие, потому что случайное не так. Для меня более интересным вопросом будет тот, кто написал это, грубо форсируя это, или есть простой способ предсказать, какое случайное число сгенерирует для следующих N значений для данного семени. Грубое принуждение легко и с современным оборудованием не должно занять слишком много времени, так что это был жизнеспособный способ сделать это. Учитывая, что он статический, вы можете легко распределить поиск по сети.
Показать ещё 32 комментария
Теги:
string
random

14 ответов

816
Лучший ответ

Когда экземпляр java.util.Random создается с определенным параметром семени (в этом случае -229985452 или -147909649), он следует алгоритму генерации случайных чисел, начиная с этого начального значения.

Каждый Random, построенный с одним и тем же семенем, каждый раз генерирует одинаковый шаблон чисел.

  • 0
    Было ли это тем, что люди, которые специально разрабатывали Java, делали это (как пасхальное яйцо?). Просто немного ошеломляюще, что вы могли бы создать строку Hello, world String, которая оказалась самой любимой строкой в любых языках программирования.
  • 8
    @Vulcan - Javadoc говорит, что семя 48 бит. docs.oracle.com/javase/7/docs/api/java/util/Random.html . И кроме того, фактические семена - это 32-битные значения.
Показать ещё 6 комментариев
1040

Другие ответы объясняют, почему, но вот как.

Для экземпляра Random:

Random r = new Random(-229985452)

Первые 6 чисел, которые r.nextInt(27) порождают:

8
5
12
12
15
0

а первые 6 чисел, которые r.nextInt(27) генерируют заданные Random r = new Random(-147909649), следующие:

23
15
18
12
4
0

Затем просто добавьте эти числа в целочисленное представление символа ` (что равно 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
  • 44
    Педантично, new Random(-229985452).nextInt(27) всегда возвращает 8.
  • 1
    @immibis почему? я имею в виду, что Random () должен возвращать случайное число каждый раз, а не фиксированный номер?
Показать ещё 4 комментария
262

Все здесь проделали большую работу по разъяснению того, как работает код, и показывая, как вы можете создавать свои собственные примеры, но здесь есть теоретический ответ на информацию, объясняющий, почему мы можем разумно ожидать, что существует решение, которое в конечном итоге найдет поиск грубой силы.

26 различных строчных букв образуют наш алфавит Σ. Чтобы разрешить генерировать слова разной длины, добавим еще символ терминатора , чтобы получить расширенный алфавит Σ' := Σ ∪ {⊥}.

Пусть α - символ, а X - равномерно распределенная случайная величина над Σ'. Вероятность получения этого символа P(X = α) и его информационного содержимого I(α) дается следующим образом:

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

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

Для слова ω ∈ Σ* и его ⊥- завершенного аналога ω' := ω · ⊥ ∈ (Σ')* имеем

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

Поскольку генератор псевдослучайных чисел (PRNG) инициализируется 32-битным семенем, мы можем ожидать, что большинство слов длиной до

λ = пол [32/log₂ (27)] - 1 = 5

чтобы сгенерировать по меньшей мере одно семя. Даже если мы будем искать 6-символьное слово, мы все равно будем добиваться успеха примерно в 41,06% случаев. Не слишком потрепанный.

Для 7 букв мы смотрим ближе к 1.52%, но я не понял, что перед тем, как попробовать:

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

Смотрите вывод: http://ideone.com/JRGb3l

  • 0
    моя теория информации слабовата, но я люблю это доказательство. может ли кто-нибудь объяснить мне лямбда-линию, ясно, что мы делим информационное содержание одного с другим, но почему это дает нам нашу длину слова? как я уже сказал, я немного извиняюсь, поэтому извиняюсь за то, что спрашиваю об очевидном (обратите внимание, что это как-то связано с пределом Шеннона от вывода кода)
  • 1
    @ MikeH-R Лямбда-линия - это I(⍵) уравнение I(⍵) . I(⍵) составляет 32 (бит) и |⍵| оказывается 5 (символы).
251

Я просто оставлю это здесь. Тот, у кого есть много (CPU) времени, чтобы избавиться, не стесняйтесь экспериментировать:) Кроме того, если вы освоили некоторый fork-join-fu, чтобы заставить эту вещь записывать все ядра процессора (просто потоки скучны, правильно?), Пожалуйста, поделитесь ваш код. Я был бы очень признателен.

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

Вывод:

-9223372036808280701
Using base: 'Z'
stack
-9223372036853943469
Using base: 'b'
over
-9223372036852834412
Using base: 'e'
flow
-9223372036838149518
Using base: 'd'
rulez
Took 7087 ms
  • 3
    Я заметил, что сгенерированные семена очень близки к Long.MAX_VALUE . Было бы интересно узнать, сколько разных начальных чисел может генерировать каждую строку, но у меня нет возможности обработать ее, чтобы добиться этого разумным и своевременным способом.
  • 2
    Почему это должно быть .nextInt(27) ? Что такого особенного в номере 27 ? Я думал, что если вы просто nextInt() , у вас будет больше шансов найти семя? Кроме того, почему вы проверяете только первые x чисел последовательности (где x == goal.length() )?
Показать ещё 15 комментариев
68

Я написал краткую программу для поиска этих семян:

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

У меня он работает в фоновом режиме сейчас, но он уже нашел достаточно слов для классической панграмы:

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

(Демо на идеоне.)

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

31

Я был заинтригован этим, я запустил этот генератор случайных слов в списке словарных слов. Диапазон: Integer.MIN_VALUE для Integer.MAX_VALUE

Я получил 15131 хитов.

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

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

Печать

the quick browny fox jumps over a lazy dog 
  • 6
    Вы сделали мой день человек: DI пробовал с Long.Min / Max и искать имена моих коллег и найти только питер: (питер 4611686018451441623 питер 24053719 питер -4611686018403334185 питер -9223372036830722089 питер -4611686017906248127 питер 521139777 питер 4611686018948527681 питер -9223372036333636031 питер - 4611686017645756173 peter 781631731 peter 4611686019209019635 peter -9223372036073144077 peter -4611686017420317288 peter 1007070616 peter -9223372035847705192)
26

Большинство генераторов случайных чисел являются, по сути, "псевдослучайными". Это линейные конгруэнтные генераторы или LCG (http://en.wikipedia.org/wiki/Linear_congruential_generator)

LCG вполне предсказуемы при фиксированном семени. В основном, используйте семя, которое дает вам первую букву, а затем напишите приложение, которое продолжает генерировать следующий int (char), пока вы не нажмете следующую букву в целевой строке и не запишите, сколько раз вам приходилось вызывать LCG, Продолжайте, пока не сгенерируете каждую букву.

  • 2
    Что является примером генератора не псевдослучайных чисел
  • 1
    @chiliNUT Такие генераторы являются внешними гаджетами. Какой-то электронный светильник. Или плохо записанный бит, который читается 0 или 1. Вы не можете сделать чистый цифровой генератор случайных чисел, цифровые алгоритмы НЕ случайны, они абсолютно точны.
Показать ещё 3 комментария
23

Поскольку многопоточность с Java очень проста, вот вариант, который ищет семя, используя все доступные ядра: 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();
    }
  }
}
  • 0
    Чтобы java noob нравился мне, вам нужно randomString(long i) суффикс выходного числа к L и изменить тип аргумента на long , то есть randomString(long i) , чтобы поиграть. :)
22

Случайные всегда возвращают одну и ту же последовательность. Он использовался для перетасовки массивов и других операций в качестве перестановок.

Чтобы получить разные последовательности, необходимо инициализировать последовательность в некоторой позиции, называемой "семя".

Случайное значение получает случайное число в позиции я (seed = -229985452) "случайной" последовательности. Затем используется ASCII код для следующего 27 символов в последовательности после позиции семени, пока это значение не станет равно 0. Это возвращает "привет". Та же операция выполняется для "мира".

Я думаю, что код не работал ни на какие другие слова. Парень, который запрограммировал, что очень хорошо знает случайную последовательность.

Это очень классный код!

  • 10
    Я сомневаюсь, что он «очень хорошо знает Случайную последовательность». Скорее всего, он просто пробовал миллиарды возможных семян, пока не нашел тот, который сработал.
  • 24
    @ dan04 Настоящие программисты не просто используют PRNG, они помнят весь период наизусть и по мере необходимости перечисляют значения.
Показать ещё 1 комментарий
13

Принцип действия - это случайный класс, построенный с одним и тем же семенем, каждый раз генерирует одинаковый шаблон чисел.

13

Получено из Денис Тульский, этот метод генерирует семя.

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 :/");
}
11

В документах Java это преднамеренная функция при задании начального значения для класса Random.

Если два экземпляра Random созданы с одним и тем же семенем, а для каждой из них выполняется одна и та же последовательность вызовов методов, они будут генерировать и возвращать идентичные последовательности чисел. Чтобы гарантировать это свойства, определенные алгоритмы заданы для класса Random. Реализации Java должны использовать все алгоритмы, показанные здесь для class Random, для абсолютной переносимости Java-кода.

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

Однако, вы бы подумали, что существуют неявные проблемы безопасности при наличии предсказуемых "случайных" чисел.

  • 3
    Вот почему конструктор по умолчанию для Random «устанавливает начальное значение генератора случайных чисел на значение, которое, скорее всего, будет отличаться от любого другого вызова этого конструктора» ( javadoc ). В текущей реализации это комбинация текущего времени и счетчика.
  • 0
    В самом деле. Предположительно, существуют практические варианты использования для указания начального начального значения. Я думаю, что это принцип действия этих псевдослучайных брелков, которые вы можете получить (RSA?)
Показать ещё 1 комментарий
8

Это о "семени". Те же самые семена дают тот же результат.

3

Вот небольшое улучшение для Denis Tulskiy answer. Он сокращает время наполовину

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 :/");
}

Ещё вопросы

Сообщество Overcoder
Наверх
Меню