再谈随机化

· · 算法·理论

前言

书接上文(浅谈随机化), @Redshift_Shine 在评论区里指出了优化的方向。在此感谢他!

本文支持的语言标准是 C++14,更高版本应该也适用。

根据 CCF 所说,NOI 系列比赛 C++ 语言标准为 C++14。

正文

mt19937

上文说:

你看这下面的代码:

srand(k); // 其中 k 是任意一个正整数

就是我们的初始化操作。

也可以使用

srand(time(0));

这两个写法都可以:第一种方便调试,第二种随机性强。两种都可以使用。我们这里使用第一种。

当我们想要调用一个随机数的时候,直接调用 rand() 就可以了。

消息过时了。

实际上我们可以使用 mt19937 来实现。

mt19937 的初始化操作也极其简单:

mt19937 rnd(time(NULL));

其中 rnd() 是我们定义的随机函数,当我们想要调用一个随机数的时候,直接调用 rnd() 就可以了。

为什么要用 time(NULL) 呢?当然是为了随机性啦。

mt19937 的最小值为 0,最大值为 2^{32} - 1 = 4294967295。C++ 官方知道你记不下来,所以给你放到 rnd.min()rnd.max() 里去了。

shuffle

在上文中说:

而当我们想要打乱一个数组 a,其下标从 1n,我们可以使用这段代码:

random_shuffle(a + 1, a + n + 1);

实际上,消息过时了。

random_shuffle 在 C++14 中被弃用,在 C++17 中被移除。所以,这里推荐更加厉害的东西:shuffle。

当我们想要打乱一个数组 a,其下标从 1n,我们可以使用这段代码:

shuffle(a + 1, a + n + 1, rnd);

欸?这个 rnd 是什么?其实就是我们上面 mt19937 定义的随机函数。所以 shuffle 要和 mt19937 一起使用。

shuffle 相比于 random_shuffle 的优势是:随机性强。

实战

根据我们刚才介绍的 shuffle 和 mt19937,我们来试着做一道题目。

题目:P2210 [USACO13OPEN] Haywire B

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N = 12 + 5;
const int M = 5;
const int INF = 0x3f3f3f;
const double down = 0.996;
int n, a[N], fri[N][M];
signed main() { 
    mt19937 rnd(time(NULL));
    cin >> n;
    for (int i = 1; i <= n; i++) {
        a[i] = i;
    }
    for (int i = 1; i <= n; i++) {
        cin >> fri[i][1] >> fri[i][2] >> fri[i][3];
    }

    int temp = 2000000;
    int ans = INF;
    while (temp--) {
        shuffle(a + 1, a + n + 1 , rnd);

        int tot = 0;
        for (int i = 1; i <= n; i++) {
            for(int j = 1; j <= 3; j++) {
                tot += abs(a[i] - a[fri[i][j]]);
            }
        }
        ans = min(ans, tot / 2);
    }

    cout << ans;
    return 0;
}

认真做一道题

就拿我们上文的最后一道习题:P6505 Run Away 来分析吧。我个人认为这道题是非常不错的,有非常多的方法解决它。

首先我们看一看这道题:

这道题题面清晰易懂,我们思考可以使用什么算法:如果你往计算几何找,你几乎想不到任何做法(除非你了解笔者看了一眼就马上爆炸的三角剖分或 Voronoi 图)。如果你思路清奇,往经典算法想,你或许能够想到二分答案。但是以上的三种做法都挺难实现的,码量极大,思路极复杂。于是,我们选择了第四条路线:随机化。

那使用模拟退火还是爬山算法呢?如果你运气极佳且不愿思考了话,我的建议是模拟退火。但是我们应该严谨分析一下。

分析一下函数,最高峰点旁的其他点,这个点必定劣于先前的点。那么,搜索的点越接近最高峰,值便越大,所以这是单峰函数。综上,爬山算法,启动!

确认的算法,基本就没什么好说的了,使用模板就可以了:

#include <bits/stdc++.h>
using namespace std;
const int N = 1000 + 5;
const double down = 0.9975;
int w, h, n;
int x[N], y[N];
double dis(double x_1, double y_1, double x_2, double y_2) {
    return sqrt((x_1 - x_2) * (x_1 - x_2) + (y_1 - y_2) * (y_1 - y_2));
}
int rand(int l, int r) {
    return rand() % (r - l + 1) + l;
}

double best;
double tx, ty;

double energy(double x0, double y0) {
    double tot = 2e9;
    for (int i = 1; i <= n; i++) {
        tot = min(tot, dis(x0, y0, x[i], y[i]));
    }
    return tot;
}

void work() {
    tx = rand(0, w), ty = rand(0, h);
    double good = -1;
    double t = 3000;
    while (t > 1e-10) {
        double dx = tx + (rand() * 2 - RAND_MAX) * t;
        double dy = ty + (rand() * 2 - RAND_MAX) * t;
        if (dx < 0) {
            dx = 0;
        }
        if (dy < 0) {
            dy = 0;
        }
        if (dx > w) {
            dx = w;
        }
        if (dy > h) {
            dy = h;
        }

        double now = energy(dx, dy);
        if (now > good) {
            tx = dx, ty = dy;
            good = now;
            best = max(best, now);
        }
        t *= down;
    }
}
int main() {
    srand(78321);
    cin >> w >> h >> n;
    for (int i = 1; i <= n; i++) {
        cin >> x[i] >> y[i];
    }
    while(((double)clock() / CLOCKS_PER_SEC) < 0.9) {
        work();
    }
    cout << fixed << setprecision(7) << best;
    return 0;
}

但是,你有没有发现,我们使用的随机数是使用的 rand,没有使用 mt19937,我经过调试,搓出来了这份代码:

// 省略 dis,energy,main 的实现
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1000 + 5;
const double down = 0.996;
mt19937 rnd(time(NULL));
int w, h, n;
int x[N], y[N];

int rand(int l, int r) {
    return rnd() % (r - l + 1) + l;
}

double best;
double tx, ty;

void work() {
    tx = rand(0, w), ty = rand(0, h);
    double good = -1;
    double t = 3000;
    while (t > 1e-10) {
        double dx = tx + (rnd() % 2 == 0 ? 1 : -1) * rand(1, 2) * t;
        double dy = ty + (rnd() % 2 == 0 ? 1 : -1) * rand(1, 2) * t;
        if (dx < 0) {
            dx = 0;
        }
        if (dy < 0) {
            dy = 0;
        }
        if (dx > w) {
            dx = w;
        }
        if (dy > h) {
            dy = h;
        }

        double now = energy(dx, dy);
        if (now > good) {
            tx = dx, ty = dy;
            good = now;
            best = max(best, now);
        }
        t *= down;
    }
}

如你所见,使用 rand 和 mt19937 各有优势:rand 写的快,mt19937 随机性好。这个纯粹看个人习惯。

总结

学好随机化,到了哪里都骗分。

我整理了一个题单,适合练习:Python_enjoy 的随机化好题。