Existe-t-il un moyen efficace de générer N nombres entiers aléatoires dans une plage ayant une somme ou une moyenne donnée?


14

Existe-t-il un moyen efficace de générer une combinaison aléatoire de N entiers telle que ...

  • chaque entier est dans l'intervalle [ min, max],
  • les entiers ont une somme de sum,
  • les entiers peuvent apparaître dans n'importe quel ordre (par exemple, un ordre aléatoire), et
  • la combinaison est choisie uniformément au hasard parmi toutes les combinaisons qui répondent aux autres exigences?

Existe-t-il un algorithme similaire pour les combinaisons aléatoires dans lequel les entiers doivent apparaître dans l'ordre trié par leurs valeurs (plutôt que dans n'importe quel ordre)?

(Choisir une combinaison appropriée avec une moyenne de meanest un cas spécial, si sum = N * mean. Ce problème équivaut à générer une partition aléatoire uniforme de sumen N parties qui sont chacune dans l'intervalle [ min, max] et apparaissent dans n'importe quel ordre ou dans l'ordre trié par leur selon le cas.)

Je suis conscient que ce problème peut être résolu de la manière suivante pour les combinaisons qui apparaissent dans un ordre aléatoire (EDIT [27 avr.]: Algorithme modifié.):

  1. Si N * max < sumou N * min > sum, il n'y a pas de solution.

  2. Si N * max == sum, il n'y a qu'une seule solution, dans laquelle tous les Nnombres sont égaux à max. Si N * min == sum, il n'y a qu'une seule solution, dans laquelle tous les Nnombres sont égaux à min.

  3. Utilisez l'algorithme donné dans Smith et Tromble («Sampling from the Unit Simplex», 2004) pour générer N nombres entiers non négatifs aléatoires avec la somme sum - N * min.

  4. Ajoutez minà chaque numéro généré de cette façon.

  5. Si un nombre est supérieur à max, passez à l'étape 3.

Cependant, cet algorithme est lent s'il maxest bien inférieur à sum. Par exemple, selon mes tests (avec une implémentation du cas particulier ci-dessus impliquant mean), l'algorithme rejette, en moyenne—

  • environ 1,6 échantillon si N = 7, min = 3, max = 10, sum = 42, mais
  • environ 30,6 échantillons si N = 20, min = 3, max = 10, sum = 120.

Existe-t-il un moyen de modifier cet algorithme pour qu'il soit efficace pour un grand N tout en répondant aux exigences ci-dessus?

ÉDITER:

Comme alternative suggérée dans les commentaires, un moyen efficace de produire une combinaison aléatoire valide (qui satisfait toutes les exigences sauf la dernière) est:

  1. Calculer X, le nombre de combinaisons valides de GIVEN possible sum, minet max.
  2. Choisissez Yun entier aléatoire uniforme dans [0, X).
  3. Convertissez ("non classé") Yen une combinaison valide.

Cependant, existe-t-il une formule pour calculer le nombre de combinaisons (ou permutations) valides, et existe-t-il un moyen de convertir un entier en une combinaison valide? [EDIT (28 avril): Idem pour les permutations plutôt que pour les combinaisons].

EDIT (27 avril):

Après avoir lu la génération aléatoire non uniforme de Devroye (1986), je peux confirmer qu'il s'agit d'un problème de génération d'une partition aléatoire. De plus, l'exercice 2 (en particulier la partie E) à la page 661 est pertinent pour cette question.

EDIT (28 avril):

Il s'est avéré que l'algorithme que j'ai donné est uniforme où les entiers impliqués sont donnés dans un ordre aléatoire , par opposition à un ordre trié par leurs valeurs . Étant donné que les deux problèmes sont d'intérêt général, j'ai modifié cette question pour rechercher une réponse canonique à ces deux problèmes.

Le code Ruby suivant peut être utilisé pour vérifier les solutions potentielles d'uniformité (où se algorithm(...)trouve l'algorithme candidat):

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

EDIT (29 avril): Re-ajouté le code Ruby de l'implémentation actuelle.

L'exemple de code suivant est donné en Ruby, mais ma question est indépendante du langage de programmation:

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
}

Pourriez-vous clarifier votre troisième exigence? Avez-vous besoin d'une uniformité entre toutes les combinaisons possibles (y compris celles avec la mauvaise moyenne), ou parmi toutes les combinaisons valides (c'est-à-dire celles avec la bonne moyenne)?
user58697

Toutes les combinaisons valides, c'est-à-dire toutes les combinaisons qui répondent aux autres exigences.
Peter O.

Si nous avions un moyen de compter et de ne pas classer les partitions d'une somme limitée à N entiers en [min, max], le choix d'une de ces partitions au hasard et sans classement représenterait-il une distribution uniforme, et serait-ce plus efficace que votre méthode actuelle? Quelle peut être la somme et N?
גלעד ברקן

Je ne sais pas ce que vous entendez par "partitions non classées d'une somme", et je ne suis pas au courant d'une preuve que cela entraîne une distribution uniforme au sens de cette question. Pour cette question, les deux sumet Nsont effectivement illimités (dans des limites raisonnables). Je cherche une réponse canonique parce que le problème sous-jacent apparaît dans de nombreuses questions posées sur Stack Overflow, y compris celle-ci et celle-ci . @ גלעדברקן
Peter O.

Si nous donnons à chaque combinaison possible un "rang" (ou indice) dans une disposition ordonnée de toutes, "sans classement", cela signifierait générer la combinaison, compte tenu de son rang (et N, min et max, bien sûr). Pourquoi un tel choix d'une des combinaisons possibles ne serait-il pas conforme à une distribution uniforme?
גלעד ברקן

Réponses:


5

Voici ma solution en Java. Il est entièrement fonctionnel et contient deux générateurs: PermutationPartitionGeneratorpour les partitions non triées et CombinationPartitionGeneratorpour les partitions triées. Votre générateur a également été implémenté dans la classe SmithTromblePartitionGeneratorpour comparaison. La classe SequentialEnumeratorénumère toutes les partitions possibles (non triées ou triées, selon le paramètre) dans un ordre séquentiel. J'ai ajouté des tests approfondis (y compris vos cas de test) pour tous ces générateurs. La mise en œuvre s'explique en grande partie d'elle-même. Si vous avez des questions, je vais y répondre dans quelques jours.

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();
        }
    }
}

Vous pouvez essayer ceci sur Ideone .


Merci pour votre réponse; Ça marche bien. J'ai décrit le générateur de permutation dans une autre réponse ici; répondu à une autre question avec votre aide; et inclura bientôt votre algorithme dans l'exemple de code Python pour mon article sur les méthodes de génération aléatoire.
Peter O.

Juste pour être clair. Cet algorithme repose-t-il sur la génération de toutes les partitions / compositions possibles pour échantillonner?
Joseph Wood

@JosephWood Non, cela dépend de tous les compter . Ceci n'est fait qu'une seule fois lors de l'initialisation du générateur et est plutôt efficace car il utilise l'approche de programmation dynamique.
John McClane

Comment la programmation dynamique peut-elle résoudre le problème connexe de choisir une partition aléatoire uniforme de `` somme '' en N entiers choisis au hasard avec remplacement dans une liste ( exemple ) ou sans remplacement ( exemple ), ou comment résoudre ce problème autrement?
Peter O. Il y a

@PeterO. Vous devez compter toutes les partitions possibles via la même méthode que dans mon algorithme, mais cette fois, vous devez soustraire uniquement les nombres autorisés de la somme. C'est trop long pour commenter, vous pouvez poser une question séparée. Je soupçonne que l'on peut résoudre quatre problèmes différents via la même approche. Supposons que vous ayez une liste d'entiers distincts parmi lesquels choisir (ce n'est qu'une plage continue dans cette question). Ensuite, vous pouvez générer des tableaux aléatoires d'une longueur donnée constitués de nombres de cette liste avec la somme donnée si les tableaux doivent être triés / non triés et permettre / interdire une répétition.
John McClane Il y a

3

Voici l'algorithme de PermutationPartitionGenerator de John McClane, dans une autre réponse sur cette page. Il comporte deux phases, à savoir une phase de configuration et une phase d'échantillonnage, et génère ndes nombres aléatoires en [ min, max] avec la somme sum, où les nombres sont répertoriés dans un ordre aléatoire.

Phase d'installation: Tout d'abord, une table de solutions est créée à l'aide des formules suivantes ( t(y, x)yest dans [0, n] et xest dans [0, sum - n * min]):

  • t (0, j) = 1 si j == 0; 0 sinon
  • t (i, j) = t (i-1, j) + t (i-1, j-1) + ... + t (i-1, j- (max-min))

Ici, t (y, x) stocke la probabilité relative que la somme des ynombres (dans la plage appropriée) soit égale x. Cette probabilité est relative à tous les t (y, x) de même y.

Phase d'échantillonnage: Ici, nous générons un échantillon de nnombres. Réglez ssur sum - n * min, puis pour chaque position i, en commençant par n - 1et en revenant à 0:

  • Défini vsur un entier aléatoire dans [0, t (i + 1, s)).
  • Réglez rsur min.
  • Soustrayez t (i, s) de v.
  • Alors qu'il vreste 0 ou plus, soustrayez t (i, s-1) de v, ajoutez 1 à ret soustrayez 1 de s.
  • Le nombre à la position idans l'échantillon est défini sur r.

ÉDITER:

Il semble qu'avec des changements triviaux à l'algorithme ci-dessus, il est possible que chaque nombre aléatoire utilise une plage distincte plutôt que d'utiliser la même plage pour chacun d'eux:

Chaque nombre aléatoire aux positions i∈ [0, n) a une valeur minimale min (i) et une valeur maximale max (i).

Soit adjsum= sum- Σmin (i).

Phase d'installation: Tout d'abord, une table de solutions est créée à l'aide des formules suivantes ( t(y, x)yest dans [0, n] et xest dans [0, adjsum]):

  • t (0, j) = 1 si j == 0; 0 sinon
  • t (i, j) = t (i-1, j) + t (i-1, j-1) + ... + t (i-1, j- (max (i-1) -min (i -1)) )

La phase d'échantillonnage est alors exactement la même que précédemment, sauf que nous avons réglé ssur adjsum(plutôt que sum - n * min) et réglé rsur min (i) (plutôt que min).


ÉDITER:

Pour CombinationPartitionGenerator de John McClane, les phases de configuration et d'échantillonnage sont les suivantes.

Phase d'installation: Tout d'abord, une table de solutions est créée à l'aide des formules suivantes ( t(z, y, x)zest dans [0, n], yest dans [0, max - min] et xest dans [0, sum - n * min]):

  • t (0, j, k) = 1 si k == 0; 0 sinon
  • t (i, 0, k) = t (i - 1, 0, k)
  • t (i, j, k) = t (i, j-1, k) + t (i - 1, j, k - j)

Phase d'échantillonnage: Ici, nous générons un échantillon de nnombres. Réglez ssur sum - n * minet mrangesur max - min, puis pour chaque position i, en commençant par n - 1et en revenant à 0:

  • Défini vsur un entier aléatoire dans [0, t (i + 1, mrange, s)).
  • Réglez mrangesur min ( mrange, s)
  • Soustraire mrangede s.
  • Réglez rsur min + mrange.
  • Soustraire t ( i, mrange, s) à partir de v.
  • Alors que vreste 0 ou plus, ajouter 1 à s, soustraire 1 ret 1 à partir mrange, puis soustrayez t ( i, mrange, s) à partir v.
  • Le nombre à la position idans l'échantillon est défini sur r.

2

Je n'ai pas testé cela, donc ce n'est pas vraiment une réponse, juste quelque chose à essayer qui est trop long pour entrer dans un commentaire. Commencez avec un tableau qui répond aux deux premiers critères et jouez avec lui afin qu'il réponde toujours aux deux premiers, mais est beaucoup plus aléatoire.

Si la moyenne est un entier, votre tableau initial peut être [4, 4, 4, ... 4] ou peut-être [3, 4, 5, 3, 4, 5, ... 5, 8, 0] ou quelque chose de simple comme ça. Pour une moyenne de 4,5, essayez [4, 5, 4, 5, ... 4, 5].

Ensuite, choisissez une paire de nombres num1et num2, dans le tableau. Probablement, le premier nombre doit être pris dans l'ordre, comme avec le shuffle de Fisher-Yates, le deuxième nombre doit être choisi au hasard. Prendre le premier numéro dans l'ordre garantit que chaque numéro est choisi au moins une fois.

Maintenant, calculez max-num1et num2-min. Ce sont les distances entre les deux nombres maxet les minfrontières. Réglez limitsur la plus petite des deux distances. C'est le changement maximum autorisé qui ne mettra pas l'un ou l'autre des nombres en dehors des limites autorisées. Si limitest nul, sautez cette paire.

Choisissez un entier aléatoire dans la plage [1, limit]: appelez-le change. J'omets 0 de la plage sélectionnable car cela n'a aucun effet. Les tests peuvent montrer que vous obtenez un meilleur caractère aléatoire en l'incluant; Je ne suis pas sûr.

Maintenant, réglez num1 <- num1 + changeet num2 <- num2 - change. Cela n'affectera pas la valeur moyenne et tous les éléments du tableau sont toujours dans les limites requises.

Vous devrez parcourir l'ensemble du tableau au moins une fois. Le test devrait montrer si vous devez le parcourir plusieurs fois pour obtenir quelque chose de suffisamment aléatoire.

ETA: inclure le pseudocode

// 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

Je l'ai testé et malheureusement, votre algorithme ne forme pas une distribution uniforme de toutes les solutions, quel que soit le nombre d'itérations que je fais.
Peter O.

Tant pis. Cela valait quand même la peine d'essayer. :(
rossum

2

Comme le souligne l'OP, la capacité de se défaire efficacement est très puissante. Si nous sommes en mesure de le faire, la génération d'une distribution uniforme des partitions peut se faire en trois étapes (en reformulant ce que l'OP a exposé dans la question):

  1. Calculez le nombre total, M , de partitions de longueur N du nombre sumtel que les pièces soient dans la plage [ min, max].
  2. Générez une distribution uniforme d'entiers à partir de [1, M].
  3. Annulez le classement de chaque entier de l'étape 2 dans sa partition respective.

Ci-dessous, nous nous concentrons uniquement sur la génération de la n ième partition car il existe une quantité abondante d'informations sur la génération d'une distribution uniforme d'entiers dans une plage donnée. Voici un C++algorithme de classement simple qui devrait être facile à traduire dans d'autres langues (NB je n'ai pas encore compris comment défaire le cas de composition (c'est-à-dire que l'ordre est important)).

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;
}

La pCountfonction cheval de bataille est donnée par:

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;
}

Cette fonction est basée sur l'excellente réponse à Existe-t-il un algorithme efficace pour le partitionnement entier avec un nombre restreint de parties? par l'utilisateur @ m69_snarky_and_unwelcoming. Celui donné ci-dessus est une légère modification de l'algorithme simple (celui sans mémorisation). Cela peut facilement être modifié pour incorporer la mémorisation pour une plus grande efficacité. Nous allons laisser cela de côté pour l'instant et nous concentrer sur la partie non classée.

Explication de unRank

Nous notons d'abord qu'il existe un mappage un à un des partitions de longueur N du nombre de sumsorte que les parties sont dans la plage [ min, max] aux partitions restreintes de longueur N du nombre sum - N * (min - 1)avec des parties en [ 1, max - (min - 1)].

À titre d'exemple, considérons les partitions 50de longueur 4telles que le min = 10et le max = 15. Cela aura la même structure que les partitions restreintes 50 - 4 * (10 - 1) = 14de longueur 4avec la partie maximale égale à 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

Dans cet esprit, afin de pouvoir compter facilement, nous pourrions ajouter une étape 1a pour traduire le problème dans le cas "unité" si vous le souhaitez.

Maintenant, nous avons simplement un problème de comptage. Comme le montre brillamment @ m69, le comptage des partitions peut être facilement réalisé en divisant le problème en problèmes plus petits. La fonction fournie par @ m69 nous permet d'obtenir 90% du chemin, il nous suffit de comprendre ce qu'il faut faire avec la restriction supplémentaire qu'il y a un plafond. C'est là que nous obtenons:

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

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

Nous devons également garder à l'esprit que myMaxcela diminuera à mesure que nous avancerons. Cela est logique si l' on considère la 6 e partition ci - dessus:

2   2   4   6

Afin de compter le nombre de partitions à partir de maintenant, nous devons continuer d'appliquer la traduction au cas "unité". Cela ressemble à ceci:

1   1   3   5

Alors que l'étape précédente, nous avions un max de 6, maintenant nous ne considérons qu'un max de 5.

Dans cette optique, le classement de la partition n'est pas différent du classement d'une permutation ou combinaison standard. Il faut pouvoir compter le nombre de partitions dans une section donnée. Par exemple, pour compter le nombre de partitions commençant par 10ci-dessus, tout ce que nous faisons est de supprimer le 10dans la première colonne:

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

Traduire dans le cas de l'unité:

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

et appelez pCount:

pCount(13, 3, 6) = 5

Étant donné un entier aléatoire à non classé, nous continuons de calculer le nombre de partitions dans des sections de plus en plus petites (comme nous l'avons fait ci-dessus) jusqu'à ce que nous ayons rempli notre vecteur d'index.

Exemples

Compte tenu min = 3, max = 10, n = 7et sum = 42, voici une ideone démo qui génère 20 partitions aléatoires. La sortie est ci-dessous:

42: 3 3 6 7 7 8 8 
123: 4 4 6 6 6 7 9 
2: 3 3 3 4 9 10 10 
125: 4 4 6 6 7 7 8 
104: 4 4 4 6 6 8 10 
74: 3 4 6 7 7 7 8 
47: 3 4 4 5 6 10 10 
146: 5 5 5 5 6 7 9 
70: 3 4 6 6 6 7 10 
134: 4 5 5 6 6 7 9 
136: 4 5 5 6 7 7 8 
81: 3 5 5 5 8 8 8 
122: 4 4 6 6 6 6 10 
112: 4 4 5 5 6 8 10 
147: 5 5 5 5 6 8 8 
142: 4 6 6 6 6 7 7 
37: 3 3 6 6 6 9 9 
67: 3 4 5 6 8 8 8 
45: 3 4 4 4 8 9 10 
44: 3 4 4 4 7 10 10

L'index lexicographique est à gauche et la partition non classée à droite.


1
Il s'avère que c'est une très bonne alternative, et devient effectivement efficace avec la mémorisation.
Peter O. Il y a

1
Grande observation sur la cartographie un à un.
גלעד ברקן Il y a

0

Si vous générez uniformément 0≤a≤1 des valeurs aléatoires dans la plage [l, x-1] et 1-a des valeurs aléatoires dans la plage [x, h] uniformément, la moyenne attendue serait:

m = ((l+x-1)/2)*a + ((x+h)/2)*(1-a)

Donc, si vous voulez un m spécifique, vous pouvez jouer avec a et x.

Par exemple, si vous définissez x = m: a = (hm) / (h-l + 1).

Pour garantir une probabilité plus proche de l'uniformité pour différentes combinaisons, choisissez a ou x au hasard dans l'ensemble des solutions valides pour l'équation ci-dessus. (x doit être compris entre [l, h] et doit être (proche) d'un entier; N * a doit également être (proche) d'un entier.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.