关于Java:如何获得第n个随机的int值?

How to obtain the nth random “nextInt” value?

当使用java.util.random类时,如何获得通过多次调用nextint()方法获得的值,但要以更有效的方式(特别是在o(1)中)?

例如,如果我用一个特定的种子值构造一个随机对象,并且我想要以一种快速的方式得到100000个"nextint()值"(也就是说,在调用方法nextint()100000次后获得的值),我可以这样做吗?

为了简单起见,假设JDK的1.7.06版本,因为可能需要知道类随机中某些私有字段的确切值。说到这里,我发现在计算一个随机值时,下列字段是相关的:

1
2
3
private static final long multiplier = 0x5DEECE66DL;
private static final long addend = 0xBL;
private static final long mask = (1L << 48) - 1;

在研究了一些随机性之后,我发现使用线性同余生成器可以获得随机值。执行算法的实际方法是method next(int):

1
2
3
4
5
6
7
8
9
protected int next(int bits) {
    long oldseed, nextseed;
    AtomicLong seed = this.seed;
    do {
        oldseed = seed.get();
        nextseed = (oldseed * multiplier + addend) & mask;
    } while (!seed.compareAndSet(oldseed, nextseed));
    return (int)(nextseed >>> (48 - bits));
}

算法的相关行是获得下一个种子值的行:

1
nextseed = (oldseed * multiplier + addend) & mask;

所以,更具体地说,有没有一种方法,我可以推广这个公式,得到"nth nextseed"值?在这里我假设在拥有了它之后,我可以简单地通过让变量"bits"为32来获得nth int值(方法next int()只调用next(32)并返回结果)。

提前谢谢

附:也许这是一个更适合数学交流的问题?


你可以在O(log N)时间内完成。从s(0)开始,如果暂时忽略模量(248),我们可以看到(用ma作为multiplieraddend的简写)

1
2
3
4
5
s(1) = s(0) * m + a
s(2) = s(1) * m + a = s(0) * m2 + (m + 1) * a
s(3) = s(2) * m + a = s(0) * m3 + (m2 + m + 1) * a
...
s(N) = s(0) * m^N + (m^(N-1) + ... + m + 1) * a

现在,m^N (mod 2^48)可以很容易地用O(log N)步通过重复平方的模幂运算来计算。

另一部分有点复杂。此时再次忽略模量,几何和为

1
(m^N - 1) / (m - 1)

计算这个模2^48有点不平凡的原因是m - 1不是模的互质性。然而,由于

1
m = 0x5DEECE66DL

m-1的最大公约数和模为4,(m-1)/4的模逆inv的模为2^48。让

1
c = (m^N - 1) (mod 4*2^48)

然后

1
(c / 4) * inv ≡ (m^N - 1) / (m - 1) (mod 2^48)

所以

  • 计算M ≡ m^N (mod 2^50)
  • 计算inv

获得

1
s(N) ≡ s(0)*M + ((M - 1)/4)*inv*a (mod 2^48)


我接受了丹尼尔·费舍尔的回答,因为它是正确的,并给出了一般的解决方案。使用丹尼尔的答案,这里有一个Java代码的具体示例,它显示了公式的基本实现(我广泛使用类BigTigple,因此它可能不是最优的,但是我确认了在实际调用方法NExtTin()n次的基本方法上的显著加速):

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import java.math.BigInteger;
import java.util.Random;


public class RandomNthNextInt {

    // copied from java.util.Random =========================
    private static final long   multiplier  = 0x5DEECE66DL;
    private static final long   addend      = 0xBL;
    private static final long   mask        = (1L << 48) - 1;


    private static long initialScramble(long seed) {

        return (seed ^ multiplier) & mask;
    }

    private static int getNextInt(long nextSeed) {

        return (int)(nextSeed >>> (48 - 32));
    }
    // ======================================================

    private static final BigInteger mod = BigInteger.valueOf(mask + 1L);
    private static final BigInteger inv = BigInteger.valueOf((multiplier - 1L) / 4L).modInverse(mod);


    /**
     * Returns the value obtained after calling the method {@link Random#nextInt()} {@code n} times from a
     * {@link Random} object initialized with the {@code seed} value.
     * <p>

     * This method does not actually create any {@code Random} instance, instead it applies a direct formula which
     * calculates the expected value in a more efficient way (close to O(log N)).
     *
     * @param seed
     *            The initial seed value of the supposed {@code Random} object
     * @param n
     *            The index (starting at 1) of the"nextInt() value"
     * @return the nth"nextInt() value" of a {@code Random} object initialized with the given seed value
     * @throws IllegalArgumentException
     *             If {@code n} is not positive
     */

    public static long getNthNextInt(long seed, long n) {

        if (n < 1L) {
            throw new IllegalArgumentException("n must be positive");
        }

        final BigInteger seedZero = BigInteger.valueOf(initialScramble(seed));
        final BigInteger nthSeed = calculateNthSeed(seedZero, n);

        return getNextInt(nthSeed.longValue());
    }

    private static BigInteger calculateNthSeed(BigInteger seed0, long n) {

        final BigInteger largeM = calculateLargeM(n);
        final BigInteger largeMmin1div4 = largeM.subtract(BigInteger.ONE).divide(BigInteger.valueOf(4L));

        return seed0.multiply(largeM).add(largeMmin1div4.multiply(inv).multiply(BigInteger.valueOf(addend))).mod(mod);
    }

    private static BigInteger calculateLargeM(long n) {

        return BigInteger.valueOf(multiplier).modPow(BigInteger.valueOf(n), BigInteger.valueOf(1L << 50));
    }

    // =========================== Testing stuff ======================================

    public static void main(String[] args) {

        final long n = 100000L; // change this to test other values
        final long seed = 1L; // change this to test other values

        System.out.println(n +"th nextInt (formula) =" + getNthNextInt(seed, n));
        System.out.println(n +"th nextInt (slow)    =" + getNthNextIntSlow(seed, n));
    }

    private static int getNthNextIntSlow(long seed, long n) {

        if (n < 1L) {
            throw new IllegalArgumentException("n must be positive");
        }

        final Random rand = new Random(seed);
        for (long eL = 0; eL < (n - 1); eL++) {
            rand.nextInt();
        }
        return rand.nextInt();
    }
}

注意:注意方法的缩写cramble(long),它用于获取第一个种子值。这是使用特定种子初始化实例时类random的行为。