12

I want to split 10 into an array of 4 random numbers, but neither can be 0 or higher than 4. For example [1,2,3,4], [1,4,4,1] or [4,2,3,1].

I think it's an easy question, but for some reason I can't think of how to do this. If someone has some instruction that would be very helpful!

Edit: This is the code I have now, but I generates also a total number under 10:

  let formation = [];
  let total = 0;

   for (let i = 0; i < 4; i ++) {
    if (total < 9) {
      formation[i] = Math.floor(Math.random() * 4) + 1; 
    } else {
      formation[i] = 1;
    }
  }
Emma
  • 27,428
  • 11
  • 44
  • 69
MikeHrn
  • 153
  • 1
  • 7
  • 1
    a requirement is not clear and what have you tried so far? – Harsh Patel May 18 '18 at 06:52
  • calculate the valid combinations - there's very few - pick a random one :p – Jaromanda X May 18 '18 at 06:53
  • WIll it always be 10? If yes, as @JaromandaX has mentioned, the combinations are very few. You can define all the combinations and then pick a random one. – Pankit Kapadia May 18 '18 at 06:54
  • Please add more input data – Mihai Alexandru-Ionut May 18 '18 at 06:56
  • 1
    You probably want to implement a [Partition](https://en.wikipedia.org/wiki/Partition_(number_theory)) function, and then filter out all partitions which contain a number higher than 4. – wiesion May 18 '18 at 07:06
  • 1
    Ambiguous question. `[1,2,3,4]` and `[4,2,3,1]` are essentially the same so as [4,3,2,1]`. Do you need `[4,4,1,1]` alongside `[1,4,4,1]` as well..? – Redu Nov 01 '19 at 21:10
  • Does this answer your question? [Is there an efficient way to generate N random integers in a range that have a given sum or average?](https://stackoverflow.com/questions/61393463/is-there-an-efficient-way-to-generate-n-random-integers-in-a-range-that-have-a-g) – Peter O. Jun 10 '21 at 07:28

8 Answers8

41

You could create all possible combinations and pick a random array.

function get4() {

    function iter(temp) {
        return function (v) {
            var t = temp.concat(v);
            if (t.length === 4) {
                if (t.reduce(add) === 10) {
                    result.push(t);
                }
                return;
            }
            values.forEach(iter(t));
        };
    }
    
    const
        add = (a, b) => a + b,
        values = [1, 2, 3, 4],
        result = [];

    values.forEach(iter([]));
    return result;
}

console.log(get4().map(a => a.join(' ')));
.as-console-wrapper { max-height: 100% !important; top: 0; }

An algorithm for getting random values without a list of all possible combinations

It works by using a factor for the random value and an offset, based on the actual sum, index, minimum sum which is needed for the next index, and the maximum sum.

The offset is usually the minimum sum, or the greater value of the difference of sum and maximum sum. For getting the factor, three values are taken for the minimum for multiplying the random value.

The table illustrates all possible values of the sum and the needed iterations, based on a given value and the iteration for getting all values.

At the beginning the sum is the value for distribution in small parts. The result is the second block with a rest sum of 14 ... 10, because it is possible to take a value of 1 ... 5. The third round follows the same rules. At the end, the leftover sum is taken as offset for the value.


An example with 1, ..., 5 values and 5 elements with a sum of 15 and all possibilities:

min:     1
max:     5
length:  5
sum:    15

smin = (length - index - 1) * min
smax = (length - index - 1) * max
offset = Math.max(sum - smax, min)
random = 1 + Math.min(sum - offset, max - offset, sum - smin - min)

    index     sum    sum min  sum max   random   offset
  -------  -------  -------  -------  -------  -------
_      0       15        4       20        5        1
       1       14        3       15        5        1
       1       13        3       15        5        1
       1       12        3       15        5        1
       1       11        3       15        5        1
_      1       10        3       15        5        1
       2       13        2       10        3        3
       2       12        2       10        4        2
       2       11        2       10        5        1
       2       10        2       10        5        1
       2        9        2       10        5        1
       2        8        2       10        5        1
       2        7        2       10        5        1
       2        6        2       10        4        1
_      2        5        2       10        3        1
       3       10        1        5        1        5
       3        9        1        5        2        4
       3        8        1        5        3        3
       3        7        1        5        4        2
       3        6        1        5        5        1
       3        5        1        5        4        1
       3        4        1        5        3        1
       3        3        1        5        2        1
_      3        2        1        5        1        1
       4        5        0        0        1        5
       4        4        0        0        1        4
       4        3        0        0        1        3
       4        2        0        0        1        2
       4        1        0        0        1        1

The example code takes the target 1, ..., 4 with a length of 4 parts and a sum of 10.

function getRandom(min, max, length, sum) {
    return Array.from(
        { length },
        (_, i) => {
            var smin = (length - i - 1) * min,
                smax = (length - i - 1) * max,
                offset = Math.max(sum - smax, min),
                random = 1 + Math.min(sum - offset, max - offset, sum - smin - min),
                value = Math.floor(Math.random() * random + offset);

            sum -= value;
            return value;
        }
    );
}

console.log(Array.from({ length: 10 }, _ => getRandom(1, 4, 4, 10).join(' ')));
.as-console-wrapper { max-height: 100% !important; top: 0; }
Community
  • 1
  • 1
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • Thank you, I went with this answer as I feel it's the most efficient one (only need to calculate all options once), thanks! – MikeHrn May 18 '18 at 07:30
  • 4
    Your second algorithm produces some partitions much more often than other ones (by a factor of four, in the case of the example). – rici Sep 21 '18 at 15:56
  • @rici, how do you come to this result? – Nina Scholz Sep 21 '18 at 16:10
  • 4
    @Nina: I was certain it was true from experience with partitioning algorithms, but I verified it with experimentation to find the actual bias. I just ran geRandom(1, 4, 4, 10) a million times, counting the result in a dictionary. – rici Sep 21 '18 at 16:13
  • 3
    The results `1 1 4 4` and `4 4 1 1` are the most common (around 62,500). Most partitions have a count of around 15,000 but a few are at either 21,000 or 31,000 – rici Sep 21 '18 at 16:16
  • 1
    then it's better to use the brute force method than the later one. thank you for finding this. – Nina Scholz Sep 21 '18 at 16:22
  • @rici do you know what this is called? The name for a flaw in the distribution? – Anthony Raimondo Oct 07 '18 at 09:22
  • the problem start with same distribution of the first level, then the next level has a different distribution and this increase the unequal distribution for all following next levels. – Nina Scholz Oct 07 '18 at 09:37
  • https://jsfiddle.net/n1w0f9od/ Came up with a pretty similar result - added a little optimization - working on a even faster one - like your solution @NinaScholz – SirPilan Nov 19 '19 at 21:58
3

A litte late to the show, but I found this a fun task to think about so here you go. My approach does not need to create all partitions, it also does not rely on pure luck of finding a random match, it is compact and it should be unbiased.

It works efficiently even when large values are used, as long as max is not too limiting.

const len = 4;
const total = 10;
const max = 4;

let arr = new Array(len);
let sum = 0;
do {
  // get some random numbers
  for (let i = 0; i < len; i++) {
    arr[i] = Math.random();
  }
  // get the total of the random numbers
  sum = arr.reduce((acc, val) => acc + val, 0);
  // compute the scale to use on the numbers
  const scale = (total - len) / sum;
  // scale the array
  arr = arr.map(val => Math.min(max, Math.round(val * scale) + 1));
  // re-compute the sum
  sum = arr.reduce((acc, val) => acc + val, 0);
  // loop if the sum is not exactly the expected total due to scale rounding effects
} while (sum - total);

console.log(arr);
Lucero
  • 59,176
  • 9
  • 122
  • 152
2

The simplest solution is brute force.

  1. Make a while loop to nest your calculations in
  2. In the loop, create an empty array and fill it with random values until length is reached
  3. Check if the sum of the array is your desired value, and if it is then break the loop

The above should run until you have a result.

Two things worth considering though.

  1. Your can easily test if a solution is at all possible by calculating, that length-of-array times minimum-value isn't more than the sum and length-of-array times maximum-value isn't less than the sum.
  2. A loop based on random conditions could potentially run forever, so a maximum amount of iterations might be desirable.

Both of these points are considered in the snippet below:

function randomNumber(max, min) {
  while (true) {
    var r = Math.round(Math.random() * max);
    if (r >= min) {
      return r;
    }
  }
}

function splitXintoYComponentsBetweenMaxAndMin(numberToSplit, numberOfSplits, maxValue, minValue, onUpdate) {
  if (minValue === void 0) {
    minValue = 1;
  }
  //Test that a result can exist
  if (maxValue * numberOfSplits < numberToSplit || minValue * numberOfSplits > numberToSplit) {
    return new Promise(function(resolve, reject) {
      resolve(false);
    });
  }
  //Create returner array
  var arr = [];
  var accumulator = 0;
  while (arr.length < numberOfSplits) {
    var val = randomNumber(Math.floor(numberToSplit / numberOfSplits), minValue);
    accumulator += val;
    arr.push(val);
  }
  return new Promise(function(resolve, reject) {
    function runTest() {
      var d = Date.now();
      var localMaxValue = Math.min(maxValue, Math.ceil((numberToSplit - accumulator) / 4));
      //Combination loop
      while (accumulator < numberToSplit && Date.now() - d < 17) {
        var index = Math.round(Math.random() * (arr.length - 1));
        if (arr[index] >= maxValue) {
          continue;
        }
        var r = randomNumber(localMaxValue, minValue);
        while (arr[index] + r > maxValue || accumulator + r > numberToSplit) {
          if (Date.now() - d >= 17) {
            break;
          }
          r = randomNumber(localMaxValue, minValue);
        }
        if (arr[index] + r > maxValue || accumulator + r > numberToSplit) {
          continue;
        }
        arr[index] += r;
        accumulator += r;
      }
      if (accumulator < numberToSplit) {
        if (onUpdate !== void 0) {
          onUpdate(arr);
        }
        requestAnimationFrame(runTest);
      } else {
        resolve(arr);
      }
    }
    runTest();
  });
}
//TEST
var table = document.body.appendChild(document.createElement('table'));
table.innerHTML = "<thead><tr><th>Number to split</th><th>Number of splits</th><th>Max value</th><th>Min value</th><th>Run</th></tr></thead>" +
  "<tbody><tr><th><input id=\"number-to-split\" value=\"10\" type=\"number\" min=\"1\"/></th><th><input id=\"number-of-splits\" value=\"4\" type=\"number\" min=\"1\"/></th><th><input id=\"max-value\" type=\"number\" min=\"1\" value=\"4\"/></th><th><input id=\"min-value\" type=\"number\" min=\"1\" value=\"1\"/></th><th><input id=\"run\" type=\"button\" value=\"Run\"/></th></tr></tbody>";
var output = document.body.appendChild(document.createElement('pre'));
output.style.overflowX = "scroll";
document.getElementById("run").onclick = function() {
  splitXintoYComponentsBetweenMaxAndMin(parseInt(document.getElementById("number-to-split").value, 10), parseInt(document.getElementById("number-of-splits").value, 10), parseInt(document.getElementById("max-value").value, 10), parseInt(document.getElementById("min-value").value, 10))
    .then(function(data) {
      if (data !== false) {
        output.textContent += data.join("\t") + '\n';
      } else {
        output.textContent += 'Invalid data\n';
      }
    });
};

EDIT 1 - Big calculations

Using requestAnimationFrame and Promises the code can now execute asynchronously, which allows for longer calculation time without bothering the user.

I also made the random function scale with the remaining range, greatly reducing the amount of calculations needed for big numbers.

Emil S. Jørgensen
  • 6,216
  • 1
  • 15
  • 28
  • a while loop trying random numbers is definitely not brute force. Brute force is a method using a systematic approach, trying every possible combination until a valid combination is found. Therefore brute force will eventually find a solution (as long as there is at least one), whereas your loop might still return false even for really high `maxIterations` values. – Daniel May 27 '18 at 22:16
0

Basically you need the partitions (See https://en.wikipedia.org/wiki/Partition_(number_theory)) of 10 and apply your conditions on the resulting set.

// Partition generator taken from 
// https://gist.github.com/k-hamada/8aa85ac9b334fb89ac4f

function* partitions(n) {

    if (n <= 0) throw new Error('positive integer only');
    yield [n];

    var x = new Array(n);
    x[0] = n;
    for (var i = 1; i < n; i++) x[i] = 1;

    var m = 0, h = 0, r, t;
    while (x[0] != 1) {
        if (x[h] == 2) {
            m += 1;
            x[h] = 1;
            h -= 1;
        } else {
            r = x[h] - 1;
            x[h] = r;

            t = m - h + 1;
            while (t >= r) {
                h += 1;
                x[h] = r;
                t -= r;
            }
            m = h + (t !== 0 ? 1 : 0);
            if (t > 1) {
                h += 1;
                x[h] = t;
            }
        }
        yield x.slice(0, m + 1);
    }
}

results = [];
// Get all possible partitions for your number
for (var partition of partitions(10)) {
    // Apply your conditions (must be 4 numbers, none of them greater than 4)
    if(partition.length != 4 || partition.some((x) => x > 4)) continue;
    results.push(partition);
}
console.log(results);
wiesion
  • 2,349
  • 12
  • 21
0

Given that:

In a collection of n positive numbers that sum up to S, at least one of them will be less than S divided by n (S/n)

and that you want a result set of exactly 4 numbers,

you could use the following algorithm:

  1. Get a random number from range [1, floor(S/n)], in this case floor(10/4) = 2, so get a random number in the range of [1,2]. Lets mark it as x1.
  2. Get a random number from range [1, floor((S - x1)/(n - 1))]. Lets mark it as x2.
  3. Get a random number from range [1, floor((S - x1 - x2)/(n - 2))].
  4. Continue until you get x(n-1).
  5. Get the last number by doing S - x1 - x2 .... - x(n-1).

Finally, extend the above algorithm with a condition to limit the upper limit of the random numbers.

In n steps, you can get a collection.

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

    function getRandomCollection(min, max, length, sum) {
        var collection = [];
        var leftSum = sum - (min - 1);

        for(var i = 0; i < length - 1; i++) {
             var number = getRandomInt(min, Math.min(Math.ceil(leftSum/(length - i)), max));
             leftSum -= number;
             collection.push(number);
        }
        leftSum += min - 1;
        while(leftSum > max) {
             var randomIndex = Math.floor(Math.random() * collection.length);
             if(collection[randomIndex] < max) {
                  collection[randomIndex]++;
                  leftSum--;
             }
        }
        
        collection.push(leftSum);
        return collection;
    }
    console.log(getRandomCollection(1, 4, 4, 10).join(' + ') + ' = 10');
    console.log(getRandomCollection(3, 20, 10, 100).join(' + ') + ' = 100');

Reference

My answer using the same algorithm for another question

Jannes Botis
  • 11,154
  • 3
  • 21
  • 39
0

Quick and simple but biased and nondeterministically terminating

function partition(sum, len, min, max) {
    const a = Array(len).fill(min)
    while (a.reduce((acc,val)=>acc+val) < sum) {
        const i = Math.random()*len|0
        if (a[i] < max) a[i]++
    }
    return a
}

console.log(Array(10).fill().map(_=>partition(10, 4, 1, 4).join(' ')))
.as-console-wrapper { max-height: 100% !important; top: 0; }

The while loop can loop forever with an infinitesimal probability. To prevent this, you can keep another array of "valid indexes" and delete keys of it when the value reaches max.

Monday Fatigue
  • 223
  • 1
  • 3
  • 19
-1

this calculates a random number from 1 to 4

wrap it on a function to your needs to generate the arrays

Math.floor(Math.random() * 4) + 1 

var randomNumber = Math.floor(Math.random() * 4) + 1 ;
console.log(randomNumber);
Panos K
  • 1,061
  • 12
  • 26
-1

It was too easy.

var values = null;
while(true) {
    var currentSum = 0;
    var expectedSum = 10;
    values = [];
    while(expectedSum !== currentSum) {
        //var value = Math.floor(Math.random() * 9) + 1;
        var value = Math.floor(Math.random() * 4) + 1;
        if(value + currentSum > expectedSum) {
            continue;
        }
        currentSum += value;
        values.push(value);
    }
    if(values.length === 4) {
        break;
    } else {
        console.log('false iteration')
    }
}
console.log(values);
degr
  • 1,559
  • 1
  • 19
  • 37