4

I'm confused as to how exactly I would make 9 random numbers add to whatever number the user may input. Let's say the user inputs "200" as the number, how do I make it so that I could get 9 random numbers add up exactly to 200?

Obviously, the code below doesn't work the way I want it to because it's literally just 9 random numbers that don't add up to a specific number. I just have no idea how to get this built properly.

public static void RandomStats()
{
    Random RandomClass = new Random();

    int[] intRandomStats = {
        RandomClass.Next(0, 101), 
        RandomClass.Next(0, 101), 
        RandomClass.Next(0, 101), 
        RandomClass.Next(0, 101), 
        RandomClass.Next(0, 101), 
        RandomClass.Next(0, 101), 
        RandomClass.Next(0, 101), 
        RandomClass.Next(0, 101), 
        RandomClass.Next(0, 101)
    };

    // ...
}
gunr2171
  • 16,104
  • 25
  • 61
  • 88
kayobi
  • 51
  • 1
  • 2
    @PalleDue I hope negative numbers are acceptable as an output. – gunr2171 Nov 15 '22 at 15:23
  • @gunr2171: Yes, otherwise you could go bust. – Palle Due Nov 15 '22 at 15:24
  • 3
    generate 8 random numbers between 0 and 2, and the 9th the remainder? You need to specify what requirements you have. Do all numbers need to be in some range? Or some distribution? – JonasH Nov 15 '22 at 15:31
  • 1
    This question could use more information to get a better answer. Do you expect all the numbers to be (non-negative) integers? Do you expect each number to be drawn from the same probability distribution? (or is it OK that e.g. one of the numbers tend to be larger/smaller than the others?) – DrPhil Nov 15 '22 at 17:02

7 Answers7

6

I think your question is more of math question than a code question.

It sounds like what you are looking for is a multinomial distribution. A very naive way of generating a distribution like that would be to think of it like throwing dice. Imagine that you have 200 dice with 9 sides each. Roll all of them. Count all the ones that ended up with the 1 side up, that would be your first number. Then count the ones that ended up with the 2 side up, that would be your second number. Continue until all dice are counted. There are 200 dice, so the sum of the counts will be 200. Each count would have the same probability distribution.

The above pseudo-algorithm would not be so efficient, basically looping over each die. Maybe efficiency is not so important in your case, (and 200 is a small number, so it does not matter) so feel free to write this algorithm.

If efficiency matters, try to find an existing implementation in a library. Maybe the MathNet library would work for you? See the Sample method if you are interested. At the very least, now that you know the term "multinomial distribution" it should be a bit easier to google for inspiration.

DrPhil
  • 377
  • 1
  • 12
3

Imagine you have a bag of 200 coins. You need to divvy those coins into 9 random piles. A pile can have all the coins in the bag, some of the coins in the bag, or no coins.

Each time you allocate coins for a pile, the number of coins in the bag gets smaller (unless you grabbed 0 coins in which case it stays the same). This new count is referenced for the next pile allocation.

var rand = new Random();
var amount = 200;
var targetOutputValueCount = 9;
var outputValues = new List<int>();
for (int i = 1; i < targetOutputValueCount; i++) // 1 less than all groups
{
    var groupAmount = rand.Next(0, amount);
    amount -= groupAmount;
    outputValues.Add(groupAmount);
}

// for the last group, it's whatever is left over
outputValues.Add(amount);

foreach (var outputValue in outputValues)
{
    Console.WriteLine(outputValue);
}

An example output would be

148
28
0
2
12
2
1
6
1

The advantage of this approach is that you are always guaranteed to have positive output numbers.

gunr2171
  • 16,104
  • 25
  • 61
  • 88
  • 2
    The disadvantage is that the piles are getting smaller and smaller. A simple approach to avoid at least that the order determines the size, randomize the order as well, so [shuffle the array](https://stackoverflow.com/questions/108819/best-way-to-randomize-an-array-with-net). – Tim Schmelter Nov 15 '22 at 16:02
  • 1
    Note that (even with shuffling) this will tend to produce a few large values and then many small values. Maybe that's OK (the op has not specified), but at least something to be aware of. – DrPhil Nov 15 '22 at 17:05
1

Just generate eight numbers and compute the ninth as the missing difference:

int theSum = 200;
var randomNumbers = new int[9];
for(int i = 0; i < 8; i)
{
    randomNumbers[i] = random.Next(0, theSum);
}

randomNumbers[8] = theSum - randomNumbers.Sum();
MakePeaceGreatAgain
  • 35,491
  • 6
  • 60
  • 111
  • 1
    Note that (even with shuffling) this will tend to produce a few large values and then many small values. Maybe that's OK (the op has not specified), but at least something to be aware of. – DrPhil Nov 15 '22 at 17:16
1

You could also repeatedly generate 9 random numbers until they sum up to the desired sum. The optimal range for the random numbers is twice the target sum (200) divided by the number of random numbers (9) because then their average will be close to 200/9.

var random = new Random();
var randomNumbers = new int[9];
int input = 200;
int optimalRange = 2 * input / randomNumbers.Length;
int iterations = 0;
do {
    for (int i = 0; i < randomNumbers.Length; i++) {
        randomNumbers[i] = random.Next(optimalRange);
    }
    iterations++;
} while (randomNumbers.Sum() != input);

Console.WriteLine($"iterations = {iterations}");
Console.WriteLine($"numbers = {String.Join(", ", randomNumbers)}");

Example output:

iterations = 113
numbers = 2, 24, 39, 28, 6, 28, 34, 17, 22

In a test I repeated one million times I got these # of iterations:

  • average = 98.4
  • min = 1
  • max = 1366

And in 10170 cases I got it right at the first iteration.

Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188
  • I think this answer is good, but I think it's worth mentioning that it doesn't seem to be so efficient. I'm not exactly sure how to calculate the average time complexity, but I'm suspecting it's somewhere around O(totalSum*numberOfCounts) while the other solutions on the page seems to be either O(totalSum) or O(numberOfCounts). – DrPhil Nov 15 '22 at 17:15
  • Yes, but if all the 9 numbers must be random and not only 8 except of the last one, then this is a possible solution. Selecting numbers out of a set of given random numbers would be another possibility, but time complexity could be even worse. But this also depends on the exact formulation of the assignment which we don't know. – Olivier Jacot-Descombes Nov 15 '22 at 17:29
  • A test shows that out of 1 million repetitions I get these # of iterations: average = 98.4, min = 1, max = 1366. And in 10170 cases I got it right at the first iteration. – Olivier Jacot-Descombes Nov 15 '22 at 17:47
1

The methods proposed so far are workable, but tend to produce results that are skewed. For example, forcing the last number to give the correct sum can give you a value that is a long way off the other values (and possibly negative, which might be a problem in some cases). Calculating random values in the range from zero up to the remaining sum will give you a series of numbers that rapidly approach zero.

Instead, to generate n random numbers from 0 to total, I would suggest picking n-1 random values in the range from 0 to total (inclusive). Consider each of these values as the location of a bookmark in a deck of total cards. If the deck is then separated into n piles at these bookmarks, then the number of cards in each pile will give you a uniformly distributed set of values that sum to total.

Here's some code to illustrate the idea (in C, sorry):

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int cmp(const void *a, const void *b) {
    return *((int*)a) - *((int*)b);
}

int main(int argc, char *argv[]) {
    int nterms, total, x, i, checksum;
    int *array;
    
    srand(time(0));
    
    if (argc != 3) return puts("Require 2 arguments: <nterms> and <total>");
    nterms = atoi(argv[1]);     /* N.B. Input value checks omitted.      */
    total = atoi(argv[2]);      /* Avoid large or negative values!       */
    
    /* We want to generate nterms intervals across the range from 0 to   */
    /* total (inclusive), so we need an array of nterms+1 values to mark */
    /* the start and end of each interval.                               */
    array = malloc((nterms+1) * sizeof(int));
    
    /* The first and last items in this list must be zero and total (to  */
    /* ensure that the list of numbers add up to the correct amount)     */
    array[0] = 0;
    array[nterms] = total;
    
    /* Fill the rest of the array with random values from 0 to total.    */
    for (i=1; i<nterms; i++) {
        array[i] = rand() % (total+1);
    }
    /* Sort these values in ascending order.                             */
    qsort(array, nterms+1, sizeof(int), cmp);
    
    /* Our list of random numbers can now be calculated from the         */
    /* difference between each pair of values in this list.              */
    printf("Numbers:");
    for (i=checksum=0; i<nterms; i++) {
        x = array[i+1] - array[i];
        checksum += x;
        printf(" %d", x);
    }
    printf("\nTotal:   %d\n", checksum);
    return 0;
}
r3mainer
  • 23,981
  • 3
  • 51
  • 88
  • 1
    I like this answer a lot, and think it could be the best ones for OPs case. It's not obvious that the expected value for each count is the same - but it is! See https://math.stackexchange.com/questions/195245/average-distance-between-random-points-on-a-line-segment for the proof. – DrPhil Nov 15 '22 at 17:29
1

Thanks for the hint @DrPhil. Here's a method using linq.

Random rnd = new Random();
int[] dice = new int[200];
var sidesUp = dice.Select(x => rnd.Next(1, 10));
List<int> randomNumbers = sidesUp.GroupBy(p => p).Select(x => x.Count()).ToList();
thewallrus
  • 645
  • 4
  • 8
0

My approach ain't too different from others, but here it is:

  • Started by declaring an integer with the total value;
  • Subtracted the user input;
  • Used a for loop to iterate 8 times;
  • Each time get the division from remaining total and remaining iterations;

Option 1:

  • Used previous division as the maximum random value if remaining total is less than the max value;

Option 2:

  • Used previous division as the maximum random value;

  • The 9th number is the remaining total.

//run: RandomStats(0,101,200) for your particular example.
public static void RandomStats(int min, int max, int total)
{
    Random randomClass = new Random();

    int[] randomStats = new int[9];

    int value = 0;
    int totalValue = total;
    bool parsed = false;
    while (!parsed)
    {
        Console.WriteLine("Please enter a number:");
        if (int.TryParse(Console.ReadLine(), out value))
        {
            parsed = true;
            totalValue -= value;

            for (int i = 0; i < randomStats.Length-1; i++)
            {
                //option 1
                int remainMax = (int) Math.Floor((float) totalValue / (randomStats.Length - 1 - i));
                int randomValue = randomClass.Next(min, totalValue < max ? remainMax : max);  
                //option 2
                //max = (int) Math.Floor((float) totalValue / (randomStats.Length - 1 - i));
                //int randomValue = randomClass.Next(min, max); 
                totalValue -= randomValue;
                randomStats[i] = randomValue;
            }
            randomStats[8] = totalValue;
        }
        else
        {
            Console.WriteLine("Not a valid input");
        }
    }

    Console.WriteLine($"min value: {min}\tmax value: {max}\ttotal value: {total}");
    int testValue = value;
    Console.WriteLine("Input value - " + value);
    for (int i = 0; i < randomStats.Length; i++)
    {
        testValue += randomStats[i];
        int randomIndex = i + 1;
        Console.WriteLine(randomIndex + " Random value - " + randomStats[i]);
    }
    Console.WriteLine("test value - " + testValue);
    Console.ReadKey();
}

option 1 output:

min value: 0    max value: 101  total value: 200
Input value - 10
1 Random value - 13
2 Random value - 2
3 Random value - 95
4 Random value - 10
5 Random value - 0
6 Random value - 15
7 Random value - 10
8 Random value - 10
9 Random value - 35
test value - 200

option 2 output:

min value: 0    max value: 67*   total value: 200
Input value - 10
1 Random value - 1
2 Random value - 16
3 Random value - 5
4 Random value - 29
5 Random value - 17
6 Random value - 7
7 Random value - 48
8 Random value - 19
9 Random value - 48
test value - 200

*this was still 101 but became irrelevant since I divided remaining totals by the iterations

Barreto
  • 374
  • 2
  • 14