匹配大多数可能组合的算法

Algorithm to match most possible combinations

我目前正在努力为我的配对系统编写算法。对于懂数学的人来说,这可能是儿戏,嗯,我不懂。

举个例子:

一个大厅由 正好 8 名玩家组成。
队列中有 9 方。聚会人数为:1、6、3、8、2、1、5、3、2。
该算法需要从这些派对中创建尽可能多的完整 游说团体。

可能只有完整的大厅。所以可能会有无法配对的派对。然后他们将等待新的派对进入队列。

我当前的代码确实将各方与大厅相匹配,但它缺少找到最佳组合以找到尽可能多的完整大厅的功能。

示例:大厅大小刚好为 7,派对:2,2,4,3,1,5 -> 2,5 和 4,3 可以匹配,但它会匹配 2,2,3 和 4 ,1,5 无法匹配。

非常感谢任何帮助。

我当前的代码:

    // Limitation: Is not intelligent. Example: Size 7, Teams 2,2,4,3,1,5 -> 2,5 and 4,3 could be matched, but it will do 2,2,3 and 4,1,5
    // Possible solution: Sort for size or match parties with lowest possible match e.g try 5,1 then 5,2 ...
    public List<MatchedParty> getLobbies(List<Party> parties, int teamSize) {
        List<MatchedParty> lobbies = new ArrayList<>();

        parties.sort(Comparator.comparing(Party::getJoinedQueueTime));

        while (!(parties.isEmpty())) {
            MatchedParty matchedParty = new MatchedParty();
            List<Party> team1 = new ArrayList<>();
            List<Party> team2 = new ArrayList<>();

            boolean teamReset = false;
            int counter = 0;
            while (team1.stream().mapToLong(c -> c.getMembers().size()).sum() != teamSize || team2.stream().mapToLong(c -> c.getMembers().size()).sum() != teamSize) {

                if (team1.stream().mapToLong(c -> c.getMembers().size()).sum() != teamSize) {
                    team1.clear();
                }

                if (team2.stream().mapToLong(c -> c.getMembers().size()).sum() != teamSize) {
                    team2.clear();
                }

                // Iterate over all parties and add matching ones to the teams
                for (int i = counter; i < parties.size(); i++) {
                    if (team1.stream().mapToLong(c -> c.getMembers().size()).sum() + parties.get(i).getMembers().size() <= teamSize && !(team2.contains(parties.get(i)))) {
                        team1.add(parties.get(i));
                    } else if (team2.stream().mapToLong(c -> c.getMembers().size()).sum() + parties.get(i).getMembers().size() <= teamSize && !(team1.contains(parties.get(i)))) {
                        team2.add(parties.get(i));
                    }
                }

                // Reset search when first team is full
                if ((team1.stream().mapToLong(c -> c.getMembers().size()).sum() == teamSize || team2.stream().mapToLong(c -> c.getMembers().size()).sum() == teamSize) && !(teamReset)) {
                    counter = 0;
                    teamReset = true;
                }

                // Check if we iterated over all parties, if so exit the loop
                if (counter <= parties.size()) {
                    counter++;
                } else {
                    break;
                }
            }

            // Are there still teams found? If not, return the lobbies
            if (team1.stream().mapToLong(c -> c.getMembers().size()).sum() == teamSize && team2.stream().mapToLong(c -> c.getMembers().size()).sum() == teamSize) {
                matchedParty.setTeam1(team1);
                matchedParty.setTeam2(team2);
                lobbies.add(matchedParty);
                parties.removeAll(team1);
                parties.removeAll(team2);
            } else {
                return lobbies;
            }
        }

        // Maybe all parties found a team, if so return too
        return lobbies;
    }

因此,在阅读了评论者提供的几个链接后,@Andreas 的回答似乎是最正确的。看来我的问题是装箱问题的一个变体,只是在我的情况下,我确实要求只装满箱子。

我的问题大部分可以通过使用我已经在问题中发布的算法来解决,但是将所有条目从最高到最低排序,并且在填充大厅(垃圾箱)时,首先插入最大的条目。

您的算法通常是正确的方法,但您可以做一项改进。由于您只关心给定派对人数的精确匹配,我会将每个派对映射到包含给定人数的所有派对的列表: 即:{1: [parties of size 1], 2: [parties of size 2], ...}

再看看这个:

https://en.wikipedia.org/wiki/Partition_%28number_theory%29

棘手的一点是我们希望尽可能完美地匹配事物。基本上,我们希望从最少的参与方开始,到需要合并的最多参与方。 8 人聚会的 IE:8, 7 + 1, 6 + 2, 5 + 3 等等。一旦所有这些都匹配,我们就会查看需要组合 3 个部分的那些(总是按照从多到少的顺序):6 + 1 + 1, 5 + 2 + 1, 4 + 2 + 2... 然后是 4 个部分,然后是 5 个部分、6 个部分、7 个部分,然后最后 8 个部分(全部)。

看起来有 22 个分区,每分区 8 个,您可能只需对它们进行硬编码并对其进行循环。根据您的最大派对数量,您可以为您需要的所有派对数量构建分区 table。

这就是该算法在您的示例列表中的大致工作方式:

1, 6, 3, 8, 2, 1, 5, 3, 2

{party size: number of parties remaining}
{8: 1, 6: 1, 5: 1, 3: 2, 2: 2, 1: 1}
{6: 1, 5: 1, 3: 2, 2: 2, 1: 1} => (8)
{5: 1, 3: 2, 2: 1, 1: 1} => (8), (6,2)
{3: 1, 2: 1, 1: 1} => (8), (6,2), (5,3)
{3: 1, 2: 1, 1: 1}

如果您关注剩余派对的总数,您将停止检查是否有 3 + 2 + 1 = 6 < 8,因此您无法创建任何额外的有效派对。我相信这创造了派对的想法。

您打算举办 7 人聚会的示例:

2,2,4,3,1,5
{5: 1, 4: 1, 3: 1, 2: 2}
{4: 1, 3: 1, 2: 1} => (5,2),
{2: 1} => (5,2),(4,3)

再一次,7 的聚会不可能了。

至于生成分区,这似乎很可靠:

https://www.geeksforgeeks.org/generate-unique-partitions-of-an-integer/

是否需要优化取决于最大派对人数。 16231 个分区,但是 328349 仍然不错,但是 641741630,除非你有很多派对。 http://www.maths.surrey.ac.uk/hosted-sites/R.Knott/Partitions/partitions.html#pcalc1

编辑: 这里唯一的问题是小政党可能处于不利地位。在这种情况下,您可以颠倒搜索顺序,因此它会开始查找最小大小的聚会(全部)而不是最小大小(完整聚会)。我可能会在每次搜索 3-10 次派对搜索时颠倒搜索顺序。取决于你做的频率。

您可能还想尝试两个方向,然后只选择效果更好的那个。

倒序:

1, 6, 3, 8, 2, 1, 5, 3, 2
{8: 1, 6: 1, 5: 1, 3: 2, 2: 2, 1: 1}
{8: 1, 6: 1, 5: 1, 3: 1} => (1,2,2,3)
{6: 1} => (1,2,2,3),(3,5),(8)

虽然在这种情况下,两种方式都创建了 3 个完整的组,但我怀疑情况是否会一直如此。但是,请注意,这种方法确实将其减少到只有 6 人的 1 方,而不是 3、2 和 1 方。

这种方法基本上是为了尽可能减少当事人的数量。另一种方法旨在最大化完整组的数量。两者都有各自的用途,建议您同时使用,只是一个问题是您使用一个与另一个的频率。

嗯,第三个选项可能会从两边攻击它,但在这种情况下,你还有其他问题,它对中间的人有偏见。

1, 6, 3, 8, 2, 1, 5, 3, 2
{8: 1, 6: 1, 5: 1, 3: 2, 2: 2, 1: 1}
{6: 1, 5: 1, 3: 2, 2: 2, 1: 1} => (8)
{6: 1, 5: 1, 3: 1} => (8),(1,2,2,3)
{6: 1} => (8),(1,2,2,3),(5,3)

真的,如果你想让事情变得更有趣,你可以随机打乱所有分区和 运行 算法,尽管怀疑这会产生高于平均水平的结果。要点是 N 的整数分区是您需要查看的内容。