it-swarm.com.de

Codiermuster für zufällige prozentuale Verzweigung?

Nehmen wir also an, wir haben einen Codeblock, den wir 70% und ein weiterer 30% ausführen möchten.

if(Math.random() < 0.7)
    70percentmethod();
else
    30percentmethod();

Einfach genug. Was aber, wenn wir wollen, dass es leicht erweiterbar ist, um 30%/60%/10% usw. zu sagen? Hier müssten alle if-Anweisungen zu Änderungen hinzugefügt und geändert werden, die nicht gerade großartig sind, langsam und fehleranfällig sind.

Bisher habe ich festgestellt, dass große Schalter für diesen Anwendungsfall anständig nützlich sind, zum Beispiel:

switch(Rand(0, 10)){
    case 0:
    case 1:
    case 2:
    case 3:
    case 4:
    case 5:
    case 6:
    case 7:70percentmethod();break;
    case 8:
    case 9:
    case 10:30percentmethod();break;
}

Was kann sehr leicht geändert werden in:

switch(Rand(0, 10)){
    case 0:10percentmethod();break;
    case 1:
    case 2:
    case 3:
    case 4:
    case 5:
    case 6:
    case 7:60percentmethod();break;
    case 8:
    case 9:
    case 10:30percentmethod();break;
}

Diese haben jedoch auch ihre Nachteile, da sie umständlich sind und sich auf eine vorbestimmte Anzahl von Teilungen aufteilen.

Etwas Ideales würde auf einem "Frequenzzahl" -System basieren, denke ich, so:

(1,a),(1,b),(2,c) -> 25% a, 25% b, 50% c

wenn Sie dann noch einen hinzugefügt haben:

(1,a),(1,b),(2,c),(6,d) -> 10% a, 10% b, 20% c, 60% d

Also addieren Sie einfach die Zahlen, machen die Summe gleich 100% und teilen Sie diese dann auf.

Ich nehme an, es wäre nicht so schwierig, einen Handler dafür mit einer angepassten Hashmap oder etwas anderem zu erstellen, aber ich frage mich, ob es dafür einen etablierten Weg/ein Muster oder ein Lambda gibt, bevor ich mich mit allen Spaghetti beschäftige.

51
Moff Kalast

BEARBEITEN: Eine elegantere Lösung finden Sie unter Bearbeiten am Ende. Ich lasse das aber.

Sie können eine NavigableMap verwenden, um diese ihren Prozentsätzen zugeordneten Methoden zu speichern.

NavigableMap<Double, Runnable> runnables = new TreeMap<>();

runnables.put(0.3, this::30PercentMethod);
runnables.put(1.0, this::70PercentMethod);

public static void runRandomly(Map<Double, Runnable> runnables) {
    double percentage = Math.random();
    for (Map.Entry<Double, Runnable> entry : runnables){
        if (entry.getKey() < percentage) {
            entry.getValue().run();
            return; // make sure you only call one method
        }
    }
    throw new RuntimeException("map not filled properly for " + percentage);
}

// or, because I'm still practicing streams by using them for everything
public static void runRandomly(Map<Double, Runnable> runnables) {
    double percentage = Math.random();
    runnables.entrySet().stream()
        .filter(e -> e.getKey() < percentage)
        .findFirst().orElseThrow(() -> 
                new RuntimeException("map not filled properly for " + percentage))
        .run();
}

Die Variable NavigableMap ist sortiert (z. B. HashMap gibt keine Garantien für die Einträge aus), und zwar nach Schlüsseln, sodass die Einträge nach ihren Prozentsätzen sortiert werden. Dies ist relevant, denn wenn Sie zwei Elemente (3, r1), (7, r2) haben, führen diese zu folgenden Einträgen: r1 = 0.3 und r2 = 1.0 und müssen in dieser Reihenfolge ausgewertet werden (z Wenn sie in umgekehrter Reihenfolge ausgewertet werden, wäre immerr2).

Was die Aufteilung angeht, sollte es ungefähr so ​​aussehen: Mit einer Tuple-Klasse wie dieser

static class Pair<X, Y>
{
    public Pair(X f, Y s)
    {
        first = f;
        second = s;
    }

    public final X first;
    public final Y second;
}

Sie können eine Karte wie folgt erstellen

// the parameter contains the (1,m1), (1,m2), (3,m3) pairs
private static Map<Double,Runnable> splitToPercentageMap(Collection<Pair<Integer,Runnable>> runnables)
{

    // this adds all Runnables to lists of same int value,
    // overall those lists are sorted by that int (so least probable first)
    double total = 0;
    Map<Integer,List<Runnable>> byNumber = new TreeMap<>();
    for (Pair<Integer,Runnable> e : runnables)
    {
        total += e.first;
        List<Runnable> list = byNumber.getOrDefault(e.first, new ArrayList<>());
        list.add(e.second);
        byNumber.put(e.first, list);
    }

    Map<Double,Runnable> targetList = new TreeMap<>();
    double current = 0;
    for (Map.Entry<Integer,List<Runnable>> e : byNumber.entrySet())
    {
        for (Runnable r : e.getValue())
        {
            double percentage = (double) e.getKey() / total;
            current += percentage;
            targetList.put(current, r);
        }
    }

    return targetList;
}

Und das alles zu einer Klasse hinzugefügt

class RandomRunner {
    private List<Integer, Runnable> runnables = new ArrayList<>();
    public void add(int value, Runnable toRun) {
        runnables.add(new Pair<>(value, toRun));
    }
    public void remove(Runnable toRemove) {
        for (Iterator<Pair<Integer, Runnable>> r = runnables.iterator();
            r.hasNext(); ) {
            if (toRemove == r.next().second) {
               r.remove();
               break;
            }
        }
    }
    public void runRandomly() {
        // split list, use code from above
    }
}

BEARBEITEN:
Tatsächlich erhalten Sie das Obige, wenn Sie eine Idee in Ihrem Kopf stecken lassen und sie nicht richtig in Frage stellen. Das Beibehalten der RandomRunner-Klassenschnittstelle ist viel einfacher:

class RandomRunner {
    List<Runnable> runnables = new ArrayList<>();
    public void add(int value, Runnable toRun) {
        // add the methods as often as their weight indicates.
        // this should be fine for smaller numbers;
        // if you get lists with millions of entries, optimize
        for (int i = 0; i < value; i++) {
            runnables.add(toRun);
        }
    }
    public void remove(Runnable r) {
        Iterator<Runnable> myRunnables = runnables.iterator();
        while (myRunnables.hasNext()) {
            if (myRunnables.next() == r) {
                myRunnables.remove();
            }
    }
    public void runRandomly() {
        if (runnables.isEmpty()) return;
        // roll n-sided die
        int runIndex = ThreadLocalRandom.current().nextInt(0, runnables.size());
        runnables.get(runIndex).run();
    }
}
27
daniu

Alle diese Antworten scheinen ziemlich kompliziert zu sein, deshalb poste ich einfach die einfache Alternative:

double rnd = Math.random()
if((rnd -= 0.6) < 0)
    60percentmethod();
else if ((rnd -= 0.3) < 0)
    30percentmethod();
else
    10percentmethod();

Muss keine anderen Zeilen ändern und man kann leicht sehen, was passiert, ohne in die Zusatzklassen zu graben. Ein kleiner Nachteil ist, dass die Prozentsätze nicht 100% ergeben.

25
jpa

Ich bin mir nicht sicher, ob es einen gemeinsamen Namen gibt, aber ich glaube, ich habe das als Glücksrad in der Universität gelernt.

Es funktioniert im Wesentlichen so, wie Sie es beschrieben haben: Es empfängt eine Liste von Werten und "Häufigkeitsnummern", und eine wird entsprechend den gewichteten Wahrscheinlichkeiten ausgewählt.

list = (1,a),(1,b),(2,c),(6,d)

total = list.sum()
rnd = random(0, total)
sum = 0
for i from 0 to list.size():
    sum += list[i]
    if sum >= rnd:
        return list[i]
return list.last()

Die Liste kann ein Funktionsparameter sein, wenn Sie dies verallgemeinern möchten.

Dies funktioniert auch mit Fließkommazahlen und die Zahlen müssen nicht normalisiert werden. Wenn Sie normalisieren (um beispielsweise bis zu 1 zu summieren), können Sie den list.sum()-Teil überspringen.

BEARBEITEN:

Aufgrund der Nachfrage gibt es hier ein aktuelles Java-Implementierungs- und Anwendungsbeispiel:

import Java.util.ArrayList;
import Java.util.Random;

public class RandomWheel<T>
{
  private static final class RandomWheelSection<T>
  {
    public double weight;
    public T value;

    public RandomWheelSection(double weight, T value)
    {
      this.weight = weight;
      this.value = value;
    }
  }

  private ArrayList<RandomWheelSection<T>> sections = new ArrayList<>();
  private double totalWeight = 0;
  private Random random = new Random();

  public void addWheelSection(double weight, T value)
  {
    sections.add(new RandomWheelSection<T>(weight, value));
    totalWeight += weight;
  }

  public T draw()
  {
    double rnd = totalWeight * random.nextDouble();

    double sum = 0;
    for (int i = 0; i < sections.size(); i++)
    {
      sum += sections.get(i).weight;
      if (sum >= rnd)
        return sections.get(i).value;
    }
    return sections.get(sections.size() - 1).value;
  }

  public static void main(String[] args)
  {
    RandomWheel<String> wheel = new RandomWheel<String>();
    wheel.addWheelSection(1, "a");
    wheel.addWheelSection(1, "b");
    wheel.addWheelSection(2, "c");
    wheel.addWheelSection(6, "d");

    for (int i = 0; i < 100; i++)
        System.out.print(wheel.draw());
  }
}
15
SteakOverflow

Während die ausgewählte Antwort funktioniert, ist sie für Ihren Anwendungsfall leider asymptotisch langsam. Anstatt dies zu tun, können Sie auch etwas verwenden, das Alias ​​Sampling heißt. Alias-Sampling (oder Alias-Methode) ist eine Technik zur Auswahl von Elementen mit einer gewichteten Verteilung. Wenn sich das Gewicht der Auswahl dieser Elemente nicht ändert, können Sie die Auswahl in O (1) Zeit ! vornehmen. Wenn dies nicht der Fall ist, können Sie immer noch abgeschrieben O(1) Zeit, wenn das Verhältnis zwischen der Anzahl von Die Auswahl, die Sie treffen, und die Änderungen, die Sie an der Alias-Tabelle vornehmen (Ändern der Gewichte), sind hoch. Die aktuell gewählte Antwort schlägt einen Algorithmus O(N) vor, das nächstbeste ist O(log(N)) gegebene sortierte Wahrscheinlichkeiten und binäre Suche , aber nichts wird die von mir vorgeschlagene O(1) Zeit übertreffen.

Diese Seite bietet einen guten Überblick über die Alias-Methode, die hauptsächlich sprachunabhängig ist. Im Wesentlichen erstellen Sie eine Tabelle, in der jeder Eintrag das Ergebnis von zwei Wahrscheinlichkeiten darstellt. Für jeden Eintrag in der Tabelle gibt es einen einzelnen Schwellenwert. Unterhalb des Schwellenwerts erhalten Sie einen Wert, über dem Sie einen anderen Wert erhalten. Sie verteilen größere Wahrscheinlichkeiten auf mehrere Tabellenwerte, um für alle Wahrscheinlichkeiten zusammen ein Wahrscheinlichkeitsdiagramm mit einer Fläche von eins zu erstellen.

Angenommen, Sie haben die Wahrscheinlichkeiten A, B, C und D, die die Werte 0,1, 0,1, 0,1 bzw. 0,7 haben. Die Alias-Methode würde die Wahrscheinlichkeit von 0,7 auf alle anderen verteilen. Ein Index würde jeder Wahrscheinlichkeit entsprechen, wobei Sie 0,1 und 0,15 für ABC und 0,25 für den Index von D hätten. Damit normalisieren Sie jede Wahrscheinlichkeit, so dass Sie am Ende eine Chance von 0,4 haben, A und eine Chance von 0,6, D im Index von A (0,1/(0,1 + 0,15) bzw. 0,15/(0,1 + 0,15)) sowie in B und C zu erhalten Index und 100% ige Chance, D im Index von D zu erhalten (0,25/0,25 ist 1).

Bei einer unparteiischen Uniform PRNG (Math.Random ())) für die Indizierung erhalten Sie die gleiche Wahrscheinlichkeit, jeden Index zu wählen, aber Sie machen auch einen Münzwurf pro Index, der die gewichtete Wahrscheinlichkeit liefert haben eine Chance von 25%, auf dem A- oder D-Slot zu landen, aber innerhalb dessen haben Sie nur eine Chance von 40%, A auszuwählen, und 60% von D. .40 * .25 = 0.1, unserer ursprünglichen Wahrscheinlichkeit, und wenn Sie hinzufügen Wenn Sie alle Wahrscheinlichkeiten von D über die anderen Indizes verteilt haben, erhalten Sie erneut 0,70.

Um eine zufällige Auswahl zu treffen, müssen Sie nur einen zufälligen Index von 0 bis N generieren und dann eine Münzwurfaktion durchführen, unabhängig davon, wie viele Artikel Sie hinzufügen. Dies ist sehr schnell und kostet konstant. Das Erstellen einer Alias-Tabelle nimmt auch nicht so viele Codezeilen in Anspruch. Meine python version enthält 80 Zeilen, einschließlich Importanweisungen und Zeilenumbrüchen, und die in Pandas Artikel ist ähnlich groß (und es ist C++)

Für Ihre Java) Implementierung könnte man zwischen Wahrscheinlichkeiten und Arraylistenindizes auf Ihre auszuführenden Funktionen abbilden und ein Array von Funktionen erstellen, das ausgeführt wird, wenn Sie einen Index für jede Funktion erstellen. Alternativ können Sie Funktionsobjekte ( Funktoren ) verwenden, die eine Methode haben, mit der Sie Parameter zur Ausführung übergeben.

ArrayList<(YourFunctionObject)> function_list;
// add functions
AliasSampler aliassampler = new AliasSampler(listOfProbabilities);
// somewhere later with some type T and some parameter values. 
int index = aliassampler.sampleIndex();
T result = function_list[index].apply(parameters);

BEARBEITEN:

Ich habe eine Version in Java der AliasSampler-Methode unter Verwendung von Klassen erstellt. Diese verwendet die Beispielindexmethode und sollte in der Lage sein, wie oben verwendet zu werden.

import Java.util.ArrayList;
import Java.util.Collections;
import Java.util.Random;

public class AliasSampler {
    private ArrayList<Double> binaryProbabilityArray;
    private ArrayList<Integer> aliasIndexList;
    AliasSampler(ArrayList<Double> probabilities){
        // Java 8 needed here
        assert(DoubleStream.of(probabilities).sum() == 1.0);
        int n = probabilities.size();
        // probabilityArray is the list of probabilities, this is the incoming probabilities scaled
        // by the number of probabilities.  This allows us to figure out which probabilities need to be spread 
        // to others since they are too large, ie [0.1 0.1 0.1 0.7] = [0.4 0.4 0.4 2.80]
        ArrayList<Double> probabilityArray;
        for(Double probability : probabilities){
            probabilityArray.add(probability);
        }
        binaryProbabilityArray = new ArrayList<Double>(Collections.nCopies(n, 0.0));
        aliasIndexList = new ArrayList<Integer>(Collections.nCopies(n, 0));
        ArrayList<Integer> lessThanOneIndexList = new ArrayList<Integer>();
        ArrayList<Integer> greaterThanOneIndexList = new ArrayList<Integer>();
        for(int index = 0; index < probabilityArray.size(); index++){
            double probability = probabilityArray.get(index);
            if(probability < 1.0){
                lessThanOneIndexList.add(index);
            }
            else{
                greaterThanOneIndexList.add(index);
            }
        }

        // while we still have indices to check for in each list, we attempt to spread the probability of those larger
        // what this ends up doing in our first example is taking greater than one elements (2.80) and removing 0.6, 
        // and spreading it to different indices, so (((2.80 - 0.6) - 0.6) - 0.6) will equal 1.0, and the rest will
        // be 0.4 + 0.6 = 1.0 as well. 
        while(lessThanOneIndexList.size() != 0 && greaterThanOneIndexList.size() != 0){
            //https://stackoverflow.com/questions/16987727/removing-last-object-of-arraylist-in-Java
            // last element removal is equivalent to pop, Java does this in constant time
            int lessThanOneIndex = lessThanOneIndexList.remove(lessThanOneIndexList.size() - 1);
            int greaterThanOneIndex = greaterThanOneIndexList.remove(greaterThanOneIndexList.size() - 1);
            double probabilityLessThanOne = probabilityArray.get(lessThanOneIndex);
            binaryProbabilityArray.set(lessThanOneIndex, probabilityLessThanOne);
            aliasIndexList.set(lessThanOneIndex, greaterThanOneIndex);
            probabilityArray.set(greaterThanOneIndex, probabilityArray.get(greaterThanOneIndex) + probabilityLessThanOne - 1);
            if(probabilityArray.get(greaterThanOneIndex) < 1){
                lessThanOneIndexList.add(greaterThanOneIndex);
            }
            else{
                greaterThanOneIndexList.add(greaterThanOneIndex);
            }
        }
        //if there are any probabilities left in either index list, they can't be spread across the other 
        //indicies, so they are set with probability 1.0. They still have the probabilities they should at this step, it works out mathematically.
        while(greaterThanOneIndexList.size() != 0){
            int greaterThanOneIndex = greaterThanOneIndexList.remove(greaterThanOneIndexList.size() - 1);
            binaryProbabilityArray.set(greaterThanOneIndex, 1.0);
        }
        while(lessThanOneIndexList.size() != 0){
            int lessThanOneIndex = lessThanOneIndexList.remove(lessThanOneIndexList.size() - 1);
            binaryProbabilityArray.set(lessThanOneIndex, 1.0);
        }
    }
    public int sampleIndex(){
        int index = new Random().nextInt(binaryProbabilityArray.size());
        double r = Math.random();
        if( r < binaryProbabilityArray.get(index)){
            return index;
        }
        else{
            return aliasIndexList.get(index);
        }
    }

}
8
opa

Sie können die kumulative Wahrscheinlichkeit für jede Klasse berechnen. Wählen Sie eine Zufallszahl aus [0; 1) und sehen, wo diese Zahl fällt.

class WeightedRandomPicker {

    private static Random random = new Random();

    public static int choose(double[] probabilties) {
        double randomVal = random.nextDouble();
        double cumulativeProbability = 0;
        for (int i = 0; i < probabilties.length; ++i) {
            cumulativeProbability += probabilties[i];
            if (randomVal < cumulativeProbability) {
                return i;
            }
        }
        return probabilties.length - 1; // to account for numerical errors
    }

    public static void main (String[] args) {
        double[] probabilties = new double[]{0.1, 0.1, 0.2, 0.6}; // the final value is optional
        for (int i = 0; i < 20; ++i) {
            System.out.printf("%d\n", choose(probabilties));
        }
    }
}
6
NPE

Das folgende ist ein bisschen wie @daniu Antwort, verwendet aber die von TreeMap zur Verfügung gestellten Methoden:

private final NavigableMap<Double, Runnable> map = new TreeMap<>();
{
    map.put(0.3d, this::branch30Percent);
    map.put(1.0d, this::branch70Percent);
}
private final SecureRandom random = new SecureRandom();

private void branch30Percent() {}

private void branch70Percent() {}

public void runRandomly() {
    final Runnable value = map.tailMap(random.nextDouble(), true).firstEntry().getValue();
    value.run();
}

Auf diese Weise muss die gesamte Map nicht wiederholt werden, bis der passende Eintrag gefunden wurde. Es werden jedoch die Funktionen von TreeSet zum Suchen eines Eintrags mit einem Schlüssel verwendet, der speziell mit einem anderen Schlüssel verglichen wird. Dies macht jedoch nur einen Unterschied, wenn die Anzahl der Einträge in der Karte groß ist. Es speichert jedoch einige Zeilen Code.

1
SpaceTrucker

Ich würde so etwas tun:

class RandomMethod {
    private final Runnable method;
    private final int probability;

    RandomMethod(Runnable method, int probability){
        this.method = method;
        this.probability = probability;
    }

    public int getProbability() { return probability; }
    public void run()      { method.run(); }
}

class MethodChooser {
    private final List<RandomMethod> methods;
    private final int total;

    MethodChooser(final List<RandomMethod> methods) {
        this.methods = methods;
        this.total = methods.stream().collect(
            Collectors.summingInt(RandomMethod::getProbability)
        );
    }

    public void chooseMethod() {
        final Random random = new Random();
        final int choice = random.nextInt(total);

        int count = 0;
        for (final RandomMethod method : methods)
        {
            count += method.getProbability();
            if (choice < count) {
                method.run();
                return;
            }
        }
    }
}

Verwendungsbeispiel:

MethodChooser chooser = new MethodChooser(Arrays.asList(
    new RandomMethod(Blah::aaa, 1),
    new RandomMethod(Blah::bbb, 3),
    new RandomMethod(Blah::ccc, 1)
));

IntStream.range(0, 100).forEach(
    i -> chooser.chooseMethod()
);

Führe es hier aus .

0
Michael