I have an array of colors that will populate a pie chart to act as a game spinner. I don\'t want the same colors to appear next to each other, making one huge chunk in the c
Disclaimer: In order to generate a "random" solution I am going to use backtracking. This approach is NOT fast and is NOT cheap by a space point of view.
Infact both Time And Space Complexity are O(n!)... and this is HUGE!
However it gives you a valid solution as random as possible.
So you want a random combination of a list of values with the condition that the solution is valid if there are not be 2 consecutive equals elements.
In order to have a real random solution I suggest the following approach.
I generate every possible valid combination. For this I'm using a backtracking approach
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
}
Now given a list of colors
let colors = ["blue", "red", "green", "red", "blue", "blue", "blue", "green"]
I can generate every valid possible combination
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"]]
Finally I just need to pick one of these combinations
let comb = combs[Int(arc4random_uniform(UInt32(combs.count)))]
// ["red", "blue", "green", "blue", "green", "blue", "red", "blue"]
If you don't need a true random solution, but simply a permutation that doesn't have 2 consecutive equal elements we can change the previous function in order to return the first valid solution.
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
}
Now you can simply write
combination(["blue", "red", "green", "red", "blue", "blue", "blue", "green"])
to get a valid solution (if it does exists)
["blue", "red", "green", "blue", "red", "blue", "green", "blue"]
This approach can be much faster (when the solution does exist) however the worst case scenario is still O(n!) for both space and time complexity.
Despite appearances, this is non-trivial. As the commentator @antonio081014 points out, it's actually an algorithmic question, and (as @MartinR points out) is addressed here. Here's a very simple heuristic that (unlike the solution from @appzYourLife) is not an algorithm, but will work in most cases, and is much faster (O(n^2) rather than O(n!)). For randomness, simply shuffle the input array first:
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"]
I've started with images as it's always more interesting :)
First, I want to note you can't have a uniformly distributed sequence, since in your case quantities of colors are not the same.
To answer how to generate a random sequence let's go from the simplest case.
Having all colors unique, you generate a random value from 1 - N
, take the color out, generate from 1 - (N-1)
an so forth.
Now, having some colors more than others, you do the same thing as in the previous approach, but now probabilities of each color to appear differ - if you have more of black color its probability is higher.
Now, in your case you have the exact case, but with an additional requirement - the current random color doesn't equal to the previous one. So, just apply this requirement when generating each color - it'll be the best in terms of randomness.
For example, you have 4 colors in total:
The first sequence of steps that comes to mind is the following:
B B R G
;B
, take away all the same colors so the next is guaranteed to be different. Now you have R G
;R
, take away all the same colors, bring all the same colors of the previous color as it's now available for choice. At this step you end up with B G
.But it's wrong. Note, at the step 3 the colors black and green have similar probability to appear (B G
- it's either black or green), whereas at the beginning black had a bigger probability.
To avoid this, use bins of colors. Bins has width (probability) and quantity of colors remained in it. The width never changes and is set at the startup.
So proper steps are:
0.0 <-> 1.0
. For example it's 0.4, which means black (0.9, for example, would mean green). After that, provided you can't pick black color at this step, the choice you have is:
0.0 <-> (1.0 - 0.5) =
0.0 <-> 0.5
. Let it be 0.4, i.e. red.Take red away (-0.25
), but bring black color back (+0.5
). At this step you have:
And the range for the next random value is 0.0 <-> (0.5 - 0.25 + 0.5) =
0.0 <-> 0.75
. Note that colors preserved its starting probabilities (black has a bigger one), in comparison to the previous approach.
The algorithm is O(N)
in time complexity, because you do the same amount of work O(1)
(choose a random bin, exclude it, include the previous one) as many times as many colors you have O(N)
.
The last thing I should note - since it's a probabilistic approach, a few colors of the biggest bin may be left at the end of the algorithm. In this case just iterate over the final list of colors and place them in suitable places (between colors both different from it).
And it's also possible that there's no such arrangement of colors so that no two same colors are adjacent (for example: black - 2, red - 1). For such cases I throw an exception in the code below.
The example of a result of the algorithm is present in the pictures at the beginning.
Java (Groovy).
Note, for readability deletion of an element from a list is left standard (bins.remove(bin)
) which is O(N)
operation in Groovy. Therefore the algorithm doesn't work O(N)
in total. Deletion should be rewritten as changing the last element of the list with the element to be deleted and decrementing the size
property of the list - 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.")
}
}
The GKShuffledDistribution class in GameplayKit has two features that should make fulfilling this requirement pretty easy:
It draws "random" numbers from the range it's initialized with in such a way that it must use all the numbers in that range before repeating any one of them.
Alone, this behavior creates "chunks" (for lack of a better word) in the random sequence. For example, if you have 4 possible values, the first four nextInt()
calls would exhaust all four of them. But on the fifth call you're on a new "chunk", you'd be able to randomly get any of the 4 values again, including the final value from the last "chunk".
So, GKShuffledDistribution
also makes sure that there aren't any repeats across "chunk" boundaries.
You can see this pretty easy by trying the following in a playground, and showing a value graph for the nextInt()
line:
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()
}
Or with more colors:
You'll notice that some values get repeated with a skip in between (note the sequence of 11, 10, 11
early on in the second graph), but never is one value repeated consecutively.
To use this to shuffle an array of colors, just work from a shuffled index:
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]
}
(Also showing a couple of other things in this example: using color literals instead of color names, though you could just as well do either, and using the dieWithSideCount
initializer for GKShuffledDistribution
. Note that color literals look a lot nicer in Xcode than on the web in SO.)