生成随机数,使其总和为预定值
这里的情况是这样的:我想生成4个伪随机数,这4个数加起来要等于40。请问在Python中怎么做到这一点呢?我可以先生成一个1到40之间的随机数,然后再生成一个1到剩余数之间的随机数,依此类推。但这样的话,第一个生成的数就更有可能“占用”更多的值。
8 个回答
21
使用多项分布
from numpy.random import multinomial
multinomial(40, [1/4.] * 4)
在这个例子中,每个变量的分布就像是一个二项分布,平均值是n * p
,也就是40 * 1/4 = 10
。
122
这是一个标准的解决方案。它和Laurence Gonsalves的回答很相似,但有两个优点。
- 它是统一的:每种组合的4个正整数加起来等于40的可能性是一样的。
还有
- 它很容易适应其他总和(比如7个数字加起来等于100等等)。
import random
def constrained_sum_sample_pos(n, total):
"""Return a randomly chosen list of n positive integers summing to total.
Each such list is equally likely to occur."""
dividers = sorted(random.sample(range(1, total), n - 1))
return [a - b for a, b in zip(dividers + [total], [0] + dividers)]
示例输出:
>>> constrained_sum_sample_pos(4, 40)
[4, 4, 25, 7]
>>> constrained_sum_sample_pos(4, 40)
[9, 6, 5, 20]
>>> constrained_sum_sample_pos(4, 40)
[11, 2, 15, 12]
>>> constrained_sum_sample_pos(4, 40)
[24, 8, 3, 5]
解释:这里有一个一一对应的关系,(1) 4个正整数的组合 (a, b, c, d)
满足 a + b + c + d == 40
,和 (2) 三个整数的组合 (e, f, g)
满足 0 < e < f < g < 40
。而且用 random.sample
生成后者是很简单的。这个对应关系是这样的:从 (e, f, g)
可以得到 (a, a + b, a + b + c)
,反过来从 (a, b, c, d)
可以得到 (e, f - e, g - f, 40 - g)
。
如果你想要的是非负整数(也就是允许有0
),那么有一个简单的转换方法:如果 (a, b, c, d)
是非负整数,加起来等于40
,那么 (a+1, b+1, c+1, d+1)
就是正整数,加起来等于44
,反之亦然。利用这个思路,我们得到了:
def constrained_sum_sample_nonneg(n, total):
"""Return a randomly chosen list of n nonnegative integers summing to total.
Each such list is equally likely to occur."""
return [x - 1 for x in constrained_sum_sample_pos(n, total + n)]
这是 constrained_sum_sample_pos(4, 10)
的图示,感谢 @FM。(稍作编辑。)
0 1 2 3 4 5 6 7 8 9 10 # The universe.
| | # Place fixed dividers at 0, 10.
| | | | | # Add 4 - 1 randomly chosen dividers in [1, 9]
a b c d # Compute the 4 differences: 2 3 4 1
13
b = random.randint(2, 38)
a = random.randint(1, b - 1)
c = random.randint(b + 1, 39)
return [a, b - a, c - b, 40 - c]
(我假设你想要的是整数,因为你提到了“1-40”,不过这个方法也可以很容易地适用于小数。)
下面是这个方法的工作原理:
- 首先,把总范围随机分成两部分,这个分割点叫做b。之所以说是奇数范围,是因为在中间点的两边至少会有2个数。(这是因为你要求每个值的最小值是1。)
- 接着,再把这两部分各自随机分成两部分。同样,这样做是为了保证每部分的最小值是1。
- 最后,返回每一部分的大小。所有部分的大小加起来会等于40。