【问题标题】:How to generate a predictable shuffling of a sequence without generating the whole sequence in advance?如何在不预先生成整个序列的情况下生成可预测的序列改组?
【发布时间】:2011-04-24 00:45:23
【问题描述】:

以下 python 代码准确地描述了我想要为任意大小(人口)的序列实现的目标:

import random
fixed_seed = 1 #generate the same sequence every time with a fixed seed
population = 1000
sample_count = 5 #demonstration number
num_retries = 3  #just enough to show the repeatable behaviour
for trynum in xrange(num_retries):
    #generate the fresh/ordered sequence (0->population)...
    seq = range(population)
    #seed the random number generator the same way every time...
    random.seed(fixed_seed)
    #shuffle the sequence...
    random.shuffle(seq)
    #display results for this try...
    sample_sequence = [str(x) for x in seq[:sample_count]]
    print "try %s: %s..." % (trynum + 1, ", ".join(sample_sequence))
#Sample output...
#try 1: 995, 721, 62, 326, 541...
#try 2: 995, 721, 62, 326, 541...
#try 3: 995, 721, 62, 326, 541...

该方法的问题在于它需要生成整个 首先在内存中排序。对于庞大的人口来说,这可能是一个问题。

请注意,此方法的一个潜在优势是您可以随时选择任何数组位置。

现在 - 如果手头的问题恰好让您将人口规模设置为 2 的幂 (减 1),线性反馈移位寄存器可用于获得可预测的随机序列。 LFSR 很简洁,在wikipedia article 中解释得很好。

下面的 python 代码演示了这一点(我做了一堆唯一性测试以确保它像宣传的那样工作)。再次查看 wikipedia article 以了解代码的工作原理 (Galois configuration)。

TAP_MASKS = { #only one needed, but I included 3 to make the code more useful
    10: 0x00000240, #taps at 10, 7
    16: 0x0000B400, #taps at 16, 14, 13, 11
    32: 0xE0000200, #taps at 32, 31, 30, 10
}

def MaxLengthLFSR(seed, register_length):
    "Gets next value from seed in max-length LFSR using Galois configuration."
    lsb = seed & 1
    next_val = seed >> 1
    if lsb == 1:
        mask = TAP_MASKS[register_length]
        next_val ^= mask
    return next_val

reglen = 16  #number of bits in register
population = (2**reglen) - 1 #not used, just showing it
fixed_seed = 1   #seed == startval in this case (could randomize in population)
sample_count = 5 #demonstration number
num_retries = 3  #just enough to show the repeatable behaviour
for trynum in xrange(num_retries):
    next_val = fixed_seed
    seq = [fixed_seed, ]
    for x in xrange(sample_count - 1):
        next_val = MaxLengthLFSR(next_val, reglen)
        seq.append(next_val)
    seq = [str(x) for x in seq]
    print "try %s: %s..." % (trynum + 1, ", ".join(seq))
#Sample output...
#try 1: 1, 46080, 23040, 11520, 5760...
#try 2: 1, 46080, 23040, 11520, 5760...
#try 3: 1, 46080, 23040, 11520, 5760...

这很好,因为您可以拥有庞大的人口并轻松计算可重复的非重复随机数序列,而无需使用大量内存。

缺点是 a) 它仅限于大小为 (2**N - 1) 的“洗牌”序列,以及 b) 您无法确定随机序列中特定位置的值在任意位置。您需要知道某个特定点的值并从那里遍历序列。

后者 (b) 大多是可以的,因为大多数时候你会按顺序生成序列,所以你只需要记住最后一个值。 2 限制 (a) 的力量是一种交易杀手,但...取决于应用程序。

如何为任意序列长度实现类似最大长度 LFSR 的非重复结果?

作为奖励,最好有一个解决方案,您可以知道给定序列位置的数字,而无需遍历序列到该位置。


注意:如果您想为最大长度的 LFSR(生成整个寄存器群而不重复一次)提供一个良好的 LFSR 抽头位置起始集,this link is quite good 并且每个寄存器大小有大量的抽头位置(最多到 32 位,无论如何)。

另外请注意,我已经看到许多与我的问题和洗牌/LFSR 密切相关的问题,但没有一个与我所追求的完全相关(任意大小线性序列的可预测洗牌)。或者至少据我所知,无论如何。

我最近一直在研究Linear Congruential Generators,这似乎很有希望,但我还没有让它们工作。我不会继续坐在这个问题上,而是会问它,如果我想通了并且它们起作用了,就会发布答案。

【问题讨论】:

    标签: python algorithm random lcg


    【解决方案1】:

    实际上我以前写过这个:Secure Permutations with Block Ciphers。简而言之:

    1. 是的,您可以使用 LFSR 生成长度为 2 的幂的排列。您还可以使用任何分组密码。使用分组密码,您还可以找到索引 n 处的元素,或元素 n 的索引。
    2. 要生成具有任意长度 l 的排列,请创建一个长度大于 l 的 2 的最小幂。当你想找到第n个置换元素时,应用置换函数,如果它生成的数字超出了所需范围,则再次应用它;重复,直到数字在可接受的范围内。

    第 2 步所需的迭代次数平均不超过 2 次;最坏的情况很高,但极不可能发生。

    【讨论】:

    • 谢谢。在“足够大”的 LFSR 中跳过超出范围的数字非常有效,而且非常简单。我专注于计算任意位置的值(即使我说我不是),并且还试图使其完美而不是丢掉数字。
    【解决方案2】:

    首先,请注意这不是随机序列。它只生成一个单一的、固定的、重复的序列,种子会选择你从序列中的哪个位置开始。当然,这与所有 PRNG 相同,但通常 PRNG 的周期远大于 16 位或 32 位。按照您所描述的使用方式,循环长度等于您正在迭代的项目数,因此它所要做的就是采用一个“随机”顺序并更改您的位置开始。 “种子”值更像是一个起始索引而不是种子。

    这不是最令人满意的答案,但它可能是实用的:您可以将长度填充到 2 的下一个幂,并跳过实际最大值以上的任何索引。因此,如果您有 5000 个项目,则生成一个包含 8192 个项目的序列,并丢弃 [5000,8191] 之间的任何结果。这样做的开销听起来很难看,但从长远来看,它并没有那么糟糕:因为这最多可以使列表的长度增加一倍,平均而言,您必须丢弃两个结果中的一个,所以最坏情况下的平均开销是加倍的工作量。

    以下代码演示了这一点(并展示了一种更简洁的实现方式)。 MaxLengthLFSR 的第三个参数(如果给定)是实际最大值。您可能希望为更多大小填写 TAP_MASKS,然后选择适合所请求序列长度的最小寄存器大小;这里我们只使用请求的那个,它可以工作,但是如果序列的长度比它需要的长得多,则会导致更多的开销。

    TAP_MASKS = { # only one needed, but I included 3 to make the code more useful
        10: 0x00000240, # taps at 10, 7
        16: 0x0000B400, # taps at 16, 14, 13, 11
        32: 0xE0000200, # taps at 32, 31, 30, 10
    }
    
    def MaxLengthLFSR(next_val, reglen, max_value=None):
        """Iterate values from seed in max-length LFSR using Galois configuration."""
        # Ensure that max_value isn't 0, or we'll infinitely loop without yielding any values.
        if max_value is not None:
            assert max_value > 0
    
        while True:
            if max_value is None or next_val < max_value:
                yield next_val
    
            lsb = next_val & 1
            next_val = next_val >> 1
            if lsb == 1:
                mask = TAP_MASKS[reglen]
                next_val ^= mask
    
    sample_count = 5 # demonstration number
    num_retries = 3  # just enough to show the repeatable behaviour
    for trynum in xrange(num_retries):
        it = MaxLengthLFSR(1, 16, 2000)
        seq = []
        for x in xrange(sample_count):
            seq.append(next(it))
        seq = [str(x) for x in seq]
        print "try %s: %s..." % (trynum + 1, ", ".join(seq))
    

    【讨论】:

    • 谢谢!我知道 LFSR 每次对于相同的位长度和抽头集都会产生完全相同的数字重复序列,因此并不是真正随机的。但这适用于许多应用程序。关于您的代码清理,我的“真正”实现实际上是一个生成器,但由于许多人在理解它们时遇到问题,我对其进行了反生成,并使其成为(我希望!)易于阅读的经典功能。为了简洁起见,我还去掉了一个完整的抽头集和一个新的抽头计算器。
    猜你喜欢
    • 2010-10-05
    • 1970-01-01
    • 2019-04-10
    • 1970-01-01
    • 1970-01-01
    • 2020-01-08
    • 1970-01-01
    • 2023-03-09
    • 1970-01-01
    相关资源
    最近更新 更多