关于算法:复制java.util.Random类型的实例变量以创建处于相同状态的对象

Copy instance variable of type java.util.Random to create object in same state

我正在实现一个模拟退火(SA)算法,在这里我需要复制状态(例如,要记住迄今为止最好的解决方案)。

我实现了一个复制方法,因为它不鼓励使用Java的EDCOX1 0。

SA是一种启发式算法,因此要采取的下一步是随机确定的。这是通过使用一个Random对象来完成的,我也想复制它。

虽然算法没有要求它,但我希望副本具有完全相同的状态。但是,如果我在对象创建之后直接创建一个"拷贝"并用相同的种子初始化它,情况就只有这样了。

但是,如果在复制过程之前随机执行一些操作,那么theRandom对象的内部状态(即种子)会发生变化,并且复制的行为也会有所不同。

那么,我怎样才能得到一个java.util.Random实例的精确副本呢?

例子

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
public class State
{
  private final Random r;
  private final long seed;

  private Object currentOperand;

  public State()
  {
    this(System.nanoTime(), null);
  }

  private State(long seed, Object currentOperand)
  {
    this.seed = seed;
    this.r = new Random(seed);
    this.currentOperand = currentOperand;
  }

  public State copy()
  {
    return new State(seed, currentOperand);
  }

  public void doSth()
  {
    /* operation with random operand */
    currentOperand = r.nextInt(100);
  }

  public void redo()
  {
    // redo then set to null
    currentOperand = null;
  }

  /* for completeness' sake... since it's simulated annealing */
  public int computeEnergy() { return 0; }
}


我知道这是一个老问题,有一个公认的答案,但我在寻找答案的时候遇到了这个问题,最后我采取了不同的方法。

看到Mike上面提到的Random实现Serializable,我只是用它来复制:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
 * Uses serialization to create a copy of the given Random, needed for
 * repeatability in some tests.
 */

public static Random cloneRandom(Random src) throws Exception {
    ByteArrayOutputStream bo = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(bo);
    oos.writeObject(src);
    oos.close();
    ObjectInputStream ois = new ObjectInputStream(
            new ByteArrayInputStream(bo.toByteArray()));
    return (Random)(ois.readObject());
}

它的性能可能不如Mike的CopyableRandom,但它要简单得多,而且对于我的小单元测试来说已经足够了。

(我有一个现有的单元测试,从一个已知种子的Random开始,然后执行了一系列操作;我试图在测试中间添加一点,并且想要一个Random的副本;在测试中间调用nextLong()或类似的名称,以获得/a种子将要改变种子,将其余的F测试。真的,我只是想要一个像Random.getSeed()这样的东西。)


我想出了自己的解决办法。它主要覆盖Random中的next()(因为所有其他方法都依赖于该方法),以及一些其他的东西来保持一致性。

它提供调用此方法的实例的精确副本(制作随机实例的副本是否有意义是另一个主题…^^)。它应该表现得像它的超级班级,至少这是我的意图。

请随意添加您的想法!

因为其他的问题是如何获得种子:我们可以很容易地在我的解决方案中添加一个getSeed()方法。或getInitialSeed()getCurrentSeed()

1
2
3
4
5
6
7
/* Bounded parameter type since a class that implements this interface
 * should only be able to create copies of the same type (or a subtype).
 */

public interface Copyable<T extends Copyable<T>>
{
  public T copy();
}
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
public class CopyableRandom extends Random implements Copyable<CopyableRandom>
{
  private final AtomicLong seed = new AtomicLong(0L);

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

  public CopyableRandom() { this(++seedUniquifier + System.nanoTime()); }
  private static volatile long seedUniquifier = 8682522807148012L;

  public CopyableRandom(long seed) { this.seed.set((seed ^ multiplier) & mask); }

  /* copy of superclasses code, as you can seed the seed changes */
  @Override
  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));
  }

  /* necessary to prevent changes to seed that are made in constructor */
  @Override
  public CopyableRandom copy() { return new CopyableRandom((seed.get() ^ multiplier) & mask); }

  public static void main(String[] args)
  {
    CopyableRandom cr = new CopyableRandom();

    /* changes intern state of cr */
    for (int i = 0; i < 10; i++)
      System.out.println(cr.nextInt(50));

    Random copy = cr.copy()

    System.out.println("
TEST: INTEGER
"
);
    for (int i = 0; i < 10; i++)
      System.out.println("CR\t=" + cr.nextInt(50) +"
COPY\t="
+ copy.nextInt(50) +"
"
);

    Random anotherCopy = (copy instanceof CopyableRandom) ? ((CopyableRandom) copy).copy() : new Random();
    System.out.println("
TEST: DOUBLE
"
);
    for (int i = 0; i < 10; i++)
      System.out.println("CR\t=" + cr.nextDouble() +"
A_COPY\t="
+ anotherCopy.nextDouble() +"
"
);
  }
}

主要方法的输出如下:

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
19
23
26
37
41
34
17
28
29
6

TEST: INTEGER

CR      = 3
COPY    = 3

CR      = 18
COPY    = 18

CR      = 25
COPY    = 25

CR      = 9
COPY    = 9

CR      = 24
COPY    = 24

CR      = 5
COPY    = 5

CR      = 15
COPY    = 15

CR      = 5
COPY    = 5

CR      = 30
COPY    = 30

CR      = 26
COPY    = 26


TEST: DOUBLE

CR      = 0.7161924830704971
A_COPY  = 0.7161924830704971

CR      = 0.06333509362539957
A_COPY  = 0.06333509362539957

CR      = 0.6340753697524675
A_COPY  = 0.6340753697524675

CR      = 0.13546677259518425
A_COPY  = 0.13546677259518425

CR      = 0.37133033932410586
A_COPY  = 0.37133033932410586

CR      = 0.796277965335522
A_COPY  = 0.796277965335522

CR      = 0.8610310118615391
A_COPY  = 0.8610310118615391

CR      = 0.793617231340077
A_COPY  = 0.793617231340077

CR      = 0.3454111197621874
A_COPY  = 0.3454111197621874

CR      = 0.25314618087856255
A_COPY  = 0.25314618087856255

我还做了一个测试,我比较了可复制随机和随机。结果是一样的。

1
2
3
4
long seed = System.nanoTime();

Random cr  = new CopyableRandom(seed);
Random cmp = new Random(seed);


我认为你应该在你的State类中存储,不仅要开始种子,还要存储你已经调用nextInt()的次数。这是由于Random生成伪随机数序列这一固有事实造成的。即:

A pseudo random number generator can be started from an arbitrary starting state using a seed state. It will always produce the same sequence thereafter when initialized with that state

让我先给你解释一下样品:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args){

     Random r = new Random(42);      
     Random s = new Random(42);

     for(int i = 0; i < 5; i++){
       System.out.println("First random" +r.nextInt());
     }

     for(int i = 0; i < 5; i++){
       System.out.println("Second random" +s.nextInt());
     }

  }

结果是:

1
2
3
4
5
6
7
8
9
10
First random -1170105035
First random 234785527
First random -1360544799
First random 205897768
First random 1325939940
Second random -1170105035
Second random 234785527
Second random -1360544799
Second random 205897768
Second random 1325939940

由于两个随机实例都以相同的种子开始,所以我总是得到相同的数字序列。

因此,在复制对象时,您应该将新的Random初始化到源的相同种子(您已经这样做了),并"使用"您已经在源对象中使用的nextInt()的调用(这就是您必须保留该编号的原因)。

完成此操作后,在副本上调用nextInt()将得到与源对象相同的结果。我希望我的代码足以重构您的代码,让您理解我的解决方案。

为了更好地理解伪随机数生成器(prng),也称为确定性随机位生成器(drbg),您可以查看这篇维基百科文章。