Có cách nào hiệu quả để tạo N số nguyên ngẫu nhiên trong một phạm vi có tổng hoặc trung bình nhất định không?


13

Có cách nào hiệu quả để tạo ra một tổ hợp ngẫu nhiên gồm N số nguyên sao cho

  • mỗi số nguyên nằm trong khoảng [ min, max],
  • các số nguyên có tổng bằng sum,
  • các số nguyên có thể xuất hiện theo bất kỳ thứ tự nào (ví dụ: thứ tự ngẫu nhiên) và
  • sự kết hợp được chọn ngẫu nhiên trong số tất cả các kết hợp đáp ứng các yêu cầu khác?

Có một thuật toán tương tự cho các kết hợp ngẫu nhiên trong đó các số nguyên phải xuất hiện theo thứ tự được sắp xếp theo các giá trị của chúng (chứ không phải theo bất kỳ thứ tự nào)?

(Việc chọn một kết hợp thích hợp với giá trị trung bình meanlà một trường hợp đặc biệt, nếu sum = N * mean. Vấn đề này tương đương với việc tạo một phân vùng ngẫu nhiên thống nhất sumthành N phần mỗi phần trong khoảng [ min,max ] và xuất hiện trong bất kỳ thứ tự hoặc theo thứ tự sắp xếp của họ các giá trị, tùy từng trường hợp.)

Tôi biết rằng vấn đề này có thể được giải quyết theo cách sau cho các kết hợp xuất hiện theo thứ tự ngẫu nhiên (EDIT [27 tháng 4]: Thuật toán được sửa đổi.):

  1. Nếu N * max < sumhoặc N * min > sum, không có giải pháp.

  2. Nếu N * max == sum, chỉ có một giải pháp, trong đó tất cả các Nsố đều bằng max. Nếu N * min == sum, chỉ có một giải pháp, trong đó tất cả các Nsố đều bằng min.

  3. Sử dụng thuật toán được đưa ra trong Smith và Tromble ("Lấy mẫu từ Đơn vị Simplex", 2004) để tạo N số nguyên không âm ngẫu nhiên với tổng sum - N * min.

  4. Thêm minvào mỗi số được tạo theo cách này.

  5. Nếu bất kỳ số nào lớn hơn max, hãy đến bước 3.

Tuy nhiên, thuật toán này chậm nếu maxít hơn nhiều sum. Ví dụ, theo các thử nghiệm của tôi (với việc thực hiện trường hợp đặc biệt liên quan ở trên mean), thuật toán từ chối, trung bình là Wap

  • khoảng 1,6 mẫu nếu N = 7, min = 3, max = 10, sum = 42 , nhưng
  • khoảng 30,6 mẫu nếu N = 20, min = 3, max = 10, sum = 120.

Có cách nào để sửa đổi thuật toán này thành hiệu quả cho N lớn trong khi vẫn đáp ứng các yêu cầu ở trên không?

BIÊN TẬP:

Như một sự thay thế được đề xuất trong các ý kiến, một cách hiệu quả để tạo ra một sự kết hợp ngẫu nhiên hợp lệ (thỏa mãn tất cả trừ yêu cầu cuối cùng) là:

  1. Tính toán X, số lượng kết hợp hợp lệ có thể được sum, minmax .
  2. Chọn Y, một số nguyên ngẫu nhiên thống nhất trong[0, X) .
  3. Chuyển đổi ("unrank") Ythành một kết hợp hợp lệ.

Tuy nhiên, có một công thức để tính số lượng kết hợp hợp lệ (hoặc hoán vị) không, và có cách nào để chuyển đổi một số nguyên thành một kết hợp hợp lệ không? [EDIT (28 tháng 4): Tương tự cho hoán vị thay vì kết hợp].

EDIT (27 tháng 4):

Sau khi đọc Thế hệ biến thể ngẫu nhiên không đồng nhất của Devroye (1986), tôi có thể xác nhận rằng đây là vấn đề tạo phân vùng ngẫu nhiên. Ngoài ra, Bài tập 2 (đặc biệt là phần E) ở trang 661 có liên quan đến câu hỏi này.

EDIT (28 tháng 4):

Hóa ra thuật toán tôi đưa ra là thống nhất trong đó các số nguyên liên quan được đưa ra theo thứ tự ngẫu nhiên , trái ngược với thứ tự được sắp xếp theo các giá trị của chúng . Vì cả hai vấn đề đều được quan tâm chung, tôi đã sửa đổi câu hỏi này để tìm câu trả lời chính tắc cho cả hai vấn đề.

Mã Ruby sau đây có thể được sử dụng để xác minh các giải pháp tiềm năng cho tính đồng nhất ( algorithm(...)thuật toán ứng cử viên ở đâu):

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 tháng 4): Đã thêm lại mã Ruby của triển khai hiện tại.

Ví dụ mã sau đây được đưa ra trong Ruby, nhưng câu hỏi của tôi không phụ thuộc vào ngôn ngữ lập trình:

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
}

Bạn có thể làm rõ yêu cầu thứ ba của bạn? Bạn có cần sự đồng nhất giữa tất cả các kết hợp có thể (bao gồm cả các kết hợp có nghĩa sai) hoặc trong số tất cả các kết hợp hợp lệ (nghĩa là các kết hợp có nghĩa chính xác) không?
user58697

Tất cả các kết hợp hợp lệ, nghĩa là tất cả các kết hợp đáp ứng các yêu cầu khác.
Peter O.

Nếu chúng ta có cách đếm và hủy bỏ các phân vùng của một tổng bị giới hạn ở N số nguyên trong [min, max], thì việc chọn một trong các phân vùng đó một cách ngẫu nhiên và không phân chia có đại diện cho phân phối đồng nhất không, và nó có hiệu quả hơn phương pháp hiện tại của bạn không? Tổng và N có thể lớn đến mức nào?
גלעד

Tôi không biết ý của bạn là gì khi "không phân chia các phân số của một khoản tiền" và tôi không biết về một bằng chứng cho thấy làm như vậy dẫn đến phân phối thống nhất theo nghĩa của câu hỏi này. Đối với câu hỏi này, cả hai sumNcó hiệu quả không giới hạn (trong lý do). Tôi đang tìm kiếm một câu trả lời kinh điển vì vấn đề cơ bản bật lên trong nhiều câu hỏi về Stack Overflow, trong đó có một nàycái này . @ גלעדב ž
Peter O.

Nếu chúng ta cung cấp cho mỗi kết hợp có thể một "thứ hạng" (hoặc chỉ mục) trong một sự sắp xếp có trật tự của tất cả chúng, "unranking", có nghĩa là tạo ra sự kết hợp, tất nhiên là xếp hạng (và N, min và max). Tại sao một lựa chọn như vậy trong số tất cả các kết hợp có thể không phù hợp với phân phối thống nhất?
גלעד

Câu trả lời:


5

Đây là giải pháp của tôi trong Java. Nó có đầy đủ chức năng và chứa hai trình tạo: PermutationPartitionGeneratorcho các phân vùng chưa được sắp xếp và CombinationPartitionGeneratorcho các phân vùng được sắp xếp. Trình tạo của bạn cũng được thực hiện trong lớp SmithTromblePartitionGeneratorđể so sánh. Lớp SequentialEnumeratorliệt kê tất cả các phân vùng có thể (chưa sắp xếp hoặc sắp xếp, tùy thuộc vào tham số) theo thứ tự tuần tự. Tôi đã thêm các bài kiểm tra kỹ lưỡng (bao gồm cả các trường hợp kiểm tra của bạn) cho tất cả các trình tạo này. Việc thực hiện là tự giải thích cho hầu hết các phần. Nếu bạn có bất kỳ câu hỏi, tôi sẽ trả lời chúng trong vài ngày.

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

Bạn có thể thử điều này trên Ideone .


Cảm ơn câu trả lời của bạn; nó hoạt động tốt Tôi đã mô tả trình tạo hoán vị trong một câu trả lời khác ở đây; trả lời một câu hỏi khác với sự giúp đỡ của bạn; và sẽ sớm đưa thuật toán của bạn vào mã mẫu Python cho bài viết của tôi về các phương thức tạo ngẫu nhiên.
Peter O.

Chỉ để được rõ ràng. Liệu thuật toán này dựa vào việc tạo ra tất cả các phân vùng / thành phần có thể để lấy mẫu?
Joseph Wood

@JosephWood Không, nó phụ thuộc vào việc đếm tất cả chúng. Điều này chỉ được thực hiện một lần khi khởi tạo trình tạo và khá hiệu quả vì nó sử dụng phương pháp lập trình động.
John McClane

Làm thế nào để lập trình động có thể giải quyết vấn đề liên quan đến việc chọn phân vùng ngẫu nhiên thống nhất 'tổng' thành N số nguyên được chọn ngẫu nhiên với thay thế từ danh sách ( ví dụ ) hoặc không thay thế ( ví dụ ) hoặc làm thế nào để giải quyết vấn đề đó?
Peter O.

@PeterO. Bạn cần đếm tất cả các phân vùng có thể thông qua cùng một phương thức như trong thuật toán của tôi, nhưng lần này bạn chỉ cần trừ các số cho phép khỏi tổng. Điều này quá dài để bình luận, bạn có thể hỏi một câu hỏi riêng biệt. Tôi nghi ngờ rằng người ta có thể giải quyết bốn vấn đề khác nhau thông qua cùng một cách tiếp cận. Giả sử bạn có một danh sách các số nguyên riêng biệt để lựa chọn (đây chỉ là một phạm vi liên tục trong câu hỏi này). Sau đó, bạn có thể tạo các mảng ngẫu nhiên có độ dài nhất định bao gồm các số từ danh sách này với tổng đã cho nếu các mảng phải được sắp xếp / không sắp xếp và cho phép / không cho phép lặp lại.
John McClane

2

Tôi chưa thử nghiệm điều này, vì vậy nó không thực sự là một câu trả lời, chỉ là một cái gì đó để thử quá dài để phù hợp với một nhận xét. Bắt đầu với một mảng đáp ứng hai tiêu chí đầu tiên và chơi với nó để nó vẫn đáp ứng hai tiêu chí đầu tiên, nhưng ngẫu nhiên hơn rất nhiều.

Nếu giá trị trung bình là một số nguyên, thì mảng ban đầu của bạn có thể là [4, 4, 4, ... 4] hoặc có thể [3, 4, 5, 3, 4, 5, ... 5, 8, 0] hoặc một cái gì đó đơn giản như thế Đối với giá trị trung bình là 4,5, hãy thử [4, 5, 4, 5, ... 4, 5].

Tiếp theo chọn một cặp số num1num2trong mảng. Có lẽ nên lấy số thứ nhất theo thứ tự, như với shuffle Fisher-Yates, số thứ hai nên được chọn ngẫu nhiên. Lấy số đầu tiên để đảm bảo rằng mọi số được chọn ít nhất một lần.

Bây giờ tính toán max-num1num2-min. Đó là khoảng cách từ hai số đến maxminranh giới. Đặt limitnhỏ hơn trong hai khoảng cách. Đó là thay đổi tối đa được phép sẽ không đặt một hoặc các số khác ngoài giới hạn cho phép. Nếu limitbằng 0 thì bỏ qua cặp này.

Chọn một số nguyên ngẫu nhiên trong phạm vi [1, limit]: gọi nó change. Tôi bỏ qua 0 từ phạm vi có thể chọn vì nó không có hiệu lực. Kiểm tra có thể cho thấy rằng bạn có được sự ngẫu nhiên tốt hơn bằng cách bao gồm nó; Tôi không chắc.

Bây giờ thiết lập num1 <- num1 + changenum2 <- num2 - change . Điều đó sẽ không ảnh hưởng đến giá trị trung bình và tất cả các yếu tố của mảng vẫn nằm trong ranh giới bắt buộc.

Bạn sẽ cần phải chạy qua toàn bộ mảng ít nhất một lần. Kiểm tra sẽ hiển thị nếu bạn cần chạy qua nó nhiều lần để có được một cái gì đó đủ ngẫu nhiên.

ETA: bao gồm mã giả

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

Tôi đã thử nghiệm nó và thật không may, thuật toán của bạn không tạo thành một phân phối thống nhất của tất cả các giải pháp, bất kể tôi có thực hiện bao nhiêu lần lặp.
Peter O.

Ồ tốt Dù sao nó cũng đáng để thử. :(
rossum

2

Đây là thuật toán từ PermulationPartitionGenerator của John McClane, trong một câu trả lời khác trên trang này. Nó có hai giai đoạn, cụ thể là giai đoạn thiết lập và giai đoạn lấy mẫu và tạo ra ncác số ngẫu nhiên trong [ min, max] với tổng sum, trong đó các số được liệt kê theo thứ tự ngẫu nhiên.

Thiết lập giai đoạn: Thứ nhất, một bảng giải pháp được xây dựng bằng cách sử dụng công thức sau đây ( t(y, x)nơi ylà trong [0, n] và xlà trong [0, sum - n * min]):

  • t (0, j) = 1 nếu j == 0; 0 khác
  • t (i, j) = t (i-1, j) + t (i-1, j-1) + ... + t (i-1, j- (tối đa))

Ở đây, t (y, x) lưu trữ xác suất tương đối rằng tổng các ysố (trong phạm vi thích hợp) sẽ bằng nhau x. Xác suất này là tương đối với tất cả t (y, x) với cùng y.

Giai đoạn lấy mẫu: Ở đây chúng tôi tạo ra một mẫu nsố. Đặt sthành sum - n * min, sau đó cho từng vị trí i, bắt đầu n - 1và làm việc ngược về 0:

  • Đặt vthành một số nguyên ngẫu nhiên trong [0, t (i + 1, s)).
  • Đặt rthành min.
  • Trừ t (i, s) từ v.
  • Trong khi vvẫn là 0 hoặc lớn hơn, trừ t (i, s-1) từ v, thêm 1 vào rvà trừ 1 từ s.
  • Số tại vị trí itrong mẫu được đặt thành r.

BIÊN TẬP:

Dường như với các thay đổi nhỏ đối với thuật toán ở trên, có thể có mỗi số ngẫu nhiên sử dụng một phạm vi riêng thay vì sử dụng cùng một phạm vi cho tất cả chúng:

Mỗi số ngẫu nhiên tại các vị trí i∈ [0, n) có giá trị tối thiểu min (i) và giá trị tối đa tối đa (i).

Đặt adjsum= sum- Σmin (i).

Thiết lập giai đoạn: Thứ nhất, một bảng giải pháp được xây dựng bằng cách sử dụng công thức sau đây ( t(y, x)nơi ylà trong [0, n] và xlà trong [0, adjsum]):

  • t (0, j) = 1 nếu j == 0; 0 khác
  • t (i, j) = t (i-1, j) + t (i-1, j-1) + ... + t (i-1, j- (tối đa (i-1) -min (i -1)) )

Giai đoạn lấy mẫu sau đó giống hệt như trước đây, ngoại trừ chúng ta đặt sthành adjsum(chứ không phải sum - n * min) và đặt rthành min (i) (chứ không phải min).


BIÊN TẬP:

Đối với CombPartitionGenerator của John McClane, các giai đoạn thiết lập và lấy mẫu như sau.

Thiết lập giai đoạn: Thứ nhất, một bảng giải pháp được xây dựng bằng cách sử dụng công thức sau đây ( t(z, y, x)nơi zlà trong [0, n], ylà trong [0, max - min], và xlà trong [0, sum - n * min]):

  • t (0, j, k) = 1 nếu k == 0; 0 khác
  • t (i, 0, k) = t (i - 1, 0, k)
  • t (i, j, k) = t (i, j-1, k) + t (i - 1, j, k - j)

Giai đoạn lấy mẫu: Ở đây chúng tôi tạo ra một mẫu nsố. Đặt sthành sum - n * minmrange đến max - min, sau đó cho từng vị trí i, bắt đầu n - 1và làm việc ngược về 0:

  • Đặt vthành một số nguyên ngẫu nhiên trong [0, t (i + 1, mrange, s)).
  • Bộ mrange thành tối thiểu ( mrange, s)
  • Trừ đi mrangetừ s.
  • Bộ rthành min + mrange.
  • Trừ t ( i,mrange , s) từv .
  • Trong khi vvẫn là 0 hoặc cao hơn, thêm 1 đến s, trừ 1 từ rvà 1 từ mrange, sau đó trừ t ( i,mrange , s) từv .
  • Số tại vị trí itrong mẫu được đặt thành r.

1

Như OP chỉ ra, khả năng unrank hiệu quả là rất mạnh. Nếu chúng ta có thể làm như vậy, việc tạo phân phối đồng đều các phân vùng có thể được thực hiện theo ba bước (khôi phục lại những gì OP đã trình bày trong câu hỏi):

  1. Tính tổng số M , của các phân vùng có độ dài N của số sumsao cho các phần nằm trong phạm vi [ min,max ].
  2. Tạo một phân phối thống nhất của các số nguyên từ [1, M].
  3. Bỏ từng số nguyên từ bước 2 vào phân vùng tương ứng.

Dưới đây, chúng tôi chỉ tập trung vào việc tạo phân vùng thứ n vì có một lượng thông tin dồi dào về việc tạo phân phối số nguyên thống nhất trong một phạm vi nhất định. Đây là một C++thuật toán đơn giản , dễ dịch sang các ngôn ngữ khác (NB Tôi chưa tìm ra cách hủy bỏ trường hợp sáng tác (ví dụ như vấn đề thứ tự)).

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

Hàm workhorse pCountđược cho bởi:

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

Hàm này dựa trên câu trả lời tuyệt vời cho Có một thuật toán hiệu quả để phân vùng số nguyên với số lượng phần bị hạn chế không?bởi người dùng @ m69_snarky_and_unwelasing. Cái được đưa ra ở trên là một sửa đổi nhỏ của thuật toán đơn giản (cái không có ghi nhớ). Điều này có thể dễ dàng được sửa đổi để kết hợp ghi nhớ cho hiệu quả cao hơn. Chúng tôi sẽ loại bỏ điều này ngay bây giờ và tập trung vào phần không thể bỏ qua.

Giải thích về unRank

Trước tiên, chúng tôi lưu ý rằng có một ánh xạ một-một từ các phân vùng có độ dài N của số sumsao cho các phần nằm trong phạm vi [ min, max] đến các phân vùng bị hạn chế về độ dài N của số sum - m * (min - 1)có các phần trong [ 1, max - (min - 1)].

Như một ví dụ nhỏ, hãy xem xét các phân vùng 50có độ dài 4sao cho min = 10max = 15. Điều này sẽ có cấu trúc tương tự như các phân vùng bị hạn chế 50 - 4 * (10 - 1) = 14về chiều dài 4với phần tối đa bằng 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

Với suy nghĩ này, để dễ dàng đếm, chúng tôi có thể thêm bước 1a để dịch vấn đề sang trường hợp "đơn vị" nếu bạn muốn.

Bây giờ, chúng tôi chỉ đơn giản là có một vấn đề đếm. Khi @ m69 hiển thị rực rỡ, việc đếm các phân vùng có thể dễ dàng đạt được bằng cách chia nhỏ vấn đề thành các vấn đề nhỏ hơn. Hàm @ m69 cung cấp cho chúng tôi 90%, chúng tôi chỉ cần tìm ra những việc cần làm với hạn chế được thêm vào là có giới hạn. Đây là nơi chúng tôi nhận được:

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

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

Chúng tôi cũng phải ghi nhớ rằng myMaxsẽ giảm khi chúng tôi di chuyển. Điều này có ý nghĩa nếu chúng ta nhìn vào phân vùng thứ 6 ở trên:

2   2   4   6

Để đếm số lượng phân vùng từ đây trở đi, chúng ta phải tiếp tục áp dụng bản dịch cho trường hợp "đơn vị". Điều này trông giống như:

1   1   3   5

Trường hợp như bước trước, chúng tôi đã có tối đa 6, bây giờ chúng tôi chỉ xem xét tối đa 5.

Với ý nghĩ này, việc hủy bỏ phân vùng không khác gì việc hủy bỏ một hoán vị hoặc kết hợp tiêu chuẩn. Chúng ta phải có thể đếm số lượng phân vùng trong một phần nhất định. Ví dụ: để đếm số lượng phân vùng bắt đầu 10ở trên, tất cả những gì chúng tôi làm là xóa 10cột trong cột đầu tiên:

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

Dịch trường hợp đơn vị:

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

và gọi pCount:

pCount(13, 3, 6) = 5

Đưa ra một số nguyên ngẫu nhiên cho unrank, chúng tôi tiếp tục tính toán số lượng phân vùng trong các phần nhỏ hơn và nhỏ hơn (như chúng tôi đã làm ở trên) cho đến khi chúng tôi điền vào vectơ chỉ mục của chúng tôi.

Ví dụ

Với min = 3, max = 10, n = 7, và sum = 42, đây là một ideone bản demo mà tạo ra 20 phân vùng ngẫu nhiên. Đầu ra dưới đây:

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

Chỉ số từ vựng ở bên trái và phân vùng không được đặt ở bên phải.


0

Nếu bạn tạo 0≤a≤1 các giá trị ngẫu nhiên trong phạm vi [l, x-1] đồng nhất và 1-a của các giá trị ngẫu nhiên trong phạm vi [x, h] một cách đồng đều, giá trị trung bình sẽ là:

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

Vì vậy, nếu bạn muốn một m cụ thể, bạn có thể chơi với a và x.

Ví dụ: nếu bạn đặt x = m: a = (hm) / (h-l + 1).

Để có được tất cả các giải pháp có thể, chọn x ngẫu nhiên (x phải nằm trong phạm vi [l, h] và a phải (gần) một số nguyên.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.