为什么使用随机字符串的此代码会打印“ hello world”?


1769

以下打印语句将打印“ 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();
}

158
好吧,这些特殊的种子恰好可以完美地发挥作用。随机不是真正的随机,而是伪随机。
Doorknob

341
正如其他人所说,它是有效的,因为随机不是。对我来说,写那个的人会做一个更有趣的问题,将其强行使用,或者有一种简单的方法来预测给定种子的下一个N值将随机产生什么。暴力破解很容易,并且使用现代硬件不应该花费太长时间,因此这无疑是一种可行的方法。鉴于它是静态的,您甚至可以轻松地在网络上分布搜索。
jmoreno

78
我不知道nin 的目的for (int n = 0; ; n++)。他们可以使用for(;;)while(true)代替!
Eng.Fouad 2013年

13
以真正随机的顺序,每个可能的字符串最终都会出现。在高质量的伪随机序列中,可以合理预期每个可能的长度为(log_s(N)-n)位的字符串(其中N是PRNG内部状态的位数,n是一个小数,为方便起见,请选择8) )出现在循环中。该代码通过使用自由选择的硬编码起点(字符反引号的值)而获得了一些帮助,该起点几乎将整个8位取回来。
dmckee ---前主持人小猫

Answers:


917

java.util.Random使用特定的种子参数(在这种情况下为-229985452-147909649)构造的实例时,它遵循以该种子值开头的随机数生成算法。

每个Random使用相同种子构造的对象每次都会生成相同的数字模式。


8
@Vulcan-Javadoc说种子是48位。 docs.oracle.com/javase/7/docs/api/java/util/Random.html。此外,实际种子是32位值。
斯蒂芬·C

80
所述随机数序列中的每个元素是取模27,并有在每个元件6 "hello\0""world\0"。如果您假设使用真正的随机生成器,则获得所需序列的几率将为27 ^ 6(387,420,489)中的1个-因此,这非常令人印象深刻,但令人难以置信!
罗素·博罗戈夫

17
@RussellBorogove:但是有了这些赔率,加上2 ^ 64个可能的种子,预计该序列有476亿个种子值。这只是找到一个问题。
dan04年

8
@ dan04-我不太愿意做出这个估计;根据PRNG的实现,种子字的大小可能不等于状态的大小,并且序列路径可能不会平均分布。但是仍然,赔率绝对是好的,如果您找不到一对,可以用其他大小写的("Hello" "World")再次尝试,或者使用122-k替代的96+k,或...
Russell Borogove

7
@ThorbjørnRavnAndersen 的Javadoc规定,“特定的算法为Random类。Java实现必须使用一切都在这里显示为类随机算法,对Java代码的可移植性绝对的缘故规定。”
FThompson

1137

其他答案解释了原因,但这是如何做的。

给定一个实例Random

Random r = new Random(-229985452)

r.nextInt(27)生成的前6个数字是:

8
5
12
12
15
0

r.nextInt(27)生成给定的前6个数字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
user253751

1
@immibis为什么?我的意思是Random()应该每次都返回随机数,而不是固定的有序数集?
roottraveller

5
@rootTraveller首先,new Random()根本不返回任何数字。
user253751'3

2
有没有办法计算这些种子?必须有一些逻辑……或者仅仅是蛮力。
Sohit Gore

2
@SohitGore鉴于Java的默认Random密码不是加密安全的(我很确定它是Mersenne Twister,但不要在此引用我的名字),可能有可能从“我想要这些数字”倒退到“这是我会用的种子”。我已经使用标准C线性同余生成器执行了类似的操作。
Fund Monica的诉讼案

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]
Enu.Fouad

30
@Vulcan大多数种子都非常接近最大值,就像您选择1到1000之间的随机数一样,最终选择的大多数数字将具有三位数。当您考虑这并不奇怪:)
托马斯(Thomas)

18
@Vulcan实际上,如果您进行数学计算,您会发现它们几乎与最大值接近零(我认为种子在生成代码中被解释为无符号的)。但是,因为数字的数量仅与实际值成对数增长,所以数字看上去真的很接近,而实际上不是。
汤玛斯

10
好答案。对于奖励积分,您是否可以找到将初始化随机数的种子,该种子将产生初始化最终随机数所需的4个种子的序列?
2013年

13
@Marek:我不认为伪随机神会赞成这种行为。
丹尼斯·图斯基

254

这里的每个人都很好地解释了代码的工作原理,并展示了如何构造自己的示例,但这是一个信息理论性的答案,表明了我们为什么可以合理地期望存在一种最终可以找到蛮力搜索的解决方案。

我们的字母表由26种不同的小写字母组成Σ。为了允许生成不同长度的单词,我们进一步添加了一个终止符以产生扩展的字母Σ' := Σ ∪ {⊥}

α是一个符号和X均匀分布的随机变量了Σ'。获得该符号P(X = α)及其信息内容的概率为I(α)

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

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

对于一个词ω ∈ Σ*及其⊥-终止的对应词ω' := ω · ⊥ ∈ (Σ')*,我们有

I(ω):= I(ω')= |ω'| * log 2(27)=(|ω| + 1)* log 2(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


我的信息理论有点薄弱,但我喜欢这个证明。有人可以向我解释lambda行吗?很明显,我们正在将一个信息的内容除以另一个,但是为什么这会使我们的字长变长?正如我说的那样,我有点生疏,因此很抱歉提出这样的要求(请注意,这与香农限制有关-从代码输出)
Mike HR

1
@ MikeH-R lambda行是I(⍵)重新排列的方程式。I(⍵)是32(位),|⍵|结果是5(符号)。
iceman's

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

我现在在后台运行它,但是已经找到了足够用于经典pangram的单词:

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

我对此很感兴趣,我在词典单词列表上运行了这个随机单词生成器。范围: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 

7
您让我当日的人:DI用Long.Min / Max尝试了一下,并搜索了我的同事的名字,才发现彼得:(彼得4611686018451441623彼得24053719彼得-4611686018403403185彼得-9223372036830722089彼得-4611686017906248248彼得521139777彼得4611686018948527681彼得-92233720363336360 4611686017645756173彼得781631731彼得4611686019209019635彼得-9223372036073144077彼得-4611686017420317288彼得1007070616彼得-9223372035847705192)
马塞尔

25

实际上,大多数随机数生成器都是“伪随机数”。它们是线性同余生成器或LCG(http://en.wikipedia.org/wiki/Linear_congruential_generator

给定一个固定的种子,LCG完全可以预测。基本上,使用可以为您提供第一个字母的种子,然后编写一个继续生成下一个int(字符)的应用程序,直到您击中目标字符串中的下一个字母并记下必须调用LCG的次数为止。继续,直到生成每个字母。


3
什么是非伪随机数生成器的示例
chiliNUT

1
@chiliNUT这样的生成器是外部小工具。一些电子灯。或写入0或1的写错误位。您不能执行随机数的纯数字生成器,数字算法不是随机的,它们绝对精确。
Gangnus

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

对于像我这样的java noob,您需要在输出编号后缀后缀,L并将参数类型更改为longrandomString(long i)以便进行游戏。:)
水果

21

随机总是返回相同的序列。它用于将数组和其他运算作为排列进行改组。

要获得不同的序列,必须在某个位置(称为“种子”)初始化序列。

randomSting获取“随机”序列的i位置(种子= -229985452)的随机数。然后使用ASCII码表示种子位置之后序列中的下一个27个字符,直到该值等于0。这将返回“ hello”。对“世界”执行相同的操作。

我认为该代码不适用于其他任何词。编程的人非常了解随机序列。

这是很棒的极客代码!


10
我怀疑他是否“非常了解随机序列”。更有可能的是,他只是尝试了数十亿种种子,直到找到了可行的种子。
dan04

24
@ dan04真正的程序员不仅使用PRNG,他们还牢记整个过程并根据需要枚举值。
托马斯

1
“随机总是返回相同的序列”-将()放在Random之后或将其显示为代码。否则该句子是错误的。
Gangnus

14

主体是使用相同种子构造的随机类,每次都会生成相同的数字模式。


12

源自Denis Tulskiy的答案,此方法生成种子。

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代码的绝对可移植性,Java实现必须将此处显示的所有算法用于Random类。

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

奇怪的是,您会认为具有可预测的“随机”数字会隐含安全问题。


3
这就是为什么默认构造函数Random“将随机数生成器的种子设置为很可能与该构造函数的任何其他调用不同的值”(javadoc)的原因。在当前实现中,这是当前时间和计数器的组合。
马丁

确实。大概有一些实际的用例来指定初始种子值。我想这就是您可以获得(RSA伪随机密钥)的那些伪随机密钥的工作原理
deed02392 2013年

4
@ deed02392当然,有一些实际的用例来指定种子值。如果您要模拟数据以使用某种蒙特卡洛方法来解决问题,那么能够重现结果将是一件好事。设置初始种子是最简单的方法。
戴森


3

这是Denis Tulskiy 回答的一个小改进。将时间缩短一半

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.