【问题标题】:Algorithm for generating lists of shuffled Integers from one original list of Integers从一个原始整数列表生成混洗整数列表的算法
【发布时间】:2018-11-06 15:22:25
【问题描述】:

拥有一个 x 唯一 Integers 的 ArrayList,我需要在 y 大小为 z 的 ArrayLists 之间随机分配它们。请记住:

  • x y z 是变量值。
  • 数字不能在结果数组上重复。
  • 结果列表不能包含相同的数字! (订购它们必须不同)
  • 如果您计算结果数组中的出现次数,则原始数组中的每个数字的使用次数必须尽可能与其他次数相同。
  • 必须使用原始数组的所有数字,没有一个 它们不能不被使用。
  • 如果可能,必须在 Java 7 中工作。不是 100% 强制性的,但...
  • 产生的组合将用于类似于彩票的事情,因此它们不能太连续并且必须非常随机。它们还将按从最小值到最大值排序。
  • 最初我尝试生成所有可能的组合,目的是获得所需的数量,但这不可行,因为如果您选择 40 个数字和 11 个组合的高值,则有数百万个并且 CPU 会卡住计算很多时间,所以我尝试开发一个更简单的算法而不计算所有组合(我在下面发布代码)。

当你有一个由 8 个元素组成的数组的原点并且你想要输出 3 个大小为 6 的数组时,一个示例是这样的:

原始数组列表:[1, 2, 3, 4, 5, 6, 7, 8]

结果输出:[7, 5, 3, 6, 4, 8], [7, 5, 1, 8, 2, 3], [8, 1, 2, 3, 4, 6]

我开发了一个在 cmets 上解释的算法。首先,我创建了一个包含总位置数的数组,并计算每个数字必须重复多少次才能填充输出数组。然后我用重复必要次数的每个数字填充数组,如果数组不满(因为当我除以得到placesByNumber时,我四舍五入为整数)我用原始集合中的随机数填充它的数字。之后我打乱了数字,最后我填充了结果数组,记住我不能在每个结果数组中重复数字。

问题来了,有时,我会遇到最后一个数组没有完全填充的情况,因为洗牌后的numbersGroup 变量的最后一个数字包含在最后一个数组中。

这是一个失败的例子:

原始数组列表:[1, 2, 3, 4, 5, 6, 7, 8]

用于填充结果数组的随机数字组:

[8, 2, 4, 4, 5, 7, 2, 3, 8, 2, 1, 5, 7, 1, 6, 3, 6, 1]

结果数组:(第三个没有 6 个元素,因为 6 和 1 是 包含在上面)

[[8, 2, 4, 5, 7, 3], [4, 2, 8, 1, 5, 7], [2, 1, 6, 3]]

我发现了一些非常丑陋的方法来解决它,但效率非常低,我正在尝试找到一种更好、更有效的算法来实现这一点。

这是我的源代码:

public static List<List<Integer>> getOptimizedCombinations(List<Integer> numbers, int numbersPerCombination, int desiredCombinations){
    List<List<Integer>> result = new ArrayList<>();
    
    //calculate total places and how many places correspond to each number.
    int totalPlaces = numbersPerCombination * desiredCombinations;
    int placesByNumber = totalPlaces / numbers.size();
    
    //instantiating array with the total number of places
    Integer[] numbersGroup = new Integer[totalPlaces];
    
    //filling the array with the numbers, now we know how many times a number must be inside the array, 
    //so we put the numbers. First we do it in order, later we will shuffle the array.
    int pos = 0;
    for (int n : numbers) {
        for (int i=0; i<placesByNumber; i++) {
            numbersGroup[pos] = n;
            pos++;
        }
    }
    
    //if there are places for fill, we fill it with random numbers. This can be possible because when we divide the total places between the 
    //numbers size, it can give a decimal as a result, and we round it to lower binary number without decimals, so it is possible to
    //have non filled places.       
    if (pos<totalPlaces) {
        while(pos<totalPlaces) {                
            numbersGroup[pos] = numbers.get(getRandom(0, numbers.size()));
            pos++;              
        }
    }       
    
    shuffleArray(numbersGroup);
    
    //we instantiate the arraylists
    for (int i=0; i<desiredCombinations; i++) {
        result.add(new ArrayList<Integer>());
    }
                    
    //filling the arraylists with the suffled numbers
    for (int i=0; i<numbersGroup.length; i++) {
        for (int j=0; j<result.size(); j++) {
            //if the combination doesn't have the number and the combination is not full, we add the number
            if (!result.get(j).contains(numbersGroup[i]) && result.get(j).size()<numbersPerCombination) {
                result.get(j).add(numbersGroup[i]);
                break;
            }
        }
    }
    
    return result;
}

static void shuffleArray(Integer[] ar){
    Random rnd = new Random();
    for (int i = ar.length - 1; i > 0; i--)
    {
        int index = rnd.nextInt(i + 1);
        // Simple swap
        int a = ar[index];
        ar[index] = ar[i];
        ar[i] = a;
    }
}

public static int getRandom(int min, int max) {
    return (int)(Math.random() * max + min);
}

这样称呼的:

    ArrayList<Integer> numbers = new ArrayList<Integer>() {{ 
        add(1); 
        add(2);
        add(3); 
        add(4); 
        add(5); 
        add(6); 
        add(7);
        add(8);
    }};
    getOptimizedCombinations(numbers, 6, 3);

【问题讨论】:

  • 为什么不直接打乱原始数组,将其截断为所需的大小,然后重复此过程所需的次数?
  • @MitchelPaulin 你是什么意思?你能用一个样本解释一下吗?我也忘了告诉如果你计算结果数组中的出现次数,原始数组的每个数字都必须与其他数字或多或少地使用相同的次数
  • 您的工作样本在每个输出数组中包含 8、3,那么您的失败样本不能包含 6、1。为什么?
  • @NullPointerException 好的,现在我看到了问题所在。我假设每次洗牌都彼此无关
  • @zlakad 数字不能在结果数组上重复,第三个数组已经有 6,1

标签: java arrays algorithm math statistics


【解决方案1】:

您可以使用Streams 将洗牌列表限制为z 元素:

List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8);

List<List<Integer>> result = new LinkedList<>();
for(int i = 0; i < y; i++) {
  Collections.shuffle(numbers);
  List<Integer> list = numbers.stream().limit(z).collect(Collectors.toList());
  result.add(list);
}

System.out.println(result);

也许可以用更优雅的方式完成,但输出应该是这样的:

[[2, 8, 7, 3, 4, 6], [4, 3, 6, 5, 2, 8], [5, 2, 4, 1, 6, 8]]

【讨论】:

  • 为此创建一个流我认为是矫枉过正,只需使用numbers.sublist()
  • @QBrute 请记住,数字不能在结果数组上重复。此外,如果您计算结果数组中的出现次数,则必须尽可能使用原始数组的每个数字与其他数组相同的次数。您的算法适用于这些要求吗?此外,这些函数适用于 Java 7 吗?
  • 更优雅的方法是重写 shuffle 方法,就像在原始 Java 代码中一样(我认为这是使用 Knuth 算法),但 STOP 之后的改组 次(关于 OP 的示例)
  • @zlakad 谁会满足这些要求? “数字不能在结果数组上重复”和“如果计算结果数组中的出现次数,则原始数组的每个数字必须尽可能使用与其他数字相同的次数”
  • 使用此算法我认为可能并非所有数字都尽可能使用相同的次数,甚至可能有些数字不会在结果数组中使用。我生成了一个数组,其中所有数字都重复了必要的次数,以确保我使用所有数字的次数或多或少相同,并且它们都没有被使用
【解决方案2】:

理念

要完成这项工作,我们需要

  • z &lt; x(每个新列表的长度
  • y·z(列表数量·列表长度)必须是 x 的倍数,否则某些数字必须比其他数字出现得更多。

这个想法是

  1. 随机播放输入列表。
  2. 重复输入列表,最终得到y·z 数字。这可以在不实际重复列表的情况下完成。诀窍是使用模 % 运算符。
  3. 将重复的输入列表平均拆分为长度为 zy 列表。
  4. 随机播放每个新列表。

输入

1 2 3 4 5 6 7 8

随机播放

3 5 8 6 7 2 4 1

重复

3 5 8 6 7 2 4 1 3 5 8 6 7 2 4 1 3 5 8 6 7 2 4 1

拆分

3 5 8 6 7 2    4 1 3 5 8 6    7 2 4 1 3 5    8 6 7 2 4 1

随机播放每个列表

7 3 5 6 2 8    1 3 4 8 6 5    3 4 1 5 7 2    2 7 4 1 8 6

打乱列表列表

1 3 4 8 6 5    2 7 4 1 8 6    7 3 5 6 2 8    3 4 1 5 7 2

程序

这个程序应该可以在 Java 7 中运行。但是我只用 Java 11 对其进行了测试。

import java.util.*;
public class Shuffle {
    public static void main(String[] args) {
        System.out.println(splitShuffle(Arrays.asList(1,2,3,4,5,6,7,8), 6, 3));
    }
    public static List<List<Integer>> splitShuffle(
            List<Integer> input, int newLength, int listCount) {        
        assert newLength * listCount % input.size() == 0 : "Cannot distribute numbers evenly";
        input = new ArrayList<>(input);
        Collections.shuffle(input);
        List<List<Integer>> result = new ArrayList<>(listCount);
        for (int i = 0; i < listCount; ++i) {
            result.add(rotatingCopy(input, i * newLength, newLength));
        }
        Collections.shuffle(result);
        return result;
    }
    private static List<Integer> rotatingCopy(List<Integer> input, int startIndex, int length) {
        assert length < input.size() : "copy would have to contain duplicates";
        List<Integer> copy = new ArrayList<>(length);
        for (int i = 0; i < length; ++i) {
            copy.add(input.get((startIndex + i) % input.size()));
        }
        Collections.shuffle(copy);
        return copy;
    }
}

示例输出

我运行了四次程序。这是它的输出。每一行都是程序的一次运行。

[[2, 6, 7, 8, 1, 3], [4, 3, 7, 5, 2, 8], [1, 2, 6, 5, 4, 8]]
[[2, 7, 5, 4, 6, 1], [4, 7, 2, 6, 8, 3], [1, 3, 5, 8, 6, 4]]
[[4, 1, 2, 5, 6, 3], [5, 3, 8, 4, 6, 7], [5, 1, 2, 7, 3, 8]]
[[5, 3, 8, 2, 6, 4], [1, 7, 4, 5, 6, 3], [1, 6, 2, 8, 7, 4]]

我们可以看到,每个数字恰好出现两次,每个子列表只有唯一的数字。

完整性

至少对于输入列表[1, 2, 3]y=3, z=2,我可以验证是否可以生成所有可能的48 个输出。我知道使用以下 bash 命令有 48 种组合:

printf %s\\n {1..3}{1..3},{1..3}{1..3},{1..3}{1..3} | grep -Pv '(\d)\1' |
tr -d , | awk '{print $1, gsub(1,""), gsub(2,""), gsub(3,"")}' |
grep -F ' 2 2 2' | cut -d' ' -f1 | sort -u | wc -l

【讨论】:

  • 第三个输出中的数字 1 在哪里?
  • 发现错误。应该是 % input.size() 而不是 % size
  • 不错的尝试,但是...一旦您将原始数据打乱,并给我输出和其他变量,结果数组是可预测的。我认为这不是一个好主意。
  • 真的吗?我认为您无法预测输出,但让我们尝试一下。这是一个简单的案例:x=3, y=3, z=2。输入是[1, 2, 3],洗牌后的版本是[2, 3, 1]。输出是什么?一个提示:有多个答案,所以你不应该给我我收到的输出。但是,如果您能给我一个该程序永远无法生成的有效结果,那么我会同意您并寻找另一种解决方案。
  • 我读的是你的步骤而不是你的代码,所以重复后我得到了 231231,拆分后 231231。我对吗?操作 - 我没有阅读 Shuffle each list 和 list of lists。
【解决方案3】:

我的做法是打乱原始列表,然后不断迭代直到填充目标列表,然后打乱每个目标列表。这将使每个数字的出现保持平衡。如果numbersPerCombination > numbers.size(),它也可以工作。

public class FairLists {

    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
        List<List<Integer>> fairLists = getOptimizedCombinations(numbers, 6, 3);
        System.out.println(fairLists);
    }

    public static List<List<Integer>> getOptimizedCombinations(List<Integer> numbers, int numbersPerCombination, int desiredCombinations){
        List<Integer> source = new ArrayList<>(numbers);
        Collections.shuffle(source);

        List<List<Integer>> fairNumbersLists = new ArrayList<>(desiredCombinations);

        int sourceIndex = 0;
        while (desiredCombinations > 0) {

            List<Integer> fairNumbers = new ArrayList<>(numbersPerCombination);
            for (int i = 0; i < numbersPerCombination; i++) {
                fairNumbers.add(source.get(sourceIndex));
                sourceIndex++;
                if (sourceIndex == source.size()) {
                    sourceIndex = 0;
                }
            }

            Collections.shuffle(fairNumbers);
            fairNumbersLists.add(fairNumbers);

            desiredCombinations--;
        }

        Collections.shuffle(fairNumbersLists);
        return fairNumbersLists;
    }
}

【讨论】:

  • 您无法编写适用于numbersPerCombination &gt; numbers.size() 的程序。对于列表[1, 2]y=z=3,您的程序给了我[[2, 2, 1], [2, 2, 1], [2, 1, 1]],这违反了»每个列表中只有唯一的数字«»排序的列表必须彼此不同« i>.
  • sorry @AndrewS 我忘记了一些规则,但现在都写好了,请你调整你的算法吗?
【解决方案4】:

在我们聊天期间,您让我编写有关组合的代码,所以这不是您问题的答案,只是您可以探索的代码

public class Combination {

    private Combination() {
    }

    /**
     *
     * @param n:
     *            n-set
     * @param m:
     *            m-subset
     * @return number of combinations C(n, m) = (n(n - 1)...(n - m + 1)) / m!
     */
    public static BigInteger C(int n, int m) {
        if (m > n) {
            return BigInteger.ZERO;
        } else {
            if ((n - m) > m) {
                return C(n, (n - m));
            }
        }
        BigInteger numerator = BigInteger.ONE;
        BigInteger denominator = BigInteger.ONE;

        for (int i = n; i > m; i--) {
            numerator = numerator.multiply(BigInteger.valueOf(i));
        }

        for (int i = (n - m); i > 1; i--) {
            denominator = denominator.multiply(BigInteger.valueOf(i));
        }

        return numerator.divide(denominator);
    }

    /**
     *
     * @param <T>
     *            Type
     * @param elements
     *            List of elements to combine
     * @param numberOfRequiredElements
     *            must be less or equal to elements.size()
     * @param combinatios
     *            result: List&lt;List&lt;T&gt;&gt; of all combinations
     * @param temp
     *            used for recursive purposes
     * @return combinations<br>
     * 
     *         Example of usage:<br>
     *         List&lt;Integer&gt; elements = new ArrayList&lt;&gt;();<br>
     *         for (int i = 1; i &lt;= 7; i++) {<br>
     *         &emsp;elements.add(i);<br>
     *         }<br>
     *         List&lt;Integer&gt; temp = new ArrayList&lt;&gt;();<br>
     *         List&lt;List&lt;Integer&gt;&gt; combinations = new
     *         ArrayList&lt;&gt;();<br>
     *         System.out.println(Combination.allCombinations(elements, 6,
     *         combinations, temp));<br>
     *
     */
    public static <T> List<List<T>> allCombinations(List<T> elements, int numberOfRequiredElements,
            List<List<T>> combinatios, List<T> temp) {
        if (numberOfRequiredElements == 0) {
            // System.out.print(temp);
            combinatios.add(new ArrayList<>(temp));
        } else {
            for (int i = 0; i < elements.size(); i++) {
                temp.add(elements.get(i));
                List<T> subList = elements.subList(i + 1, elements.size());
                allCombinations(subList, numberOfRequiredElements - 1, combinatios, temp);
                temp.remove(temp.size() - 1);
            }
        }
        return combinatios;
    }

    /**
     *
     * @param args
     *            Not required for this purpose
     */
    public static void main(String[] args) {
        int NO_OF_ELEMENS = 10;
        int REQURED_COMBINATION_SIZE = 6;

        List<Integer> elements = new ArrayList<>();
        for (int i = 1; i <= NO_OF_ELEMENS; i++) {
            elements.add(i);
        }
        System.out.println("This is an example of using methods in this class\n");
        System.out.println("Elements are " + elements + " (size = " + elements.size() + ")");
        System.out.println("Requred size of combination is " + REQURED_COMBINATION_SIZE);
        System.out.println("Number of all combinations is " + Combination.C(NO_OF_ELEMENS, REQURED_COMBINATION_SIZE));
        List<Integer> temp = new ArrayList<>();
        List<List<Integer>> combinations = new ArrayList<>();
        System.out.println("All combinations are:");
        Combination.allCombinations(elements, REQURED_COMBINATION_SIZE, combinations, temp);
        int i = 0;
        for (List<Integer> combination : combinations) {
            System.out.println(++i + "\t" + combination);
        }
    }
}

我希望这段代码能有所帮助。 附言对无法读取的 cmets 感到抱歉 - 我之前在 NetBeans 中写过这个,现在我正在使用 IntelliJ...

编辑:使用 long 而不是 BigInteger 来计算组合数...(我个人不建议这样做)。

public static long noOfCombinations(int n, int m){

    //this part is for fewer multiplications
    //b/c 4 out of 6 has the same number of combinations as 2 out of 6
    if (m > n) {
        return 0;
    } else {
        if ((n - m) > m) {
            return noOfCombinations(n, (n - m));
        }
    }

    long numerator = 1;
    long denominator = 1;

    //these two loops are for partial factorial
    for (int i = n; i > m; i--) {
        numerator *= i;
    }

    for (int i = (n - m); i > 1; i--) {
        denominator *= i;
    }

    // no of combinations
    return numerator / denominator;
}

【讨论】:

  • 谢谢。我会研究你的代码并检查我是否能想象出一种方法。顺便说一句,如果你还记得我们昨天的聊天记录,当我处理 40 个数字的集合以及例如 8 个或 10 个数字的组合时,我们正在谈论数百万个组合和 CPU 块计算。
  • 例如,40个数字和8个大小的组合,有76904685个组合,CPU获取阻塞计算。你能想象这个算法的一个变体,从这 76904685 个组合中随机获取所需的组合(例如 50 个)吗?例如,将 76904685 划分为 50 以获得一个区间,并获得具有该区间的 50 个组合或类似的东西。目标是不计算所有组合,而只计算所需的组合,而不仅仅是获得第一个组合(因为如果这样,数字将不是随机的)
  • 是的,硬件资源总是有问题。例如,BigInteger 理论上是无限的,但受实际内存限制......我知道你在问什么,我会尽力解决你的问题。不幸的是,我现在很忙……
  • 谢谢@zlakad 我希望你能解决这个问题,这很令人沮丧
  • 非常感谢@zlakad 非常感谢您的帮助!
猜你喜欢
  • 2011-10-20
  • 1970-01-01
  • 2012-04-19
  • 2021-09-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-02-14
相关资源
最近更新 更多