Java的Random类可能分布不均或执行不好?

时间:2017-11-28 14:34:08

标签: java

我正在修补细胞自动机,我的动作检测功能非常奇怪。我80%肯定这是我的实施,但我不知道问题出在哪里。有人可以看看并启发我,因为我已经花了7H的更好的部分试图让它工作,它不会:

private int[] cellularSearch(short xPos, short yPos)
{
 // the center position is our current position; the others are potentially free positions
 byte[][] positions = new byte[][]{{0,0,0},{0,1,0},{0,0,0}};          
 int[] result = new int[2];
 byte strike=0;
 int dice0=0, dice1=0;


  while(strike<9)
  {
    dice0 =  r.nextInt(3)-1;
    result[0] = xPos + dice0;

    if((result[0] >= 0)
    && (result[0] < img.getWidth()))
    {
        dice1 = r.nextInt(3)-1;
        result[1] = yPos + dice1;

        if((result[1] >= 0)
        && (result[1] < img.getHeight()))
        {
           if((positions[dice1+1][dice0+1] != 1))       // if this isn't our own cell and wasn't tried before
           {
                if(img.getRGB(result[0], result[1]) == Color.white.getRGB())     // if the new cell is free
                {
                    return result;
                }
           }

           positions[dice1+1][dice0+1]=1;       // we need to use +1 to create a correlation between the linkage in the matrix and the actual positions around our cell
           strike++;
        }
    }
  }
}

代码有效,它可以正确识别像素何时为白色并返回其位置。我的问题是结果的分配。鉴于我对行和列都使用了Random,我期望在所有可能的位置上分布几乎相等,但是会发生的是这个代码似乎更喜欢正在输入的坐标上方的单元格(它击中它比其他的大约3倍)和坐标下方的那个(它比其他坐标高出约2倍)。

当我启动我的程序时,我的所有像素在每次运行时慢慢移向窗口的顶部(与我的旧冗长代码的真实随机性是3倍的长度),所以在某处有一个错误。有人可以帮忙吗?

提前谢谢!

编辑:谢谢大家的努力!对于非编译代码很抱歉,但我删除了函数的主要目的,同时删除了大量注释代码(我实现此函数的其他方法)。在本地,代码具有return语句并且运行。我会在接下来的几个小时内慢慢完成你的所有答案(很快就会吃晚餐)。

EDIT2:我尝试了@DodgyCodeException和@tevemadar建议并列出了所有8个位置的列表,然后每次进入函数时将它们随机播放,然后迭代它们,尝试各个部分。仍然恰好在当前单元格的正上方和下方的位置被选中。我很困惑。这是我为这个函数编写的旧的超级意大利面条代码,它完美地运行,没有错误,平等分配,并且(奇怪的是)它是我尝试过的最有效的实现。在我吃完午餐并提交一些文书工作之后,我会彻底研究它(我写这篇文章已经有两年了),看看为什么它能很好地运作。如果还有人有想法,我会完全开放。

 boolean allRan=false;
 int lastDice=0, anteLastDice=0, dice = r.nextInt(3)+1;

 //the initial dice usage is for selecting the row on which we'll operate:
 //dice = 1 or 3 -> we operate above or under our current cell; dice = 2 -> we operate on the same row
while(!allRan)
{
 if((dice==1) || (dice==3))
    {
    int i= r.nextInt(3);

    if(((xPos-1+i) < img.getWidth())
    && ((xPos-1+i) >= 0))
       {
       if(((yPos-1) >= 0)
       && (img.getRGB(xPos-1+i, yPos-1) == Color.white.getRGB())
       && (dice==1))
          {
          result[0] = xPos-1+i;
          result[1] = yPos-1;
          above++;

          endTime = (int) System.currentTimeMillis();
          section4Runtime += (double) (endTime - startTime) / 1000;
          return result;
          }
       else if(((yPos+1) < img.getHeight())
       && (img.getRGB(xPos-1+i, yPos+1) == Color.white.getRGB())
       && (dice==3))
          {
          result[0] = xPos-1+i;
          result[1] = yPos+1;
          below++;

          endTime = (int) System.currentTimeMillis();
          section4Runtime += (double) (endTime - startTime) / 1000;
          return result;
          }
       }

    // if this section is reached, it means that: the initial dice roll didn't find a free cell, or the position was out of bounds, or the dice rolled 2
    // in this section we do a dice reroll (while remembering and avoiding our previous values) so that we cover all dice rolls
    if(dice==1)
        {
         if(lastDice==0)
            {
            lastDice=dice;
            dice += r.nextInt(2)+1;     // we incrmeent randomly towards 2 or 3.
            }
         else
            {
             if(lastDice==2)
                 {
                  if(anteLastDice==0)
                    {
                     anteLastDice= lastDice;
                     lastDice=dice;
                     dice=3;
                    }
                  else
                    {
                     allRan=true;
                    }
                 }
             else if(lastDice==3)
                 {
                  if(anteLastDice==0)
                    {
                     anteLastDice= lastDice;
                     lastDice=dice;
                     dice=2;
                    }
                  else
                    {
                     allRan=true;
                    }
                 }
            }
        }
        else        // dice is 3
        {
            if(lastDice==0)
            {
             lastDice=dice;
             dice -= r.nextInt(2)+1;     // we decrement randomly towards 2 or 1.
            }
         else
            {
             if(lastDice==2)
                 {
                  if(anteLastDice==0)
                    {
                     anteLastDice= lastDice;
                     lastDice=dice; 
                     dice=1;
                    }
                  else
                    {
                     allRan=true;
                    }
                 }
             else if(lastDice==1)
                 {
                  if(anteLastDice==0)
                    {
                     anteLastDice= lastDice;
                     lastDice=dice;
                     dice=2;
                    }
                  else
                    {
                     allRan=true;
                    }
                 }
            }
        }
    }

 if(dice==2)
    {
    int i=0;
    i += r.nextInt(2)==0?-1:1;

    if(((xPos+i) < img.getWidth())
    && ((xPos+i) >= 0)
    && (img.getRGB(xPos+i, yPos) == Color.white.getRGB()))
       {
       result[0] = xPos+i;
       result[1] = yPos;
       leveled++;

       endTime = (int) System.currentTimeMillis();
       section4Runtime += (double) (endTime - startTime) / 1000;
       return result;
       }

    // same as above: a dice reroll (with constrictions)
    if(lastDice==0)
        {
        lastDice=dice;
        dice+= r.nextInt(2)==0?-1:1;        // randomly chose if you decrement by 1 or increment by 1
        }
    else
        {
         if(lastDice==1)
            {
             if(anteLastDice==0)
                {
                 anteLastDice= lastDice;
                 lastDice=dice;
                 dice =3;   
                }
             else
                {
                 allRan=true; 
                }
            }
         else if(lastDice==3)
            {
             if(anteLastDice==0)
                {
                 anteLastDice= lastDice;
                 lastDice=dice;
                 dice =1;
                }   
             else
                {
                 allRan=true;
                }
            }

        }
    }
}

return result;

经过深思熟虑后,我终于明白了。我们所有的想法都违反了我正在使用的第一个实现的基本“规则”:第一个实现是在3行中的一行上尝试随机位置,然后转到下一行(不返回到尝试该线上的其他位置)。示例:如果算法选择了上面的行,它将随机尝试左上角以查看它是否空闲;如果不是那么它将尝试与当前单元格和下面的行相同的行(再次,只是其中一个可能的位置)而不返回。我们所有的想法都在迭代细胞周围的所有可能性,这意味着顶部和底部线比中间有更多的命中是不可避免的(因为顶部和底部各有3个可能的点,而中间只有2个)。此外,当场上有洞时,最容易填充的洞是对角移动的洞(最后是向上或向下)或直接向上或向下移动的洞,因为那些侧向移动的洞只有选项左/右。唯一尚未解决的谜团是(使用我们提出的实施方案)模型通常会使用恰好高于我们当前单元格的点。我不知道为什么它喜欢大部分时间直接使用该实现。然而,新算法(反映旧算法,但更轻)是:

boolean[] lines = new boolean[]{false, false, false};
byte checks =0;

while(checks < 3)       // just 3 tries in total
{
    dice = r.nextInt(3);

    if(lines[dice]== false)
    {
        lines[dice] = true;             // just 1 try per line
        // calculated here since we reuse dice below
        result[1] = yPos - 1 + dice;    // will be above if dice==0; will be below if dice==2; same line if dice==1

        if((dice == 0) || (dice == 2))        // top/bottom line
            {dice = r.nextInt(3)-1;}
        else if(dice == 1)        // middle line
            {dice = r.nextInt(2)==0?-1:1;}        // we exclude the middle point since that's our current position

        result[0] = xPos + dice;    // logic is calculated above and just applied here
        checks++;
    }

    if((result[0] >= 0)
    && (result[0] < img.getWidth())
    && (result[1] >= 0)
    && (result[1] < img.getHeight()))
    {
        if (img.getRGB(result[0], result[1]) == Color.white.getRGB())     // if the new cell is free
        {
            return result;
        }
    }
}
result[0] = -1;     // in case we get here, reset the value so it's not used

这使代码从167行减少到33行(并使其更具可读性)。我不知道选择谁作为最佳解决方案。如果您有任何想法,请提出建议。

4 个答案:

答案 0 :(得分:2)

java.util.Random的分布不是那么不均匀。您可以使用以下代码进行确认:

public static void main(String[] args) throws Exception {
    final int N = 3;
    Random r = new Random();
    int[] counts = new int[N];
    for (int i = 0; i <= 100_000; i++) {
        counts[r.nextInt(N)]++;
    }
    System.out.println(Arrays.toString(counts));
}

<强>更新

正如您所说,上面的代码产生了相当均匀的分布值。但是,在循环开头添加以下行:

        if (i % 6 == 0)
            r = new Random(0);

然后你得到[16667,33333,50000]。一个值的频率是第一个的两倍,另一个频率是第一个频率的3倍。这将随机数生成器设置为具有常量种子的新创建的生成器。它模拟你的代码,你在其中创建一个new Random()的函数入口(尽管没有种子参数),然后你的函数调用nextInt()六次 - 这个if (i % 6 == 0)语句确保每6次迭代也会创建一个新的RNG。

检查您的代码并确保您只在整个程序中创建一次RNG。

答案 1 :(得分:2)

首先,我必须承认我无法看到你的算法应该做什么 - 我不清楚你为什么要在你做的时候滚动每个骰子,其他时候使用现有的值。

为了获得一个清晰,易于理解的算法,我建议在循环中确定您的dice变量,同时滚动两个变量,然后将它们final设置为使您知道每次迭代都只有一个双模辊:

while(strike < 9) {
    final int roll1 = r.nextInt(3) - 1;
    final int roll2 = r.nextInt(3) - 1;

    strike += handleRoll(roll1,roll2);
}

您可以通过为handleRoll()编写一个简单的计数器来证明自己的分布,然后再替换您的真实代码。

int[] counts = int[6];
void handleRoll(int roll1, int roll2) {
     counts[1 + roll1] ++;
     counts[4 + roll2] ++;
     return 1;
}

(增加所需的击球次数以获得足够大的样本来推理)

确保您在整个计划中使用相同的Random实例 - 不要继续制作新的实例。

(您可以通过创建Coordinate类和创建随机类的工厂来稍微整理一下这些内容。

我简化了你的代码:

  • 制作了一系列提取方法重构以整理细节
  • 将您的掷骰更改为使用范围0到2而不是-1到+1 - 因为您在两个地方使用它们,而在其中一个地方再次添加一个!
  • 使用xy,仅在需要时创建result
  • 使用final作为回滚以及生成的xy,将其范围限定在循环内部
  • 将嵌套if转换为&&逻辑
  • 改变了一些奇怪的选择。似乎为positions制作了boolean网格。在Java中使用short很少有任何价值。

所以:

private int[] cellularSearch(int xPos, int yPos) {
     boolean[][] positions =
            new boolean[][] { { false, false, false }, 
                              { false, true, false }, 
                              { false, false, false } };
    int strike = 0;

    while (strike < 9) {
        final int dice0 = r.nextInt(3);
        final int dice1 = r.nextInt(3);

        final int x = xPos + dice0 - 1;
        final int y = yPos + dice1 - 1;

        if (isInXrange(x) && isInYRange(y)) {
            if (!alreadyTried(positions, dice1, dice0) && isWhite(x, y)) {
                return new int[] { x, y };
            }

            markAsTried(positions, dice1, dice0);
            strike++;
        }
    }
    return null; // or whatever you intend to happen here
}

private boolean isInXrange(int x) {
    return (x >= 0) && (x < img.getWidth());
}

private boolean isInYRange(int y) {
    return (y >= 0) && (y < img.getHeight());
}

private boolean alreadyTried(boolean[][] positions, final int dice1, final int dice0) {
    return positions[dice1 + 1][dice0 + 1];
}

private static void markAsTried(boolean[][] positions, int dice1, int dice0) {
    positions[dice1][dice0] = true;
}

private boolean isWhite(final int x, final int y) {
    return img.getRGB(x, y) == Color.white.getRGB();
}

我认为这相当于你的代码,只有一个例外 - 如果第一次滚动超出图像的宽度,你的第二次掷骰就不会滚动。如果您愿意,可以稍后将其重新添加为性能改进。

但它暴露了一些问题。看起来好像是尝试每个单元格(你有一个3x3网格,你已经选择了9&#34;罢工&#34;) - 但它并没有增加strike x,y在图像之外。当以前尝试过该位置时, 会增加strike。所以你可以退出没有尝试过每个单元格的循环。

我没有看到这导致你所描述的加权的具体方式 -  但它看起来像是一种可能导致意外结果的事情。

(无论如何 - 因为你提供的代码没有编译,你没有用你给我们的代码观察它)

如果打算检查每个单元格,最好将一组单元格随机化,然后按顺序测试它们:

 List<Coords> coordsToTry = new ArrayList<>();
 for(int x=0; x<2; x++) {
     for(int y=0; y<2; y++) {
         coordsToTry.add(new Coords( x, y));
     }
 }
 Collections.shuffle(coordsToTry);

 for(Coords coords : coordsToTry) {
    if(isWhite(coords)) {
        return coords;
    }
 }
 return null; // or whatever is meant to happen when nothing found

答案 2 :(得分:1)

java.util.Random是伪随机数生成器(wikipedia上的定义),需要进行种子处理。 来自文档:

  

如果使用相同的种子创建了两个Random实例,并且为每个实例创建了相同的方法调用序列,则它们将生成并返回相同的数字序列。为了保证这个属性,为Random类指定了特定的算法。

如果你想确保获得好的随机数,请使用SecureRandom,这可以保证产生非确定性输出

答案 3 :(得分:1)

由于您对两个'骰子'的组合分布感兴趣,在@ DodgyCodeException的建议之上,您可以查看统计信息,如

public static void main(String[] args) {
    Random r=new Random();
    int stat[]=new int[9];
    for(int i=0;i<9000;i++)
        stat[r.nextInt(3)+r.nextInt(3)*3]++;
    for (int i : stat)
        System.out.println(i);
}

然而它甚至也是如此。

<小时/> 从两个幂的范围生成随机数和其他方面之间存在细微的差别,所以如果你真的想要做一些魔术,你可以使用这样一个事实:你实际上是从8种可能性中选择一个位置(因为中间一个在开头排除了)。 像

这样的东西
final int xhelper[]=new int[]{-1, 0, 1,-1, 1,-1, 0, 1};
final int yhelper[]=new int[]{-1,-1,-1, 0, 0, 1, 1, 1};
...
int dir=r.nextInt(8);
int dice0=xhelper[dir];
int dice1=yhelper[dir];

但实际上我并不认为这会有所作为。