【问题标题】:How to generate a pseudo-random involution?如何生成伪随机对合?
【发布时间】:2016-12-24 00:54:42
【问题描述】:

为了生成伪随机排列,可以使用Knuth shuffles。对合是一种自逆排列,我想,我可以通过禁止多次触摸一个元素来调整洗牌。但是,我不确定我是否可以有效地做到这一点,以及它是否会等概率地产生每个对合。

恐怕需要一个例子:在集合{0,1,2} 上,有 6 个排列,其中 4 个是对合。我正在寻找一种算法以相同的概率随机生成其中一个。

一个正确但效率非常低的算法是:使用 Knuth shuffle,如果不是对合则重试。

【问题讨论】:

  • 为什么投反对票?这对我来说似乎是一个有趣的问题。
  • 我更改了答案,所以我的代码现在更加优雅和高效。
  • @RoryDaulton 我敢肯定,您的回答从一开始就值得接受;只是我没有时间深入了解。
  • 我不是为了让你接受我的回答。我真的认为这个问题很有趣,所以我花了额外的时间。我正在将我的排列代码从 Borland Delphi 转移到 Python,现在这是我关于对合的部分。询问您是否有任何问题,尤其是如果您不了解 Python(我最近学习了)。
  • @RoryDaulton 很高兴你喜欢这个问题。我的 Python 比较弱,但是 Python 是一种可读性很强的语言。我花了一些时间思考是否有不使用invo_count 的解决方案......感觉应该有(Knuth shuffle 没有使用这样的测试,虽然它可能会留下一个元素不变),但我能想象的一切都会有偏见(如我看不到一种“自然”的方式来获得像 76./26 这样的概率,只有 7 个元素)。

标签: algorithm math random permutation inverse


【解决方案1】:

我们在这里使用a(n) 作为一组大小n (as OEIS does) 的对合次数。对于给定大小的集合n 和该集合中的给定元素,该集合上的对合总数为a(n)。该元素必须通过对合保持不变或与另一个元素交换。使我们的元素保持不变的对合次数是a(n-1),因为这些是对其他元素的对合。因此,对合上的均匀分布必须有a(n-1)/a(n) 的概率保持该元素固定。如果要修复它,请不要理会该元素。否则,选择另一个尚未被我们的算法检查的元素与我们的元素交换。我们刚刚决定了集合中的一两个元素会发生什么:继续并决定一次一两个元素会发生什么。

为此,我们需要每个i <= n 的对合计数列表,但这很容易通过递归公式完成

a(i) = a(i-1) + (i-1) * a(i-2)

(请注意,OEIS 中的这个公式也来自我的算法:第一项计算对合,将第一个元素保持在原位,第二项用于与它交换的元素。)如果您正在使用involutions,这可能很重要,可以分解成另一个函数,预先计算一些较小的值,并缓存函数的结果以获得更快的速度,如下代码所示:

# Counts of involutions (self-inverse permutations) for each size
_invo_cnts = [1, 1, 2, 4, 10, 26, 76, 232, 764, 2620, 9496, 35696, 140152]

def invo_count(n):
    """Return the number of involutions of size n and cache the result."""
    for i in range(len(_invo_cnts), n+1):
        _invo_cnts.append(_invo_cnts[i-1] + (i-1) * _invo_cnts[i-2])
    return _invo_cnts[n]

我们还需要一种方法来跟踪尚未决定的元素,以便我们可以有效地选择具有统一概率的元素和/或将元素标记为已决定。我们可以将它们保存在一个缩小列表中,并在列表的当前末尾添加一个标记。当我们决定一个元素时,我们将当前元素移动到列表的末尾以替换决定的元素,然后减少列表。有了这个效率,这个算法的复杂度是O(n),除了最后一个元素之外,每个元素都有一个随机数计算。没有更好的订单复杂性了。

这是 Python 3.5.2 中的代码。由于未决定元素列表所涉及的间接性,代码有些复杂。

from random import randrange

def randinvolution(n):
    """Return a random (uniform) involution of size n."""

    # Set up main variables:
    # -- the result so far as a list
    involution = list(range(n))
    # -- the list of indices of unseen (not yet decided) elements.
    #    unseen[0:cntunseen] are unseen/undecided elements, in any order.
    unseen = list(range(n))
    cntunseen = n

    # Make an involution, progressing one or two elements at a time
    while cntunseen > 1:  # if only one element remains, it must be fixed
        # Decide whether current element (index cntunseen-1) is fixed
        if randrange(invo_count(cntunseen)) < invo_count(cntunseen - 1):
            # Leave the current element as fixed and mark it as seen
            cntunseen -= 1
        else:
            # In involution, swap current element with another not yet seen
            idxother = randrange(cntunseen - 1)
            other = unseen[idxother]
            current = unseen[cntunseen - 1]
            involution[current], involution[other] = (
                involution[other], involution[current])
            # Mark both elements as seen by removing from start of unseen[]
            unseen[idxother] = unseen[cntunseen - 2]
            cntunseen -= 2

    return involution

我做了几个测试。这是我用来检查有效性和均匀分布的代码:

def isinvolution(p):
    """Flag if a permutation is an involution."""
    return all(p[p[i]] == i for i in range(len(p)))

# test the validity and uniformness of randinvolution()
n = 4
cnt = 10 ** 6
distr = {}
for j in range(cnt):
    inv = tuple(randinvolution(n))
    assert isinvolution(inv)
    distr[inv] = distr.get(inv, 0) + 1
print('In {} attempts, there were {} random involutions produced,'
    ' with the distribution...'.format(cnt, len(distr)))
for x in sorted(distr):
    print(x, str(distr[x]).rjust(2 + len(str(cnt))))

结果是

In 1000000 attempts, there were 10 random involutions produced, with the distribution...
(0, 1, 2, 3)     99874
(0, 1, 3, 2)    100239
(0, 2, 1, 3)    100118
(0, 3, 2, 1)     99192
(1, 0, 2, 3)     99919
(1, 0, 3, 2)    100304
(2, 1, 0, 3)    100098
(2, 3, 0, 1)    100211
(3, 1, 2, 0)    100091
(3, 2, 1, 0)     99954

这对我来说看起来很统一,我检查的其他结果也是如此。

【讨论】:

    【解决方案2】:

    对合是一对一的映射,它是它自己的逆。任何密码都是一对一的映射;它必须是为了明确地解密密文。

    对于对合,您需要一个自身逆的密码。存在这样的密码,ROT13 就是一个例子。其他一些请参见Reciprocal Cipher

    对于您的问题,我建议使用 XOR 密码。选择一个至少与初始数据集中最长的数据一样长的随机密钥。如果您使用 32 位数字,则使用 32 位密钥。为了置换,依次对每条数据的密钥进行异或。反向排列(相当于解密)是完全一样的异或运算,会回到原始数据。

    这将解决数学问题,但它绝对不是密码安全的。重复使用相同的密钥将允许攻击者发现密钥。我假设除了需要看似随机且分布均匀的对合之外,没有任何安全要求。

    ETA:这是我在第二条评论中所说的 Java 演示。作为 Java,我为您的 13 个元素集使用索引 0..12。

    public static void Demo() {
    
        final int key = 0b1001;
    
        System.out.println("key = " + key);
        System.out.println();
    
        for (int i = 0; i < 13; ++i) {
    
            System.out.print(i + " -> ");
            int ctext = i ^ key;
    
            while (ctext >= 13) {
                System.out.print(ctext + " -> ");
                ctext = ctext ^ key;
            }
            System.out.println(ctext);
        }
    
    } // end Demo()
    

    演示的输出是:

    key = 9
    
    0 -> 9
    1 -> 8
    2 -> 11
    3 -> 10
    4 -> 13 -> 4
    5 -> 12
    6 -> 15 -> 6
    7 -> 14 -> 7
    8 -> 1
    9 -> 0
    10 -> 3
    11 -> 2
    12 -> 5
    

    转换后的键会从数组末尾掉出,它会再次转换,直到它落在数组内。我不确定while 构造是否属于函数的严格数学定义。

    【讨论】:

    • 我想要对我的输入集进行对合,例如,它有 13 个元素,因此异或可能会超出范围。此外,我想要一个任意的对合,而不仅仅是那些可以通过 xoring 获得的对合。实际上,我想要的是正是 Knuth shuffle 所做的,除了对合约束。对,没有安全要求。
    • 由于范围有限,您需要像 Hasty Pudding 密码方法这样的东西。使用一对一的属性得到正确范围内的结果;只需根据需要多次重复 XOR 操作以进入范围。使用四位密钥和十三个元素,您无需重复太多次即可获得范围内的结果。如果您的元素大于四位,则只需使用密码结果作为索引。
    • 异或的问题是你实际上不需要while。要么是单词,要么你重复一次然后它就被撤消了。请注意,对于 13 个输入元素,只有 16 个可能的键,但是超过 100k 对合。
    • 需要 while 或 `if` 来检查第一个 XOR 是否超出范围。 OP 似乎只需要一个对合,每个可能的键都会给出不同的对合。当然不是全系列,但有一些可供选择。
    • 我是 OP。我正在寻找的正是类似 Knuth Shuffle 的东西:生成 all 可能的结果 equiprobably;很抱歉在第一个版本中没有明确说明。另一个答案似乎正是这样做的。
    猜你喜欢
    • 2014-10-30
    • 2023-01-03
    • 2015-08-07
    • 2012-02-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-05
    • 2014-05-18
    相关资源
    最近更新 更多