在两个值之间获得n个不同的随机数,这两个值的总和等于给定的数字

我想在一个范围内找到不同的随机数,总和达到给定的数字。

注意:我在stackoverflow中发现了类似的问题,但是它们并没有解决这个问题(即它们不考虑范围的负lowerLimit)。

如果我想要我的随机数的总和等于1,我只需生成所需的随机数,计算总和并将它们除以总和; 但是在这里我需要一些不同的东西; 我需要我的随机数加起来不同于1而且我的随机数必须在给定范围内。

示例:我需要在-50和50之间的30个不同的随机数(非整数),其中30个生成的数字的总和必须等于300; 我编写了下面的代码,但是当n远大于范围(upperLimit – lowerLimit)时它不起作用,函数可以返回范围[lowerLimit – upperLimit]之外的数字。 有什么帮助改善现有的解决方案?

static void Main(string[] args) { var listWeights = GetRandomNumbersWithConstraints(30, 50, -50, 300); } private static List GetRandomNumbersWithConstraints(int n, int upperLimit, int lowerLimit, int sum) { if (upperLimit <= lowerLimit || n < 1) throw new ArgumentOutOfRangeException(); Random rand = new Random(Guid.NewGuid().GetHashCode()); List weight = new List(); for (int k = 0; k < n; k++) { //multiply by rand.NextDouble() to avoid duplicates double temp = (double)rand.Next(lowerLimit, upperLimit) * rand.NextDouble(); if (weight.Contains(temp)) k--; else weight.Add(temp); } //divide each element by the sum weight = weight.ConvertAll(x => x / weight.Sum()); //here the sum of my weight will be 1 return weight.ConvertAll(x => x * sum); } 

编辑 – 澄清

运行当前代码将生成以下30个数字,最多可添加300个。但这些数字不在-50和50之间

 -4.425315699 67.70219958 82.08592061 46.54014109 71.20352208 -9.554070146 37.65032717 -75.77280868 24.68786878 30.89874589 142.0796933 -1.964407284 9.831226893 -15.21652248 6.479463312 49.61283063 118.1853036 -28.35462683 49.82661159 -65.82706541 -29.6865969 -54.5134262 -56.04708803 -84.63783048 -3.18402453 -13.97935982 -44.54265204 112.774348 -2.911427266 -58.94098071 

好的,这是怎么做的

我们将使用Dirichlet分布 ,它是[0 … 1]范围内随机数x i的分布

Sum i x i = 1

因此,在自动满足求和的线性重新缩放条件之后。 Dirichlet分布由αi参数化,但我们假设所有RN来自相同的边际分布,因此每个索引只有一个参数α。

对于合理的α大值,采样随机数的平均值为= 1 / n,方差为~1 /(n *α),因此较大的α导致随机值更接近均值。

好的,现在回到重新调整,

v i = A + B * x i

我们必须得到AB 正如@HansKe正确指出的那样,只有两个自由参数,我们只能满足两个约束,但你有三个。 因此我们将严格满足下界约束,和值约束,但偶尔会违反上限约束。 在这种情况下,我们只是扔掉整个样品并做另一个。

同样,我们有一个转动旋钮,α越大意味着我们接近平均值并且不太可能达到上限。 当α= 1时,我很少得到任何好的样本,但是当α= 10时,我接近40%的好样本。 随着α= 16,我接近80%的良好样本。

Dirichlet采样通过Gamma分布完成,使用MathDotNet中的代码。

代码,使用.NET Core 2.1测试

 using System; using MathNet.Numerics.Distributions; using MathNet.Numerics.Random; class Program { static void SampleDirichlet(double alpha, double[] rn) { if (rn == null) throw new ArgumentException("SampleDirichlet:: Results placeholder is null"); if (alpha <= 0.0) throw new ArgumentException($"SampleDirichlet:: alpha {alpha} is non-positive"); int n = rn.Length; if (n == 0) throw new ArgumentException("SampleDirichlet:: Results placeholder is of zero size"); var gamma = new Gamma(alpha, 1.0); double sum = 0.0; for(int k = 0; k != n; ++k) { double v = gamma.Sample(); sum += v; rn[k] = v; } if (sum <= 0.0) throw new ApplicationException($"SampleDirichlet:: sum {sum} is non-positive"); // normalize sum = 1.0 / sum; for(int k = 0; k != n; ++k) { rn[k] *= sum; } } static bool SampleBoundedDirichlet(double alpha, double sum, double lo, double hi, double[] rn) { if (rn == null) throw new ArgumentException("SampleDirichlet:: Results placeholder is null"); if (alpha <= 0.0) throw new ArgumentException($"SampleDirichlet:: alpha {alpha} is non-positive"); if (lo >= hi) throw new ArgumentException($"SampleDirichlet:: low {lo} is larger than high {hi}"); int n = rn.Length; if (n == 0) throw new ArgumentException("SampleDirichlet:: Results placeholder is of zero size"); double mean = sum / (double)n; if (mean < lo || mean > hi) throw new ArgumentException($"SampleDirichlet:: mean value {mean} is not within [{lo}...{hi}] range"); SampleDirichlet(alpha, rn); bool rc = true; for(int k = 0; k != n; ++k) { double v = lo + (mean - lo)*(double)n * rn[k]; if (v > hi) rc = false; rn[k] = v; } return rc; } static void Main(string[] args) { double[] rn = new double [30]; double lo = -50.0; double hi = 50.0; double alpha = 10.0; double sum = 300.0; for(int k = 0; k != 1_000; ++k) { var q = SampleBoundedDirichlet(alpha, sum, lo, hi, rn); Console.WriteLine($"Rng(BD), v = {q}"); double s = 0.0; foreach(var r in rn) { Console.WriteLine($"Rng(BD), r = {r}"); s += r; } Console.WriteLine($"Rng(BD), summa = {s}"); } } } 

UPDATE

通常,当人们提出这样的问题时,存在隐含的假设/要求 – 所有随机数应以相同的方式分配。 这意味着如果我从采样数组中为索引为0的项绘制边际概率密度函数(PDF),我将获得与绘制数组中最后一项的边际概率密度函数相同的分布。 人们通常会对随机数组进行采样,将其传递给其他例程来做一些有趣的事情。 如果项目0的边际PDF与最后一个索引项目的边缘PDF不同,那么只需恢复数组将使用使用此类随机值的代码产生截然不同的结果。

在这里,我使用我的采样例程绘制了项目0和最后一项(#29)的原始条件([ – 50 … 50] sum = 300)的随机数分布。 看起来很相似,不是吗?

在此处输入图像描述

好的,这是您的采样程序中的图片,相同的原始条件([ – 50 … 50] sum = 300),相同数量的样本

在此处输入图像描述

更新II

用户应该检查采样例程的返回值,并且如果(并且仅当)返回值为真,则接受并使用采样数组。 这是接受/拒绝方法。 作为说明,下面是用于直方图样本的代码:

  int[] hh = new int[100]; // histogram allocated var s = 1.0; // step size int k = 0; // good samples counter for( ;; ) { var q = SampleBoundedDirichlet(alpha, sum, lo, hi, rn); if (q) // good sample, accept it { var v = rn[0]; // any index, 0 or 29 or .... var i = (int)((v - lo) / s); i = System.Math.Max(i, 0); i = System.Math.Min(i, hh.Length-1); hh[i] += 1; ++k; if (k == 100000) // required number of good samples reached break; } } for(k = 0; k != hh.Length; ++k) { var x = lo + (double)k * s + 0.5*s; var v = hh[k]; Console.WriteLine($"{x} {v}"); } 

干得好。 它可能会在实际返回列表之前运行几个世纪,但它会遵守:)

  public List TheThing(int qty, double lowest, double highest, double sumto) { if (highest * qty < sumto) { throw new Exception("Impossibru!"); // heresy highest = sumto / 1 + (qty * 2); lowest = -highest; } double rangesize = (highest - lowest); Random r = new Random(); List ret = new List(); while (ret.Sum() != sumto) { if (ret.Count > 0) ret.RemoveAt(0); while (ret.Count < qty) ret.Add((r.NextDouble() * rangesize) + lowest); } return ret; } 

我想出了这个快速的解决方案。 我相信它可以改进,但目前它完成了这项工作。

n =我需要找到的随机数的数量

约束

  • n个随机数必须加起来为finalSum n个随机数

  • n个随机数必须在lowerLimitupperLimit之内

这个想法是从随机数的初始列表(总结为finalSum )中删除范围之外的数字[ lowerLimitupperLimit ]。

然后计算列表左边的数字(称为nValid )及其总和(称为sumOfValid )。 现在,迭代搜索[ lowerLimitupperLimit ]范围内的( n-nValid )随机数,其总和为( finalSum-sumOfValid

我用输入变量的几种组合(包括负和)测试了它,结果看起来很好。

 static void Main(string[] args) { int n = 100; int max = 5000; int min = -500000; double finalSum = -1000; for (int i = 0; i < 5000; i++) { var listWeights = GetRandomNumbersWithConstraints(n, max, min, finalSum); Console.WriteLine("============="); Console.WriteLine("sum = " + listWeights.Sum()); Console.WriteLine("max = " + listWeights.Max()); Console.WriteLine("min = " + listWeights.Min()); Console.WriteLine("count = " + listWeights.Count()); } } private static List GetRandomNumbersWithConstraints(int n, int upperLimit, int lowerLimit, double finalSum, int precision = 6) { if (upperLimit <= lowerLimit || n < 1) //todo improve here throw new ArgumentOutOfRangeException(); Random rand = new Random(Guid.NewGuid().GetHashCode()); List randomNumbers = new List(); int adj = (int)Math.Pow(10, precision); bool flag = true; List weights = new List(); while (flag) { foreach (var d in randomNumbers.Where(x => x <= upperLimit && x >= lowerLimit).ToList()) { if (!weights.Contains(d)) //only distinct weights.Add(d); } if (weights.Count() == n && weights.Max() <= upperLimit && weights.Min() >= lowerLimit && Math.Round(weights.Sum(), precision) == finalSum) return weights; /* worst case - if the largest sum of the missing elements (ie we still need to find 3 elements, * then the largest sum is 3*upperlimit) is smaller than (finalSum - sumOfValid) */ if (((n - weights.Count()) * upperLimit < (finalSum - weights.Sum())) || ((n - weights.Count()) * lowerLimit > (finalSum - weights.Sum()))) { weights = weights.Where(x => x != weights.Max()).ToList(); weights = weights.Where(x => x != weights.Min()).ToList(); } int nValid = weights.Count(); double sumOfValid = weights.Sum(); int numberToSearch = n - nValid; double sum = finalSum - sumOfValid; double j = finalSum - weights.Sum(); if (numberToSearch == 1 && (j <= upperLimit || j >= lowerLimit)) { weights.Add(finalSum - weights.Sum()); } else { randomNumbers.Clear(); int min = lowerLimit; int max = upperLimit; for (int k = 0; k < numberToSearch; k++) { randomNumbers.Add((double)rand.Next(min * adj, max * adj) / adj); } if (sum != 0 && randomNumbers.Sum() != 0) randomNumbers = randomNumbers.ConvertAll(x => x * sum / randomNumbers.Sum()); } } return randomNumbers; }