Чому цей код за допомогою випадкових рядків друкує "привіт світ"?


1769

Наступна заява про друк надрукувала б "привіт світ". Хтось може це пояснити?

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

158
Що ж, саме ці насіння так виходять ідеально. Випадковий насправді не є випадковим, це псевдовипадковий.
Doorknob

341
Це працює, як говорили інші, тому що випадковість не є. Для мене більш цікавим питанням буде людина, яка це написала, груба сила, чи є простий спосіб передбачити, який випадковий стан буде генерувати для наступних N значень для даного насіння. Насильна груба робота проста, а сучасне обладнання не повинно зайняти занадто багато часу, так що це було достатньо можливим для цього. Враховуючи, що він є статичним, ви навіть можете легко розподілити пошук по мережі.
jmoreno

78
Цікаво, мета nв for (int n = 0; ; n++). Вони могли використовувати for(;;)або while(true)замість цього!
Eng.Fouad

13
У справді випадковій послідовності з часом з'явиться кожен можливий рядок. У псевдослучайній послідовності високої якості на розумному можна очікувати всіх можливих рядків довжини (log_s (N) - n) бітів (де N - кількість бітів у внутрішньому стані PRNG, а n - невелике число, давайте вибрати 8 для зручності ) з'являтися в циклі. Цей код отримує деяку допомогу від використання вільно вибраного твердо кодованого початкового пункту (значення символу backtick), який отримує майже цілих 8 біт назад.
dmckee --- кошеня колишнього модератора

13
Це з допису, який я написав пару років тому. vanillajava.blogspot.co.uk/2011/10/randomly-no-so-random.html
Peter Lawrey

Відповіді:


917

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

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


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, розмір початкового слова може не дорівнювати розміру стану, а послідовності шляхів можуть бути не рівномірно розподілені. Але все-таки шанси, безумовно, хороші, і якщо ви не змогли знайти пару, ви можете спробувати ще раз з іншим корпусом ( "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

48
Педантично new Random(-229985452).nextInt(27)завжди повертається 8.
користувач253751

1
@immibis чому? я маю на увазі Random () повинен кожного разу повертати випадкове число, а не встановити впорядкований номер?
roottraveller

5
@rootTraveller Для початку new Random()взагалі не повертає номер.
користувач253751

2
Чи є спосіб обчислити ці насіння? Повинна бути якась логіка ... або це просто груба сила.
Sohit Gore

2
@SohitGore Враховуючи, що за замовчуванням Java не Randomє криптографічно захищеною (я впевнений, що це Mersenne Twister, але не цитуйте мене на цьому), можливо, можна працювати назад від "я хочу цих номерів" до "це насіння я б використав ". Я щось подібне зробив зі стандартним лінійним конгрурентним генератором C.
Фонд позову Моніки

280

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

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, більшість цифр, які ви закінчите збирати, матимуть три цифри. Це не дивно, коли ви задумаєтесь над цим :)
Thomas

18
@Vulcan Насправді, якщо ви зробите математику, ви побачите, що вони приблизно наближені до максимального значення, як до нуля (я вважаю, що насіння трактується як непідписане в коді генерації). Але оскільки кількість цифр зростає лише логарифмічно з фактичним значенням, число виглядає дійсно близьким, коли воно насправді не є.
Томас

10
Чудова відповідь. А для бонусних балів ви можете знайти насіння, яке буде ініціалізувати Random, яке створить послідовність із 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


моя теорія інформації є якоюсь слабкою, але я люблю це підтвердження. чи може хтось пояснити мені лямбда-рядок, чітко ми ділимо інформаційний зміст одного з іншим, але чому це дає нам нашу довжину слова? як я вже говорив, я начебто іржавий, тому вибачте за запитання очевидного (NB - це щось стосується межі Шеннона - від виводу коду)
Майк HR

1
@ MikeH-R Лінія лямбда - це I(⍵)перегруповане рівняння. I(⍵)дорівнює 32 (біт) і |⍵|виявляється 5 (символи).
iceman

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. )

Пс. -727295876, -128911, -1611659, -235516779.


35

Мене це заінтригувало, я запустив цей генератор випадкових слів у список слів словників. Діапазон: Цілий.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 

7
Ви зробили мою людину дня: DI спробував це з Long.Min / Max і знайдіть імена моїх колег і знайшов лише пітер: (peter 4611686018451441623 peter 24053719 peter -4611686018403334185 peter -9223372036830722089 peter -4611686017906248127 peter 521136117777 peter 521136117777 peter 521136377777 peter 521136377777 peter 52113776877 peter 521136117687 peter 52113776877 4611686017645756173 peter 781631731 peter 4611686019209019635 peter -9223372036073144077 peter -4611686017420317288 peter 1007070616 peter -9223372035847705192)
Marcel

25

Більшість генераторів випадкових чисел насправді є "псевдовипадковими". Вони є лінійними конгрурентними генераторами або LCG ( http://en.wikipedia.org/wiki/Linear_congruential_generator )

РГГ є цілком передбачуваним за умови фіксованого насіння. В основному використовуйте насіння, яке дає вам першу літеру, а потім напишіть додаток, який продовжує генерувати наступний int (char), поки ви не натиснете наступну букву в цільовому рядку та запишіть, скільки разів вам довелося викликати LCG. Продовжуйте, поки ви не згенерували кожну букву.


3
Що є прикладом генератора випадкових чисел без псевдо
chiliNUT

1
@chiliNUT Такі генератори - це зовнішні гаджети. Якийсь електронний світильник. Або погано написаний біт, який читається 0 або 1. Ви не можете зробити чистий цифровий генератор випадкових чисел, цифрові алгоритми НЕ випадкові, вони абсолютно точні.
Gangnus

@chiliNUT Багато операційних систем збирають ентропію . Наприклад, в Linux ви можете використовувати /dev/urandomпристрій для читання випадкових даних. Однак це дефіцитний ресурс. Отже, такі випадкові дані зазвичай використовуються для висіву PRNG.
Адріан Ш

@AdrianW Wikipedia каже urandom, що все ще є псевдо випадковим en.wikipedia.org/wiki//dev/random
chiliNUT

1
Так, але це криптографічно безпечно, а це означає, що не можна робити грубі напади (як знайти насіння для "випадкової" послідовності "привіт світ") з випадковими послідовностями, створеними з /dev/random. У статті, яку я цитував вище, йдеться про те, що ядро ​​Linux генерує ентропію за допомогою таймінгів клавіатури, руху миші та IDE та робить дані про випадкові символи доступними для інших процесів операційної системи за допомогою спеціальних файлів / dev / random та / dev / urandom. Це дозволило мені повірити, що це дійсно випадково. Можливо, це не зовсім коректно. Але /dev/randomпринаймні містить деяку ентропію.
Адріан Ш

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

Щоб java noob, як я, потрібно суфіксувати вихідний номер Lі змінити тип аргументу на long, тобто randomString(long i)для того, щоб пограти. :)
Фрукти

21

Випадкові завжди повертають ту саму послідовність. Він використовується для перетасування масивів та інших операцій як перестановки.

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

ВипадковийSting отримує випадкове число у i-положенні (seed = -229985452) послідовності "випадкових". Потім використовує ASCII код для наступних 27 символів у послідовності після позиції насіння, поки це значення не дорівнює 0. Це поверне "привіт". Така ж операція робиться і для "світу".

Я думаю, що код не спрацював для будь-яких інших слів. Хлопець, який запрограмував, дуже добре знає випадкову послідовність.

Це дуже чудовий код geek!


10
Я сумніваюся, чи він "дуже добре знає послідовність випадкових випадків". Швидше за все, він просто спробував мільярди можливих насінин, поки не знайшов того, що спрацювало.
dan04

24
@ dan04 Справжні програмісти не просто використовують PRNG, вони запам’ятовують весь період напам’ять і перераховують значення за потребою.
Томас

1
"Випадкові завжди повертають ту саму послідовність" - поставте () після Random або покажіть її як код. Інакше речення є помилковим.
Gangnus

14

Принциповим є випадковий клас, побудований з одного і того ж насіння, щоразу генерує однаковий візерунок чисел.


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

10

Для документів Java це цілеспрямована функція при вказівці насіннєвого значення для класу Random.

Якщо два екземпляри Random створені з одним і тим самим насінням, і однакова послідовність викликів методів робиться для кожного, вони будуть генерувати та повертати однакові послідовності чисел. Щоб гарантувати цю властивість, конкретні алгоритми задаються для класу Random. Реалізація Java повинна використовувати всі алгоритми, показані тут для класу Random, задля абсолютної портативності коду Java.

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

Як не дивно, ви можете подумати, що при наявності передбачуваних "випадкових" чисел є неявні проблеми безпеки.


3
Ось чому конструктор за замовчуванням Random"встановлює насіння генератора випадкових чисел на величину, імовірно, відрізняється від будь-якого іншого виклику цього конструктора" ( javadoc ). У поточній реалізації це поєднання поточного часу та лічильника.
мартін

Справді. Імовірно, існують випадки практичного використання для визначення початкової вартості насіння. Я думаю, що це принцип роботи тих псевдовипадкових клавіатур, які ви можете отримати (RSA?)
deed02392

4
@ deed02392 Звичайно, є випадки практичного використання для визначення значення насіння. Якщо ви моделюєте дані, щоб використовувати якийсь підхід Монте-Карло до вирішення проблеми, то це добре, щоб мати можливість відтворити свої результати. Встановити початкове насіння - це найпростіший спосіб зробити це.
Дейсон


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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.