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
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 finalColors = []
List 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.")
}
}