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

1769

Следующее утверждение print выдает «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();
}
0x56794E
источник
158
Ну, эти особые семена просто отлично работают. Случайное не является действительно случайным, это псевдослучайный.
TKMN
341
Это работает, как говорили другие, потому что случайное не так. Для меня более интересным вопросом будет тот, кто написал это, грубо форсируя это, или есть простой способ предсказать, какое случайное число сгенерирует для следующих N значений для данного семени. Грубое принуждение легко и с современным оборудованием не должно занять слишком много времени, так что это был жизнеспособный способ сделать это. Учитывая, что он статический, вы можете легко распределить поиск по сети.
Jmoreno
78
Интересно, цель nв for (int n = 0; ; n++). Они могли бы использовать for(;;)или while(true)вместо этого!
Eng.Fouad
13
В действительно случайной последовательности каждая возможная строка в конечном итоге появится. В высококачественной псевдослучайной последовательности можно ожидать каждой возможной строки длиной (log_s (N) - n) битов (где N - количество бит во внутреннем состоянии PRNG, а n - небольшое число, для удобства выберем 8 ) появляться в цикле. Этот код получает некоторую помощь от использования свободно выбранной жестко заданной начальной точки (значение обратного удара символа), которая возвращает почти целые 8 битов назад.
dmckee --- котенок экс-модератора
13
Это из публикации, которую я написал пару лет назад. vanillajava.blogspot.co.uk/2011/10/randomly-no-so-random.html
Питер Лори

Ответы:

917

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

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

FThompson
источник
8
@Vulcan - Javadoc говорит, что семя 48 бит. docs.oracle.com/javase/7/docs/api/java/util/Random.html . И кроме того, фактические семена являются 32-битными значениями.
Стивен С
80
Каждый элемент последовательности случайных чисел берется по модулю 27, и в каждом из "hello\0"и есть 6 элементов "world\0". Если вы предполагаете, что генератор действительно случайный, то шансы получить искомую последовательность будут равны 1 к 27 ^ 6 (387 420 489), так что это довольно впечатляюще, но не настолько ошеломительно!
Рассел Борогове
17
@RussellBorogove: Но с этими шансами и 2 ^ 64 возможными начальными значениями, есть ожидаемые 47,6 миллиардов начальных значений, которые дают эту последовательность. Это просто вопрос поиска.
dan04
8
@ dan04 - Я не очень хотел делать такую ​​оценку; в зависимости от реализации PRNG размер начального слова может не совпадать с размером состояния, а пути последовательности могут быть неравномерно распределены. Но, тем не менее, шансы, безусловно, хорошие, и если вы не можете найти пару, вы можете попробовать еще раз с другим casing ( "Hello" "World"), или использовать 122-kвместо 96+k, или ...
Рассел Борогове
7
@ ThorbjørnRavnAndersen В Javadoc указано, что «для класса Random указаны конкретные алгоритмы. Реализации Java должны использовать все алгоритмы, показанные здесь для класса Random, для абсолютной переносимости кода Java».
FThompson
1137

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

Учитывая экземпляр 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
Eng.Fouad
источник
48
Педантично, new Random(-229985452).nextInt(27)всегда возвращает 8.
user253751
1
@immibis почему? я имею в виду, что Random () должен возвращать случайное число каждый раз, а не фиксированный номер?
roottraveller
5
@rootTraveller Для начала new Random()не возвращает число вообще.
user253751
2
Есть ли способ рассчитать эти семена? Должна быть какая-то логика ... или это просто грубая сила.
Сохит Гор
2
@SohitGore Учитывая, что Java по умолчанию Randomне криптографически безопасен (я уверен, что это Twister Mersenne, но не цитируйте меня по этому поводу), возможно, можно вернуться назад от «Я хочу эти цифры» к «это семя я бы использовал ". Я сделал нечто подобное со стандартным C линейным конгруэнтным генератором.
Фонд Моника судебный процесс
280

Я просто оставлю это здесь. Тот, у кого есть много свободного времени (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
Денис Тульский
источник
24
@OneTwoThree nextInt(27)означает в пределах диапазона [0, 26].
Eng.Fouad
30
@Vulcan Большинство семян очень близко к максимальному значению, так же, как если вы выберете случайные числа от 1 до 1000, большинство чисел, которые вы в итоге выберете, будет иметь три цифры. Это не удивительно, когда вы думаете об этом :)
Томас
18
@Vulcan На самом деле, если вы выполните математику, вы увидите, что они примерно так же близки к максимальному значению, как и к нулю (я полагаю, что семя интерпретируется как неподписанный в коде генерации). Но так как количество цифр растет только логарифмически с фактическим значением, число выглядит действительно близко, когда это действительно не так.
Томас
10
Отличный ответ. И для бонусных баллов, можете ли вы найти начальное число, которое инициализирует Случайное число, которое произведет последовательность из 4 начальных чисел, необходимых для инициализации финальных рандомов?
Марек
13
@Marek: Я не думаю, что боги псевдослучайного характера одобрили бы такое поведение.
Денис Тульский
254

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

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

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

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

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

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

I (ω): = 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

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

Я написал быструю программу, чтобы найти эти семена:

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

( Демонстрация на ideone. )

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

Илмари Каронен
источник
35

Я был заинтригован этим, я запустил этот генератор случайных слов в списке словарных слов. Диапазон: от 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 
Puru--
источник
7
Вы сделали мой день человек: DI пробовал с Long.Min / Max и искать имена моих коллег и найти только питер: (питер 4611686018451441623 питер 24053719 питер -4611686018403334185 питер -9223372036830722089 питер -4611686017906248127 питер 521139777 питер 4611686018948527681 питер -9223372036333636031 питер - 4611686017645756173 Питер 781631731 Питер 4611686019209019635 Питер -9223372036073144077 Питер -4611686017420317288 Питер 1007070616 Питер -9223372035847705192)
Марсель
25

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

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

Синклер Шуллер
источник
3
Что является примером генератора не псевдослучайных чисел
chiliNUT
1
@chiliNUT Такие генераторы - это внешние гаджеты. Какой-то электронный светильник. Или плохо записанный бит, который читается 0 или 1. Вы не можете сделать чистый цифровой генератор случайных чисел, цифровые алгоритмы НЕ случайны, они абсолютно точны.
Гангнус
@chiliNUT Многие операционные системы накапливают энтропию . Например, в Linux вы можете использовать /dev/urandomустройство для чтения случайных данных. Однако это скудный ресурс. Таким образом, такие случайные данные обычно используются для заполнения PRNG.
Адриан W
@AdrianW Википедия говорит, urandomчто все еще псевдослучайный en.wikipedia.org/wiki//dev/random
chiliNUT
1
Да, но это криптографически безопасно, что означает, что никто не может совершать атаки методом грубой силы (например, найти семя для "случайной" последовательности "hello world") со случайными последовательностями, созданными из /dev/random. В статье, которую я цитировал выше, говорится, что ядро Linux генерирует энтропию из таймингов клавиатуры, движений мыши и IDE и делает случайные символьные данные доступными для других процессов операционной системы через специальные файлы / dev / random и / dev / urandom. Это позволило мне поверить, что это действительно случайно. Может быть, это не совсем правильно. Но, /dev/randomпо крайней мере, содержит некоторую энтропию.
Адриан W
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();
    }
  }
}
TwoThe
источник
Чтобы java noob нравился мне, вам нужно добавить суффикс к выходному номеру Lи изменить тип аргумента на long, т.е. randomString(long i)чтобы поиграться. :)
Фрукты
21

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

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

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

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

Это очень хороший компьютерный код!

Арнальдо Игнасио Гаспар Вехар
источник
10
Я сомневаюсь, что он "очень хорошо знает Случайную последовательность". Скорее всего, он просто пробовал миллиарды возможных семян, пока не нашел тот, который сработал.
2013 года
24
@ dan04 Настоящие программисты не просто используют PRNG, они запоминают весь период наизусть и по мере необходимости перечисляют значения.
Томас
1
«Случайные всегда возвращают одну и ту же последовательность» - поставьте () после «Случайного» или покажите это как код. В противном случае предложение является ложным.
Гангнус
14

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

tomj0101
источник
12

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

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
источник
10

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

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

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

Как ни странно, вы могли бы подумать, что существуют неявные проблемы безопасности, связанные с предсказуемыми «случайными» числами.

deed02392
источник
3
Вот почему конструктор по умолчанию Random«устанавливает начальное значение генератора случайных чисел на значение, которое, скорее всего, будет отличаться от любого другого вызова этого конструктора» ( javadoc ). В текущей реализации это комбинация текущего времени и счетчика.
мартин
Верно. Предположительно, существуют практические варианты использования для определения начального начального значения. Я думаю, что это принцип действия тех псевдослучайных брелков, которые вы можете получить (RSA?)
deed02392
4
@ deed02392 Конечно, существуют практические варианты использования для определения начального значения. Если вы моделируете данные, чтобы использовать какой-то подход Монте-Карло для решения проблемы, то хорошо бы иметь возможность воспроизвести ваши результаты. Установка начального семени - самый простой способ сделать это.
Дейсон
8

Речь идет о «семени». Те же семена дают тот же результат.

Бурак Кечели
источник
3

Вот небольшое улучшение для ответа Дениса Тульского . Это сокращает время вдвое

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 :/");
}
Илья Газман
источник
1

Это все о входном семени . Одно и то же семя все время дает одни и те же результаты. Даже если вы снова и снова запускаете свою программу, это один и тот же вывод.

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

}

Вывод

-755142161
-1073255141
-369383326
1592674620
-1524828502
------------
-755142161
-1073255141
-369383326
1592674620
-1524828502
nagendra547
источник