java:为什么这段代码使用随机字符串打印”hello world”?

下面的print语句将打印"hello world"。有人能解释一下吗?

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

randomString()看起来是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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();
}


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

给出一个Random的实例:

1
Random r = new Random(-229985452)

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

1
2
3
4
5
6
8
5
12
12
15
0

给定Random r = new Random(-147909649)r.nextInt(27)生成的前6个数字为:

1
2
3
4
5
6
23
15
18
12
4
0

然后将这些数字添加到字符`(即96)的整数表示中:

1
2
3
4
5
6
7
8
9
10
11
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


当使用特定的种子参数构造java.util.Random的实例时(在本例中是-229985452-147909649),它遵循以该种子值开始的随机数生成算法。

每一个由相同种子构造的Random每次都会生成相同的数字模式。


我把它放在这里。无论谁有很多(CPU)时间空闲,都可以进行实验:)此外,如果您掌握了一些fork-join-fu来烧坏所有CPU内核(只是线程很无聊,对吧?),请共享您的代码。非常感谢。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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();
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
-9223372036808280701
Using base: 'Z'
stack
-9223372036853943469
Using base: 'b'
over
-9223372036852834412
Using base: 'e'
flow
-9223372036838149518
Using base: 'd'
rulez
Took 7087 ms


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

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)

由于伪随机数生成器(伪随机数生成器)是用一个32位种子初始化的,所以我们可以期望大多数长度不超过

λ = floor[32/log?(27)] - 1 = 5

由至少一粒种子产生。即使我们搜索一个6个字符的单词,我们仍然有41.06%的成功率。不是太寒酸。

对于7个字母,我们看到的接近1.52%,但在尝试之前我没有意识到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#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


我写了一个快速程序来寻找这些种子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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。)

p -727295876, -128911, -1611659, -235516779


我对此很感兴趣,我在字典单词列表上运行这个随机单词生成器。范围:整数。MIN_VALUE, Integer.MAX_VALUE

我有15131支安打。

1
2
3
4
5
6
7
int[] arrInt = {-2146926310, -1885533740, -274140519,
                -2145247212, -1845077092, -2143584283,
                -2147483454, -2138225126, -2147375969};

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

打印

1
the quick browny fox jumps over a lazy dog


实际上,大多数随机数生成器都是"伪随机"的。它们是线性同全等生成器,或LCGs (http://en.wikipedia.org/wiki/linear_同全等生成器)

给定一个固定的种子,LCGs是可以预测的。基本上,使用一个给你第一个字母的种子,然后编写一个应用程序,继续生成下一个int (char),直到你命中目标字符串中的下一个字母,并写下你调用LCG的次数。继续,直到您生成了每一个字母。


由于使用Java进行多线程非常简单,所以这里有一个变体,它使用所有可用的内核搜索种子:http://ideone.com/ROhmTA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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) &amp;&amp; (seed < LIMIT)) {
        random.setSeed(seed);
        position = 0;
        rnd = random.nextInt(27);
        while (((rnd == 0) &amp;&amp; (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();
    }
  }
}


Random总是返回相同的序列。它用于将数组和其他操作打乱排列。

为了得到不同的序列,需要在某个位置初始化序列,称为"种子"。

random sting得到随机序列i位置的随机数(seed = -229985452)。然后对序列中种子位置之后的下27个字符使用ASCII码,直到该值等于0。这将返回"hello"。同样的操作也适用于"world"。

我认为这段代码对其他任何单词都不起作用。编程的人对随机序列非常了解。

这是非常棒的极客代码!


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


根据丹尼斯·图尔斯基的答案,这个方法产生了种子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 :/");
}


在Java文档中,当为Random类指定种子值时,这是一个有意的特性。

If two instances of Random are created with the same seed, and the
same sequence of method calls is made for each, they will generate and
return identical sequences of numbers. In order to guarantee this
property, particular algorithms are specified for the class Random.
Java implementations must use all the algorithms shown here for the
class Random, for the sake of absolute portability of Java code.

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

不过,奇怪的是,您可能会认为拥有可预测的"随机"数字存在隐含的安全问题。


它是关于"种子"的。同样的种子产生同样的结果。


下面是丹尼斯·图尔斯基的一个小改进。它把时间缩短了一半

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 :/");
}