我们在这里使用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
这对我来说看起来很统一,我检查的其他结果也是如此。