7

I have an enum that I would like to randomly select a value from, but not truly random. I would like some of the values to be less likely of being selected so far. Here is what I have so far...

private enum Type{
        TYPE_A, TYPE_B, TYPE_C, TYPE_D, TYPE_E;

        private static final List<Type> VALUES =
            Collections.unmodifiableList(Arrays.asList(values()));
          private static final int SIZE = VALUES.size();
          private static final Random RANDOM = new Random();

          public static Type randomType()  {
            return VALUES.get(RANDOM.nextInt(SIZE));
          }
    }

Is there an efficient way of assigning probabilities to each of these values?

Code found from here

Community
  • 1
  • 1
tgrosinger
  • 2,463
  • 2
  • 30
  • 38

7 Answers7

7

several ways to do it, one of them, similar to your approach

private enum Type{
    TYPE_A(10 /*10 - weight of this type*/),
    TYPE_B(1),
    TYPE_C(5),
    TYPE_D(20),
    TYPE_E(7);

    private int weight;
        
    private Type(int weight) {
        this.weight = weight;
    }
    
    private int getWeight() {
        return weight;
    }
    
    private static final List<Type> VALUES =
            Collections.unmodifiableList(Arrays.asList(values()));
    
    
    
    private int summWeigts() {
       int summ = 0;
       for(Type value: VALUES) 
          summ += value.getWeight();
       return summ;
    }
    
    private static final int SIZE = summWeigts();
    private static final Random RANDOM = new Random();
    
    public static Type randomType()  {
        int randomNum = RANDOM.nextInt(SIZE);
        int currentWeightSumm = 0;
        for(Type currentValue: VALUES) {
           if (randomNum > currentWeightSumm && 
               randomNum <= (currentWeightSumm + currentValue.getWeight()) {
             break;
           }
           currentWeightSumm += currentValue.getWeight();
        }
    
        return currentValue.get();
    }
}
Aserre
  • 4,916
  • 5
  • 33
  • 56
Alexey Sviridov
  • 3,360
  • 28
  • 33
  • I'm think you misunderstood. Idea is - you summ all weights. Now imagine ruler with max = calculated value. Now dash each weight on this rule, so first will be (according my example in answer) 0 to 10, second 10 to 11,thrird 11 to 16 and so on. Now point your finger to random place of our ruler and see in which segment you point. Edited answer. – Alexey Sviridov Mar 11 '11 at 07:05
  • That is actually what I ended up doing. Thanks! – tgrosinger Mar 11 '11 at 07:16
  • BTW you can improve perfomance if precalculate bounds of you types in constructor, but on five types it's not actual. – Alexey Sviridov Mar 11 '11 at 11:11
  • This algorithm is the standard one when there are only a few values to select from. When many options, you can accelerate the algorithm by using a tree based selection algorithm. To do this, split the [0,1) interval into some number of segments (say 100). Then, for each interval, you determine whether there is exactly one possible outcome for the sample. If so, mark that leaf in the tree with that value. For any segments that have multiple possible outcomes, you build another tree that spans just that segment. Limit recursion depending on the resolution you have on your probabilities. – Ted Dunning May 03 '11 at 22:54
0

Here is another alternative which allows the distribution to be specified at runtime.

Includes suggestion from Alexey Sviridov. Also method random() could incorporate suggestion from Ted Dunning when there are many options.

     private enum Option {

        OPTION_1, OPTION_2, OPTION_3, OPTION_4;
        static private final Integer OPTION_COUNT = EnumSet.allOf(Option.class).size();
        static private final EnumMap<Option, Integer> buckets = new EnumMap<Option, Integer>(Option.class);
        static private final Random random = new Random();
        static private Integer total = 0;

        static void setDistribution(Short[] distribution) {
           if (distribution.length < OPTION_COUNT) {
              throw new ArrayIndexOutOfBoundsException("distribution too short");
           }
           total = 0;
           Short dist;
           for (Option option : EnumSet.allOf(Option.class)) {
              dist = distribution[option.ordinal()];
              total += (dist < 0) ? 0 : dist;
              buckets.put(option, total);
           }
        }

        static Option random() {
           Integer rnd = random.nextInt(total);
           for (Option option : EnumSet.allOf(Option.class)) {
              if (buckets.get(option) > rnd) {
                 return option;
              }
           }
           throw new IndexOutOfBoundsException();
        }
     }
nhoj
  • 135
  • 2
  • 11
0

You can use EnumeratedDistribution from the Apache Commons Math library.

EnumeratedDistribution<Type> distribution = new EnumeratedDistribution<>(
        RandomGeneratorFactory.createRandomGenerator(new Random()),
        List.of(
                new Pair<>(Type.TYPE_A, 0.2), // get TYPE_A with probability 0.2
                new Pair<>(Type.TYPE_B, 0.5), // get TYPE_B with probability 0.5
                new Pair<>(Type.TYPE_C, 0.3)  // get TYPE_C with probability 0.3
        )
);

Type mySample = distribution.sample();
frido
  • 13,065
  • 5
  • 42
  • 56
0

Here's a generic approach to choosing an enum value at random. You can adjust the probabilities as suggested here.

Community
  • 1
  • 1
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
0

Assuming you have a finite number of values you could have a separate array (float[] weights;) of weights for each value. These values would be between 0 and 1. When you select a random value also generate another random number between and only select the value if the second generated number is below the weight for that value.

null0pointer
  • 1,533
  • 3
  • 15
  • 29
0

You can create an enum with associated data bby provding a custom constructor, and use the constructor to assign weightings for the probabilities and then

public enum WeightedEnum {
    ONE(1), TWO(2), THREE(3);
    private WeightedEnum(int weight) {
        this.weight = weight;
    }
    public int getWeight() {
        return this.weight;
    }
    private final int weight;

    public static WeightedEnum randomType()  {
        // select one based on random value and relative weight
    }
}
David O'Meara
  • 2,983
  • 25
  • 38
0
import java.util.*;
enum R {
    a(.1),b(.2),c(.3),d(.4);
    R(final double p) {
        this.p=p;
    }
    private static void init() {
        sums=new double[values().length+1];
        sums[0]=0;
        for(int i=0;i<values().length;i++)
            sums[i+1]=values()[i].p+sums[i];
        once=true;
    }
    static R random() {
        if (!once) init();
        final double x=Math.random();
        for(int i=0;i<values().length;i++)
            if (sums[i]<=x&&x<sums[i+1]) return values()[i];
        throw new RuntimeException("should not happen!");
    }
    static boolean check() {
        double sum=0;
        for(R r:R.values())
            sum+=r.p;
        return(Math.abs(sum-1)<epsilon);
    }
    final double p;
    static final double epsilon=.000001;
    static double[] sums;
    static boolean once=false;
}
public class Main{
    public static void main(String[] args) {
        if (!R.check()) throw new RuntimeException("values should sum to one!");
        final Map<R,Integer> bins=new EnumMap<R,Integer>(R.class);
        for(R r:R.values())
            bins.put(r,0);
        final int n=1000000;
        for(int i=0;i<n;i++) {
            final R r=R.random();
            bins.put(r,bins.get(r)+1);
        }
        for(R r:R.values())
            System.out.println(r+" "+r.p+" "+bins.get(r)/(double)n);
    }
}
Ray Tayek
  • 9,841
  • 8
  • 50
  • 90
  • I don't understand the stuff being done in the main method. Seems to be a lot more convoluted than the method proposed by @AlexeySviridov which I simplified a little bit more even. – tgrosinger Mar 11 '11 at 23:00
  • just some code to exercise random puts and see how the distribution looks. – Ray Tayek Oct 06 '11 at 00:21