【问题标题】:Generating random numbers in ranges from 32 bytes of random data, without bignum library生成 32 字节随机数据范围内的随机数,无需 bignum 库
【发布时间】:2019-06-06 06:24:15
【问题描述】:

我有 32 字节的随机数据。

我想在 0-9 和 0-100 之间的可变范围内生成随机数。

如果我使用任意精度算术 (bignum) 库,并将 32 个字节视为一个大数,我可以简单地这样做:

random = random_source % range;
random_source = random_source / range;

按照我喜欢的频率(使用不同的范围),直到范围的乘积接近 2^256。

有没有办法只使用(固定大小的)整数算术来做到这一点?

【问题讨论】:

  • bignum 库仅使用固定大小的整数算术实现,因此可以肯定。如果你只写一个 32 字节的模和除函数呢?
  • 编写一个库来处理 32 字节长整数的除法应该不难。

标签: c bignum


【解决方案1】:

当然,您可以通过以 256 为底的长除法(或上推乘法)来做到这一点。就像你在小学学过的长除法一样,只不过用的是字节而不是数字。它涉及依次对每个字节进行级联的除法和余数。请注意,您还需要了解如何使用大数字,并且随着您使用它并且它变得更小,对范围内较大值的偏差会越来越大。例如,如果您只剩下 110 个,并且您要求一个 rnd(100),则 0-9 的值将比 10-99 的每个值高 10%。

但是,您实际上并不需要 bignum 技术,您可以使用算术编码压缩中的想法,在其中构建单个数字而无需实际处理整个事情。

如果您从读取 4 个字节到一个无符号 uint_32 缓冲区开始,它的范围是 0..4294967295,不包含最大值为 4294967296。我将这个综合值称为“结转”,而这个独占记录最大值也很重要。

[为简单起见,您可以从向缓冲区读取 3 个字节开始,最多生成 16M。这避免了处理无法保存在 32 位整数中的 4G 值。]

有 2 种使用方法,都具有准确性影响:

流下来:

做你的模数范围。模数是您的随机答案。除法结果是您的新结转,范围更小。
假设你想要 0..99,所以你以 100 为模,你的上半部分有一个最大范围 42949672 (4294967296/100),你可以将其用于下一个随机请求 我们还不能输入另一个字节...
假设你现在想要 0..9,所以你以 10 为模,现在你的上半部分的范围是 0..4294967 (42949672/100)
由于 max 小于 16M,我们现在可以引入下一个字节。将其乘以当前最大值 4294967 并将其添加到结转中。最大值也乘以 256 -> 1099511552

这种方法对小值有轻微的偏差,因为在“next max”次中为1,可用的值范围不会是全范围,因为最后一个值被截断,而是通过选择保持3-4最大的好字节,这种偏差被最小化。它只会在 1600 万次中最多出现 1 次。

该算法的计算成本是 div 乘以进位和最大值的随机范围,然后每次输入一个新字节时相乘。我假设编译器会优化模

串流:
说你想要 0..99
将你的最大值除以范围,得到下一个最大值,并将结转除以下一个最大值。现在,您的随机数在除法结果中,余数形成您结转以获得下一个随机数的值。
当 nextmax 小于 16M 时,只需将 nextmax 和您的结转乘以 256 并添加下一个字节。
如果这种方法的缺点是,根据用于生成 nextmax 的除法,最高值结果(即 99 或 9)严重偏向于,或者有时您会生成高值(100) - 这取决于您是否四舍五入或向下做第一个除法。

这里的计算成本又是 2 除,假设编译器优化器混合了 div 和 mod 操作。乘以 256 很快。

在这两种情况下,您都可以选择说,如果输入结转值在这个“高偏差范围”内,那么您将执行不同的技术。您甚至可以在这两种技术之间摇摆不定——优先使用第二种技术,但如果它产生高值,则使用第一种技术,尽管就其本身而言,两种技术可能会在结转时偏向相似的输入随机流值接近最大值。可以通过使第二种方法生成 -1 作为超出范围来减少这种偏差,但是这些修复中的每一个都增加了一个额外的乘法步骤。

请注意,在算术编码中,这个溢出区域在提取每个符号时被有效地丢弃。在解码过程中保证不会出现这些边缘值,这会导致轻微的次优压缩。

【讨论】:

    【解决方案2】:
    /*  The 32 bytes in data are treated as a base-256 numeral following a "." (a
        radix point marking where fractional digits start).  This routine
        multiplies that numeral by range, updates data to contain the fractional
        portion of the product, and returns the integer portion.
    
        8-bit bytes are assumed, or "t /= 256" could be changed to
        "t >>= CHAR_BIT". But then you have to check the sizes of int
        and unsigned char to consider overflow.
    */
    int r(int range, unsigned char *data)
    {
        // Start with 0 carried from a lower position.
        int t = 0;
    
        // Iterate through each byte.
        for (int i = 32; 0 < i;)
        {
            --i;
    
            // Multiply next byte by our multiplier and add the carried data.
            t = data[i] * range + t;
    
            // Store the low bits of the result.
            data[i] = t;
    
            // Carry the high bits of the result to the next position.
            t /= 256;
        }
    
        // Return the bits that carried out of the multiplication.
        return t;
    }
    

    【讨论】:

    • 乘法通常是比除法更快的运算,所以这是一个很好的改进。并且有很好的均匀分布保证。唯一的缺点:它每次都在整个值上工作,并写入它。
    猜你喜欢
    • 1970-01-01
    • 2010-12-24
    • 1970-01-01
    • 1970-01-01
    • 2015-07-21
    • 2011-03-01
    • 2016-05-04
    • 2010-11-26
    相关资源
    最近更新 更多