获取两个值之和等于给定数字的n个不同的随机数

时间:2018-07-13 12:44:44

标签: c# random

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

注意:我在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<double> 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<double> weight = new List<double>();

    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<double>(x => x / weight.Sum());  //here the sum of my weight will be 1 

    return weight.ConvertAll<double>(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

3 个答案:

答案 0 :(得分:2)

好,这是怎么做

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

Sum i x i = 1

因此,线性和比例换算条件求和后,将自动得到满足。 Dirichlet分布由α i 参数化,但是我们假设所有RN来自相同的边际分布,因此每个索引只有一个参数α。

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

好的,现在回到重新缩放,

v i = A + B * x i

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

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

使用MathDotNet中的代码通过Gamma分布进行狄利克雷采样。

代码,已通过.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}");
        }
    }
}

更新

通常,当人们问这样的问题时,会有一个隐含的假设/要求-所有随机数应以相同的方式分配。这意味着,如果我从采样数组中为索引为0的项绘制边际概率密度函数(PDF),则将获得与为数组中的最后一项项绘制边际概率密度函数的分布相同的分布。人们通常对随机数组进行采样,以将其传递给其他例程以执行一些有趣的工作。如果项目0的边际PDF与最后索引的项目的边际PDF不同,则仅返回数组将产生使用这种随机值的代码而产生完全不同的结果。

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

enter image description here

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

enter image description here

UPDATE II

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

        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}");
        }

答案 1 :(得分:0)

您在这里。在实际返回列表之前,它可能运行了几个世纪,但它符合要求:)

    public List<double> 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<double> ret = new List<double>();

        while (ret.Sum() != sumto)
        {
            if (ret.Count > 0)
                ret.RemoveAt(0);
            while (ret.Count < qty)
                ret.Add((r.NextDouble() * rangesize) + lowest);
        }

        return ret;
    }

答案 2 :(得分:0)

我想出了快速的解决方案。我敢肯定它可以改进,但目前可以完成。

n =我需要找到的随机数

约束

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

  • n 随机数必须在 lowerLimit upperLimit

  • 之内

这个想法是从随机数的初始列表(总计为 finalSum )中删除[ lowerLimit upperLimit ]。

然后计算列表的剩余数量(称为 nValid )及其总和(称为 sumOfValid )。 现在,迭代搜索范围为[ lowerLimit upperLimit ]的( 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<double> 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<double> randomNumbers = new List<double>();

    int adj = (int)Math.Pow(10, precision);

    bool flag = true;
    List<double> weights = new List<double>();
    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<double>(x => x * sum / randomNumbers.Sum());
        }
    }

    return randomNumbers;
}