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

时间:2018-08-19 14:22:34

标签: python python-3.x random

让我们说这段代码 random.seed(42) random.sample(range(0,40), 4) 输出:[7, 1, 17, 15] 我应该在此代码中进行哪些更改以生成随机数,其中列表中任意两个数字之间的最小距离至少应为10或更大。类似于[0, 10, 25, 39] or [0, 12, 23, 38 ]。 可能重复的是this。谢谢。

6 个答案:

答案 0 :(得分:3)

针对已排序案例的单行解决方案

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

[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)中的7个样本,最小距离为5,有超过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个),因此,如果分布均匀,我们希望每个组合大约会发生{{1} }次,或大约14000次。我们上面检查过的两个组合的最小值和最大值都在14000左右,但这并不奇怪,存在一些随机变化。

该随机变化是否在可接受的范围内?要找出答案,我们可以对10**7 / 715进行卡方检验。我们的零假设是,我们从抽取的总体是均匀的:即,我们的代码以相同的可能性生成每个可能的样本。

SciPy使chi-squared test变得更加容易:

p = 0.01

我们获得的p值小于0.01,所以我们不能拒绝原假设:也就是说,我们没有不均匀性的证据。

答案 1 :(得分:2)

生成数字后,由于您知道没有数字可以在原始数字的+/- 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个元素。

答案 2 :(得分:0)

由于4个数字之间的距离必须保持为10,因此,要随机分配4个数字(因为40-3 * 10 = 10),所以在40个数字中只有10个“摆动空间”。因此,您只需在10个房间内随机分配4个数字,计算增量,然后将增量和相应的10s相加即可得到完整列表。

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]

答案 3 :(得分:0)

如果样本大小与您的域的长度成比例,那么一种选择是改组域并选择满足要求的前四个元素。

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

代码

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.shuffle runs in O(n)并且算法遍历了随机列表一次,因此算法为 O(n * step)

关于域长度的算法是线性的,这是要求样本大小与域大小成比例的原因,否则列表可能会被改组以仅选择一些元素。

答案 4 :(得分:0)

对于任何寻求对最佳答案的单行解决方案进行澄清的人,我认为这可能有用:

java.lang.ExeptionInInitializerError: null

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

4 代表:min_distance - 1

13 代表:sample_size

例如; 40 - 9*3 = 13 在示例中。

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

random.sample()

其中的值反映了我上面列出的内容,除了 [7*i+x for i,x in enumerate(sorted(random.choices(list(range(72)),k=100)))]) 替换为 min_distance - 1。因此,7 等于 min_distance,100 等于 min_distance,72 = sample size,即 765 - 7*99。此方法外推到距离 * 样本 < 范围的范围、距离、样本的任何值,原始解没有。

此处使用 range_size - (min_distance * (sample_size - 1)) 的问题在于,虽然它确实产生了所有可能的结果,但它并不能保证所有可能结果的可能性均等,因为在原始解决方案中。但是,根据任务的不同,这对您来说可能并不重要。

答案 5 :(得分:-1)

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

input_type