da1lbi3
da1lbi3

Reputation: 4519

Split a random value into four that sum up to it

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.)

Upvotes: 3

Views: 2454

Answers (7)

Martin R
Martin R

Reputation: 539965

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
}

Upvotes: 3

Finbar
Finbar

Reputation: 1353

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

Upvotes: 0

Qbyte
Qbyte

Reputation: 13263

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
    }
}

Upvotes: 0

exists-forall
exists-forall

Reputation: 4258

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.

Upvotes: 0

vacawama
vacawama

Reputation: 154661

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])")
}

Upvotes: 2

milo526
milo526

Reputation: 5083

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.

Upvotes: 1

zaph
zaph

Reputation: 112855

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.

Upvotes: 1

Related Questions