3

I have one value like 24, and I have four textboxes. How can I dynamically generate four values that add up to 24?

All the values must be integers and can't be negative, and the result cannot be 6, 6, 6, 6; they must be different like: 8, 2, 10, 4. (But 5, 6, 6, 7 would be okay.)

jscs
  • 63,694
  • 13
  • 151
  • 195
da1lbi3
  • 4,369
  • 6
  • 31
  • 65

7 Answers7

3

Here is a Swift implementation of the algorithm given in https://stackoverflow.com/a/8064754/1187415, with a slight modification because all numbers are required to be positive.

The method to producing N positive random integers with sum M is

  • Build an array containing the number 0, followed by N-1 different random numbers in the range 1 .. M-1, and finally the number M.
  • Compute the differences of subsequent array elements.

In the first step, we need a random subset of N-1 elements out of the set { 1, ..., M-1 }. This can be achieved by iterating over this set and choosing each element with probability n/m, where m is the remaining number of elements we can choose from and n is the remaining number of elements to choose.

Instead of storing the chosen random numbers in an array, the difference to the previously chosen number is computed immediately and stored.

This gives the following function:

func randomNumbers(#count : Int, withSum sum : Int) -> [Int] {
    
    precondition(sum >= count, "`sum` must not be less than `count`")
    
    var diffs : [Int] = []
    var last = 0        // last number chosen

    var m = UInt32(sum - 1)     // remaining # of elements to choose from
    var n = UInt32(count - 1)   // remaining # of elements to choose
    for i in 1 ..< sum  {
        // Choose this number `i` with probability n/m:
        if arc4random_uniform(m) < n {
            diffs.append(i - last)
            last = i
            n--
        }
        m--
    }
    diffs.append(sum - last)
    
    return diffs
}

println(randomNumbers(count: 4, withSum: 24))

If a solution with all elements equal (e.g 6+6+6+6=24) is not allowed, you can repeat the method until a valid solution is found:

func differentRandomNumbers(#count : Int, withSum sum : Int) -> [Int] {

    precondition(count >= 2, "`count` must be at least 2")

    var v : [Int]
    do {
        v = randomNumbers(count: count, withSum: sum)
    } while (!contains(v, { $0 != v[0]} ))
    return v
}

Here is a simple test. It computes 1,000,000 random representations of 7 as the sum of 3 positive integers, and counts the distribution of the results.

let set = NSCountedSet()
for i in 1 ... 1_000_000 {
    let v = randomNumbers(count: 3, withSum: 7)
    set.addObject(v)
}
for (_, v) in enumerate(set) {
    let count = set.countForObject(v)
    println("\(v as! [Int]) \(count)")
}

Result:

[1, 4, 2] 66786
[1, 5, 1] 67082
[3, 1, 3] 66273
[2, 2, 3] 66808
[2, 3, 2] 66966
[5, 1, 1] 66545
[2, 1, 4] 66381
[1, 3, 3] 67153
[3, 3, 1] 67034
[4, 1, 2] 66423
[3, 2, 2] 66674
[2, 4, 1] 66418
[4, 2, 1] 66292
[1, 1, 5] 66414
[1, 2, 4] 66751

Update for Swift 3:

func randomNumbers(count : Int, withSum sum : Int) -> [Int] {
    
    precondition(sum >= count, "`sum` must not be less than `count`")
    
    var diffs : [Int] = []
    var last = 0        // last number chosen
    
    var m = UInt32(sum - 1)     // remaining # of elements to choose from
    var n = UInt32(count - 1)   // remaining # of elements to choose
    for i in 1 ..< sum  {
        // Choose this number `i` with probability n/m:
        if arc4random_uniform(m) < n {
            diffs.append(i - last)
            last = i
            n -= 1
        }
        m -= 1
    }
    diffs.append(sum - last)
    
    return diffs
}

print(randomNumbers(count: 4, withSum: 24))

Update for Swift 4.2 (and later), using the unified random API:

func randomNumbers(count : Int, withSum sum : Int) -> [Int] {

    precondition(sum >= count, "`sum` must not be less than `count`")

    var diffs : [Int] = []
    var last = 0        // last number chosen

    var m = sum - 1     // remaining # of elements to choose from
    var n = count - 1   // remaining # of elements to choose
    for i in 1 ..< sum  {
        // Choose this number `i` with probability n/m:
        if Int.random(in: 0..<m) < n {
            diffs.append(i - last)
            last = i
            n -= 1
        }
        m -= 1
    }
    diffs.append(sum - last)

    return diffs
}
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • All integers should also be at least 1. (stated in comments of question by OP) – milo526 May 10 '15 at 20:15
  • ilo526: The question mentioned non-negative integers, but you are right, this is corrected in a comment. – I have adjusted the function accordingly, thanks for letting me know! – Martin R May 10 '15 at 20:24
  • 1
    I like the method,it is without a doubt the best one here! I have one small quibble, that is the use of 0 and 100, I feel that may be forcing a small bias shift. – zaph May 10 '15 at 20:30
  • @zaph: You are right. I made some tests and for count>=3 the solutions are equally distributed. I don't know yet if that can be fixed in this algorithm. – Martin R May 10 '15 at 20:54
  • I ran 1M iterations and here are the average values for the four positions: 4.76, 5.23, 5.24, 8.76 which sum to 24.0. There seems to be a bias shift. What this algorythm is trying to do is form a ring of the values but choosing an abitrary fixed starting point. – zaph May 10 '15 at 21:18
  • @zaph: I have (hopefully) improved the method. In my tests the results were well distributed. Please let me know what you think! – Martin R May 10 '15 at 21:31
  • It looks good, averages for 10K iterations: 5.979, 5.999, 6.032, 5.993. Correct me if I am reading the code wrong, I'm an old ObjC programmer: you are randomly swapping positions. If that is the case I am not convinced that removes as opposed to hiding the bias. My reticence is along the lines of solving the semaphore problem. The proposed solutions kept getting more convoluted to the point no one could prove them-they all failed until Dijkstra came along with real understanding and a simple solution still used today. – zaph May 10 '15 at 21:57
  • @zaph: I have added some explanations. It is the Fisher-Yates shuffle (http://en.wikipedia.org/wiki/Fisher–Yates_shuffle), modified here because only the first `count-1` numbers are needed. The Fisher-Yates shuffle is known to produce a random permutation. – Martin R May 10 '15 at 22:00
  • Martin, I'm comming from a crypto bias where we don't guess and there is considerable peer review by domain experts for methods. For this application: sure-fine. For securing something: no. In fact it may be 100% correct, I am not competent enough to say one way or another. Would my wife accept it: no, she is paranoid. Randomness is real hard to determine. – zaph May 10 '15 at 22:11
  • @zaph: Sure, and as I said, I am not an expert on this topic. The only thing I can say is that I tested the method (using the code at the end of my answer) with various values for the parameters, and always got a very equal distribution of all possible values. – Thanks for your feedback and good night (it is 1am here in Germany :) – Martin R May 10 '15 at 23:01
  • @zaph: I have rewritten the function, hopefully it is less obscure now. The idea is to find a random subset of the numbers 1, .., sum-1, and then take the differences. – Martin R May 11 '15 at 00:46
  • 1
    It seems that somebody downvoted almost all answers. It would be nice to know why. – Martin R May 11 '15 at 00:49
  • Not me, I upvoted 3 and down voted 1. I have been asked not to tell why bnu the moderators due to revenge down votes. – zaph May 11 '15 at 01:54
  • That is a very interesting and straight forward solution! It does not seem to have a bias based in my testing with averages or your testing. I am however biased toward the answer by @SelectricSimian due to the clear manner of selecting the values and do not feel the argument by user85109 is valid on several counts. Yes: I upvoted this answer, it is a good answer. – zaph May 11 '15 at 14:56
  • @zaph: How did you test the method? How exactly is the "bias" computed ? With 100,000 calls to `getRandomInts(count: 3, total: 5)` from @SelectricSimian's answer I get the distribution [3, 1, 1]: 13316, [1, 1, 3]: 13354, [2, 2, 1]: 19904, [1, 3, 1]: 13507, [2, 1, 2]: 20066, [1, 2, 2]: 19853, which does not look very uniform to me. – Martin R May 11 '15 at 15:09
  • I tested 4, 24 with averages. I will test more. If there is a bias, seems that there is from your testing, it is in the rounding which I found overly complicated. Your testing with 3,5 really stresses the rounding. There is nothing wrong with the concept but the implementation may be "lacking". – zaph May 11 '15 at 15:17
  • @zaph: I did 1,000,000 calls to `getRandomInts(count: 4, total: 24)`. There are 1171 possible solutions of 24=a+b+c+d, so every solution should occur approximately 854 times. But [1, 2, 2, 19] occurred only 59 times and [6, 6, 6, 6] 3536 times. – Martin R May 11 '15 at 15:50
  • OK, scalling can not work at least with a scaling factor that can be easily calculated. The scaling factor depends on the distribution of the random numbers. In some fasion scaling is a trap-door function, easy to scale up, difficult to scale down for this problem. As Myth Busters say: Busted. – zaph May 11 '15 at 20:43
2

For your stated problem, it is possible to generate an array of all possible solutions and then pick one randomly. There are in fact 1,770 possible solutions.

var solutions = [[Int]]()

for i in 1...21 {
    for j in 1...21 {
        for k in 1...21 {
            let l = 24 - (i + j + k)
            if l > 0 && !(i == 6 && j == 6 && k == 6) {
                solutions.append([i, j, k, l])
            }
        }
    }
}

// Now generate 20 solutions
for _ in 1...20 {
    let rval = Int(arc4random_uniform(UInt32(solutions.count)))
    println(solutions[rval])
}

This avoids any bias at the cost of initial setup time and storage.


This could be improved by:

  • Reducing storage space by only storing the first 3 numbers. The 4th one is always 24 - (sum of first 3)
  • Reducing storage space by storing each solution as a single integer: (i * 10000 + j * 100 + k)
  • Speeding up the generation of solutions by realizing that each loop doesn't need to go to 21.

Here is the solution that stores each solution as a single integer and optimizes the loops:

var solutions = [Int]()

for i in 1...21 {
    for j in 1...22-i {
        for k in 1...23-i-j {
            if !(i == 6 && j == 6 && k == 6) {
                solutions.append(i * 10000 + j * 100 + k)
            }
        }
    }
}

// Now generate 20 solutions
for _ in 1...20 {
    let rval = Int(arc4random_uniform(UInt32(solutions.count)))
    let solution = solutions[rval]

    // unpack the values
    let i = solution / 10000
    let j = (solution % 10000) / 100
    let k = solution % 100
    let l = 24 - (i + j + k)

    // print the solution
    println("\([i, j, k, l])")
}
vacawama
  • 150,663
  • 30
  • 266
  • 294
  • and the loop can be set to 21 - the previous values? This would give you problems with the bias again. Or what else would you be able to set the loop to? – milo526 May 10 '15 at 19:17
  • 1
    @milo526, making the loops more efficient wouldn't introduce bias because they're generating all possible solutions. As long as you don't miss any, there is no problem. – vacawama May 10 '15 at 19:48
1
func getRandomValues(amountOfValues:Int, totalAmount:Int) -> [Int]?{
    if amountOfValues < 1{
        return nil
    }

    if totalAmount < 1{
        return nil
    }

    if totalAmount < amountOfValues{
        return nil
    }

    var values:[Int] = []
    var valueLeft = totalAmount

    for i in 0..<amountOfValues{

        if i == amountOfValues - 1{
            values.append(valueLeft)
            break
        }
       var value = Int(arc4random_uniform(UInt32(valueLeft - (amountOfValues - i))) + 1)
        valueLeft -= value
        values.append(value)
    }

    var shuffledArray:[Int] = []

    for i in 0..<values.count {
        var rnd = Int(arc4random_uniform(UInt32(values.count)))
        shuffledArray.append(values[rnd])
        values.removeAtIndex(rnd)
    }

    return shuffledArray
}

getRandomValues(4, 24)

This is not a final answer, but it should be a (good) starting point.

How it works: It takes 2 parameters. The amount of random values (4 in your case) and the total amount (24 in your case).

It takes a random value between the total Amount and 0, stores this in an array and it subtracts this from a variable which stores the amount that is left and stores the new value.

Than it takes a new random value between the amount that is left and 0, stores this in an array and it again subtracts this from the amount that is left and stores the new value.

When it is the last number needed, it sees what amount is left and adds that to the array

EDIT:

Adding a +1 to the random value removes the problem of having 0 in your array.

EDIT 2:

Shuffling the array does remove the increased chance of having a high value as the first value.

milo526
  • 5,012
  • 5
  • 41
  • 60
  • 2
    There is going to be a bias of successiv e values being smaller because the range keeps decreasing. – zaph May 10 '15 at 18:24
  • 1
    @zaph If your first item is small, the second item can be big. Also shuffling the array could fix this problem. – milo526 May 10 '15 at 18:31
  • For a total of 24 in 4 numbers: The first number can be 1 to 21, the second number can be 1 to 1 to (21 -first number). The range becomes small for each following number. Ex: if the first random number is 10 the next can be no larger than 21-10 or 11. – zaph May 10 '15 at 18:40
  • @zaph It does indeed decrease the chance of having bigger values as the third or fourth value, but there can still be high values. Added 100 random values in my answer – milo526 May 10 '15 at 18:45
  • 2
    The bias becomes more apparent when you ask for bigger values, like 16 and 1000: [600, 137, 151, 94, 5, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2]. Still, this answer addresses OP's needs correctly, I think, given the known constraints... – Eric Aya May 10 '15 at 18:50
  • 1
    Shuffling the array does not remove the bias toward smaller numbers, see the above comment. Shuffling just tends to hide the bias. Any dependence of one number on another creates a bias. – zaph May 10 '15 at 19:07
  • Just for grins, the averages of the four numbers in the abswer in order: 9.31, 6.48, 3.58, 4.63. Note the 3rd and 4th, not unexpected for random numbers. – zaph May 10 '15 at 19:17
  • @zaph Did this include the randomize array function? Including it I get random values of (in order) 5.95, 6.56, 5.92, 5.57 (random of 100 iterations) – milo526 May 10 '15 at 19:30
  • Moving the numbers around does not eliminate the bias. Consider the resiults from a previous comment: "[600, 137, 151, 94, 5, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2]", re-ordering will not fix the bias, there will still be an overwelming numnbers of smaller digits. – zaph May 10 '15 at 19:42
  • @zaph Let’s first be clear I’m not trying to attack somebody, just interested. How would your answer decrease the odds of small numbers? – milo526 May 10 '15 at 19:44
  • There are no attacks. You can't decrease the odds of small numbers, only decrease any bias. Obviously only one 21 can be present where three 1s can be present. See my answer, each number position has an equally unbiased chance. – zaph May 10 '15 at 20:04
  • @zaph So the “problem” with my “algorithm” is that out of all possibilities it tends to choose one with lower values where yours choices a random one out of all possibilities? – milo526 May 10 '15 at 20:08
  • The solution in this answer chooses smaller values for progressive positions, that is a demonstrable bias. Randomizing the value positions may resolve that bias but I'm not convinced that it solves an over-all bias, a strong proof would be required. I believe that my solution requires a simple and rather obvious proof. – zaph May 10 '15 at 20:12
  • Running the test at the end of my answer (with `let v = getRandomValues(3, 7)!`) shows that some solutions (e.g. 1+1+5) occur significantly less frequently than others (e.g. 2+2+3). – Martin R May 10 '15 at 22:51
  • When the variables in: var value = Int(arc4random_uniform(UInt32(valueLeft - (amountOfValues - i))) + 1) become negative the whole program crashes – da1lbi3 May 16 '15 at 13:24
1

One solution that is unfortunatly non-deterministic but completely random is as follows:

For a total of 24 in 4 numbers:
pick four random numbers between 1 and 21
repeat until the total of the numbers equals 24 and they are not all 6.

This will, on average, loop about 100 times before finding a solution.

zaph
  • 111,848
  • 21
  • 189
  • 228
0

Here's a solution which should have significantly* less bias than some of the other methods. It works by generating the requested number of random floating point numbers, multiplying or dividing all of them until they add up to the target total, and then rounding them into integers. The rounding process changes the total, so we need to correct for that by adding or subtracting from random terms until they add up to the right amount.

func getRandomDoubles(#count: Int, #total: Double) -> [Double] {
    var nonNormalized = [Double]()
    nonNormalized.reserveCapacity(count)
    for i in 0..<count {
        nonNormalized.append(Double(arc4random()) / 0xFFFFFFFF)
    }
    let nonNormalizedSum = reduce(nonNormalized, 0) { $0 + $1 }
    let normalized = nonNormalized.map { $0 * total / nonNormalizedSum }
    return normalized
}

func getRandomInts(#count: Int, #total: Int) -> [Int] {
    let doubles = getRandomDoubles(count: count, total: Double(total))
    var ints = [Int]()
    ints.reserveCapacity(count)
    for double in doubles {
        if double < 1 || double % 1 >= 0.5 {
            // round up
            ints.append(Int(ceil(double)))
        } else {
            // round down
            ints.append(Int(floor(double)))
        }
    }
    let roundingErrors = total - (reduce(ints, 0) { $0 + $1 })
    let directionToAdjust: Int = roundingErrors > 0 ? 1 : -1
    var corrections = abs(roundingErrors)
    while corrections > 0 {
        let index = Int(arc4random_uniform(UInt32(count)))
        if directionToAdjust == -1 && ints[index] <= 1 { continue }
        ints[index] += directionToAdjust
        corrections--
    }
    return ints
}

*EDIT: Martin R has correctly pointed out that this is not nearly as uniform as one might expect, and is in fact highly biased towards numbers in the middle of the 1-24 range. I would not recommend using this solution, but I'm leaving it up so that others can know not to make the same mistake.

exists-forall
  • 4,148
  • 4
  • 22
  • 29
  • (The `24` should be `total` :) – I ran some tests with 100,000 calls to `getRandomInts(count: 3, total: 5)`, and the results do not seem well distributed. I am not an expert on this topic, but according to http://stackoverflow.com/a/8068956/1187415, choosing a vector of random numbers and then scaling it to the required sum is *not* a good algorithm. – Martin R May 10 '15 at 21:39
  • I ran 10K iterations of 4, 24 and optained the following averages: 6.03, 5.98, 6.01, 5.98, not bad. But again randomness is hard to determine,this is just measuring in one dimension, possible the important one, possibly not. I understand where any bias is comming from and I like that, there is no fix-up applied to initially skewed results. – zaph May 10 '15 at 22:32
  • Note that the referenced answer is using r`and()`. If that is the "C" library function (it seems so) they there is a major problem becasue `rand()` does not produce random numbers. What is beiong done is not what this question is about. It is takiong 10M numbers and reducing them to sum to 1.0. We are taking 4 numbers in a range of 24. – zaph May 10 '15 at 22:49
  • @zaph: `rand()` in that answer is from MATLAB and creates 10M pairs of 2 random numbers in the range 0.0 .. 1.0. Each *pair* is then reduced to sum=1.0. – Martin R May 11 '15 at 18:30
  • See may response to @MartinR. There is no simple scale factor for scalling down, it is different for different spreads of the random numbers. It has nothjing to do with converting an integer random number to a floating point. – zaph May 11 '15 at 20:46
0

As a recursive function the algorithm is very nice:

func getRandomValues(amount: Int, total: Int) -> [Int] {
    if amount == 1 { return [total] }
    if amount == total { return Array(count: amount, repeatedValue: 1) }
    let number = Int(arc4random()) % (total - amount + 1) + 1
    return [number] + getRandomValues(amount - 1, total - number)
}

And with safety check:

func getRandomValues(amount: Int, total: Int) -> [Int]? {
    if !(1...total ~= amount) { return nil }
    if amount == 1 { return [total] }
    if amount == total { return Array(count: amount, repeatedValue: 1) }
    let number = Int(arc4random()) % (total - amount + 1) + 1
    return [number] + getRandomValues(amount - 1, total - number)!
}

As @MartinR pointed out the code above is extremely biased. So in order to have a uniform distribution of the output values you should use this piece of code:

func getRandomValues(amount: Int, total: Int) -> [Int] {
    var numberSet = Set<Int>()

    // add splitting points to numberSet
    for _ in 1...amount - 1 {
        var number = Int(arc4random()) % (total - 1) + 1
        while numberSet.contains(number) {
            number = Int(arc4random()) % (total - 1) + 1
        }
        numberSet.insert(number)
    }

    // sort numberSet and return the differences between the splitting points
    let sortedArray = (Array(numberSet) + [0, total]).sort()
    return sortedArray.enumerate().flatMap{
        indexElement in
        if indexElement.index == amount { return nil }
        return sortedArray[indexElement.index + 1] - indexElement.element
    }
}
Qbyte
  • 12,753
  • 4
  • 41
  • 57
  • This is extremely biased. As an example, `getRandomValues(3, 7)` produces `[5, 1, 1]` much more frequently than `[1, 1, 5]`. – Martin R Jul 05 '15 at 10:34
  • @MartinR You're right but doesn't that fulfill the desired conditions since he doesn't said anything about the order of the resulting numbers? – Qbyte Aug 31 '15 at 14:47
  • I am not sure if I understand fully what you mean, but `[5, 1, 1]` is also returned significantly more often than (for example) `[1, 3, 3]` or `[1, 2, 4]`. You are welcome to verify that with the test code at the end of my answer. – Martin R Sep 06 '15 at 09:31
  • @MartinR You're right it is biased but it the OP only want to have random numbers that add up to 24 without saying something about their distribution. I will add another example with uniform distribution. – Qbyte Sep 06 '15 at 13:49
0

A javascript implementation for those who may be looking for such case:

const numbersSumTo = (length, value) => {
    const fourRandomNumbers = Array.from({ length: length }, () => Math.floor(Math.random() * 6) + 1);
    const res = fourRandomNumbers.map(num => (num / fourRandomNumbers.reduce((a, b) => a + b, 0)) * value).map(num => Math.trunc(num));
    res[0] += Math.abs(res.reduce((a, b) => a + b, 0) - value);
    return res;
}


// Gets an array with 4 items which sum to 100
const res = numbersSumTo(4, 100);
const resSum = res.reduce((a, b) => a + b, 0);

console.log({
  res,
  resSum
});

Also plenty of different methods of approach can be found here on this question: https://math.stackexchange.com/questions/1276206/method-of-generating-random-numbers-that-sum-to-100-is-this-truly-random

Finbar
  • 1,127
  • 1
  • 5
  • 17