使用 srand() 随机游走出现蝴蝶图案,为什么?

Butterfly pattern appears in random walk using srand(), why?

大约 3 年前,我和同事一起用 C++ 编写了一个 2D 随机游走程序,起初它似乎工作正常,因为我们每次都获得了不同的模式。但是,每当我们决定将步数增加到某个阈值以上时,就会出现一个明显的蝴蝶图案,我们注意到代码的每个 运行 都会重复该图案,但从蝴蝶的不同位置开始。我们当时总结报告说是srand()函数关联的伪随机数生成器的问题,今天又找到了这个报告,还有一些想了解的地方。我想更好地理解伪随机生成器是如何工作的,以获得这种对称性和 ciclic 模式。我正在谈论的模式是这样的(这些步骤以彩虹序列进行颜色编码以评估步行的进展):

编辑:

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

#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;
}

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

您的实现有一个 "short" 周期,因为它在小于宇宙年龄的时间内重复。它可能有 32 位状态,所以周期最多 2^32.

由于您使用的是 C++,您可以使用随机种子再试一次 std::mt19937,您将不会看到重复。

您永远不会达到 rand() 的周期,但请记住,您实际上并没有使用整个 rand() 范围,它完全保证了 2^32 周期。

考虑到这一点,您有 2 个选择:

  1. 使用所有位。 rand() returns 2 个字节(16 位),您需要 2 位(用于 4 个可能的值)。将 16 位输出拆分为 2 位块并按顺序使用它们。
  2. 至少,如果您坚持使用惰性 %n 方式,请选择一个不是周期除数的模数。例如选择 5 而不是 4,因为 5 是素数,如果你得到第 5 个值,则重新掷骰。

您可能想看看我对另一个关于旧 rand() 实现的问题 的回答。有时,对于旧的 rand() and srand() 函数,低阶位的随机性远低于高阶位。其中一些较旧的实现仍然存在,您可能使用过一个。

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

您的问题是从随机生成器中删除位。让我们看看如何编写一个不丢位的随机位对源。它要求 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 应用程序。按 Return 生成一个新图像。

图像以显示它的屏幕的原始分辨率查看,即它映射到物理设备像素。图像未缩放。相反,它会在需要时旋转以更好地适应屏幕的方向(纵向与横向)。这是给肖像监视器爱好者的:)

#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();
}