简单的随机数发生器,可以在O(1)时间内串行生成第n个数

时间:2014-02-22 04:43:42

标签: algorithm random

我不打算将其用于安全目的或统计分析。我需要创建一个简单的随机数生成器,用于我的计算机图形应用程序。我不想使用“随机数生成器”这个术语,因为人们用非常严格的术语来思考它,但我想不出任何其他的词来形容它。

  • 必须快。
  • 给定一个特定的种子,它必须是可重复的。 例如:如果seed = x,则每次使用种子x时都会发生系列a,b,c,d,e,f .....

最重要的是,我需要能够在恒定时间内计算系列中的第n个术语。

看来,我无法用rand_r或srand()实现这一点,因为这些需求是依赖于状态的,我可能需要以某种未知的顺序计算nth。

我看过线性反馈移位寄存器,但这些寄存器也依赖于状态。

到目前为止,我有这个:

  

int rand =(n * prime 1 + seed)%prime 2

     

n =用于表示序列中术语的索引。例如:For   第一学期,n == 1

     

prime 1 和prime 2 是素数   prime 1 &gt;素<子> 2

     

seed =允许一个人使用相同功能的某个数字   根据种子产生不同的系列,但同一系列   对于给定的种子。

我无法分辨这是多么好或坏,因为我还没有充分利用它,但如果有更多经验的人可以指出这方面的问题,或者帮助我改进它,那将会很棒。

编辑 - 我不在乎它是否可预测。我只是想在计算机图形学中创建一些随机性。

4 个答案:

答案 0 :(得分:4)

CTR mode中使用加密分组密码。第N个输出只是加密(N)。这不仅为您提供了所需的属性(第N次输出的O(1)计算);它还具有很强的不可预测性。

答案 1 :(得分:4)

我偶然发现了这一点,寻找同样问题的解决方案。最近,我想出了如何在低常数O(log(n))时间内完成它。虽然这与作者所要求的O(1)并不完全匹配,但它可能足够快(样本运行,使用-O3编译,实现了10亿个任意索引随机数的性能,n在1和1之间变化)在25.7秒中,2 ^ 48,只是少于18M的数字/秒。

首先,解决方案背后的理论:

常见类型的RNG是Linear Congruential Generators,基本上,它们的工作方式如下:

random(n)=(m * random(n-1)+ b)mod p

其中m和b以及p是常数(有关如何选择LCG,请参阅LCG的参考)。由此,我们可以使用一些模运算来设计以下内容:

random(0) = seed mod p
random(1) = m*seed + b mod p
random(2) = m^2*seed + m*b + b mod p
...
random(n) = m^n*seed + b*Sum_{i = 0 to n - 1} m^i mod p 
          = m^n*seed + b*(m^n - 1)/(m - 1) mod p

计算上述内容可能会出现问题,因为数字会快速超过数字限制。通用案例的解决方案是使用p *(m - 1)以模数计算m ^ n,但是,如果我们采用b = 0(LCG的子案例有时称为Multiplicative congruential Generators),我们有一个更简单的解决方案,并且可以仅以模数p进行计算。

在下文中,我使用RANF使用的常数参数(由CRAY开发),其中p = 2 ^ 48且g = 44485709377909. p为2的幂的事实减少了所需的操作数量(如预期的那样) ):

#include <cassert>
#include <stdint.h>
#include <cstdlib>

class RANF{

    // MCG constants and state data
    static const uint64_t m = 44485709377909ULL;
    static const uint64_t n = 0x0000010000000000ULL; // 2^48
    static const uint64_t randMax = n - 1;
    const uint64_t seed;
    uint64_t state;

public:

    // Constructors, which define the seed
    RANF(uint64_t seed) : seed(seed), state(seed) { 
        assert(seed > 0 && "A seed of 0 breaks the LCG!"); 
    }

    // Gets the next random number in the sequence
    inline uint64_t getNext(){
        state *= m;
        return state & randMax;
    }

    // Sets the MCG to a specific index
    inline void setPosition(size_t index){
        state = seed;
        uint64_t mPower = m;
        for (uint64_t b = 1; index; b <<= 1){
            if (index & b){
                state *= mPower;
                index ^= b;
            }
            mPower *= mPower;
        }
    }
};

#include <cstdio>
void example(){
    RANF R(1);

    // Gets the number through random-access -- O(log(n))
    R.setPosition(12345); // Goes to the nth random number
    printf("fast nth number = %lu\n", R.getNext());

    // Gets the number through standard, sequential access -- O(n)
    R.setPosition(0);
    for(size_t i = 0; i < 12345; i++) R.getNext();
    printf("slow nth number = %lu\n", R.getNext());  
}

虽然我认为作者现在已经继续前进,但希望这对其他人有用。


如果您真的关注运行时性能,使用查找表可以使上述速度提高约10倍,但代价是编译时间和二进制大小(也是O(1) )按照OP)的要求,使用所需的随机索引

在下面的版本中,我使用c ++ 14 constexpr在编译时生成查找表,并且每秒获得176M任意索引随机数(这样做确实增加了大约12秒的额外编译时间) ,二进制大小增加1.5MB - 如果使用部分重新编译,可以减少增加的时间。)

class RANF{

    // MCG constants and state data
    static const uint64_t m = 44485709377909ULL;
    static const uint64_t n = 0x0000010000000000ULL; // 2^48
    static const uint64_t randMax = n - 1;
    const uint64_t seed;
    uint64_t state;

    // Lookup table
    struct lookup_t{
        uint64_t v[3][65536];

        constexpr lookup_t() : v() {
            uint64_t mi = RANF::m;
            for (size_t i = 0; i < 3; i++){
                v[i][0] = 1;
                uint64_t val = mi;
                for (uint16_t j = 0x0001; j; j++){
                    v[i][j] = val;
                    val *= mi;
                }
                mi = val;
            }
        }
    };  
    friend struct lookup_t;

public:

    // Constructors, which define the seed
    RANF(uint64_t seed) : seed(seed), state(seed) { 
        assert(seed > 0 && "A seed of 0 breaks the LCG!"); 
    }

    // Gets the next random number in the sequence
    inline uint64_t getNext(){
        state *= m;
        return state & randMax;
    }

    // Sets the MCG to a specific index
    // Note: idx.u16 indices need to be adapted for big-endian machines!
    inline void setPosition(size_t index){
        static constexpr auto lookup = lookup_t();  
        union { uint16_t u16[4]; uint64_t u64; } idx;

        idx.u64 = index;
        state = seed * lookup.v[0][idx.u16[0]] * lookup.v[1][idx.u16[1]] * lookup.v[2][idx.u16[2]];
    }
};

基本上,它的作用是将例如m^0xAAAABBBBCCCC mod p的计算分成(m^0xAAAA00000000 mod p)*(m^0xBBBB0000 mod p)*(m^CCCC mod p) mod p,然后为0x0000中的每个值预先计算表 - {{1可以填充0xFFFFAAAABBBB

的范围

答案 2 :(得分:0)

RNG在正常意义上,具有类似f(n)= S(f(n-1))的序列模式

由于计算方便,它们在某些时候也失去了精度(如%mod),因此无法将序列扩展为类似X(n)= f(n)=仅具有n的平凡函数的函数。

这意味着你最多有O(n)。


为了针对O(1),你需要放弃f(n)= S(f(n-1))的思想,并直接指定一个简单的公式,以便可以直接计算第N个数不知道(N-1)';这也使种子毫无意义。

所以,你最终得到的是一个简单的代数函数,而不是一个序列。例如:

int my_rand(int n) { return 42; } // Don't laugh!
int my_rand(int n) { 3*n*n + 2*n + 7; }

如果你想对生成的模式(如分布)加上更多约束,那么它就成了一个复杂的数学问题。


然而,对于你的原始目标,如果你想要的是获得伪随机数的恒定速度,我建议用传统的RNG预先生成它并使用查找表进行访问。

编辑:我注意到你关注很多数字的表格大小,但是你可能会引入一些混合模型,比如N个条目的表格,并且f(k)= g(tbl [k%n] ,k),至少在N个连续序列中提供良好的分布。

答案 3 :(得分:0)

这表明PRNG实现为散列计数器。这似乎与R.的建议重复(在CTR模式下使用分组密码作为流密码),但为此,我避免使用加密安全原语:执行速度和安全性不是所需的功能。 / p>

如果我们试图创建一个安全流密码,并且要求任何发出的序列都是可重复的,只要知道它的索引......

...然后我们可以选择一个安全散列算法(如SHA256)和一个具有大量位的计数器(可能是2048 - >序列重复每2 ^ 2048个生成的数字而不重新播种)。

然而,我在这里介绍的版本使用了Bob Jenkins着名的哈希函数(简单快速但不安全)以及64位计数器(它可以在我的系统上获得与整数一样大的数据,而无需自定义递增码)。

主要代码表明初始化后RNG计数器(种子)的知识允许重复PRNG序列,只要我们知道在重复点之前产生了多少个值。

实际上,如果您知道输出序列中任何一点的计数器值,您将能够检索该点之前生成的所有值,以及之后生成的所有值。这仅涉及向与输出序列中的已知点相关联的参考计数器值添加或减去序数差异。

将此类用作测试框架应该很容易 - 您可以插入其他哈希函数并更改计数器的大小,以查看速度以及生成值的分布会产生什么样的影响(我做的唯一一致性分析是在main()打印的十六进制数字屏幕中查找模式)。

#include <iostream>
#include <iomanip>
#include <ctime>

using namespace std;

class CHashedCounterRng {
    static unsigned JenkinsHash(const void *input, unsigned len) {
        unsigned hash = 0;
        for(unsigned i=0; i<len; ++i) {
            hash += static_cast<const unsigned char*>(input)[i];
            hash += hash << 10;
            hash ^= hash >> 6;
        }
        hash += hash << 3;
        hash ^= hash >> 11;
        hash += hash << 15;
        return hash;
    }

    unsigned long long m_counter;

    void IncrementCounter() { ++m_counter; }

public:
    unsigned long long GetSeed() const {
        return m_counter; 
    }
    void SetSeed(unsigned long long new_seed) {
        m_counter = new_seed; 
    }
    unsigned int operator ()() {
        // the next random number is generated here
        const auto r = JenkinsHash(&m_counter, sizeof(m_counter));
        IncrementCounter();
        return r;
    }

    // the default coontructor uses time() 
    // to seed the counter
    CHashedCounterRng() : m_counter(time(0)) {}

    // you can supply a predetermined seed here, 
    // or after construction with SetSeed(seed)
    CHashedCounterRng(unsigned long long seed) : m_counter(seed) {}
};

int main() {
    CHashedCounterRng rng;
    // time()'s high bits change very slowly, so look at low digits
    // if you want to verify that the seed is different between runs
    const auto stored_counter = rng.GetSeed();
    cout << "initial seed: " << stored_counter << endl;
    for(int i=0; i<20; ++i) {
        for(int j=0; j<8; ++j) {
            const unsigned x = rng();
            cout << setfill('0') << setw(8) << hex << x << ' ';
        }
        cout << endl;
    }
    cout << endl;

    cout << "The last line again:" << endl;
    rng.SetSeed(stored_counter + 19 * 8);
    for(int j=0; j<8; ++j) {
        const unsigned x = rng();
        cout << setfill('0') << setw(8) << hex << x  << ' ';
    }
    cout << endl << endl;
    return 0;
}