簡體   English   中英

迭代地生成自然數的排列

[英]Iteratively generating a permutation of natural numbers

我有一個不尋常的問題,可能會或可能不會被問過(雖然我沒有找到任何東西,但我可能只是尋找錯誤的流行語)。

我的任務很簡單:給出自然數字的“列表”,直到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)具有更好的內存復雜性再一次(我什么也得不到)。

有什么算法要做,我想要什么?

這是使用我在大約2年前寫的平衡Feistel網絡格式保留加密的 Python 3中相當簡單的實現。 它可以在32位系統上執行N到2 64的索引排列,或在64位構建的Python上執行2 128 這是由於hash()函數返回的整數的大小。 請參閱sys.hash_info以查找系統的限制。 使用可以返回更大位長的值的更高級的哈希函數並不難,但我不想讓這段代碼更復雜或更慢。

更新

我對之前的版本進行了一些小改進,並在評論中添加了一些更多信息。 我們使用高位來代替使用散列函數返回的低位,這通常會改善隨機性,特別是對於短位長度。 我還添加了另一個散列函數, Yann Collet的xxhash ,它的工作效果比Python的hash 好得多,特別是對於較短的位長,盡管速度稍慢。 xxhash算法具有比內置hash高得多的雪崩效應 ,因此產生的排列往往更加平滑。

雖然此代碼適用於較小的stop值,但它更適合處理stop >= 2**16 如果你需要置換較小的范圍, random.shufflelist(range(stop))使用random.shuffle list(range(stop))可能是一個好主意。 它會更快,並且它不會使用那么多的RAM: list(range(2**16))在32位機器上消耗大約1280千字節。

您會注意到我使用字符串為隨機數生成器播種。 對於這個應用程序,我們希望隨機數發生器具有足夠的熵,並且使用大字符串(或bytes )是一種簡單的方法,就像random模塊文檔提到的那樣。 即便如此,當stop很大時,該程序只能產生所有可能排列的一小部分。 對於stop == 35有35! (35階乘)不同的排列,35! > 2 132 ,但我們的密鑰的總位長僅為128,因此它們無法涵蓋所有​​這些排列。 我們可以增加Feistel輪數以獲得更多的覆蓋范圍,但顯然這對於​​大值stop來說是不切實際的。

''' 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

它的速度相當快(對於Python),但它絕對適合加密。 可以通過將Feistel輪數增加到至少5並且通過使用合適的加密散列函數(例如Blake2)來使其成為加密級。 此外,需要使用加密方法來生成Feistel密鑰。 當然,除非你確切知道自己在做什么,否則不應該編寫加密軟件,因為編寫易受時序攻擊等影響的代碼太容易了。

你正在尋找的是一個函數形式的偽隨機置換,比如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 <W ^ 2,則將x除以從0到W-1的2個字段:h = floor(x / W)並且l = x%W。 哈希h生成一個從0到W-1的值,並設置l =(l + hash)%W。 然后交換字段 - 讓x = l * W + h
  3. x =(x +(NW ^ 2))%N
  4. 重復步驟(2)和(3)若干次。 你做得越多,結果越隨機。 步驟(3)確保x <W ^ 2對於許多輪次都是正確的。

由於該函數由許多步驟組成,每個步驟將以0和N-1的數字以雙射方式將數字從0映射到N-1,整個函數也將具有此屬性。 如果你輸入從0到N-1的數字,你將以偽隨機順序將它們退回。

我認為你正在處理排列的等級。 (我可能是錯的)。 我已經寫了一個Rosetta代碼任務 ; 以及在這里這里回答其他SO問題。

這有用嗎?

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM