【问题标题】:Is there a way to shuffle an array so that no two consecutive values are the same?有没有办法对数组进行洗牌,以便没有两个连续的值是相同的?
【发布时间】:2017-01-03 08:50:38
【问题描述】:

我有一组颜色将填充饼图以充当游戏微调器。我不希望相同的颜色彼此相邻出现,从而在圆圈中形成一大块。

我的数组看起来像这样:

var colors = ["blue", "red", "green", "red", "blue", "blue", "blue", "green"]

问题当然是三个布鲁斯在一起。 Swift 中是否有任何内置功能可以让我在整个分布中平均(或尽可能接近)分布值并避免它们相邻?

我可以用下面的代码测试匹配,但重新排列它们被证明有点困难。

var lastColor = "white"

for color in colors {
    if color == lastColor {
        print("match")
    }
    lastColor = color    
}

更新:

为了创建我的colors 数组,我从每种颜色的空格数开始。它看起来像这样:

let numberOfReds = 2
let numberOfGreens = 2
let numberOfBlues = 4

let spaces = numberOfReds + numberOfGreens + numberOfBlues

for _ in 0..< spaces {
    if numberOfReds > 0 {
        numberOfReds -= 1
        colors.append("red")
    }
    if numberOfGreens > 0 {
        numberOfGreens -= 1
        colors.append("green")
    }
    if numberOfBlues > 0 {
        numberOfBlues -= 1
        colors.append("blue")
    }
}

最终吐出:

colors = ["red", "green", "blue", "red", "green", "blue", "blue", "blue" ]

【问题讨论】:

  • 对数组进行简单的排序就足够了吗?
  • 您是否需要重新排列现有数组,或者您是否可以只编写一个函数从头开始创建一个没有连续匹配元素的数组?
  • 看起来是一维的图表着色问题。我会说这是一个算法问题。
  • @vadian 排序不会导致颜色聚集而不是散开吗?
  • 不是相对的,但是:为什么要在饼图上使用多个相同的颜色?只需生成 N 种最不同的颜色。

标签: swift random distribution


【解决方案1】:

免责声明:为了生成 “随机” 解决方案,我将使用回溯。从空间的角度来看,这种方法快并且便宜。

事实上,时间和空间复杂度都是 O(n!)... 这是巨大的!

但是它为您提供了一个有效的解决方案尽可能随机

回溯

因此,如果没有 2 个连续的 equals 元素,您需要随机组合一个值列表,条件是解决方案有效

为了获得真正的随机解决方案,我建议采用以下方法。

我生成所有可能的有效组合。为此,我正在使用回溯方法

func combinations<Element:Equatable>(unusedElms: [Element], sequence:[Element] = []) -> [[Element]] {
    // continue if the current sequence doesn't contain adjacent equal elms
    guard !Array(zip(sequence.dropFirst(), sequence)).contains(==) else { return [] }
    
    // continue if there are more elms to add
    guard !unusedElms.isEmpty else { return [sequence] }
    
    // try every possible way of completing this sequence
    var results = [[Element]]()
    for i in 0..<unusedElms.count {
        var unusedElms = unusedElms
        let newElm = unusedElms.removeAtIndex(i)
        let newSequence = sequence + [newElm]
        results += combinations(unusedElms, sequence: newSequence)
    }
    return results
}

现在给出颜色列表

let colors = ["blue", "red", "green", "red", "blue", "blue", "blue", "green"]

我可以生成所有可能的有效组合

let combs = combinations(colors)

[["blue", "red", "green", "blue", "red", "blue", "green", "blue"], ["blue", "red", "green", "blue", "red", "blue", "green", "blue"], ["blue", "red", "green", "blue", "green", "blue", "red", "blue"], ["blue", "red", "green", "blue", "green", "blue", "red", "blue"], ["blue", "red", "green", "blue", "red", "blue", "green", "blue"], ["blue", "red", "green", "blue", "red", "blue", "green", "blue"], ["blue", "red", "green", "blue", "green", "blue", "red", "blue"], ["blue", "red", "green", "blue", "green", "blue", "red", "blue"], ["blue", "red", "green", "blue", "red", "blue", "green", "blue"], ["blue", "red", "green", "blue", "red", "blue", "green", "blue"], ["blue", "red", "green", "blue", "green", "blue", "red", "blue"], ["blue", "red", "green", "blue", "green", "blue", "red", "blue"], ["blue", "red", "blue", "green", "red", "blue", "green", "blue"], ["blue", "red", "blue", "green", "red", "blue", "green", "blue"], ["blue", "red", "blue", "green", "blue", "red", "blue", "green"], ["blue", "red", "blue", "green", "blue", "red", "green", "blue"], ["blue", "red", "blue", "green", "blue", "green", "red", "blue"], ["blue", "red", "blue", "green", "blue", "green", "blue", "red"], ["blue", "red", "blue", "green", "blue", "red", "blue", "green"], ["blue", "red", "blue", "green", "blue", "red", "green", "blue"], ["blue", "red", "blue", "green", "blue", "green", "red", "blue"], ["blue", "red", "blue", "green", "blue", "green", "blue", "red"], ["blue", "red", "blue", "red", "green", "blue", "green", "blue"], ["blue", "red", "blue", "red", "green", "blue", "green", "blue"], ["blue", "red", "blue", "red", "blue", "green", "blue", "green"], ["blue", "red", "blue", "red", "blue", "green", "blue", "green"], ["blue", "red", "blue", "red", "blue", "green", "blue", "green"], ["blue", "red", "blue", "red", "blue", "green", "blue", "green"], ["blue", "red", "blue", "red", "green", "blue", "green", "blue"], ["blue", "red", "blue", "red", "green", "blue", "green", "blue"], ["blue", "red", "blue", "green", "red", "blue", "green", "blue"], ["blue", "red", "blue", "green", "red", "blue", "green", "blue"], ["blue", "red", "blue", "green", "blue", "green", "red", "blue"], ["blue", "red", "blue", "green", "blue", "green", "blue", "red"], ["blue", "red", "blue", "green", "blue", "red", "green", "blue"], ["blue", "red", "blue", "green", "blue", "red", "blue", "green"], ["blue", "red", "blue", "green", "blue", "green", "red", "blue"], ["blue", "red", "blue", "green", "blue", "green", "blue", "red"], ["blue", "red", "blue", "green", "blue", "red", "green", "blue"], ["blue", "red", "blue", "green", "blue", "red", "blue", "green"], ["blue", "red", "blue", "green", "red", "blue", "green", "blue"], ["blue", "red", "blue", "green", "red", "blue", "green", "blue"], ["blue", "red", "blue", "green", "blue", "red", "blue", "green"], ["blue", "red", "blue", "green", "blue", "red", "green", "blue"], ["blue", "red", "blue", "green", "blue", "green", "red", "blue"], ["blue", "red", "blue", "green", "blue", "green", "blue", "red"], ["blue", "red", "blue", "green", "blue", "red", "blue", "green"], ["blue", "red", "blue", "green", "blue", "red", "green", "blue"], ["blue", "red", "blue", "green", "blue", "green", "red", "blue"], ["blue", "red", "blue", "green", "blue", "green", "blue", "red"], ["blue", "red", "blue", "red", "green", "blue", "green", "blue"], ["blue", "red", "blue", "red", "green", "blue", "green", "blue"], ["blue", "red", "blue", "red", "blue", "green", "blue", "green"], ["blue", "red", "blue", "red", "blue", "green", "blue", "green"], ["blue", "red", "blue", "red", "blue", "green", "blue", "green"], ["blue", "red", "blue", "red", "blue", "green", "blue", "green"], ["blue", "red", "blue", "red", "green", "blue", "green", "blue"], ["blue", "red", "blue", "red", "green", "blue", "green", "blue"], ["blue", "red", "blue", "green", "red", "blue", "green", "blue"], ["blue", "red", "blue", "green", "red", "blue", "green", "blue"], ["blue", "red", "blue", "green", "blue", "green", "red", "blue"], ["blue", "red", "blue", "green", "blue", "green", "blue", "red"], ["blue", "red", "blue", "green", "blue", "red", "green", "blue"], ["blue", "red", "blue", "green", "blue", "red", "blue", "green"], ["blue", "red", "blue", "green", "blue", "green", "red", "blue"], ["blue", "red", "blue", "green", "blue", "green", "blue", "red"], ["blue", "red", "blue", "green", "blue", "red", "green", "blue"], ["blue", "red", "blue", "green", "blue", "red", "blue", "green"], ["blue", "red", "blue", "green", "red", "blue", "green", "blue"], ["blue", "red", "blue", "green", "red", "blue", "green", "blue"], ["blue", "red", "blue", "green", "blue", "red", "blue", "green"], ["blue", "red", "blue", "green", "blue", "red", "green", "blue"], ["blue", "red", "blue", "green", "blue", "green", "red", "blue"], ["blue", "red", "blue", "green", "blue", "green", "blue", "red"], ["blue", "red", "blue", "green", "blue", "red", "blue", "green"], ["blue", "red", "blue", "green", "blue", "red", "green", "blue"], ["blue", "red", "blue", "green", "blue", "green", "red", "blue"], ["blue", "red", "blue", "green", "blue", "green", "blue", "red"], ["blue", "red", "blue", "red", "green", "blue", "green", "blue"], ["blue", "red", "blue", "red", "green", "blue", "green", "blue"], …, ["green", "blue", "green", "blue", "red", "blue", "red", "blue"], ["green", "blue", "green", "blue", "red", "blue", "red", "blue"], ["green", "blue", "green", "blue", "red", "blue", "red", "blue"], ["green", "blue", "green", "blue", "red", "blue", "red", "blue"], ["green", "blue", "green", "blue", "red", "blue", "red", "blue"], ["green", "blue", "green", "blue", "red", "blue", "red", "blue"], ["green", "blue", "green", "blue", "red", "blue", "red", "blue"], ["green", "blue", "green", "blue", "red", "blue", "red", "blue"], ["green", "blue", "red", "blue", "red", "blue", "green", "blue"], ["green", "blue", "red", "blue", "red", "blue", "green", "blue"], ["green", "blue", "red", "blue", "green", "blue", "red", "blue"], ["green", "blue", "red", "blue", "green", "blue", "red", "blue"], ["green", "blue", "red", "blue", "red", "blue", "green", "blue"], ["green", "blue", "red", "blue", "red", "blue", "green", "blue"], ["green", "blue", "red", "blue", "green", "blue", "red", "blue"], ["green", "blue", "red", "blue", "green", "blue", "red", "blue"], ["green", "blue", "red", "blue", "red", "blue", "green", "blue"], ["green", "blue", "red", "blue", "red", "blue", "green", "blue"], ["green", "blue", "red", "blue", "green", "blue", "red", "blue"], ["green", "blue", "red", "blue", "green", "blue", "red", "blue"]]

最后我只需要选择其中一种组合

let comb = combs[Int(arc4random_uniform(UInt32(combs.count)))]
// ["red", "blue", "green", "blue", "green", "blue", "red", "blue"]

改进

如果您不需要 真正的随机 解决方案,而只是一个没有 2 个连续相等元素的排列,我们可以更改前一个函数以返回 第一个 有效的解决方案。

func combination<Element:Equatable>(unusedElms: [Element], sequence:[Element] = []) -> [Element]? {
    guard !Array(zip(sequence.dropFirst(), sequence)).contains(==) else { return nil }
    guard !unusedElms.isEmpty else { return sequence }
    
    for i in 0..<unusedElms.count {
        var unusedElms = unusedElms
        let newElm = unusedElms.removeAtIndex(i)
        let newSequence = sequence + [newElm]
        if let solution = combination(unusedElms, sequence: newSequence) {
            return solution
        }
    }
    return nil
}

现在你可以简单地写

combination(["blue", "red", "green", "red", "blue", "blue", "blue", "green"])

获得有效的解决方案(如果确实存在)

["blue", "red", "green", "blue", "red", "blue", "green", "blue"]

这种方法可以更快(当解决方案确实存在时),但是对于空间和时间复杂度而言,最坏的情况仍然是 O(n!)。

【讨论】:

  • 备注(与实际问题无关):您可以省略.dropLast(),因为zip() 截断为两个参数的较短序列。
【解决方案2】:

尽管表面上看,但这并非微不足道。正如评论员@antonio081014 指出的那样,这实际上是一个算法问题,并且(正如@MartinR 指出的那样)解决了here。这是一个非常简单的启发式,它(与@appzYourLife 的解决方案不同)不是算法,但在大多数情况下都可以工作,而且速度更快(O(n^2 ) 而不是 O(n!))。为了随机性,只需先打乱输入数组:

func unSort(_ a: [String]) -> [String] {
    // construct a measure of "blockiness"
    func blockiness(_ a: [String]) -> Int {
        var bl = 0
        for i in 0 ..< a.count {
            // Wrap around, as OP wants this on a circle
            if a[i] == a[(i + 1) % a.count] { bl += 1 } 
        }
        return bl
    }
    var aCopy = a // Make it a mutable var
    var giveUpAfter = aCopy.count // Frankly, arbitrary... 
    while (blockiness(aCopy) > 0) && (giveUpAfter > 0) {
        // i.e. we give up if either blockiness has been removed ( == 0)
        // OR if we have made too many changes without solving

        // Look for adjacent pairs    
        for i in 0 ..< aCopy.count {
            // Wrap around, as OP wants this on a circle
            let prev = (i - 1 >= 0) ? i - 1 : i - 1 + aCopy.count
            if aCopy[i] == aCopy[prev] { // two adjacent elements match
                let next = (i + 1) % aCopy.count // again, circular 
                // move the known match away, swapping it with the "unknown" next element
                (aCopy[i], aCopy[next]) = (aCopy[next], aCopy[i])
            }
        }
        giveUpAfter -= 1
    }
    return aCopy
}

var colors = ["blue", "red", "green", "red", "blue", "blue", "blue", "green"]
unSort(colors) // ["blue", "green", "blue", "red", "blue", "green", "blue", "red"]

// Add an extra blue to make it impossible...
colors = ["blue", "blue", "green", "red", "blue", "blue", "blue", "green"]
unSort(colors) //["blue", "green", "blue", "red", "blue", "blue", "green", "blue"]

【讨论】:

    【解决方案3】:

    O(N)时空解

    我从图片开始,因为它总是更有趣:)

    简介

    首先,我想指出您不能有一个均匀分布的序列,因为在您的情况下,颜色的数量是不一样的。

    回答如何生成随机序列让我们从最简单的情况开始

    所有颜色都是唯一的,你从1 - N生成一个随机值,取出颜色,从1 - (N-1)生成一个等等。

    现在,某些颜色比其他颜色多,您执行与之前方法相同的操作,但现在每种颜色出现的概率不同 - 如果您有更多黑色,则其概率为更高。

    现在,在你的情况下,你有一个确切的情况,但有一个额外的要求 - 当前的随机颜色不等于前一个。因此,只需在生成每种颜色时应用此要求 - 就随机性而言,它将是最好的。

    示例

    例如,您总共有 4 种颜色:

    • 黑色:2;
    • 红色:1;
    • 绿色:1.

    首先想到的步骤如下:

    1. 将它们放在一行B B R G;
    2. 随机选择一个,例如:B,把相同的颜色都去掉,保证下一个不一样。现在你有R G;
    3. 随机选择下一个,例如R,去掉所有相同的颜色,带上所有与上一个颜色相同的颜色,因为它现在可供选择。在这一步,您最终会得到B G
    4. 等等……

    但这是错误的。请注意,在第 3 步中,黑色和绿色出现的概率相似(B G - 它是黑色或绿色),而一开始黑色的概率更大。

    为避免这种情况,请使用颜色箱。 Bins 具有宽度(概率)和保留在其中的颜色数量。宽度永远不会改变,并在启动时设置。

    所以正确的步骤是

    1. 创建 3 个 bean 并将它们放在一行中:
      • 黑色:0.5,数量:2;
      • 红色:0.25,数量:1;
      • 绿色:0.25,数量:1。
    2. 0.0 &lt;-&gt; 1.0 范围内生成一个随机数。例如,它是 0.4,表示黑色(例如,0.9 表示绿色)。之后,如果您在这一步不能选择黑色,您的选择是:
      • 红色:0.25,数量:1;
      • 绿色:0.25,数量:1。
    3. 既然您已经采用了宽度为 0.5 的黑色 bin,请从 0.0 &lt;-&gt; (1.0 - 0.5) =0.0 &lt;-&gt; 0.5 范围内生成一个随机数。让它为 0.4,即红色。
    4. 去掉红色 (-0.25),但带回黑色 (+0.5)。在这一步你有:

      • 黑色:0.5,数量:1;
      • 绿色:0.25,数量:1。

      下一个随机值的范围是0.0 &lt;-&gt; (0.5 - 0.25 + 0.5) =0.0 &lt;-&gt; 0.75。请注意,与之前的方法相比,颜色保留了它的起始概率(黑色的概率更大)。

    算法的时间复杂度是O(N),因为你做的工作量是O(1)(选择一个随机的bin,排除它,包括前一个)一样多的倍你有O(N)的颜色。

    我应该注意的最后一件事 - 由于它是一种概率方法,因此在算法结束时可能会留下最大 bin 的一些颜色。在这种情况下,只需遍历最终的颜色列表并将它们放置在合适的位置(两者都不同的颜色之间)。

    也可能没有这样的颜色排列,因此没有两个相同的颜色相邻(例如:黑色 - 2,红色 - 1)。对于这种情况,我会在下面的代码中抛出异常。

    算法结果的例子出现在开头的图片中。

    代码

    Java (Groovy)。

    注意,为了便于阅读,从列表中删除元素是标准的 (bins.remove(bin)),即 Groovy 中的 O(N) 操作。因此,该算法总共不起作用O(N)。删除应该重写为将列表的最后一个元素更改为要删除的元素并减少列表的size 属性 - O(1)

    Bin {
        Color color;
        int quantity;
        double probability;
    }
    
    List<Color> finalColors = []
    List<Bin> bins // Should be initialized before start of the algorithm.
    double maxRandomValue = 1
    
    private void startAlgorithm() {
        def binToExclude = null
    
        while (bins.size() > 0) {
            def randomBin = getRandomBin(binToExclude)
            finalColors.add(randomBin.color)
    
            // If quantity = 0, the bin's already been excluded.
            binToExclude = randomBin.quantity != 0 ? randomBin : null
    
            // Break at this special case, it will be handled below.
            if (bins.size() == 1) {
                break
            }
        }
    
        def lastBin = bins.get(0)
        if (lastBin != null) {
            // At this point lastBin.quantity >= 1 is guaranteed.
            handleLastBin(lastBin)
        }
    }
    
    private Bin getRandomBin(Bin binToExclude) {
        excludeBin(binToExclude)
    
        def randomBin = getRandomBin()
    
        randomBin.quantity--
        if (randomBin.quantity == 0) {
            excludeBin(randomBin)
        }
    
        includeBin(binToExclude)
    
        return randomBin
    }
    
    private Bin getRandomBin() {
        double randomValue = randomValue()
    
        int binIndex = 0;
        double sum = bins.get(binIndex).probability
        while (sum < randomValue && binIndex < bins.size() - 1) {
            sum += bins.get(binIndex).probability;
            binIndex++;
        }
    
        return bins.get(binIndex)
    }
    
    private void excludeBin(Bin bin) {
        if (bin == null) return
    
        bins.remove(bin)
        maxRandomValue -= bin.probability
    }
    
    private void includeBin(Bin bin) {
        if (bin == null) return
    
        bins.add(bin)
        def addedBinProbability = bin.probability
    
        maxRandomValue += addedBinProbability
    }
    
    private double randomValue() {
        return Math.random() * maxRandomValue;
    }
    
    private void handleLastBin(Bin lastBin) {
        // The first and the last color're adjacent (since colors form a circle),
        // If they're the same (RED,...,RED), need to break it.
        if (finalColors.get(0) == finalColors.get(finalColors.size() - 1)) {
            // Can we break it? I.e. is the last bin's color different from them?
            if (lastBin.color != finalColors.get(0)) {
                finalColors.add(lastBin.color)
                lastBin.quantity--
            } else {
                throw new RuntimeException("No possible combination of non adjacent colors.")
            }
        }
    
        // Add the first color to the other side of the list
        // so that "circle case" is handled as a linear one.
        finalColors.add(finalColors.get(0))
    
        int q = 0
        int j = 1
        while (q < lastBin.quantity && j < finalColors.size()) {
            // Doesn't it coincide with the colors on the left and right?
            if (finalColors.get(j - 1) != lastBin.color && finalColors.get(j) != lastBin.color) {
                finalColors.add(j, lastBin.color)
                q++
                j += 2
            }  else {
                j++
            }
        }
        // Remove the fake color.
        finalColors.remove(finalColors.size() - 1)
    
        // If still has colors to insert.
        if (q < lastBin.quantity) {
            throw new RuntimeException("No possible combination of non adjacent colors.")
        }
    }
    

    【讨论】:

      【解决方案4】:

      GameplayKit 中的 GKShuffledDistribution 类有两个特性可以很容易地满足这个要求:

      1. 它从它初始化的范围中抽取“随机”数字,其方式是在重复其中任何一个之前必须使用该范围内的所有数字。

        这种行为会在随机序列中创建“块”(因为没有更好的词)。例如,如果您有 4 个可能的值,则前四个 nextInt() 调用将耗尽所有四个值。但是在第五次调用时,您在一个新的“块”上,您可以再次随机获取 4 个值中的任何一个,包括最后一个“块”的最终值。

      2. 所以,GKShuffledDistribution 还确保没有跨“块”边界的任何重复。

      您可以通过在 Playground 中尝试以下操作并显示nextInt() 行的值图来轻松看到这一点:

      import GameplayKit
      
      let colors = ["red", "green", "blue"
      // the effect is easier to see with more than three items, so uncomment for more:
      //    , "mauve", "puce", "burnt sienna", "mahogany",
      //    "periwinkle", "fuschia", "wisteria", "chartreuse"
      ]
      
      let randomizer = GKShuffledDistribution(lowestValue: 0, highestValue: colors.count - 1)
      for _ in 1...100 {
          randomizer.nextInt()
      }
      

      或者更多颜色:

      您会注意到,有些值会重复并在其间跳过(请注意第二张图中早期11, 10, 11 的序列),但绝不会是一个值连续重复。

      要使用它来打乱颜色数组,只需从打乱的索引开始:

      extension GKShuffledDistribution {
          func shuffledInts(count: Int) -> [Int] {
              // map on a range to get an array of `count` random draws from the shuffle
              return (0..<count).map { _ in self.nextInt() }
          }
      }
      
      let colors = [#colorLiteral(red: 1, green: 0, blue: 0, alpha: 1), #colorLiteral(red: 0, green: 1, blue: 0, alpha: 1), #colorLiteral(red: 0, green: 0, blue: 1, alpha: 1)]
      let random = GKShuffledDistribution(forDieWithSideCount: colors.count)
      let dieRolls = random.shuffledInts(count: 10)
      let shuffledColors: [SKColor] = dieRolls.map { num in
          // forDieWithSideCount gives us values between 1 and count
          // we want values betwen 0 and (count-1)
          return colors[num - 1]
      }
      

      (在这个例子中还展示了一些其他的东西:使用颜色文字而不是颜色名称,尽管你也可以这样做,并使用 dieWithSideCount 初始化器为GKShuffledDistribution。请注意,颜色文字看起来在 Xcode 中比在 SO 中的 web 上要好得多。)

      【讨论】:

      • 它似乎不适用于 bkwebhero 的情况 - 当你有一种颜色比其他颜色多时,例如这种颜色 - red - 1, green - 1, yellow - 1, black - 3
      猜你喜欢
      • 1970-01-01
      • 2021-08-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-01-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多