生成一定和的范围内N个随机数

10

我想生成N个随机数,这些随机数来自于特定的分布(例如均匀分布),在[a,b]之间,并且它们的和为常数C。我尝试了一些自己能想到的解决方案,以及一些在类似帖子中提出的解决方案,但大多数都只适用于有限形式的问题,或者我无法证明结果仍然遵循所需的分布。

我尝试过: 生成N个随机数,将它们全部除以它们的总和,然后乘以所需的常数。这似乎有效,但结果不符合数字应该在[a:b]范围内的规则。

生成N-1个随机数,加上0和所需的常数C,然后进行排序。然后计算每两个相邻数字之间的差异,这些差异就是结果。这种方法再次总和为C,但与上一种方法相同(范围可能比[a:b]更大)。

我还尝试生成随机数,并始终以一种方式跟踪最小值和最大值,以保持所需的总和和范围,并得出了这段代码:

bool generate(function<int(int, int)> randomGenerator,
              int min, int max, int len, int sum,
              std::vector<int> &output) {
    /**
     * Not possible to produce such a sequence
     */
    if (min * len > sum)
        return false;
    if (max * len < sum)
        return false;

    int curSum = 0;
    int left = sum - curSum;
    int leftIndexes = len - 1;
    int curMax = left - leftIndexes*min;
    int curMin = left - leftIndexes*max;

    for (int i = 0; i < len; i++) {
        int num = randomGenerator((curMin < min) ? min : curMin,
                                  (curMax > max) ? max : curMax);
        output.push_back(num);
        curSum += num;
        left = sum - curSum;
        leftIndexes--;
        curMax = left - leftIndexes * min;
        curMin = left - leftIndexes * max;
    }

    return true;
}

这似乎可以工作,但结果有时非常扭曲,我认为它没有遵循原始分布(例如均匀分布)。例如:

//10 numbers within [1:10] which sum to 50:
generate(uniform, 1, 10, 10, 50, output);
//result:
2,7,2,5,2,10,5,8,4,5 => sum=50
//This looks reasonable for uniform, but let's change to 
//10 numbers within [1:25] which sum to 50:
generate(uniform, 1, 25, 10, 50, output);
//result:
24,12,6,2,1,1,1,1,1,1 => sum= 50

请注意输出中存在多少个 "1"。这听起来可能是合理的,因为范围更大。但它们看起来并不像是均匀分布。 我不确定是否可能实现我想要的,也许限制使问题无法解决。

那被称为蛮力破解!你知道当输入长度巨大时可能需要很长时间! - Behrooz
你预计 NC 会有多大? - rici
N 可能在 10,000 左右,C 可能约为 100GBytes,这是一个巨大的整数。实际上,这是为了生成 N 个随机文件,范围在 [a:b] 内,总共占用 C 千兆字节的空间。 - Behrooz
4
逻辑上无法解决“我想生成N个从特定分布(例如均匀分布)中抽取的随机数,在[a,b]之间,使它们的和等于一个常数C。”因此,您能否解释一下您希望通过这样做解决的更高级问题?也许有一种替代方案可以解决这个更高级的问题?(鉴于您可能已经有了答案,最好将此问题保留原样,并询问如何解决您的更外层问题。) - Neil Slater
2
如果您愿意妥协,那么最简单的选择就是不要担心达到不可能的目标。放宽其中一个限制。我建议您的初始重新缩放解决方案(允许尺寸范围滑动)将会很好地解决问题。如果您想进行某种相同的工作负载比较,则可以将随机数生成器设为测试设置的一部分。 - Neil Slater
显示剩余6条评论
5个回答

15

如果您想要样本遵循均匀分布,问题就转化为生成N个随机数,使它们的和等于1。这又是Dirichlet分布的一个特例,但也可以更容易地使用指数分布来计算。具体方法如下:

  1. 取一个均匀样本v1 … vN,其中所有vi都在0到1之间。
  2. 对于所有i,1<=i<=N,定义ui := -ln vi(注意ui > 0)。
  3. 将ui归一化为pi := ui/s,其中s是u1+...+uN的总和。

p1..pN是均匀分布的(在维度为N-1的单纯形中),它们的总和为1。

现在,您可以通过将这些pi乘以所需的常数C并通过加上某些其他常数A来进行翻译,如下所示

qi := A + pi*C.

编辑3

为了解决一些评论中提出的问题,让我补充如下:

  • 为确保最终的随机序列落在区间 [a,b] 中,请选择上述常数 A 和 C 为 A := a,C := b-a,即取 qi = a + pi*(b-a)。由于 pi 在范围 (0,1) 内,所有 qi 将在范围 [a,b] 内。
  • 如果 vi 恰好为 0,则不能取 (负) 对数 -ln(vi),因为 ln() 在 0 处未定义。这种情况发生的概率极低。然而,为了确保不会发出错误信号,在上述第 1 项中生成 v1 ... vN 时,必须以特殊方式处理任何出现的 0:将 -ln(0) 视为 +infinity(记住:当 x->0 时,ln(x) -> -infinity)。因此,总和 s = +infinity,这意味着 pi = 1,所有其他 pj = 0。如果没有这个约定,序列 (0...1...0) 将永远不会生成(非常感谢 @Severin Pappadeux 提供这个有趣的评论)。
  • 如 @Neil Slater 在问题的 第四条评论 中所解释的那样,逻辑上不可能满足原始框架的所有要求。因此,任何解决方案都必须放宽约束以满足原始约束的适当子集。@Behrooz 的其他评论似乎证实了在这种情况下这将足够。

编辑2

评论中提出了另一个问题:

为什么重新缩放均匀样本不足以满足要求?

换句话说,为什么我要费心取负对数?

原因是,如果我们只是重新缩放,那么得到的样本在区间 (0,1)(或 [a,b] 对于最终样本)上分布不均匀。

为了可视化这一点,让我们考虑二维情况,即 N=2 的情况。一个均匀样本 (v1,v2) 对应于一个随机点,该点位于以原点 (0,0) 和角落 (1,1) 为顶点的正方形内。现在,当我们通过将其除以总和 s=v1+v2 来归一化这样一个点时,我们所做的是将该点投影到对角线上,如图所示(请记住,对角线是直线 x + y = 1):

enter image description here

但是考虑到绿色线条比橙色线条更靠近从(0,0)到(1,1)的主对角线,因此投影更倾向于在投影线的中心(蓝色)周围积累,这也是缩放样本所在的位置。这表明简单的缩放不会在所描绘的对角线上产生均匀的样本。另一方面,可以通过数学证明负对数确实产生所需的均匀性。因此,我邀请大家实现两种算法,并检查生成的图形是否与本答案描述的行为相符。

注意:这里是关于这个有趣主题的博客文章,涉及到石油和天然气行业的应用)


2
@Behrooz,这个想法是将C=b-a。还要注意的是,你不能拥有所有东西,但你可以拥有一个在[a,b]中均匀分布的样本,该样本由a + pi*(b-a)给出。请参见Neil Slater对你的问题的评论。 - Leandro Caniglia
1
@Behrooz,只是为了好玩,我实现了这个采样,似乎在2D和3D中都有效。代码在https://github.com/Iwan-Zotow/SimplexSampling,尽情享受吧! - Severin Pappadeux
1
@LeandroCaniglia 所以 'ln' 对于均匀分布很管用。你有没有想过在其他分布情况下会是怎样的呢? - Behrooz
1
@Behrooz 如果你想使用其他的分布,你需要使用通用的Dirichlet分布进行抽样(它没有简单的实现方法)。均匀分布之所以简单是因为有-ln()技巧。但这只是所有Dirichlet分布可能性中最简单的一种情况,因为不同参数会导致复杂性的增加。 - Leandro Caniglia
2
@LeandroCaniglia 我不相信“必须丢弃任何0的出现并用新样本替换它。” 当其中一个v被采样为0时,这意味着分子和分母中将会有正无穷大,从而得出逻辑结论-这是一种特殊情况,在这种情况下,对于此v,外向的p应设置为1,所有其他p_i应等于0。否则,你无法生成(0,0,0,...,1,...0,0,0,0)类型的\vec{p} - Severin Pappadeux
显示剩余17条评论

5
让我们试着简化问题。 通过减去下限,我们可以将其简化为在[0,b-a]中找到N个数字,使它们的总和为C-Na
重新命名参数,我们可以寻找在[0,m]中的N个数字,它们的总和为S
现在问题类似于将长度为S的线段分成N个长度为[0,m]的不同子线段。
我认为这个问题根本无法解决。
如果S=1,N=1000,m大于0的任何值,唯一可能的重新分配是一个1和999个零,这与随机分布完全不同。 NmS之间存在相关性,即使选择随机值也无法消除它。
对于最均匀的重新分配,子线段的长度将遵循高斯曲线,平均值为S/N
如果你以不同的方式调整随机数,你将得到任何偏差,但最终你永远不会同时拥有[a,b]均匀分布和总长度为C,除非你的[a,b]区间长度恰好为2C / N-a。

1

我假设我们的分布是均匀分布。

由于我们有一个均匀分布,每个C的元组出现的概率相同。例如,对于 a = 2, b = 2, C = 12, N = 5,我们有 15 种可能的元组。其中有 10 个以 2 开头,有 4 个以 3 开头,有 1 个以 4 开头。这给了我们从 115 中选择一个随机数来选择第一个元素的想法。从 110,我们选择 2,从 1114,我们选择 3,对于 15,我们选择 4。然后我们继续递归。

#include <time.h>
#include <random>

std::default_random_engine generator(time(0));
int a = 2, b = 4, n = 5, c = 12, numbers[5];

// Calculate how many combinations of n numbers have sum c
int calc_combinations(int n, int c) {
    if (n == 1) return (c >= a) && (c <= b);
    int sum = 0;
    for (int i = a; i <= b; i++) sum += calc_combinations(n - 1, c - i);
    return sum;
}

// Chooses a random array of n elements having sum c
void choose(int n, int c, int *numbers) {
    if (n == 1) { numbers[0] = c; return; }

    int combinations = calc_combinations(n, c);
    std::uniform_int_distribution<int> distribution(0, combinations - 1);
    int s = distribution(generator);
    int sum = 0;
    for (int i = a; i <= b; i++) {
        if ((sum += calc_combinations(n - 1, c - i)) > s) {
            numbers[0] = i;
            choose(n - 1, c - i, numbers + 1);
            return;
        }
    }
}

int main() { choose(n, c, numbers); }

可能的结果:
2
2
3
2
3

由于组合数计算中的溢出、计算所需时间以及需要任意大的随机数,这个算法在大规模的 N 下无法很好地扩展(除非我们使用大整数库)。

这看起来很有趣,但是考虑到我的数字规模(N在1000-10000左右,范围大约为[10^3:10^9],总和大约为50^9),这将永远不会终止。 - Behrooz
@Behrooz:而 calc_combinations 将会迅速溢出任何标准整数类型。 - rici

0

嗯,对于n=10000,我们不能在其中放置一个非随机的小数字吗?

也许可以生成序列直到达到sum > C-max,然后只需放置一个简单的数字来总结它。

在10000个中选择1个更像是系统中的微小噪音。


我不确定那会如何影响最终分布,而且“非常小的噪音”可能需要量化。 - Behrooz
主要的问题是数字是随机生成的。因此,下一个数字有可能是您手动选择的相同数字。 您的暴力算法在某种程度上也是如此。您重复执行它,直到其中一个随机情况出现。因此,只需创建您想要的随机情况即可。 - alizelzele
为了量化噪声,从您的序列生成器中获取下一个随机数,计算您上一个数字与它之间的距离。将其除以10^4。重复几次并计算平均值:P(这对于量化来说如何!!) - alizelzele

0

虽然这是一个老话题,但我认为我有一个想法。考虑我们想要N个随机数,它们的和为C,每个随机数在a和b之间。为了解决这个问题,我们创建N个孔并准备C个球,每次我们问每个孔“你想要另一个球吗?”如果不想要,我们就传递到下一个孔,否则,我们将一个球放入孔中。每个孔都有一个容量值:b-a。如果某个孔达到容量值,则始终传递到下一个孔。

例如:
3个0到2之间的随机数,其总和为5。

模拟结果:
第一次运行:-+-
第二次运行:++-
第三次运行:---
第四次运行:+*+
最终结果:221

-:拒绝球
+:接受球
*:满足通过


这是一个很好的解决方案。唯一的问题是生成的数字变化非常小。 - Xinyao Wang

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接