【问题标题】:Butterfly pattern appears in random walk using srand(), why?使用 srand() 在随机游走中出现蝴蝶模式,为什么?
【发布时间】:2020-09-14 07:29:26
【问题描述】:

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

编辑:

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

#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 有什么关系?
  • 好吧,我试图在文本中解释它,但是它缺少轴和颜色图例......它与 PRNG 有关,因为它使用“随机数”以相等的概率向上,从点 (0,0) 开始,依次向下、向右或向左。颜色只是步数的参考,它在可见色谱中从紫色一直到红色。这是 250 万步后的结果。

标签: c++ random random-seed srand random-walk


【解决方案1】:

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

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

当您使用 C++ 时,您可以使用随机播种的 std::mt19937 重试,您不会看到重复。

【讨论】:

  • “你不会看到重复”即使是真的,这是一个可怕的推论。你提到了2^32这个时期,他没有产生40亿个数字,所以他从来没有达到这个时期。
  • 是的,我没有达到那个时期。我理解答案所说的关于 prngs 的内容,但我想理解的是为什么随机游走案例中的模式会重复自身并显示对称性,然后自行关闭......序列是否以某种方式镜像?
  • 当然可以镜像序列。 It can do anything in fact。碰巧的是,对于您选择的特定点数(250'000),您正在使用的序列的低位中有一个镜像。选择不同的 RNG,您将不会看到相同的效果。这是错误使用rand() 的输出和RNG 的选择造成的。
【解决方案2】:

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

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

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

【讨论】:

  • 第二个选项实际上解决了这个问题,我每次都通过更多的步骤获得不同的模式。如果我做更多的步骤,会出现另一个循环模式?还有,为什么蝴蝶图案是对称的?
  • rand() 的实现方式只是a * x + b,如果您只看到前 2 位,则周期远小于原本保证的 40 亿。实际周期显然取决于 ab 值,但如果您愿意,您可以从自己的 C++ RT 实现中算出。我认为总的来说,你这么快就找到了一个容易识别的模式只是盲目的运气。
  • 请记住,模运算会使您的随机分布偏向 0。更好的运算是除以浮点空间中的最大值。在这种情况下,这两个问题都可以通过一个简单的事实来避免,即您的 2 个所需位可以很容易地从 16 位结果(16 % 2 = 0)中提取出来,只需使用所有位,您就可以充分利用您的周期!
【解决方案3】:

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

【讨论】:

    【解决方案4】:

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

    您的问题是从随机生成器中删除位。让我们看看如何编写一个不丢失位的随机位对源。它要求 RAND_MAX 的形式为 2^n−1,但这个想法可以扩展到支持任何 RAND_MAX &gt;= 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();
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-03-26
      • 2014-02-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-12-01
      • 2021-05-05
      相关资源
      最近更新 更多