Python:保持最小距离的范围内的随机数字列表



假设这段代码 random.seed(42) random.sample(range(0,40), 4)输出:[7, 1, 17, 15]我应该在此代码中更改什么以生成随机数,其中列表中任何两个数字之间的最小距离至少为 10 或更大。像[0, 10, 25, 39] or [0, 12, 23, 38 ]. 可能的重复是这样的。谢谢。

排序案例的单行解决方案

这里有一个简单的单行代码,它以相等的可能性生成所有可能性:

[9*i + x for i, x in enumerate(sorted(random.sample(range(13), 4)))]

一些示例输出:

[2, 16, 26, 38]
[0, 10, 25, 35]
[2, 12, 25, 36]
[0, 13, 26, 39]
[1, 14, 24, 34]
[1, 11, 29, 39]
[0, 13, 26, 39]
[1, 12, 27, 38]

输出总是按排序顺序生成;如果这不是你想要的,你可以很容易地在结果中添加一个随机播放(或见下面的一般解决方案)。

说明:如果[a, b, c, d]是满足您要求的有序列表,则[a, b-9, c-18, d-27]是长度为range(13)4 的有序样本,反之亦然。因此,您需要做的就是从range(13)生成样本,对它们进行排序,然后重新添加必要的9倍数以获得至少相距10的值。

一般未分类解决方案

这是一个不需要对随机样本进行排序的通用解决方案。相反,我们计算样本元素的秩,并使用这些秩来计算必要的偏移量。

import random
def ranks(sample):
"""
Return the ranks of each element in an integer sample.
"""
indices = sorted(range(len(sample)), key=lambda i: sample[i])
return sorted(indices, key=lambda i: indices[i])
def sample_with_minimum_distance(n=40, k=4, d=10):
"""
Sample of k elements from range(n), with a minimum distance d.
"""
sample = random.sample(range(n-(k-1)*(d-1)), k)
return [s + (d-1)*r for s, r in zip(sample, ranks(sample))]

还有一些示例输出:

>>> sample_with_minimum_distance()
[17, 27, 3, 38]
>>> sample_with_minimum_distance()
[27, 38, 10, 0]
>>> sample_with_minimum_distance()
[36, 13, 1, 24]
>>> sample_with_minimum_distance()
[1, 25, 15, 39]
>>> sample_with_minimum_distance()
[26, 12, 1, 38]

"廉价伎俩"解决方案

如果原始问题中的各种常量是固定的(总体range(40),长度为 4 的样本,最小距离为 10),那么有一个明显的廉价技巧:只有715可能的不同排序样本,所以只需预先创建一个包含所有这些样本的列表,然后每次你需要生成一个样本时, 使用random.choice从预先创建的列表中选择一个。

对于这一代人来说,我们要么采用效率极低但明显正确的蛮力解决方案:

>>> import itertools
>>> all_samples = [  # inefficient brute-force solution
...     sample for sample in itertools.product(range(40), repeat=4)
...     if all(x - y >= 10 for x, y in zip(sample[1:], sample))
... ]
>>> len(all_samples)
715

这仍然足够快,在我的机器上只需要几秒钟。或者,我们可以使用上面确定的相同双射来做一些更精细和直接的事情。

>>> all_samples = [
...     [9*i + s for i, s in enumerate(sample)]
...     for sample in itertools.combinations(range(13), 4)
... ]
>>> len(all_samples)
715

无论哪种方式,我们只生成一次样本列表,然后在每次需要时使用random.choice选择一个:

>>> random.choice(all_samples)
(1, 11, 21, 38)
>>> random.choice(all_samples)
(0, 10, 23, 33)

当然,这种解决方案不能很好地扩展:对于range(100)个最小距离为 5 的 7 个样本,有超过 20 亿个可能的不同分类样本。

均匀性展示

我之前声称单行以相等的可能性产生所有可能性(当然,假设随机数的完美来源,但 Python 的 Mersenne Twister 足够好,我们不太可能在下面的测试中检测到由核心生成器引起的统计异常)。这是这种一致性的演示。

首先,为了方便起见,我们将单行代码包装在一个函数中。我们还将更改它以返回tuple而不是list,因为下一步我们想要一些可哈希的东西。

>>> def sorted_sample():
...     return tuple(9*i + x for i, x in
...                  enumerate(sorted(random.sample(range(13), 4))))

现在我们生成 1000 万个样本(这将需要几分钟),并计算每个样本出现的频率:

>>> from collections import Counter
>>> samples = Counter(sorted_sample() for _ in range(10**7))

几个快速检查:

>>> len(samples)
715
>>> 10**7 / 715
13986.013986013986
>>> samples[0, 10, 20, 30]
14329
>>> samples[0, 11, 22, 33]
13995
>>> min(samples.values())
13624
>>> max(samples.values())
14329

我们收集了 715 种不同的组合,一点点数学告诉我们,这正是我们期望的数字(13 选择 4),因此在均匀分布的情况下,我们预计每个组合大约出现10**7 / 715次,或大约 14000 次。我们上面检查的两个组合都在 14000 左右,出现的最小和最大计数也是如此,但毫不奇怪,有一些随机变化。

这种随机变化是否在可接受的范围内?要找出答案,我们可以用p = 0.01进行卡方检验。我们的零假设是我们绘制的群体均匀的:即,我们的代码以相等的可能性生成每个可能的样本。

SciPy 使均匀性的卡方检验变得简单:

>>> from scipy.stats import chisquare
>>> chisquare(list(samples.values()))
Power_divergenceResult(statistic=724.682234, pvalue=0.3825060783237031)

我们得到的 p 值小于 0.01,因此我们无法否定原假设:也就是说,我们没有不均匀性的证据。

生成一个数字后,它会从您的范围中删除一条带,因为您知道没有数字可以在原始数字的 +/- 10 范围内。

实现这一点的一种天真方法是列出剩余数字,并在每次选择数字时从中切出块:

domain = list(range(40))
result = []
while domain:
n = random.choice(domain)
result.append(n)
domain = [x for x in domain if x <= n - 10 or x >= x + 10]

请注意,每个示例最多会从您的网域中移除 19 个元素。这意味着您不能保证在结果中获得 4 个元素,但至少可以保证 3 个。

如果样本大小与域的长度成正比,那么一种选择是洗牌域并选择满足需求的前四个元素。

使用集合来跟踪排除哪些数字可以使该过程高效。

法典

import random

def choose_with_step(domain, step, k):
domain = list(domain)
random.shuffle(domain)
exclusions = set()
choices = []
while domain and k > 0:
choice = domain.pop()
if choice not in exclusions:
choices.append(choice)
for x in range(choice - step + 1, choice + step):
exclusions.add(x)
k -= 1
return choices

输出示例

# choose_with_step(range(40), 10, 4)
[15, 5, 33]
[11, 25, 35, 0]
[27, 12, 37, 0]
[36, 9, 26]

时间复杂度

由于random.shuffleO(n)中运行,并且算法遍历洗牌列表一次,因此该算法为O(n * 步长)。

关于域长度的线性算法是要求样本大小与域大小成正比的原因,否则列表可能会因为仅选择几个元素而被打乱。

对于任何寻求澄清顶部答案的单行解决方案的人来说,我认为这可能很有用:

[9*i + x for i, x in enumerate(sorted(random.sample(range(13), 4)))]

9代表:min_distance - 1

这4代表:sample_size

这13代表:range_size - ((min_distance - 1) * (sample_size - 1))

例如;在示例情况下为 40 - 9*3 = 13。

此外,如果您发现遇到错误,即所需的样本数量超过计算的样本范围(即示例中为 13),使用random.choices()代替random.sample()可能会对您有所帮助,因为它允许在采样时替换,并达到与原始解决方案几乎相同的效果。例如,要在 765 的范围中生成 100 个最小距离为 7 的随机整数的列表,原始解决方案将不起作用。 但是,以下内容将:

[7*i+x for i,x in enumerate(sorted(random.choices(list(range(72)),k=100)))])

其中的值反映了我上面列出的内容,除了min_distance - 1被替换为min_distance. 所以,7 等于min_distance,100 等于sample size,72 =range_size - (min_distance * (sample_size - 1)),即 765 - 7*99。此方法外推到范围,距离,距离的样本*样本<范围的任何值,而原始解决方案没有。>

在这里使用random.choices()的问题在于,虽然它确实产生了所有可能的结果,但它并不能保证所有可能的结果的同等可能性,就像在原始解决方案中一样。 但是,根据任务的不同,这对您来说可能并不重要。

由于 4 个数字必须各自保持 10 的距离,因此对于随机分布的 4 个数字来说,40 个数字中只有 10 个的"回旋余地"(因为 40 - 3 * 10 = 10)。因此,您可以简单地在 10 个房间内随机化 4 个数字,计算增量,然后将增量和相应的 10 相加以获得完整列表。

import random
d = sorted(random.randint(0, 9) for _ in range(4))
o = [b - a for a, b in zip([0] + d[:-1], d)]
print([i * 10 + sum(o[:i + 1]) for i in range(4)])

10 次运行的示例:

[1, 13, 24, 37]
[4, 17, 27, 39]
[0, 10, 23, 33]
[1, 12, 27, 37]
[0, 13, 24, 35]
[3, 14, 27, 39]
[0, 11, 21, 38]
[1, 14, 26, 37]
[0, 11, 23, 39]
[1, 15, 28, 38]

根据您想要的分布,您可以执行以下操作:

import random
def random_separated(n, start, stop, gap):
numbers = []
for i in range(n):
while True:
num = random.randint(start, stop)
if all(n - gap < num < n + gap
for n in numbers):
break
numbers.append(num)
return numbers

最新更新