marielle
marielle

Reputation: 438

Random integer numbers with fixed sum

I'm trying to create a function that returns an array of random integer numbers whose sum is fixed.

Here is my code:

function arraySum(a) {
  return a.reduce((a, b) => a + b, 0)
}

function getRandomIntInclusive(min, max) {
  const minCeil = Math.ceil(min)
  const maxFloor = Math.floor(max)
  return Math.floor(Math.random() * (maxFloor - minCeil + 1)) + minCeil
}

function randomNumbersWithFixedSum(quantity, sum) {
  const randoms = [...Array(quantity - 1).keys()].map(q => getRandomIntInclusive(0, sum/quantity))
  const last = sum - arraySum(randoms)
  return [...randoms, last]
}

console.log(randomNumbersWithFixedSum(1, 100))
console.log(randomNumbersWithFixedSum(2, 100))
console.log(randomNumbersWithFixedSum(3, 100))
console.log(randomNumbersWithFixedSum(4, 100))
console.log(randomNumbersWithFixedSum(5, 100))

It works but it's not exactly what I want. I would like that each number is random in the range [0, sum]. In the randomNumbersWithFixedSum function I forced that the first (quantity-1) numbers are in [0, sum/quantity], but I don't like.

How can I create a really random numbers in [0, sum] whose sum is sum?

Upvotes: 2

Views: 1009

Answers (5)

Stefan
Stefan

Reputation: 21

And here's one more approach

function randomWithFixedSum(sum, n) {
  //Generate an array of N random numbers using Math.random(), and calculate their    sum (randomSum);
  const arr = [];
  let randomSum = 0;
  
  for (let i = 0; i < n; i++) {
    arr[i] = Math.random();
    randomSum += arr[i];
  }

//Scale each of the random numbers proportionally so that their sum becomes the desired sum. You can do this by multipling them by sum/randomSum, and then rounding the result. 
  let prevSum = 0;
  for (let i = 0; i < n - 1; i++) {
    arr[i] = Math.round(arr[i] * sum / randomSum);
    prevSum += arr[i];
  }
  //Sometimes the rounded numbers don't add up to the sum. In order to prevent this, instead of scaling the last element, set it to the difference between the sum and the sum of all previous elements (prevSum). 
  arr[n - 1] = sum - prevSum;

  return arr;
}

console.log(randomWithFixedSum(100, 7));

Upvotes: 1

Stefan
Stefan

Reputation: 21

Here's another approach.

Imagine you have N bank accounts with a total of SUM dollars on them. We can start by distributing SUM dollars as equally as possible to the N bank accounts, and then start randomly sending random amounts from one account to another. As one account goes down, the other goes up, thus the SUM remains the same at end.

function distributeEqually(sum, n) {
  const arr = [];
  for (let i = 0; i < n; i++) {
    arr[i] = Math.floor(sum / n);
    if (i < sum % n) {
      arr[i]++;
    }
  }
  return arr;
}

function randomWithFixedSum(sum, n) {
  const arr = distributeEqually(sum, n);
  for (let i = 0; i < n; i++) {
    const amount = Math.floor(Math.random() * (arr[i] + 1));
    const randomIndex = Math.floor(Math.random() * n);
    arr[i] -= amount;
    arr[randomIndex] += amount;
  }

  return arr;

}

console.log(randomWithFixedSum(100, 7))

Upvotes: 0

str
str

Reputation: 44979

This is a prime example where a recursive function can simplify the problem. Each execution only calculates one random number and then calls itself with updated arguments. See the comments for a description.

function getRandomNumberBetweenIncluding(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

function randomNumbersWithFixedSum(quantity, sum) {
    // only a single number required; return the passed sum.
    if (quantity === 1) {
        return [sum];
    }

    // Create one random number and return an array containing that number
    // as first item. Then use the spread operator and recursively execute
    // the function again with a decremented quantity and the updated  
    // maximum possible sum.
    const randomNum = getRandomNumberBetweenIncluding(0, sum);
    return [
        randomNum,
        ...randomNumbersWithFixedSum(quantity - 1, sum - randomNum),
    ];
}

console.log(randomNumbersWithFixedSum(1, 100));
console.log(randomNumbersWithFixedSum(2, 100));
console.log(randomNumbersWithFixedSum(3, 100));
console.log(randomNumbersWithFixedSum(4, 100));
console.log(randomNumbersWithFixedSum(5, 100));

Upvotes: 3

Nina Scholz
Nina Scholz

Reputation: 386654

You could take only the rest of the sum for the max random number and shuffle the array later to omit large values only at the top of the array.

function shuffle(array) {
    let i = array.length;
    while (--i) {
        let j = Math.floor(Math.random() * (i + 1)),
            temp = array[j];
        array[j] = array[i];
        array[i] = temp;
    }
    return array;
}

function getRandomIntInclusive(min, max) {
    const minCeil = Math.ceil(min)
    const maxFloor = Math.floor(max)
    return Math.floor(Math.random() * (maxFloor - minCeil + 1)) + minCeil
}

function randomNumbersWithFixedSum(length, sum) {
    length--;
    const randoms = Array.from({ length }, q => {
        const r = getRandomIntInclusive(0, sum)
        sum -= r;
        return r;
    });
    return shuffle([...randoms, sum]);
}

console.log(randomNumbersWithFixedSum(5, 100))
console.log(randomNumbersWithFixedSum(4, 100))
console.log(randomNumbersWithFixedSum(3, 100))
console.log(randomNumbersWithFixedSum(2, 100))
console.log(randomNumbersWithFixedSum(1, 100))
.as-console-wrapper { max-height: 100% !important; top: 0; }

Upvotes: 1

falinsky
falinsky

Reputation: 7428

What about the next solution:

  1. At the first iteration, we try to get a random number between 0 and max - say we retrieve N.
  2. At the second iteration - the max possible value cannot be greater than max - N (otherwise the sum will be greater than max).
  3. Continue for quantity - 1 steps.
  4. At the last step we have to use what is left until the max

function getRandomIntInclusive(min, max) {
  const minCeil = Math.ceil(min)
  const maxFloor = Math.floor(max)
  return Math.floor(Math.random() * (maxFloor - minCeil + 1)) + minCeil
}

function randomNumbersWithFixedSum(quantity, sum) {
  const result = [];
  let total = 0;
  
  for (let i = 0; i < quantity - 1; i++) {
    let max = sum - total;
    let num = getRandomIntInclusive(0, max);
    result.push(num);
    total += num;
  }
  result.push(sum - total);
  
  return result;
}

console.log(randomNumbersWithFixedSum(1, 100))
console.log(randomNumbersWithFixedSum(2, 100))
console.log(randomNumbersWithFixedSum(3, 100))
console.log(randomNumbersWithFixedSum(4, 100))
console.log(randomNumbersWithFixedSum(5, 100))

Upvotes: 1

Related Questions