Gibt es eine effiziente Möglichkeit, N zufällige Ganzzahlen in einem Bereich mit einer bestimmten Summe oder einem bestimmten Durchschnitt zu generieren?

12

Gibt es eine effiziente Möglichkeit, eine zufällige Kombination von N ganzen Zahlen zu generieren, so dass:

  • jede ganze Zahl liegt im Intervall [ min, max],
  • Die ganzen Zahlen haben eine Summe von sum,
  • Die Ganzzahlen können in beliebiger Reihenfolge (z. B. in zufälliger Reihenfolge) und angezeigt werden
  • Die Kombination wird gleichmäßig zufällig aus allen Kombinationen ausgewählt, die die anderen Anforderungen erfüllen.

Gibt es einen ähnlichen Algorithmus für zufällige Kombinationen, bei dem die Ganzzahlen in sortierter Reihenfolge nach ihren Werten (und nicht in beliebiger Reihenfolge) angezeigt werden müssen?

(Die Auswahl einer geeigneten Kombination mit einem Mittelwert von meanist ein Sonderfall, wenn sum = N * mean. Dieses Problem entspricht der Erzeugung einer einheitlichen zufälligen Aufteilung sumin N Teile, die sich jeweils im Intervall [ min, max] befinden und in beliebiger Reihenfolge oder in sortierter Reihenfolge nach ihrer Anzahl angezeigt werden Werte, je nach Fall.)

Mir ist bekannt, dass dieses Problem für Kombinationen, die in zufälliger Reihenfolge erscheinen, folgendermaßen gelöst werden kann (EDIT [27. April]: Algorithmus geändert.):

  1. Wenn N * max < sumoder N * min > sum, gibt es keine Lösung.

  2. Wenn ja N * max == sum, gibt es nur eine Lösung, bei der alle NZahlen gleich sind max. Wenn ja N * min == sum, gibt es nur eine Lösung, bei der alle NZahlen gleich sind min.

  3. Verwenden Sie den in Smith und Tromble angegebenen Algorithmus ("Sampling from the Unit Simplex", 2004), um N zufällige nicht negative Ganzzahlen mit der Summe zu generieren sum - N * min.

  4. Fügen Sie minzu jeder auf diese Weise generierten Zahl hinzu.

  5. Wenn eine Zahl größer als ist max, fahren Sie mit Schritt 3 fort.

Dieser Algorithmus ist jedoch langsam, wenn er maxviel kleiner als ist sum. Zum Beispiel meanlehnt der Algorithmus nach meinen Tests (mit einer Implementierung des oben genannten Sonderfalls ) im Durchschnitt ab -

  • etwa 1,6 Proben wenn N = 7, min = 3, max = 10, sum = 42, aber
  • ca. 30,6 Proben wenn N = 20, min = 3, max = 10, sum = 120.

Gibt es eine Möglichkeit, diesen Algorithmus so zu ändern, dass er für große N effizient ist und gleichzeitig die oben genannten Anforderungen erfüllt?

BEARBEITEN:

Als Alternative, die in den Kommentaren vorgeschlagen wird, ist eine effiziente Methode zur Erstellung einer gültigen Zufallskombination (die alle bis auf die letzte Anforderung erfüllt):

  1. Berechnen Sie Xdie Anzahl der gültigen Kombinationen möglich gegebenen sum, minund max.
  2. Wählen Sie Yeine einheitliche zufällige Ganzzahl in [0, X).
  3. Konvertieren ("unrank") Yin eine gültige Kombination.

Gibt es jedoch eine Formel zur Berechnung der Anzahl gültiger Kombinationen (oder Permutationen) und gibt es eine Möglichkeit, eine Ganzzahl in eine gültige Kombination umzuwandeln? [EDIT (28. April): Gleiches gilt für Permutationen und nicht für Kombinationen].

EDIT (27. April):

Nachdem ich Devroyes Non-Uniform Random Variate Generation (1986) gelesen habe , kann ich bestätigen, dass dies ein Problem beim Generieren einer zufälligen Partition ist. Auch Übung 2 (insbesondere Teil E) auf Seite 661 ist für diese Frage relevant.

EDIT (28. April):

Wie sich herausstellte, ist der von mir angegebene Algorithmus einheitlich, wobei die beteiligten Ganzzahlen in zufälliger Reihenfolge angegeben werden , im Gegensatz zur sortierten Reihenfolge nach ihren Werten . Da beide Probleme von allgemeinem Interesse sind, habe ich diese Frage geändert, um eine kanonische Antwort auf beide Probleme zu finden.

Der folgende Ruby-Code kann verwendet werden, um mögliche Lösungen für die Einheitlichkeit zu überprüfen (wo algorithm(...)ist der Kandidatenalgorithmus):

combos={}
permus={}
mn=0
mx=6
sum=12
for x in mn..mx
  for y in mn..mx
    for z in mn..mx
      if x+y+z==sum
        permus[[x,y,z]]=0
      end
      if x+y+z==sum and x<=y and y<=z
        combos[[x,y,z]]=0
      end
    end
  end
end

3000.times {|x|
 f=algorithm(3,sum,mn,mx)
 combos[f.sort]+=1
 permus[f]+=1
}
p combos
p permus

BEARBEITEN (29. April): Ruby-Code der aktuellen Implementierung wurde erneut hinzugefügt.

Das folgende Codebeispiel ist in Ruby angegeben, aber meine Frage ist unabhängig von der Programmiersprache:

def posintwithsum(n, total)
    raise if n <= 0 or total <=0
    ls = [0]
    ret = []
    while ls.length < n
      c = 1+rand(total-1)
      found = false
      for j in 1...ls.length
        if ls[j] == c
          found = true
          break
        end
      end
      if found == false;ls.push(c);end
    end
    ls.sort!
    ls.push(total)
    for i in 1...ls.length
       ret.push(ls[i] - ls[i - 1])
    end
    return ret
end

def integersWithSum(n, total)
 raise if n <= 0 or total <=0
 ret = posintwithsum(n, total + n)
 for i in 0...ret.length
    ret[i] = ret[i] - 1
 end
 return ret
end

# Generate 100 valid samples
mn=3
mx=10
sum=42
n=7
100.times {
 while true
    pp=integersWithSum(n,sum-n*mn).map{|x| x+mn }
    if !pp.find{|x| x>mx }
      p pp; break # Output the sample and break
    end
 end
}
Peter O.
quelle
Könnten Sie Ihre dritte Anforderung klarstellen? Benötigen Sie eine Einheitlichkeit zwischen allen möglichen Kombinationen (einschließlich der mit dem falschen Mittelwert) oder zwischen allen gültigen Kombinationen (dh denen mit dem richtigen Mittelwert)?
user58697
Alle gültigen Kombinationen, dh alle Kombinationen, die die anderen Anforderungen erfüllen.
Peter O.
Wenn wir die Möglichkeit hätten, Partitionen einer auf N Ganzzahlen in [min, max] beschränkten Summe zu zählen und zu entfernen, würde die zufällige und unrangierte Auswahl einer dieser Partitionen eine gleichmäßige Verteilung darstellen, und wäre dies effizienter als Ihre derzeitige Methode? Wie groß kann die Summe und N sein?
ברקן ברקן
Ich weiß nicht, was Sie unter "nicht eingestuften Partitionen einer Summe" verstehen, und mir ist kein Beweis bekannt, dass dies zu einer gleichmäßigen Verteilung im Sinne dieser Frage führt. Für diese Frage sind beide sumund Npraktisch unbegrenzt (im Rahmen der Vernunft). Ich suche eine kanonische Antwort, weil das zugrunde liegende Problem in vielen Fragen zum Stapelüberlauf auftaucht, einschließlich dieser und dieser . @ גלעדברקן
Peter O.
Wenn wir jeder möglichen Kombination einen "Rang" (oder Index) in einer geordneten Anordnung von allen geben, würde "unranking" bedeuten, die Kombination angesichts ihres Ranges (und natürlich N, min und max) zu erzeugen. Warum würde eine solche Auswahl einer der möglichen Kombinationen nicht einer gleichmäßigen Verteilung entsprechen?
ברקן ברקן

Antworten:

4

Hier ist meine Lösung in Java. Es ist voll funktionsfähig und enthält zwei Generatoren: PermutationPartitionGeneratorfür unsortierte Partitionen und CombinationPartitionGeneratorfür sortierte Partitionen. Ihr Generator ist SmithTromblePartitionGeneratorzum Vergleich auch in der Klasse implementiert . Die Klasse SequentialEnumeratorlistet alle möglichen Partitionen (je nach Parameter unsortiert oder sortiert) in sequentieller Reihenfolge auf. Ich habe für alle diese Generatoren gründliche Tests (einschließlich Ihrer Testfälle) hinzugefügt. Die Implementierung ist größtenteils selbsterklärend. Wenn Sie Fragen haben, werde ich diese in ein paar Tagen beantworten.

import java.util.Random;
import java.util.function.Supplier;

public abstract class PartitionGenerator implements Supplier<int[]>{
    public static final Random rand = new Random();
    protected final int numberCount;
    protected final int min;
    protected final int range;
    protected final int sum; // shifted sum
    protected final boolean sorted;

    protected PartitionGenerator(int numberCount, int min, int max, int sum, boolean sorted) {
        if (numberCount <= 0)
            throw new IllegalArgumentException("Number count should be positive");
        this.numberCount = numberCount;
        this.min = min;
        range = max - min;
        if (range < 0)
            throw new IllegalArgumentException("min > max");
        sum -= numberCount * min;
        if (sum < 0)
            throw new IllegalArgumentException("Sum is too small");
        if (numberCount * range < sum)
            throw new IllegalArgumentException("Sum is too large");
        this.sum = sum;
        this.sorted = sorted;
    }

    // Whether this generator returns sorted arrays (i.e. combinations)
    public final boolean isSorted() {
        return sorted;
    }

    public interface GeneratorFactory {
        PartitionGenerator create(int numberCount, int min, int max, int sum);
    }
}

import java.math.BigInteger;

// Permutations with repetition (i.e. unsorted vectors) with given sum
public class PermutationPartitionGenerator extends PartitionGenerator {
    private final double[][] distributionTable;

    public PermutationPartitionGenerator(int numberCount, int min, int max, int sum) {
        super(numberCount, min, max, sum, false);
        distributionTable = calculateSolutionCountTable();
    }

    private double[][] calculateSolutionCountTable() {
        double[][] table = new double[numberCount + 1][sum + 1];
        BigInteger[] a = new BigInteger[sum + 1];
        BigInteger[] b = new BigInteger[sum + 1];
        for (int i = 1; i <= sum; i++)
            a[i] = BigInteger.ZERO;
        a[0] = BigInteger.ONE;
        table[0][0] = 1.0;
        for (int n = 1; n <= numberCount; n++) {
            double[] t = table[n];
            for (int s = 0; s <= sum; s++) {
                BigInteger z = BigInteger.ZERO;
                for (int i = Math.max(0, s - range); i <= s; i++)
                    z = z.add(a[i]);
                b[s] = z;
                t[s] = z.doubleValue();
            }
            // swap a and b
            BigInteger[] c = b;
            b = a;
            a = c;
        }
        return table;
    }

    @Override
    public int[] get() {
        int[] p = new int[numberCount];
        int s = sum; // current sum
        for (int i = numberCount - 1; i >= 0; i--) {
            double t = rand.nextDouble() * distributionTable[i + 1][s];
            double[] tableRow = distributionTable[i];
            int oldSum = s;
            // lowerBound is introduced only for safety, it shouldn't be crossed 
            int lowerBound = s - range;
            if (lowerBound < 0)
                lowerBound = 0;
            s++;
            do
                t -= tableRow[--s];
            // s can be equal to lowerBound here with t > 0 only due to imprecise subtraction
            while (t > 0 && s > lowerBound);
            p[i] = min + (oldSum - s);
        }
        assert s == 0;
        return p;
    }

    public static final GeneratorFactory factory = (numberCount, min, max,sum) ->
        new PermutationPartitionGenerator(numberCount, min, max, sum);
}

import java.math.BigInteger;

// Combinations with repetition (i.e. sorted vectors) with given sum 
public class CombinationPartitionGenerator extends PartitionGenerator {
    private final double[][][] distributionTable;

    public CombinationPartitionGenerator(int numberCount, int min, int max, int sum) {
        super(numberCount, min, max, sum, true);
        distributionTable = calculateSolutionCountTable();
    }

    private double[][][] calculateSolutionCountTable() {
        double[][][] table = new double[numberCount + 1][range + 1][sum + 1];
        BigInteger[][] a = new BigInteger[range + 1][sum + 1];
        BigInteger[][] b = new BigInteger[range + 1][sum + 1];
        double[][] t = table[0];
        for (int m = 0; m <= range; m++) {
            a[m][0] = BigInteger.ONE;
            t[m][0] = 1.0;
            for (int s = 1; s <= sum; s++) {
                a[m][s] = BigInteger.ZERO;
                t[m][s] = 0.0;
            }
        }
        for (int n = 1; n <= numberCount; n++) {
            t = table[n];
            for (int m = 0; m <= range; m++)
                for (int s = 0; s <= sum; s++) {
                    BigInteger z;
                    if (m == 0)
                        z = a[0][s];
                    else {
                        z = b[m - 1][s];
                        if (m <= s)
                            z = z.add(a[m][s - m]);
                    }
                    b[m][s] = z;
                    t[m][s] = z.doubleValue();
                }
            // swap a and b
            BigInteger[][] c = b;
            b = a;
            a = c;
        }
        return table;
    }

    @Override
    public int[] get() {
        int[] p = new int[numberCount];
        int m = range; // current max
        int s = sum; // current sum
        for (int i = numberCount - 1; i >= 0; i--) {
            double t = rand.nextDouble() * distributionTable[i + 1][m][s];
            double[][] tableCut = distributionTable[i];
            if (s < m)
                m = s;
            s -= m;
            while (true) {
                t -= tableCut[m][s];
                // m can be 0 here with t > 0 only due to imprecise subtraction
                if (t <= 0 || m == 0)
                    break;
                m--;
                s++;
            }
            p[i] = min + m;
        }
        assert s == 0;
        return p;
    }

    public static final GeneratorFactory factory = (numberCount, min, max, sum) ->
        new CombinationPartitionGenerator(numberCount, min, max, sum);
}

import java.util.*;

public class SmithTromblePartitionGenerator extends PartitionGenerator {
    public SmithTromblePartitionGenerator(int numberCount, int min, int max, int sum) {
        super(numberCount, min, max, sum, false);
    }

    @Override
    public int[] get() {
        List<Integer> ls = new ArrayList<>(numberCount + 1);
        int[] ret = new int[numberCount];
        int increasedSum = sum + numberCount;
        while (true) {
            ls.add(0);
            while (ls.size() < numberCount) {
                int c = 1 + rand.nextInt(increasedSum - 1);
                if (!ls.contains(c))
                    ls.add(c);
            }
            Collections.sort(ls);
            ls.add(increasedSum);
            boolean good = true;
            for (int i = 0; i < numberCount; i++) {
                int x = ls.get(i + 1) - ls.get(i) - 1;
                if (x > range) {
                    good = false;
                    break;
                }
                ret[i] = x;
            }
            if (good) {
                for (int i = 0; i < numberCount; i++)
                    ret[i] += min;
                return ret;
            }
            ls.clear();
        }
    }

    public static final GeneratorFactory factory = (numberCount, min, max, sum) ->
        new SmithTromblePartitionGenerator(numberCount, min, max, sum);
}

import java.util.Arrays;

// Enumerates all partitions with given parameters
public class SequentialEnumerator extends PartitionGenerator {
    private final int max;
    private final int[] p;
    private boolean finished;

    public SequentialEnumerator(int numberCount, int min, int max, int sum, boolean sorted) {
        super(numberCount, min, max, sum, sorted);
        this.max = max;
        p = new int[numberCount];
        startOver();
    }

    private void startOver() {
        finished = false;
        int unshiftedSum = sum + numberCount * min;
        fillMinimal(0, Math.max(min, unshiftedSum - (numberCount - 1) * max), unshiftedSum);
    }

    private void fillMinimal(int beginIndex, int minValue, int fillSum) {
        int fillRange = max - minValue;
        if (fillRange == 0)
            Arrays.fill(p, beginIndex, numberCount, max);
        else {
            int fillCount = numberCount - beginIndex;
            fillSum -= fillCount * minValue;
            int maxCount = fillSum / fillRange;
            int maxStartIndex = numberCount - maxCount;
            Arrays.fill(p, maxStartIndex, numberCount, max);
            fillSum -= maxCount * fillRange;
            Arrays.fill(p, beginIndex, maxStartIndex, minValue);
            if (fillSum != 0)
                p[maxStartIndex - 1] = minValue + fillSum;
        }
    }

    @Override
    public int[] get() { // returns null when there is no more partition, then starts over
        if (finished) {
            startOver();
            return null;
        }
        int[] pCopy = p.clone();
        if (numberCount > 1) {
            int i = numberCount;
            int s = p[--i];
            while (i > 0) {
                int x = p[--i];
                if (x == max) {
                    s += x;
                    continue;
                }
                x++;
                s--;
                int minRest = sorted ? x : min;
                if (s < minRest * (numberCount - i - 1)) {
                    s += x;
                    continue;
                }
                p[i++]++;
                fillMinimal(i, minRest, s);
                return pCopy;
            }
        }
        finished = true;
        return pCopy;
    }

    public static final GeneratorFactory permutationFactory = (numberCount, min, max, sum) ->
        new SequentialEnumerator(numberCount, min, max, sum, false);
    public static final GeneratorFactory combinationFactory = (numberCount, min, max, sum) ->
        new SequentialEnumerator(numberCount, min, max, sum, true);
}

import java.util.*;
import java.util.function.BiConsumer;
import PartitionGenerator.GeneratorFactory;

public class Test {
    private final int numberCount;
    private final int min;
    private final int max;
    private final int sum;
    private final int repeatCount;
    private final BiConsumer<PartitionGenerator, Test> procedure;

    public Test(int numberCount, int min, int max, int sum, int repeatCount,
            BiConsumer<PartitionGenerator, Test> procedure) {
        this.numberCount = numberCount;
        this.min = min;
        this.max = max;
        this.sum = sum;
        this.repeatCount = repeatCount;
        this.procedure = procedure;
    }

    @Override
    public String toString() {
        return String.format("=== %d numbers from [%d, %d] with sum %d, %d iterations ===",
                numberCount, min, max, sum, repeatCount);
    }

    private static class GeneratedVector {
        final int[] v;

        GeneratedVector(int[] vect) {
            v = vect;
        }

        @Override
        public int hashCode() {
            return Arrays.hashCode(v);
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            return Arrays.equals(v, ((GeneratedVector)obj).v);
        }

        @Override
        public String toString() {
            return Arrays.toString(v);
        }
    }

    private static final Comparator<Map.Entry<GeneratedVector, Integer>> lexicographical = (e1, e2) -> {
        int[] v1 = e1.getKey().v;
        int[] v2 = e2.getKey().v;
        int len = v1.length;
        int d = len - v2.length;
        if (d != 0)
            return d;
        for (int i = 0; i < len; i++) {
            d = v1[i] - v2[i];
            if (d != 0)
                return d;
        }
        return 0;
    };

    private static final Comparator<Map.Entry<GeneratedVector, Integer>> byCount =
            Comparator.<Map.Entry<GeneratedVector, Integer>>comparingInt(Map.Entry::getValue)
            .thenComparing(lexicographical);

    public static int SHOW_MISSING_LIMIT = 10;

    private static void checkMissingPartitions(Map<GeneratedVector, Integer> map, PartitionGenerator reference) {
        int missingCount = 0;
        while (true) {
            int[] v = reference.get();
            if (v == null)
                break;
            GeneratedVector gv = new GeneratedVector(v);
            if (!map.containsKey(gv)) {
                if (missingCount == 0)
                    System.out.println(" Missing:");
                if (++missingCount > SHOW_MISSING_LIMIT) {
                    System.out.println("  . . .");
                    break;
                }
                System.out.println(gv);
            }
        }
    }

    public static final BiConsumer<PartitionGenerator, Test> distributionTest(boolean sortByCount) {
        return (PartitionGenerator gen, Test test) -> {
            System.out.print("\n" + getName(gen) + "\n\n");
            Map<GeneratedVector, Integer> combos = new HashMap<>();
            // There's no point of checking permus for sorted generators
            // because they are the same as combos for them
            Map<GeneratedVector, Integer> permus = gen.isSorted() ? null : new HashMap<>();
            for (int i = 0; i < test.repeatCount; i++) {
                int[] v = gen.get();
                if (v == null && gen instanceof SequentialEnumerator)
                    break;
                if (permus != null) {
                    permus.merge(new GeneratedVector(v), 1, Integer::sum);
                    v = v.clone();
                    Arrays.sort(v);
                }
                combos.merge(new GeneratedVector(v), 1, Integer::sum);
            }
            Set<Map.Entry<GeneratedVector, Integer>> sortedEntries = new TreeSet<>(
                    sortByCount ? byCount : lexicographical);
            System.out.println("Combos" + (gen.isSorted() ? ":" : " (don't have to be uniform):"));
            sortedEntries.addAll(combos.entrySet());
            for (Map.Entry<GeneratedVector, Integer> e : sortedEntries)
                System.out.println(e);
            checkMissingPartitions(combos, test.getGenerator(SequentialEnumerator.combinationFactory));
            if (permus != null) {
                System.out.println("\nPermus:");
                sortedEntries.clear();
                sortedEntries.addAll(permus.entrySet());
                for (Map.Entry<GeneratedVector, Integer> e : sortedEntries)
                    System.out.println(e);
                checkMissingPartitions(permus, test.getGenerator(SequentialEnumerator.permutationFactory));
            }
        };
    }

    public static final BiConsumer<PartitionGenerator, Test> correctnessTest =
        (PartitionGenerator gen, Test test) -> {
        String genName = getName(gen);
        for (int i = 0; i < test.repeatCount; i++) {
            int[] v = gen.get();
            if (v == null && gen instanceof SequentialEnumerator)
                v = gen.get();
            if (v.length != test.numberCount)
                throw new RuntimeException(genName + ": array of wrong length");
            int s = 0;
            if (gen.isSorted()) {
                if (v[0] < test.min || v[v.length - 1] > test.max)
                    throw new RuntimeException(genName + ": generated number is out of range");
                int prev = test.min;
                for (int x : v) {
                    if (x < prev)
                        throw new RuntimeException(genName + ": unsorted array");
                    s += x;
                    prev = x;
                }
            } else
                for (int x : v) {
                    if (x < test.min || x > test.max)
                        throw new RuntimeException(genName + ": generated number is out of range");
                    s += x;
                }
            if (s != test.sum)
                throw new RuntimeException(genName + ": wrong sum");
        }
        System.out.format("%30s :   correctness test passed%n", genName);
    };

    public static final BiConsumer<PartitionGenerator, Test> performanceTest =
        (PartitionGenerator gen, Test test) -> {
        long time = System.nanoTime();
        for (int i = 0; i < test.repeatCount; i++)
            gen.get();
        time = System.nanoTime() - time;
        System.out.format("%30s : %8.3f s %10.0f ns/test%n", getName(gen), time * 1e-9, time * 1.0 / test.repeatCount);
    };

    public PartitionGenerator getGenerator(GeneratorFactory factory) {
        return factory.create(numberCount, min, max, sum);
    }

    public static String getName(PartitionGenerator gen) {
        String name = gen.getClass().getSimpleName();
        if (gen instanceof SequentialEnumerator)
            return (gen.isSorted() ? "Sorted " : "Unsorted ") + name;
        else
            return name;
    }

    public static GeneratorFactory[] factories = { SmithTromblePartitionGenerator.factory,
            PermutationPartitionGenerator.factory, CombinationPartitionGenerator.factory,
            SequentialEnumerator.permutationFactory, SequentialEnumerator.combinationFactory };

    public static void main(String[] args) {
        Test[] tests = {
                            new Test(3, 0, 3, 5, 3_000, distributionTest(false)),
                            new Test(3, 0, 6, 12, 3_000, distributionTest(true)),
                            new Test(50, -10, 20, 70, 2_000, correctnessTest),
                            new Test(7, 3, 10, 42, 1_000_000, performanceTest),
                            new Test(20, 3, 10, 120, 100_000, performanceTest)
                       };
        for (Test t : tests) {
            System.out.println(t);
            for (GeneratorFactory factory : factories) {
                PartitionGenerator candidate = t.getGenerator(factory);
                t.procedure.accept(candidate, t);
            }
            System.out.println();
        }
    }
}

Sie können dies auf Ideone versuchen .

John McClane
quelle
Danke für deine Antwort; Es funktioniert gut. Ich habe den Permutationsgenerator hier in einer anderen Antwort beschrieben; beantwortete eine andere Frage mit Ihrer Hilfe; und wird Ihren Algorithmus bald in Python-Beispielcode für meinen Artikel über Methoden zur zufälligen Generierung aufnehmen.
Peter O.
Nur um das klar zu stellen. Beruht dieser Algorithmus darauf, alle möglichen Partitionen / Kompositionen zu generieren, um eine Stichprobe zu erstellen?
Joseph Wood vor
@ JosephWood Nein, es hängt davon ab , alle zu zählen. Dies erfolgt nur einmal bei der Generatorinitialisierung und ist ziemlich effektiv, da der dynamische Programmieransatz verwendet wird.
John McClane vor
Wie kann die dynamische Programmierung das damit verbundene Problem der Auswahl einer einheitlichen zufälligen Partition von 'Summe' in N zufällig ausgewählte Ganzzahlen mit Ersetzung aus einer Liste ( Beispiel ) oder ohne Ersetzung ( Beispiel ) lösen, oder wie kann dieses Problem anderweitig gelöst werden?
Peter O. vor
@ PeterO. Sie müssen alle möglichen Partitionen mit derselben Methode wie in meinem Algorithmus zählen, diesmal müssen Sie jedoch nur zulässige Zahlen von der Summe abziehen . Dies ist zu lang, um einen Kommentar abzugeben. Sie können eine separate Frage stellen. Ich vermute, dass man mit demselben Ansatz vier verschiedene Probleme lösen kann. Angenommen, Sie haben eine Liste unterschiedlicher Ganzzahlen zur Auswahl (dies ist nur ein kontinuierlicher Bereich in dieser Frage). Dann können Sie zufällige Arrays einer bestimmten Länge, bestehend aus Zahlen, aus dieser Liste mit der angegebenen Summe generieren, wenn die Arrays sortiert / unsortiert werden sollen und eine Wiederholung zulassen / nicht zulassen sollen.
John McClane
1

Hier ist der Algorithmus von John McClanes PermutationPartitionGenerator in einer anderen Antwort auf dieser Seite. Es hat zwei Phasen, nämlich eine Einrichtungsphase und eine Abtastphase, und generiert nZufallszahlen in [ min, max] mit der Summe sum, wobei die Zahlen in zufälliger Reihenfolge aufgelistet sind.

Einrichtungsphase: Zunächst wird eine Lösungstabelle mit den folgenden Formeln erstellt ( t(y, x)wobei yin [0, n] und xin [0, sum - n * min] steht):

  • t (0, j) = 1, wenn j == 0 ist; 0 sonst
  • t (i, j) = t (i-1, j) + t (i-1, j-1) + ... + t (i-1, j- (max-min))

Hier speichert t (y, x) die relative Wahrscheinlichkeit, dass die Summe der yZahlen (im entsprechenden Bereich) gleich ist x. Diese Wahrscheinlichkeit ist relativ zu allen t (y, x) mit derselben y.

Abtastphase: Hier erzeugen wir eine Stichprobe von nZahlen. Stellen Sie sauf sum - n * min, dann für jede Position i, beginnend mit n - 1und arbeitet nach hinten zum 0:

  • In v[0, t (i + 1, s)) auf eine zufällige Ganzzahl setzen.
  • Set rzu min.
  • Subtrahiere t (i, s) von v.
  • Während v0 oder größer bleibt, subtrahieren Sie t (i, s-1) von v, addieren Sie 1 zu rund subtrahieren Sie 1 von s.
  • Die Nummer an der Position iin der Probe ist auf eingestellt r.

BEARBEITEN:

Es scheint, dass es bei geringfügigen Änderungen des obigen Algorithmus möglich ist, dass jede Zufallszahl einen separaten Bereich verwendet, anstatt für alle denselben Bereich zu verwenden:

Jede Zufallszahl an den Positionen i∈ [0, n) hat einen Minimalwert min (i) und einen Maximalwert max (i).

Sei adjsum= sum- Σmin (i).

Einrichtungsphase: Zunächst wird eine Lösungstabelle mit den folgenden Formeln erstellt ( t(y, x)wobei yin [0, n] und xin [0, adjsum] steht):

  • t (0, j) = 1, wenn j == 0 ist; 0 sonst
  • t (i, j) = t (i-1, j) + t (i-1, j-1) + ... + t (i-1, j- (max (i-1) -min (i -1)) )

Die Abtastphase ist dann genau die gleiche wie zuvor, außer dass wir sauf adjsum(anstatt sum - n * min) und rauf min (i) (anstatt min) setzen.


BEARBEITEN:

Für John McClanes CombinationPartitionGenerator sind die Setup- und Sampling-Phasen wie folgt.

Einrichtungsphase: Zuerst wird eine Lösungstabelle mit den folgenden Formeln erstellt ( t(z, y, x)wobei zin [0, n], yin [0, max - min] und xin [0, sum - n * min]):

  • t (0, j, k) = 1, wenn k == 0 ist; 0 sonst
  • t (i, 0, k) = t (i - 1, 0, k)
  • t (i, j, k) = t (i, j - 1, k) + t (i - 1, j, k - j)

Abtastphase: Hier erzeugen wir eine Stichprobe von nZahlen. Stellen Sie sauf sum - n * minund mrangeauf max - min, dann für jede Position i, beginnend mit n - 1und rückwärts bis 0:

  • In v[0, t (i + 1, mrange, s)) auf eine zufällige ganze Zahl setzen.
  • Auf mrangemin ( mrange, s) setzen
  • Subtrahieren mrangevon s.
  • Set rzu min + mrange.
  • Subtract t ( i, mrange, s) aus v.
  • Während vReste 0 oder größer, 1 hinzufügen ssubtrahieren 1 aus rund von 1 mrange, dann Subtrahieren - t ( i, mrange, s) aus v.
  • Die Nummer an der Position iin der Probe ist auf eingestellt r.
Peter O.
quelle
0

Ich habe dies nicht getestet, daher ist es keine wirkliche Antwort, sondern nur etwas zum Ausprobieren, das zu lang ist, um in einen Kommentar zu passen. Beginnen Sie mit einem Array, das die ersten beiden Kriterien erfüllt, und spielen Sie damit, damit es immer noch die ersten beiden Kriterien erfüllt, aber viel zufälliger ist.

Wenn der Mittelwert eine Ganzzahl ist, kann Ihr anfängliches Array [4, 4, 4, ... 4] oder vielleicht [3, 4, 5, 3, 4, 5, ... 5, 8, 0] oder sein so etwas einfaches. Versuchen Sie für einen Mittelwert von 4,5 [4, 5, 4, 5, ... 4, 5].

Wählen Sie als Nächstes ein Zahlenpaar num1und num2im Array aus. Wahrscheinlich sollte die erste Nummer in der richtigen Reihenfolge genommen werden, da beim Fisher-Yates-Shuffle die zweite Nummer zufällig ausgewählt werden sollte. Wenn Sie die erste Nummer der Reihe nach nehmen, wird sichergestellt, dass jede Nummer mindestens einmal ausgewählt wird.

Berechnen Sie nun max-num1und num2-min. Das sind die Abstände zwischen den beiden Zahlen maxund den minGrenzen. Stellen Sie limitden kleineren der beiden Abstände ein. Dies ist die maximal zulässige Änderung, bei der die eine oder andere Zahl nicht außerhalb der zulässigen Grenzen liegt. Wenn limitNull ist, überspringen Sie dieses Paar.

Wählen Sie eine zufällige Ganzzahl im Bereich [1, limit]: Rufen Sie sie auf change. Ich lasse 0 aus dem auswählbaren Bereich weg, da dies keine Auswirkung hat. Tests können zeigen, dass Sie eine bessere Zufälligkeit erhalten, wenn Sie sie einbeziehen. Ich bin mir nicht sicher.

Jetzt setzen num1 <- num1 + changeund num2 <- num2 - change. Dies hat keinen Einfluss auf den Mittelwert und alle Elemente des Arrays befinden sich noch innerhalb der erforderlichen Grenzen.

Sie müssen das gesamte Array mindestens einmal durchlaufen. Tests sollten zeigen, ob Sie es mehr als einmal durchlaufen müssen, um etwas ausreichend Zufälliges zu erhalten.

ETA: Pseudocode einschließen

// Set up the array.
resultAry <- new array size N
for (i <- 0 to N-1)
  // More complex initial setup schemes are possible here.
  resultAry[i] <- mean
rof

// Munge the array entries.
for (ix1 <- 0 to N-1)  // ix1 steps through the array in order.

  // Pick second entry different from first.
  repeat
    ix2 <- random(0, N-1)
  until (ix2 != ix1)

  // Calculate size of allowed change.
  hiLimit <- max - resultAry[ix1]
  loLimit <- resultAry[ix2] - min
  limit <- minimum(hiLimit, loLimit)
  if (limit == 0)
    // No change possible so skip.
    continue loop with next ix1
  fi

  // Change the two entries keeping same mean.
  change <- random(1, limit)  // Or (0, limit) possibly.
  resultAry[ix1] <- resultAry[ix1] + change
  resultAry[ix2] <- resultAry[ix2] - change

rof

// Check array has been sufficiently munged.
if (resultAry not random enough)
  munge the array again
fi
Rossum
quelle
Ich habe es getestet und leider bildet Ihr Algorithmus nicht eine einheitliche Verteilung aller Lösungen, egal wie viele Iterationen ich mache.
Peter O.
Naja. Es hat sich trotzdem gelohnt, es zu versuchen. :(
Rossum
0

Wie das OP hervorhebt, ist die Fähigkeit, effizient zu ranken, sehr mächtig. Wenn wir dazu in der Lage sind, kann die Erzeugung einer gleichmäßigen Verteilung der Partitionen in drei Schritten erfolgen (wobei erneut dargelegt wird, was das OP in der Frage dargelegt hat):

  1. Berechnen Sie die Gesamtzahl M der Partitionen der Länge N der Anzahl sumso, dass die Teile im Bereich [ min, max] liegen.
  2. Generieren Sie eine gleichmäßige Verteilung von Ganzzahlen aus [1, M].
  3. Entfernen Sie jede Ganzzahl aus Schritt 2 in ihre jeweilige Partition.

Im Folgenden konzentrieren wir uns nur auf die Generierung der n- ten Partition, da es eine Vielzahl von Informationen zum Generieren einer gleichmäßigen Verteilung von Ganzzahlen in einem bestimmten Bereich gibt. Hier ist ein einfacher C++Algorithmus ohne Rangfolge, der leicht in andere Sprachen zu übersetzen sein sollte.

std::vector<int> unRank(int n, int m, int myMax, int nth) {

    std::vector<int> z(m, 0);
    int count = 0;
    int j = 0;

    for (int i = 0; i < z.size(); ++i) {
        int temp = pCount(n - 1, m - 1, myMax);

        for (int r = n - m, k = myMax - 1;
             (count + temp) < nth && r > 0 && k; r -= m, --k) {

            count += temp;
            n = r;
            myMax = k;
            ++j;
            temp = pCount(n - 1, m - 1, myMax);
        }

        --m;
        --n;
        z[i] = j;
    }

    return z;
}

Die Arbeitspferdfunktion pCountist gegeben durch:

int pCount(int n, int m, int myMax) {

    if (myMax * m < n) return 0;
    if (myMax * m == n) return 1;

    if (m < 2) return m;
    if (n < m) return 0;
    if (n <= m + 1) return 1;

    int niter = n / m;
    int count = 0;

    for (; niter--; n -= m, --myMax) {
        count += pCount(n - 1, m - 1, myMax);
    }

    return count;
}

Diese Funktion basiert auf der hervorragenden Antwort auf Gibt es einen effizienten Algorithmus für die Ganzzahlpartitionierung mit begrenzter Anzahl von Teilen? vom Benutzer @ m69_snarky_and_unwelcoming. Der oben angegebene ist eine geringfügige Modifikation des einfachen Algorithmus (der ohne Memoisierung). Dies kann leicht modifiziert werden, um Memoisierung für mehr Effizienz einzubeziehen. Wir werden dies vorerst weglassen und uns auf den unranking Teil konzentrieren.

Erklärung von unRank

Wir stellen zunächst fest, dass es eine Eins-zu-Eins-Zuordnung von den Partitionen der Länge N der Nummer gibt, sumso dass die Teile im Bereich [ min, max] liegen, zu den eingeschränkten Partitionen der Länge N der Nummer sum - m * (min - 1)mit Teilen in [ 1, max - (min - 1)].

Als kleines Beispiel betrachten wir die Partitionen von 50der Länge , 4so dass das min = 10und das max = 15. Dies hat die gleiche Struktur wie die eingeschränkten Partitionen 50 - 4 * (10 - 1) = 14der Länge, 4wobei der maximale Teil gleich ist 15 - (10 - 1) = 6.

10   10   15   15   --->>    1    1    6    6
10   11   14   15   --->>    1    2    5    6
10   12   13   15   --->>    1    3    4    6
10   12   14   14   --->>    1    3    5    5
10   13   13   14   --->>    1    4    4    5
11   11   13   15   --->>    2    2    4    6
11   11   14   14   --->>    2    2    5    5
11   12   12   15   --->>    2    3    3    6
11   12   13   14   --->>    2    3    4    5
11   13   13   13   --->>    2    4    4    4
12   12   12   14   --->>    3    3    3    5
12   12   13   13   --->>    3    3    4    4

In diesem Sinne könnten wir, um leicht zählen zu können, einen Schritt 1a hinzufügen, um das Problem in den Fall "Einheit" zu übersetzen, wenn Sie so wollen.

Jetzt haben wir einfach ein Zählproblem. Wie @ m69 brillant anzeigt, kann das Zählen von Partitionen leicht erreicht werden, indem das Problem in kleinere Probleme aufgeteilt wird. Die Funktion @ m69 bietet uns 90% des Weges. Wir müssen nur herausfinden, was mit der zusätzlichen Einschränkung zu tun ist, dass es eine Obergrenze gibt. Hier bekommen wir:

int pCount(int n, int m, int myMax) {

    if (myMax * m < n) return 0;
    if (myMax * m == n) return 1;

Wir müssen auch bedenken, dass dies im myMaxLaufe der Zeit abnehmen wird. Dies macht Sinn , wenn wir uns die aussehen 6 th Partition oben:

2   2   4   6

Um die Anzahl der Partitionen von hier an zu zählen, müssen wir die Übersetzung weiterhin auf den Fall "Einheit" anwenden. Das sieht so aus:

1   1   3   5

Wo wir wie vorher ein Maximum hatten 6, betrachten wir jetzt nur noch ein Maximum von 5.

In diesem Sinne unterscheidet sich das Aufheben der Rangfolge der Partition nicht vom Auflisten einer Standardpermutation oder -kombination. Wir müssen in der Lage sein, die Anzahl der Partitionen in einem bestimmten Abschnitt zu zählen. Um beispielsweise die Anzahl der Partitionen zu zählen, die 10oben beginnen, entfernen wir lediglich die 10in der ersten Spalte:

10   10   15   15
10   11   14   15
10   12   13   15
10   12   14   14
10   13   13   14

10   15   15
11   14   15
12   13   15
12   14   14
13   13   14

Übersetzen Sie in den Einheitsfall:

1   6   6
2   5   6
3   4   6
3   5   5
4   4   5

und rufen Sie an pCount:

pCount(13, 3, 6) = 5

Bei einer zufälligen Ganzzahl bis zum Unrank berechnen wir die Anzahl der Partitionen in immer kleineren Abschnitten (wie oben beschrieben) weiter, bis wir unseren Indexvektor gefüllt haben.

Beispiele

In Anbetracht min = 3, max = 10, n = 7, und sum = 42, hier ist eine ideone Demo , die 20 zufällige Partitionen erzeugt.

Joseph Wood
quelle