【问题标题】:Split a random value into four that sum up to it将一个随机值分成四个总和
【发布时间】:2015-05-10 17:55:06
【问题描述】:

我有一个像 24 这样的值,我有四个文本框。如何动态生成四个加起来为 24 的值?

所有值必须为整数,不能为负数,结果不能为6、6、6、6;它们必须不同,例如:8、2、10、4。(但 5、6、6、7 也可以。)

【问题讨论】:

  • 这些值都是整数吗?值可以是负数吗?
  • 4 个数字都必须不同吗? 5,6,6,7 是有效的解决方案吗?
  • @vacawama 是的,这是一个有效的解决方案
  • 忘掉 Objective-C、Swift 等。如果你必须在纸上解决这个问题,也许用一对骰子来生成随机数,你会怎么做?
  • @HotLicks 像这样:XKCD Random Number?

标签: algorithm swift math


【解决方案1】:

对于您陈述的问题,可以生成一个包含所有可能解决方案的数组,然后随机选择一个。实际上有 1,770 种可能的解决方案。

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

这避免了以初始设置时间和存储为代价的任何偏差。


这可以通过以下方式改进:

  • 通过仅存储前 3 个数字来减少存储空间。第 4 个总是 24 -(前 3 个的总和)
  • 通过将每个解决方案存储为单个整数来减少存储空间:(i * 10000 + j * 100 + k)
  • 通过意识到每个循环不需要进入 21 来加速解决方案的生成。

这是将每个解决方案存储为单个整数并优化循环的解决方案:

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

【讨论】:

  • 并且循环可以设置为 21 - 以前的值?这将再次给您带来偏见问题。或者您还能将循环设置为什么?
  • @milo526,提高循环效率不会引入偏差,因为它们会生成所有可能的解决方案。只要不错过任何一个,都没有问题。
【解决方案2】:
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)

这不是最终答案,但应该是一个(好的)起点。

它是如何工作的:它需要 2 个参数。随机值的数量(在您的情况下为 4)和总量(在您的情况下为 24)。

它在总金额和 0 之间取一个随机值,将其存储在一个数组中,然后从存储剩余金额的变量中减去该值并存储新值。

它在剩余数量和 0 之间取一个新的随机值,将其存储在一个数组中,然后再次从剩余数量中减去该值并存储新值。

当它是需要的最后一个数字时,它会查看剩余的数量并将其添加到数组中

编辑:

+1 添加到随机值可以消除数组中存在0 的问题。

编辑 2:

对数组进行洗牌确实会增加将高值作为第一个值的机会。

【讨论】:

  • 由于范围不断减小,连续值会变小。
  • @zaph 如果您的第一个项目很小,那么第二个项目可能很大。洗牌阵列也可以解决这个问题。
  • 4个数字中总共有24个:第一个数字可以是1到21,第二个数字可以是1到1到(21-第一个数字)。对于以下每个数字,范围变小。例如:如果第一个随机数是 10,则下一个随机数不能大于 21-10 或 11。
  • 当您要求更大的值时,偏差会变得更加明显,例如 16 和 1000:[600, 137, 151, 94, 5, 2, 1, 1, 1, 1, 1, 1, 1、1、1、2]。尽管如此,我认为,鉴于已知的限制,这个答案正确地解决了 OP 的需求......
  • 改组数组并不能消除对较小数字的偏见,请参阅上面的评论。洗牌只是倾向于隐藏偏见。一个数字对另一个数字的任何依赖都会产生偏差。
【解决方案3】:

不幸的是,非确定性但完全随机的一种解决方案如下:

总共 4 个数字中的 24 个:
选择 1 到 21 之间的四个随机数
重复直到总数等于 24 并且它们不都是 6。

在找到解决方案之前,这将平均循环大约 100 次。

【讨论】:

    【解决方案4】:

    这是https://stackoverflow.com/a/8064754/1187415 中给出的算法的 Swift 实现,略带 修改,因为所有数字都必须是正数。

    产生N个总和为M的正随机整数的方法是

    • 构建一个包含数字 0 的数组,后跟 N-1 个不同的 1 .. M-1 范围内的随机数,最后是数字 M。
    • 计算后续数组元素的差异。

    在第一步中,我们需要 N-1 个元素的随机子集 集合 { 1, ..., M-1 }。这可以通过迭代这个来实现 以 n/m 的概率设置和选择每个元素,其中 m 是我们可以选择的剩余元素数量 n 是要选择的剩余元素数。

    不是将选择的随机数存储在数组中,而是 立即计算与先前选择的数字的差异 并存储。

    这给出了以下功能:

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

    如果所有元素都相等(例如 6+6+6+6=24)的解不是 允许,您可以重复该方法,直到找到有效的解决方案:

    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
    }
    

    这是一个简单的测试。它计算 1,000,000 个随机表示 7 作为 3 个正整数之和,并计算分布 结果。

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

    结果:

    [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

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

    【讨论】:

    • 所有整数也应至少为 1。(以 OP 问题的 cmets 表示)
    • ilo526:问题提到了非负整数,但你是对的,这在评论中得到了纠正。 – 我已经对功能进行了相应的调整,谢谢告知!
    • 我喜欢这个方法,它无疑是这里最好的方法!我有一个小问题,那就是使用 0 和 100,我觉得这可能会迫使一个小的偏差偏移。
    • @zaph:你是对的。我做了一些测试,对于 count>=3,解决方案是平均分布的。我还不知道这个算法是否可以解决这个问题。
    • 似乎有人对几乎所有答案都投了反对票。很高兴知道为什么。
    【解决方案5】:

    这是一个解决方案,它应该显着*比其他一些方法具有更少的偏差。它的工作原理是生成请求数量的随机浮点数,将它们全部相乘或相除,直到它们加起来达到目标​​总数,然后将它们四舍五入为整数。舍入过程会改变总数,因此我们需要通过对随机项进行加减来进行修正,直到它们相加到正确的数量为止。

    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
    }
    

    *编辑:Martin R 正确地指出,这并不像人们预期的那样统一,实际上高度偏向于 1-24 范围中间的数字。我推荐使用此解决方案,但我将其搁置,以便其他人知道不要犯同样的错误。

    【讨论】:

    • 24 应该是 total :) – 我运行了一些测试,对 getRandomInts(count: 3, total: 5) 进行了 100,000 次调用,结果似乎分布不均。我不是这方面的专家,但根据stackoverflow.com/a/8068956/1187415,选择一个随机数向量然后将其缩放到所需的总和是不是一个好的算法。
    • 我运行了 4、24 的 10K 次迭代并选择了以下平均值:6.03、5.98、6.01、5.98,还不错。但是随机性又很难确定,这只是在一维中测量,可能是重要的,也可能不是。我了解任何偏差的来源,并且我喜欢这样,没有对最初偏斜的结果进行修正。
    • 请注意,引用的答案是使用 rand()。如果那是“C”库函数(看起来是这样),那么他们就有一个主要问题,因为rand() 不会产生随机数。正在做的不是这个问题的目的。它需要 10M 个数字并将它们减少到 1.0。我们在 24 范围内取 4 个数字。
    • @zaph: rand() 在那个答案中来自 MATLAB,并在 0.0 .. 1.0 范围内创建 10M 对 2 个随机数。然后将每个 pair 缩减为 sum=1.0。
    • 请参阅@MartinR 的回复。缩小没有简单的比例因子,随机数的不同分布是不同的。它与将整数随机数转换为浮点数无关。
    【解决方案6】:

    作为递归函数,算法非常好:

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

    并进行安全检查:

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

    正如@MartinR 指出的那样,上面的代码非常有偏见。因此,为了使输出值均匀分布,您应该使用这段代码:

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

    【讨论】:

    • 这是非常有偏见的。例如,getRandomValues(3, 7) 产生 [5, 1, 1] 的频率远高于 [1, 1, 5]
    • @MartinR 你是对的,但由于他没有说明结果数字的顺序,这不符合预期的条件吗?
    • 我不确定我是否完全理解您的意思,但[5, 1, 1] 的返回频率也明显高于(例如)[1, 3, 3][1, 2, 4]。欢迎您使用我答案末尾的测试代码进行验证。
    • @MartinR 你说得对,这是有偏见的,但 OP 只希望随机数加起来为 24,而没有说明它们的分布。我将添加另一个均匀分布的示例。
    猜你喜欢
    • 2014-03-10
    • 1970-01-01
    • 1970-01-01
    • 2012-04-11
    • 2011-08-16
    • 1970-01-01
    • 1970-01-01
    • 2018-12-03
    • 1970-01-01
    相关资源
    最近更新 更多