Is there a way to shuffle an array so that no two consecutive values are the same?

前端 未结 4 873
醉梦人生
醉梦人生 2020-12-29 10:51

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

4条回答
  •  無奈伤痛
    2020-12-29 11:22

    O(N) time and space solution

    I've started with images as it's always more interesting :)

    Introduction

    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.

    Example

    For example, you have 4 colors in total:

    • black: 2;
    • red: 1;
    • green: 1.

    The first sequence of steps that comes to mind is the following:

    1. Place them in one line B B R G;
    2. Choose a random one, for example: B, take away all the same colors so the next is guaranteed to be different. Now you have R G;
    3. Choose a next random one, for example 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.
    4. Etc...

    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:

    1. Create 3 beans and place them in one line:
      • black: 0.5, quantity: 2;
      • red: 0.25, quantity: 1;
      • green: 0.25, quantity: 1.
    2. Generate a random number from the range 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:
      • red: 0.25, quantity: 1;
      • green: 0.25, quantity: 1.
    3. Since you've taken the black bin of width 0.5, generate a random number from the range 0.0 <-> (1.0 - 0.5) =0.0 <-> 0.5. Let it be 0.4, i.e. red.
    4. Take red away (-0.25), but bring black color back (+0.5). At this step you have:

      • black: 0.5, quantity: 1;
      • green: 0.25, quantity: 1.

      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.

    Code

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

提交回复
热议问题