Esiste un modo efficiente per generare N numeri interi casuali in un intervallo con una data somma o media?


14

Esiste un modo efficace per generare una combinazione casuale di N numeri interi tale che:

  • ogni numero intero è nell'intervallo [ min, max],
  • gli interi hanno una somma di sum,
  • gli interi possono apparire in qualsiasi ordine (es. ordine casuale) e
  • la combinazione viene scelta in modo uniforme a caso tra tutte le combinazioni che soddisfano gli altri requisiti?

Esiste un algoritmo simile per combinazioni casuali in cui gli interi devono apparire in ordine ordinato in base ai loro valori (piuttosto che in qualsiasi ordine)?

(Scegliere una combinazione appropriata con una media di meanè un caso speciale, se sum = N * mean. Questo problema equivale a generare una partizione casuale uniforme di sumin N parti che sono ciascuna nell'intervallo [ min, max] e appaiono in qualsiasi ordine o in ordine valori, a seconda dei casi.)

Sono consapevole che questo problema può essere risolto nel modo seguente per le combinazioni che compaiono in ordine casuale (EDIT [27 aprile]: algoritmo modificato.):

  1. Se N * max < sumo N * min > sum, non esiste soluzione.

  2. Se N * max == sumesiste una sola soluzione, in cui tutti i Nnumeri sono uguali max. Se N * min == sumesiste una sola soluzione, in cui tutti i Nnumeri sono uguali min.

  3. Utilizzare l'algoritmo fornito in Smith and Tromble ("Campionamento dall'unità simplex", 2004) per generare N numeri interi non negativi casuali con la somma sum - N * min.

  4. Aggiungi mina ciascun numero generato in questo modo.

  5. Se un numero è maggiore di max, andare al passaggio 3.

Tuttavia, questo algoritmo è lento se maxè molto inferiore a sum. Ad esempio, secondo i miei test (con un'implementazione del caso speciale sopra che coinvolge mean), l'algoritmo rifiuta, in media—

  • circa 1,6 campioni se N = 7, min = 3, max = 10, sum = 42, ma
  • circa 30,6 campioni se N = 20, min = 3, max = 10, sum = 120.

C'è un modo per modificare questo algoritmo in modo che sia efficiente per N di grandi dimensioni pur soddisfacendo i requisiti di cui sopra?

MODIFICARE:

Come alternativa suggerita nei commenti, un modo efficiente di produrre una combinazione casuale valida (che soddisfi tutti tranne l'ultimo requisito) è:

  1. Calcolare Xil numero di combinazioni valide possibile data sum, mine max.
  2. Scegli Yun numero intero casuale uniforme in [0, X).
  3. Converti ("non classificato") Yin una combinazione valida.

Tuttavia, esiste una formula per calcolare il numero di combinazioni (o permutazioni) valide e c'è un modo per convertire un numero intero in una combinazione valida? [EDIT (28 aprile): lo stesso vale per le permutazioni piuttosto che per le combinazioni].

MODIFICA (27 aprile):

Dopo aver letto la generazione casuale non uniforme di Devroye (1986), posso confermare che questo è un problema di generazione di una partizione casuale. Inoltre, l'esercizio 2 (in particolare la parte E) a pagina 661 è rilevante per questa domanda.

MODIFICA (28 aprile):

Come si è scoperto, l'algoritmo che ho dato è uniforme dove gli interi coinvolti sono dati in ordine casuale , al contrario di quelli ordinati per valore . Poiché entrambi i problemi sono di interesse generale, ho modificato questa domanda per cercare una risposta canonica per entrambi i problemi.

Il seguente codice Ruby può essere utilizzato per verificare potenziali soluzioni per l'uniformità (dov'è algorithm(...)l'algoritmo candidato):

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 aprile): Re-aggiunto il codice Ruby dell'attuale implementazione.

Il seguente esempio di codice è riportato in Ruby, ma la mia domanda è indipendente dal linguaggio di programmazione:

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
}

Potresti chiarire il tuo terzo requisito? Hai bisogno di un'uniformità tra tutte le possibili combinazioni (comprese quelle con la media sbagliata) o tra tutte le combinazioni valide (cioè quelle con la media corretta)?
user58697

Tutte le combinazioni valide, ovvero tutte le combinazioni che soddisfano gli altri requisiti.
Peter O.

Se avessimo il modo di contare e non classificare le partizioni di una somma limitata a N numeri interi in [min, max], scegliere una di quelle partizioni casuali e non classificate rappresenterebbe una distribuzione uniforme e sarebbe più efficiente del tuo metodo attuale? Quanto può essere grande la somma e N?
גלעד ברקן

Non so che cosa intendi per "non classificare le partizioni di una somma" e non sono a conoscenza di una prova del fatto che farlo comporti una distribuzione uniforme ai sensi di questa domanda. Per questa domanda, entrambi sume Nsono effettivamente illimitati (entro limiti ragionevoli). Sto cercando una risposta canonica perché il problema sottostante si presenta in molte domande poste su Stack Overflow, tra cui questa e questa . @ גלעדברקן
Peter O.

Se diamo ad ogni possibile combinazione un "rango" (o indice) in una disposizione ordinata di tutti loro, "non classificare" significherebbe generare la combinazione, dato il suo rango (e N, min e max, ovviamente). Perché una tale scelta di una delle possibili combinazioni non sarebbe conforme a una distribuzione uniforme?
גלעד ברקן

Risposte:


5

Ecco la mia soluzione in Java. È completamente funzionale e contiene due generatori: PermutationPartitionGeneratorper partizioni non CombinationPartitionGeneratorordinate e per partizioni ordinate. Anche il tuo generatore è stato implementato nella classe SmithTromblePartitionGeneratorper il confronto. La classe SequentialEnumeratorelenca tutte le possibili partizioni (non ordinate o ordinate, a seconda del parametro) in ordine sequenziale. Ho aggiunto test approfonditi (compresi i casi di test) per tutti questi generatori. L'implementazione è autoesplicabile per la maggior parte. In caso di domande, risponderò entro un paio di giorni.

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

Puoi provarlo su Ideone .


Grazie per la tua risposta; Funziona bene. Ho descritto il generatore di permutazione in un'altra risposta qui; ha risposto a un'altra domanda con il tuo aiuto; e includerà presto il tuo algoritmo nel codice di esempio Python per il mio articolo sui metodi di generazione casuale.
Peter O.

Giusto per essere chiari. Questo algoritmo si basa sulla generazione di tutte le possibili partizioni / composizioni per campionare?
Joseph Wood,

@JosephWood No, si basa su conteggio tutti loro. Ciò avviene una sola volta all'inizializzazione del generatore ed è piuttosto efficace perché utilizza l'approccio di programmazione dinamica.
John McClane

In che modo la programmazione dinamica può risolvere il problema correlato della scelta di una partizione casuale uniforme di 'somma' in N numeri interi scelti a caso con sostituzione da un elenco ( esempio ) o senza sostituzione ( esempio ), o come altrimenti risolvere il problema?
Peter O.

@PeterO. Devi contare tutte le possibili partizioni con lo stesso metodo del mio algoritmo, ma questa volta devi sottrarre solo i numeri consentiti dalla somma. È troppo lungo per commentare, puoi fare una domanda separata. Sospetto che si possano risolvere quattro diversi problemi con lo stesso approccio. Supponiamo di avere un elenco di interi distinti tra cui scegliere (questo è solo un intervallo continuo in questa domanda). Quindi è possibile generare array casuali di una determinata lunghezza costituiti da numeri da questo elenco con la somma indicata se gli array devono essere ordinati / non ordinati e consentire / non consentire una ripetizione.
John McClane

3

Ecco l'algoritmo di PermutationPartitionGenerator di John McClane, in un'altra risposta in questa pagina. Ha due fasi, vale a dire una fase di configurazione e una fase di campionamento, e genera nnumeri casuali in [ min, max] con la somma sum, in cui i numeri sono elencati in ordine casuale.

Fase di installazione: in primo luogo, viene creata una tabella di soluzione utilizzando le seguenti formule ( t(y, x)dove yè in [0, n] ed xè in [0, sum - n * min]):

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

Qui, t (y, x) memorizza la probabilità relativa che la somma dei ynumeri (nell'intervallo appropriato) sarà uguale x. Questa probabilità è relativa a tutte t (y, x) con lo stesso y.

Fase di campionamento: qui generiamo un campione di nnumeri. Impostare ssu sum - n * min, quindi per ciascuna posizione i, a partire dan - 1 e lavorando all'indietro su 0:

  • Impostato v su un numero intero casuale in [0, t (i + 1, s)).
  • impostare ramin .
  • Sottrai t (i, s) da v .
  • Mentre vrimane 0 o maggiore, sottrai t (i, s-1) da v, aggiungi 1 a re sottrai 1 da s.
  • Il numero in posizione inel campione è impostato su r.

MODIFICARE:

Sembra che con banali modifiche all'algoritmo sopra, è possibile fare in modo che ciascun numero casuale utilizzi un intervallo separato anziché utilizzare lo stesso intervallo per tutti:

Ogni numero casuale in posizioni i∈ [0,n ) ha un valore minimo min (i) e un valore massimo max (i).

Let adjsum= sum- Σmin (i).

Fase di installazione: in primo luogo, viene creata una tabella di soluzione utilizzando le seguenti formule ( t(y, x)dove yè in [0, n] ed xè in [0, adjsum]):

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

La fase di campionamento è quindi esattamente la stessa di prima, tranne per il fatto che abbiamo impostato ssu adjsum(anziché sum - n * min) e impostato rsu min (i) (anziché min).


MODIFICARE:

Per CombinationPartitionGenerator di John McClane, le fasi di installazione e campionamento sono le seguenti.

Fase di installazione: in primo luogo, viene creata una tabella di soluzione utilizzando le seguenti formule ( t(z, y, x)dove zè in [0, n], yè in [0, max - min] e xin [0, sum - n * min]):

  • t (0, j, k) = 1 se k == 0; 0 altrimenti
  • t (i, 0, k) = t (i - 1, 0, k)
  • t (i, j, k) = t (i, j-1, k) + t (i - 1, j, k - j)

Fase di campionamento: qui generiamo un campione di nnumeri. Impostare ssu sum - n * mine mrangesu max - min, quindi per ciascuna posizione i, iniziando con n - 1e lavorando all'indietro su 0:

  • Impostato vsu un numero intero casuale in [0, t (i + 1, mrange, s)).
  • Imposta mrangesu min ( mrange, s)
  • Sottrai mrangeda s.
  • Impostare rsumin + mrange .
  • Subtract t ( i, mrange, s) da v.
  • Mentre vresti 0 o superiore, aggiungere 1 a s, sottrarre 1 dal re 1 da mrange, quindi sottrarre t ( i, mrange, s) dav .
  • Il numero in posizione inel campione è impostato su r.

2

Non l'ho provato, quindi non è proprio una risposta, solo qualcosa da provare che è troppo lungo per essere inserito in un commento. Inizia con un array che soddisfa i primi due criteri e gioca con esso in modo che soddisfi ancora i primi due, ma è molto più casuale.

Se la media è un numero intero, l'array iniziale può essere [4, 4, 4, ... 4] o forse [3, 4, 5, 3, 4, 5, ... 5, 8, 0] o qualcosa di così semplice. Per una media di 4.5, prova [4, 5, 4, 5, ... 4, 5].

Quindi selezionare una coppia di numeri num1e num2, nell'array. Probabilmente il primo numero dovrebbe essere preso in ordine, come con il riordino Fisher-Yates, il secondo numero dovrebbe essere scelto a caso. Prendere il primo numero in ordine assicura che ogni numero venga selezionato almeno una volta.

Ora calcola max-num1e num2-min. Quelle sono le distanze dai due numeri ai limiti maxe min. Impostare limitsulla minore delle due distanze. Questa è la modifica massima consentita che non metterà l'uno o l'altro dei numeri al di fuori dei limiti consentiti. Se limitè zero, salta questa coppia.

Scegli un numero intero casuale nell'intervallo [1, limit]: chiamalochange . Ometto 0 dall'intervallo selezionabile in quanto non ha alcun effetto. I test possono mostrare che si ottiene una migliore casualità includendola; Non ne sono sicuro.

Ora imposta num1 <- num1 + changee num2 <- num2 - change. Ciò non influirà sul valore medio e tutti gli elementi dell'array sono ancora entro i limiti richiesti.

Sarà necessario eseguire l'intero array almeno una volta. I test dovrebbero mostrare se è necessario eseguirlo più di una volta per ottenere qualcosa di sufficientemente casuale.

ETA: include pseudocodice

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

L'ho provato e sfortunatamente, il tuo algoritmo non forma una distribuzione uniforme di tutte le soluzioni, indipendentemente da quante iterazioni faccio.
Peter O.

Oh bene. Ne è valsa la pena provare comunque. :(
rossum,

2

Come sottolineato dall'OP, la capacità di disordinare in modo efficiente è molto potente. Se siamo in grado di farlo, la generazione di una distribuzione uniforme delle partizioni può essere eseguita in tre fasi (riaffermando ciò che l'OP ha indicato nella domanda):

  1. Calcola il numero totale, M , delle partizioni di lunghezza N del numero in modo sumtale che le parti siano comprese nell'intervallo [ min, max].
  2. Genera una distribuzione uniforme di numeri interi da [1, M].
  3. Annullare la divisione di ciascun numero intero dal passaggio 2 nella rispettiva partizione.

Qui di seguito, ci concentriamo solo sulla generazione del n ° partizione come c'è una quantità abbondante di informazioni sulla generazione di una distribuzione uniforme di numero intero in un dato intervallo. Ecco un semplice C++algoritmo di ordinamento che dovrebbe essere facile da tradurre in altre lingue (NB Non ho ancora capito come riordinare il caso di composizione (cioè l'ordine è importante)).

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 pCountfunzione cavallo di battaglia è data da:

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

Questa funzione si basa sull'ottima risposta a Esiste un algoritmo efficiente per il partizionamento intero con un numero limitato di parti? dall'utente @ m69_snarky_and_unwelcoming. Quello sopra indicato è una leggera modifica dell'algoritmo semplice (quello senza memoizzazione). Questo può essere facilmente modificato per incorporare la memoizzazione per una maggiore efficienza. Per ora lo tralasciamo e ci concentreremo sulla parte non classificabile.

Spiegazione di unRank

Notiamo innanzitutto che esiste un mapping uno a uno dalle partizioni di lunghezza N del numero sumtale che le parti si trovano nell'intervallo [ min, max] alle partizioni ristrette di lunghezza N del numero sum - N * (min - 1)con parti in [ 1, max - (min - 1)].

Come piccolo esempio, considera le partizioni 50di lunghezza 4tali che il min = 10e il max = 15. Questo avrà la stessa struttura delle partizioni ristrette 50 - 4 * (10 - 1) = 14di lunghezza 4con la parte massima uguale a 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

Con questo in mente, per poter contare facilmente, potremmo aggiungere un passaggio 1a per tradurre il problema nel caso "unit", se lo desideri.

Ora, abbiamo semplicemente un problema di conteggio. Come @ m69 mostra brillantemente, il conteggio delle partizioni può essere facilmente raggiunto rompendo il problema in problemi più piccoli. La funzione @ m69 ci fornisce il 90% del modo, dobbiamo solo capire cosa fare con la restrizione aggiunta che esiste un limite. Questo è dove otteniamo:

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

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

Dobbiamo anche tenere presente che myMaxdiminuirà man mano che procediamo. Questo ha senso se guardiamo al 6 ° partizione di cui sopra:

2   2   4   6

Per contare il numero di partizioni da qui in avanti, dobbiamo continuare ad applicare la traduzione al caso "unit". Questo sembra:

1   1   3   5

Dove come il passo prima, avevamo un massimo di 6, ora consideriamo solo un massimo di 5.

Tenendo presente questo, disordinare la partizione non è diverso dal riordinare una permutazione o una combinazione standard. Dobbiamo essere in grado di contare il numero di partizioni in una determinata sezione. Ad esempio, per contare il numero di partizioni che iniziano con 10sopra, tutto ciò che facciamo è rimuovere il 10nella prima colonna:

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

Traduci nel case unit:

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

e chiama pCount:

pCount(13, 3, 6) = 5

Dato un numero intero casuale da riordinare, continuiamo a calcolare il numero di partizioni in sezioni sempre più piccole (come abbiamo fatto sopra) fino a quando non abbiamo riempito il nostro vettore indice.

Esempi

Dato min = 3, max = 10, n = 7, e sum = 42, qui è un Ideone demo che genera 20 partizioni casuali. L'output è inferiore:

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'indice lessicografico è sulla sinistra e la partizione non classificata sulla destra.


1
A quanto pare, questa è un'ottima alternativa e diventa effettivamente efficiente con la memoizzazione.
Peter O.

1
Grande osservazione sulla mappatura uno a uno.
גלעד ברקן

0

Se si generano 0≤a≤1 dei valori casuali nell'intervallo [l, x-1] uniformemente e 1-a dei valori casuali nell'intervallo [x, h] uniformemente, la media prevista sarebbe:

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

Quindi, se vuoi una m specifica, puoi giocare con a e x.

Ad esempio, se si imposta x = m: a = (hm) / (h-l + 1).

Per garantire una probabilità più vicina all'uniforme per diverse combinazioni, scegliere a o x in modo casuale dall'insieme di soluzioni valide all'equazione sopra. (x deve essere compreso nell'intervallo [l, h] e dovrebbe essere (vicino a) un numero intero; N * a dovrebbe essere (vicino a) anche un numero intero.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.