简体   繁体   中英

generate random integers with a specific sum

I have 5 fields, I want them all to have a generated number between 0 and 100. But, the sum of the 5 fields should be 100.

When I want to give a random number for one field I would do the following:

Random rnd = new Random();
int x= rnd.Next(1, 10);

But how should I do that for multiple fields that needs to have a sum of 100 together?

You can use the following approach:

  1. generate 4 random integers in [0, 100]
  2. sort them, let's denote the sorted values as 0 ≤ x 1 ≤ x 2 ≤ x 3 ≤ x 4 ≤ 100
  3. use the following 5 values as the random numbers with sum 100:
    • N 1 = x 1
    • N 2 = x 2 - x 1
    • N 3 = x 3 - x 2
    • N 4 = x 4 - x 3
    • N 5 = 100 - x 4

It basically corresponds to randomly choosing 4 sectioning points on the [0, 100] interval, and using the lengths of the 5 resulting intervals as the random numbers:

在此处输入图片说明

const int k = 5;
const int sum = 100;

Random rnd = new Random();
int[] x = new int[k + 1];

// the endpoints of the interval
x[0] = 0;
x[k] = sum;

// generate the k - 1 random sectioning points
for (int i = 1; i < k; i++) {
    x[i] = rnd.Next(0, sum + 1);
}

// sort the sectioning points
Array.Sort(x);

// obtain the k numbers with sum s
int[] N = new int[k];
for (int i = 0; i < k; i++) {
    N[i] = x[i + 1] - x[i];
}

In order to make your distribution uniform, you could try the following aproach:

  1. Generate some random numbers.
  2. Normalize them.
  3. Correct the last field to get exactly the expected sum, if needed.

The code:

const int ExpectedSum = 100;

Random rnd = new Random();
int[] fields = new int[5];

// Generate 4 random values and get their sum
int sum = 0;
for (int i = 0; i < fields.Length - 1; i++)
{
    fields[i] = rnd.Next(ExpectedSum);
    sum += fields[i];
}

// Adjust the sum as if there were 5 random values
int actualSum = sum * fields.Length / (fields.Length - 1);

// Normalize 4 random values and get their sum
sum = 0;
for (int i = 0; i < fields.Length - 1; i++)
{
    fields[i] = fields[i] * ExpectedSum / actualSum;
    sum += fields[i];
}

// Set the last value
fields[fields.Length - 1] = ExpectedSum - sum;

Live example: https://dotnetfiddle.net/5yXwOP

To achieve a truly random distribution, with every element having the chance to be 100 with a total sum of 100, you can use the following solution:

public static int[] GetRandomDistribution(int sum, int amountOfNumbers)
{

    int[] numbers = new int[amountOfNumbers];
    var random = new Random();
    for (int i = 0; i < sum; i++)
    {
      numbers[random.Next(0, amountOfNumbers)]++;
    }
    return numbers;
}

static void Main(string[] args)
{
    var result = GetRandomDistribution(100, 5);
}

It increases a random number by one until the sum is reached. This should fulfill all your criterias.

After thinking about it, I prefer the following solution, because it's less likely to generate an equal distribution:

public static int[] GetRandomDistribution2(int sum, int amountOfNumbers)
{

    int[] numbers = new int[amountOfNumbers];

    var random = new Random();

    for (int i = 0; i < amountOfNumbers; i++)
    {
      numbers[i] = random.Next(sum);
    }


    var compeleteSum = numbers.Sum();

    // Scale the numbers down to 0 -> sum
    for (int i = 0; i < amountOfNumbers; i++)
    {
      numbers[i] = (int)(((double)numbers[i] / compeleteSum) * sum);
    }

    // Due to rounding the number will most likely be below sum
    var resultSum = numbers.Sum();

    // Add +1 until we reach "sum"
    for (int i = 0; i < sum - resultSum; i++)
    {
      numbers[random.Next(0, amountOfNumbers)]++;
    }

    return numbers;
}

For Example.

int sum=100;
int i = 5;
Random rnd = new Random();
while (true)
{
    int cur;                
    --i;
    if (i == 0) {
        Console.WriteLine(sum + " ");
        break;
    } else 
        cur=rnd.Next(1, sum);
    sum -= cur;        
    Console.WriteLine(cur + " ");                
}

Live Example: https://dotnetfiddle.net/ltIK40

or

Random rnd = new Random();
int x= rnd.Next(1, 10);    
int y= rnd.Next(x,x+10);
int y2=rnd.Next(y,y+10);
int y3=rnd.Next(y2,y2+10);
int y4=100-(x+y+y2+y3);

My approach is this:

var rnd = new Random();
var numbers = Enumerable.Range(0, 5).Select(x => rnd.Next(0, 101)).ToArray().OrderBy(x => x).ToArray();
numbers = numbers.Zip(numbers.Skip(1), (n0, n1) => n1 - n0).ToArray();
numbers = numbers.Concat(new[] { 100 - numbers.Sum() }).ToArray();

This is as uniform as I think is possible.

Create your first random number. After that you take the difference between the value of num1 and 100 as the max def of rnd . But to guarantee that their sum is 100, you have to check at the last num if the sum of all nums is 100. If not the value of your last num is the difference that their sum and 100.

And to simply your code and get a clean strcuture, put that code in a loop and instead of single numbers work with an int[5] array.


private int[] CreateRandomNumbersWithSum()
{
    int[] nums = new int[5];
    int difference = 100;
    Random rnd = new Random();

    for (int i = 0; i < nums.Length; i++)
    {
        nums[i] = rnd.Next(0, difference);

        difference -= nums[i];
    }

    int sum = 0;

    foreach (var num in nums)
        sum += num;

    if (sum != 100)
    {
        nums[4] = 100 - sum;
    }

    return nums;
}

I think this is a very simple solution:

public void GenerateRandNr(int total)
    {
        var rnd = new Random();
        var nr1 = rnd.Next(0, total);
        var nr2 = rnd.Next(0, total - nr1);
        var nr3 = rnd.Next(0, total - nr1 - nr2);
        var nr4 = rnd.Next(0, total - nr1 - nr2 - nr3);
        var nr5 = total - nr1 - nr2 - nr3 - nr4;
    }

EDIT: Just tested it, works fine for me: 在此处输入图片说明

The solution is that it's not the numbers that need to be random so much as the distribution needs to be random. The randomness of the numbers will be a side effect of their random distribution.

So you would start with five random numbers in a given range. The exact range doesn't matter as long as the range is the same for all five, although a broader range allows for more variation. I'd use Random.NextDouble() which returns random numbers between 0 and 1.

Each of those individual numbers divided by the sum of those numbers represents a distribution.

For example, say your random numbers are .4, .7, .2, .5, .2. (Using fewer digits for simplicity.)

The total of those numbers is 2. So now the distributions are each of those numbers divided by the total.

.4 / 2 = .20
.7 / 2 = .35
.2 / 2 = .10
.5 / 2 = .25
.2 / 2 = .10

You'll notice that those distributions will equal 100% or really close to it if there are a lot more decimal places.

The output is going to be each of those distributions times the target number, in this case, 100. In other words, each of those numbers represents a piece of 100.

So multiplying each of those distributions times the target, we get 20, 35, 10, 25, and 100, which add up to 100.

The trouble is that because of rounding your numbers won't always perfectly add up to 100. To fix that you might add one to the smallest number if the sum is less than 100, or subtract one from the largest number of the the sum is greater than 100. Or you could choose to add or subtract on one of the numbers at random.

Here's a class to create the distributions. (I'm just playing around so I haven't exactly optimized this to death.)

public class RandomlyDistributesNumbersTotallingTarget
{
    public IEnumerable<int> GetTheNumbers(int howManyNumbers, int targetTotal)
    {
        var random = new Random();
        var distributions = new List<double>();
        for (var addDistributions = 0; addDistributions < howManyNumbers; addDistributions++)
        {
            distributions.Add(random.NextDouble());
        }
        var sumOfDistributions = distributions.Sum();
        var output = distributions.Select(
            distribution => 
                (int)Math.Round(distribution / sumOfDistributions * targetTotal, 0)).ToList();
        RoundUpOutput(output, targetTotal);
        return output;
    }

    private void RoundUpOutput(List<int> output, int targetTotal)
    {
        var difference = targetTotal - output.Sum();
        if (difference !=0)
        {
            var indexToAdjust =
                difference > 0 ? output.IndexOf(output.Min()) : output.IndexOf(output.Max());
            output[indexToAdjust]+= difference;
        }
    }
}

And here's a not-perfectly-scientific unit test that tests it many times over and ensures that the results always total 100.

[TestMethod]
public void OutputTotalsTarget()
{
    var subject = new RandomlyDistributesNumbersTotallingTarget();
    for (var x = 0; x < 10000; x++)
    {
        var output = subject.GetTheNumbers(5, 100);
        Assert.AreEqual(100, output.Sum());
    }
}

Some sample outputs:

5, 30, 27, 7, 31
15, 7, 26, 27, 25
10, 11, 23, 2, 54

The numbers are always going to average to 20, so while 96, 1, 1, 1 is a hypothetical possibility they're going to tend to hover closer to 20.

Okay. Having been burned by my previous attempt at this seemingly trivial problem, I decided to have another go. Why not normalise all the numbers after generation? This guarantees randomness, and avoids O(n log n) performance from a sort. It also has the advantage that even with my basic maths, I can work out that the numbers are uniformly distributed.

public static int[] UniformNormalization(this Random r, int valueCount, int valueSum)
{
    var ret = new int[valueCount];
    long sum = 0;
    for (int i = 0; i < valueCount; i++)
    {
        var next = r.Next(0, valueSum);
        ret[i] = next;
        sum += next;
    }

    var actualSum = 0;

    for (int i = 0; i < valueCount; i++)
    {
        actualSum += ret[i] = (int)((ret[i] * valueSum) / sum);
    }

    //Fix integer rounding errors.
    if (valueSum > actualSum)
    {
        for (int i = 0; i < valueSum - actualSum; i++)
        {
            ret[r.Next(0, valueCount)]++;
        }
    }

    return ret;
}

This should also be one of the fastest solutions.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM