使用srand()函数进行随机漫步时出现蝴蝶图案,为什么?

11
大约3年前,我和一位同事一起使用C++编写了一个二维随机漫步程序,开始时每次运行得到的模式都不同,看起来程序正常。但是当我们决定增加步数时,会出现明显的蝴蝶图案。我们发现,每次运行程序时,图案都会重复,但从蝴蝶的不同位置开始。我们得出结论并报告说,这是由于与srand()函数相关联的伪随机生成器导致的。今天我再次找到了这份报告,仍然有些事情我想要理解。我希望更好地理解伪随机生成器的工作原理,以便获得这种对称和循环模式。我所说的模式如下(步骤按彩虹序列进行颜色编码,以便欣赏漫步的进展):

enter image description here

编辑:

我正在添加用于获取此图的代码:

#include<iostream>
#include<cmath>
#include<stdlib.h>
#include<time.h>
#include <fstream>
#include <string.h>
#include <string>
#include <iomanip>

using namespace std;

int main ()
{
srand(time(NULL));
int num1,n=250000;



ofstream rnd_coordinates("Random2D.txt");
float x=0,y=0,sumx_f=0,sumy_f=0,sum_d=0,d_m,X,t,d;
float x_m,y_m;

x=0;
y=0;

for(int i=0;i<n;i++){

    t=i;
    num1= rand()%4;

    if(num1==0){
        x++;
    }
    if(num1==1){
        x--;
    }
    if(num1==2){
        y++;
    }
    if(num1==3){
        y--;
    }

    rnd_coordinates<<x<<','<<y<<','<<t<<endl;

}

rnd_coordinates.close();


return 0;
}

我们不知道那张图片的意思。某种颜色或页面上的位置有什么含义?这些与RNG有什么关系? - President James K. Polk
好的,我试图在文本中解释它,但是确实缺少轴和颜色图例... 它与PRNG相关,因为它使用“随机数”以相等的概率向上、向下、向右或向左进行连续步骤,从点(0,0)开始。颜色只是步数的参考,它从紫色一直到可见光谱中的红色。这是250万步后的结果。 - Isidre Mas Magre
4个回答

3

您永远不会遇到rand()的周期,但请记住,您实际上没有使用rand()范围的全部内容,全范围保证了2^32个周期。

在此基础上,您有两个选项:

  1. 使用所有位。 rand()返回2字节(16位),您需要2位(4个可能值)。将这16位输出分成2位的块,并按顺序全部使用。
  2. 最起码,如果您坚持使用懒惰的%n方式,请选择一个不是您周期的除数的模数。例如,选择5而不是4,因为5是质数,如果您得到第5个值,请重新投掷。

第二个选项实际上解决了问题,每次都会得到一个不同的模式,并且步骤更多。如果我再做更多的步骤,另一个循环模式会出现吗?此外,为什么蝴蝶图案具有对称性? - Isidre Mas Magre
rand()的实现方式只是a * x + b,如果你只看前2位,周期比本来保证的40亿要小得多。实际周期显然取决于ab的值,但是如果你想的话,可以从自己的C++ RT实现中计算出它的数学意义。我认为总体上,你能够很快地找到一个容易识别的模式只是盲目运气罢了。 - Blindy
1
请注意,模运算会使您的随机分布偏向于0。更好的操作是通过浮点空间中的最大值进行除法运算。在这种情况下,两个问题都可以避免,因为您所需的2个位可以轻松从16位结果中提取(16%2 = 0),只需使用所有位即可完全利用您的周期! - Blindy

2
下面的代码构成了一个完整可编译的示例。

screenshot of the example

您的问题是关于从随机生成器中丢弃位。让我们看看如何编写一个不丢弃位的随机比特对源代码。它要求RAND_MAX的形式为2^n−1,但这个想法可以扩展以支持任何RAND_MAX >= 3
#include <cassert>
#include <cstdint>
#include <cstdlib>

class RandomBitSource {
    int64_t bits = rand();
    int64_t bitMask = RAND_MAX;
    static_assert((int64_t(RAND_MAX + 1) & RAND_MAX) == 0, "No support for RAND_MAX != 2^(n-1)");
public:
    auto get2Bits() {
        if (!bitMask) // got 0 bits
            bits = rand(), bitMask = RAND_MAX;
        else if (bitMask == 1) // got 1 bit
            bits = (bits * (RAND_MAX+1)) | rand(), bitMask = (RAND_MAX+1) | RAND_MAX;

        assert(bitMask & 3);
        bitMask >>= 2;
        int result = bits & 3;
        bits >>= 2;
        return result;
    }
};

然后,随机游走的实现可以如下所示。请注意,' 数字分隔符是 C++14 的一个很方便的特性。
#include <vector>

using num_t = int;
struct Coord { num_t x, y; };

struct Walk {
    std::vector<Coord> points;
    num_t min_x = {}, max_x = {}, min_y = {}, max_y = {};
    Walk(size_t n) : points(n) {}
};

auto makeWalk(size_t n = 250'000)
{
    Walk walk { n };
    RandomBitSource src;
    num_t x = 0, y = 0;

    for (auto& point : walk.points)
    {
        const int bits = src.get2Bits(), b0 = bits & 1, b1 = bits >> 1;
        x = x + (((~b0 & ~b1) & 1) - ((b0 & ~b1) & 1));
        y = y + (((~b0 & b1) & 1) - ((b0 & b1) & 1));

        if (x < walk.min_x)
            walk.min_x = x;
        else if (x > walk.max_x)
            walk.max_x = x;
        if (y < walk.min_y)
            walk.min_y = y;
        else if (y > walk.max_y)
            walk.max_y = y;

        point = { x, y };
    }
    return walk;
}

通过更多的努力,我们可以将其制作成一个交互式的Qt应用程序。按下回车键会生成一张新的图片。
该图像以显示屏的本机分辨率查看,即它映射到物理设备像素。该图像不进行缩放。相反,在需要时旋转以更好地适应屏幕的方向(纵向 vs 横向)。这是为了横屏监视器爱好者 :)
#include <QtWidgets>

QImage renderWalk(const Walk& walk, Qt::ScreenOrientation orient)
{
    using std::swap;
    auto width = walk.max_x - walk.min_x + 3;
    auto height = walk.max_y - walk.min_y + 3;
    bool const rotated = (width < height) == (orient == Qt::LandscapeOrientation);
    if (rotated) swap(width, height);
    QImage image(width, height, QPixmap(1, 1).toImage().format());
    image.fill(Qt::black);

    QPainter p(&image);
    if (rotated) {
        p.translate(width, 0);
        p.rotate(90);
    }
    p.translate(-walk.min_x, -walk.min_y);

    auto constexpr hueStep = 1.0/720.0;
    qreal hue = 0;
    int const huePeriod = walk.points.size() * hueStep;
    int i = 0;
    for (auto& point : walk.points) {
        if (!i--) {
            p.setPen(QColor::fromHsvF(hue, 1.0, 1.0, 0.5));
            hue += hueStep;
            i = huePeriod;
        }
        p.drawPoint(point.x, point.y);
    }
    return image;
}

#include <ctime>

int main(int argc, char* argv[])
{
    srand(time(NULL));
    QApplication a(argc, argv);
    QLabel view;
    view.setAlignment(Qt::AlignCenter);
    view.setStyleSheet("QLabel {background-color: black;}");
    view.show();

    auto const refresh = [&view] {
        auto *screen = view.screen();
        auto orientation = screen->orientation();
        auto pixmap = QPixmap::fromImage(renderWalk(makeWalk(), orientation));
        pixmap.setDevicePixelRatio(screen->devicePixelRatio());
        view.setPixmap(pixmap);
        view.resize(view.size().expandedTo(pixmap.size()));
    };
    refresh();
    QShortcut enter(Qt::Key_Return, &view);
    enter.setContext(Qt::ApplicationShortcut);
    QObject::connect(&enter, &QShortcut::activated, &view, refresh);
    return a.exec();
}

0

每个伪随机生成器都是一些数字序列的循环。我们区分“好”的伪随机数生成器和“坏”的伪随机数生成器的方法之一是这个序列的长度。生成器与某些状态相关联,因此最大周期受到有多少不同状态的限制。

您的实现具有“短”周期,因为它在宇宙年龄内重复。它可能具有32位状态,因此周期最多为2^32。

由于您正在使用C++,因此可以尝试使用随机种子std::mt19937再次尝试,您将不会看到重复。


即使这是真的,那也是一个可怕的推论,你不会看到重复。你提到了一个 2^32 的周期,他没有生成 40 亿个数字,所以他从未达到过这个周期。 - Blindy
是的,我没有理解那个句号。我明白答案关于伪随机数生成器的内容,但我想要了解的是为什么在随机游走的情况下出现的模式会以对称的方式重复并最终闭合... 这个序列是否以某种方式镜像? - Isidre Mas Magre
这个序列当然可以镜像。实际上,它可以做任何事情。碰巧你选择的点数是25万(250,000),在使用的序列的较低位有一个镜像图像。选择不同的随机数生成器,你就看不到同样的效果了。这是rand()输出错误使用和随机数生成器的选择的副产品。 - Kuba hasn't forgotten Monica

0

你可能想看一下我在另一个问题这里中关于旧版rand()实现的回答。有时候,使用旧版的rand()和srand()函数时,低位比高位的随机性要差得多。其中一些旧版实现仍然存在,有可能你正在使用其中之一。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接