迭代生成自然数的排列

时间:2018-07-18 23:11:49

标签: algorithm permutation

我有一个不寻常的问题,这个问题以前可能没有问过(虽然我什么也没找到,但是我可能只是在寻找错误的流行词)。

我的任务很简单:给定自然数的“列表”,直到N [0,1,2,... N-1],我想重新排列此序列。例如。当我输入数字4时,一个可能的结果将是[3,0,1,2]。随机性应该由某种种子确定(但这是大多数PRNG用通用语言编写的标准)。

天真的方法是只实例化一个大小为N的数组,用数字填充并使用任何改组算法。

但是,问题是这种方法的内存复杂度是O(n),在我的特殊情况下这是很难处理的。我的想法是,编写一个生成器,以迭代方式提供结果列表中的数字。

更准确地说,我需要一些“算法”以迭代方式提供数字。更准确地说,概念类如下所示:

class Generator {
   // some state
   int nextNumber(...) {
      // some magic
   }
}

并迭代调用nextNumber方法可提供序列号(即[0,1,... N-1]的任何排列。当然,此生成器实例的状态应具有比O更好的内存复杂性(n)再说一次(我什么也得不到)。

我想要什么算法来做?

3 个答案:

答案 0 :(得分:6)

这是{3}在Python 3中的一个非常简单的实现,它使用了将近两年前编写的平衡Format-preserving encryption。它可以在32位系统上执行N最多2 64 所需的索引排列,或者在Python 64位版本上执行2 128 所需的索引排列。这是由于hash()函数返回的整数的大小。请参阅sys.hash_info来找到系统的限制。使用更高级的哈希函数可以返回更大位长的值并不难,但我不想使此代码更复杂或更慢。

更新

我对以前的版本做了一些小的改进,并且在注释中添加了更多信息。代替使用散列函数返回的低位,我们使用高位,这通常会改善随机性,尤其是对于短位长度的情况。我还添加了另一个哈希函数Feistel network,在此应用程序中,它的工作原理比Python的hash好很多,比Python的hash更好,特别是对于较短的位长,尽管它慢一点。 xxhash算法的xxhash by Yann Collet比内置的stop高得多,因此,产生的排列往往会更加混乱。

尽管此代码适用于stop >= 2**16的较小值,但它更适合处理random.shuffle。如果您需要置换较小的范围,最好在list(range(stop))上使用list(range(2**16))。这样会更快,并且不会使用太多RAM:bytes在32位计算机上消耗约1280 KB。

您会注意到,我使用一个字符串作为随机数生成器的种子。对于此应用程序,我们希望随机化器具有足够的熵,并且使用大型字符串(或stop)是实现此目的的简便方法,正如avalanche effect文档所提到的那样。即使这样,当stop == 35很大时,该程序也只能产生所有可能排列的很小一部分。 stop有35个! (35个阶乘)不同的排列,还有35个! > 2 132 ,但是我们密钥的总位长只有128,因此它们不能涵盖所有这些排列。我们可以增加Feistel发回的次数来获得更多的覆盖范围,但是显然对于大的''' Format preserving encryption using a Feistel network This code is *not* suitable for cryptographic use. See https://en.wikipedia.org/wiki/Format-preserving_encryption https://en.wikipedia.org/wiki/Feistel_cipher http://security.stackexchange.com/questions/211/how-to-securely-hash-passwords A Feistel network performs an invertible transformation on its input, so each input number produces a unique output number. The netword operates on numbers of a fixed bit width, which must be even, i.e., the numbers a particular network operates on are in the range(4**k), and it outputs a permutation of that range. To permute a range of general size we use cycle walking. We set the network size to the next higher power of 4, and when we produce a number higher than the desired range we simply feed it back into the network, looping until we get a number that is in range. The worst case is when stop is of the form 4**k + 1, where we need 4 steps on average to reach a valid n. In the typical case, where stop is roughly halfway between 2 powers of 4, we need 2 steps on average. Written by PM 2Ring 2016.08.22 ''' from random import Random # xxhash by Yann Collet. Specialised for a 32 bit number # See http://fastcompression.blogspot.com/2012/04/selecting-checksum-algorithm.html def xxhash_num(n, seed): n = (374761397 + seed + n * 3266489917) & 0xffffffff n = ((n << 17 | n >> 15) * 668265263) & 0xffffffff n ^= n >> 15 n = (n * 2246822519) & 0xffffffff n ^= n >> 13 n = (n * 3266489917) & 0xffffffff return n ^ (n >> 16) class FormatPreserving: """ Invertible permutation of integers in range(stop), 0 < stop <= 2**64 using a simple Feistel network. NOT suitable for cryptographic purposes. """ def __init__(self, stop, keystring): if not 0 < stop <= 1 << 64: raise ValueError('stop must be <=', 1 << 64) # The highest number in the range self.maxn = stop - 1 # Get the number of bits in each part by rounding # the bit length up to the nearest even number self.shiftbits = -(-self.maxn.bit_length() // 2) self.lowmask = (1 << self.shiftbits) - 1 self.lowshift = 32 - self.shiftbits # Make 4 32 bit round keys from the keystring. # Create an independent random stream so we # don't intefere with the default stream. stream = Random() stream.seed(keystring) self.keys = [stream.getrandbits(32) for _ in range(4)] self.ikeys = self.keys[::-1] def feistel(self, n, keys): # Split the bits of n into 2 parts & perform the Feistel # transformation on them. left, right = n >> self.shiftbits, n & self.lowmask for key in keys: left, right = right, left ^ (xxhash_num(right, key) >> self.lowshift) #left, right = right, left ^ (hash((right, key)) >> self.lowshift) return (right << self.shiftbits) | left def fpe(self, n, reverse=False): keys = self.ikeys if reverse else self.keys while True: # Cycle walk, if necessary, to ensure n is in range. n = self.feistel(n, keys) if n <= self.maxn: return n def test(): print('Shuffling a small number') maxn = 10 fpe = FormatPreserving(maxn, 'secret key string') for i in range(maxn): a = fpe.fpe(i) b = fpe.fpe(a, reverse=True) print(i, a, b) print('\nShuffling a small number, with a slightly different keystring') fpe = FormatPreserving(maxn, 'secret key string.') for i in range(maxn): a = fpe.fpe(i) b = fpe.fpe(a, reverse=True) print(i, a, b) print('\nHere are a few values for a large maxn') maxn = 10000000000000000000 print('maxn =', maxn) fpe = FormatPreserving(maxn, 'secret key string') for i in range(10): a = fpe.fpe(i) b = fpe.fpe(a, reverse=True) print('{}: {:19} {}'.format(i, a, b)) print('\nUsing a set to test that there are no collisions...') maxn = 100000 print('maxn', maxn) fpe = FormatPreserving(maxn, 'secret key string') a = {fpe.fpe(i) for i in range(maxn)} print(len(a) == maxn) print('\nTesting that the operation is bijective...') for i in range(maxn): a = fpe.fpe(i) b = fpe.fpe(a, reverse=True) assert b == i, (i, a, b) print('yes') if __name__ == "__main__": test() 这是不切实际的。

Shuffling a small number
0 4 0
1 2 1
2 5 2
3 9 3
4 1 4
5 3 5
6 7 6
7 0 7
8 6 8
9 8 9

Shuffling a small number, with a slightly different keystring
0 9 0
1 8 1
2 3 2
3 5 3
4 2 4
5 6 5
6 1 6
7 4 7
8 7 8
9 0 9

Here are a few values for a large maxn
maxn = 10000000000000000000
0: 7071024217413923554 0
1: 5613634032642823321 1
2: 8934202816202119857 2
3:  296042520195445535 3
4: 5965959309128333970 4
5: 8417353297972226870 5
6: 7501923606289578535 6
7: 1722818114853762596 7
8:  890028846269590060 8
9: 8787953496283620029 9

Using a set to test that there are no collisions...
maxn 100000
True

Testing that the operation is bijective...
yes
0 4
1 2
2 5
3 9
4 1
5 3
6 7
7 0
8 6
9 8

输出

def ipermute(stop, keystring):
    fpe = FormatPreserving(stop, keystring)
    for i in range(stop):
        yield fpe.fpe(i)

for i, v in enumerate(ipermute(10, 'secret key string')):
    print(i, v)

以下是使用它制作发电机的方法:

0 4
1 2
2 5
3 9
4 1
5 3
6 7
7 0
8 6
9 8

输出

AWS::EMR::Cluster

(对于Python)它相当快,但是绝对不适用于密码学。通过将Feistel轮数增加到至少5并使用适当的加密哈希函数(例如random module),可以将其设置为加密级。同样,将需要使用加密方法来生成Feistel密钥。当然,除非您确切地知道自己在做什么,否则不应该编写密码软件,因为编写容易受到定时攻击等影响的代码太容易了。

答案 1 :(得分:5)

您要查找的是以函数形式(例如 f )的伪随机排列,该伪随机排列将数字从1映射到N到伪随机双射词中从1映射到N的数字办法。然后,要生成伪随机排列的 n 个数字,只需返回 f(n)

这与加密本质上是相同的问题。带密钥的分组密码是伪随机双射函数。如果您以某种顺序将所有可能的纯文本块完全输入一次,它将以不同的伪随机顺序一次返回所有可能的密文块。

因此,要解决像您这样的问题,您实际上要做的是创建一个密码,该密码适用于从1到N的数字,而不是256位的块或任何其他数字。您可以使用密码学中的工具来做到这一点。

例如,您可以使用Feistel结构(https://en.wikipedia.org/wiki/Feistel_cipher)来构造置换函数,如下所示:

  1. 让W为floor(sqrt(N)),让函数输入为x
  2. 如果x
  3. x =(x +(N-W ^ 2))%N
  4. 重复步骤(2)和(3)几次。您做得越多,结果看起来就越随机。步骤(3)确保x

由于此函数包含多个步骤,每个步骤都将以双射方式将0到N-1的数字映射到0到N-1的数字,因此整个函数也将具有此属性。如果输入0到N-1之间的数字,则会以伪随机顺序将它们取回来。

答案 2 :(得分:-1)

我认为您正在处理排列的等级。 (我可能是错的)。 我已经为此写了Rosetta代码task;以及回答关于herehere的其他SO问题。

这有用吗?