给定一个列表,找出所有2个元素组合的特定排列。

3
给定一个由偶数(2k)个元素组成的列表L,我正在寻找一种算法来生成具有以下属性的2k-1个子列表:
1. 每个子列表都包括恰好k个来自L的2-组合(顺序不重要的对)。 2. 每个子列表都恰好包含来自L的每个元素一次。 3. 所有子列表中的所有元素的并集正好是L的所有可能的2-组合集合。
例如,如果输入列表为L=[a,b,c,d],则我们有k=2和3个子列表,每个子列表包括2个对。一个可能的解决方案看起来像[[ab,cd],[ac,bd],[ad,bc]]。如果忽略列表中所有元素的顺序(将所有列表视为集合),那么它也是k=2的唯一解决方案。
现在我的目标不仅是找到单个解决方案,而且要找到所有可能的解决方案。由于涉及的组合数量增长得非常快,因此最好以聪明的方式构建所有结果,而不是生成一个巨大的候选列表,并从中删除不符合给定属性的元素。这样一个天真的算法可能如下所示:
1. 找出L的所有2-组合的集合C。 2. 找出C的所有k-组合的集合D。 3. 选择所有联合等于L的集合从D中,称新集合为D'。 4. 找出D'的所有(2k-1)-组合的集合E。 5. 选择所有联合是集合C的集合从E中,并将新集合作为最终输出。
这个算法很容易实现,但对于更大的输入列表来说非常慢。那么有没有一种更有效地构建结果列表的方法呢?
编辑:下面是L=[a,b,c,d,e,f],k=3时上述算法计算的结果:
[[[ab,cd,ef],[ac,be,df],[ad,bf,ce],[ae,bd,cf],[af,bc,de]],
 [[ab,cd,ef],[ac,bf,de],[ad,be,cf],[ae,bc,df],[af,bd,ce]],
 [[ab,ce,df],[ac,bd,ef],[ad,be,cf],[ae,bf,cd],[af,bc,de]],
 [[ab,ce,df],[ac,bf,de],[ad,bc,ef],[ae,bd,cf],[af,be,cd]],
 [[ab,cf,de],[ac,bd,ef],[ad,bf,ce],[ae,bc,df],[af,be,cd]],
 [[ab,cf,de],[ac,be,df],[ad,bc,ef],[ae,bf,cd],[af,bd,ce]]]

所有属性都得到满足:

  1. 每个子列表都有k = 3 2组合,
  2. 每个子列表只包含每个元素一次,
  3. 一个解的五个子列表的并集正好是L的所有可能的2组合。

编辑2:基于用户58697的答案,我通过使用轮换赛程安排来改进了计算算法:

  1. 让S成为结果集,从一个空集开始,P是L的所有排列的集合。
  2. 重复以下步骤,直到P为空:
    • 从P中选择任意排列
    • 对于此排列执行完整的RRT调度。在每一轮中,来自L的元素的排列形成L的排列。从P中删除所有这些2k个排列。
    • 将得到的日程表添加到S中。
  3. 如果它们的子列表的并集具有重复元素(即不等于L的所有2组合),则从S中删除所有列表。

这个算法比第一个算法更有效率。我能够计算出k = 4时的结果数为960,k = 5时的结果数为67200。这个序列似乎没有OEIS结果,这让我怀疑这些数字是否正确,即算法是否生成了完整的解集。


你的列表L有2k个元素,每个“子列表”都是L分成k个大小为2的部分。恰好有(2k)!/(2^k k!)这么多种划分方法(对于k=2的情况,即3种)。现在,对于k=2,划分数3也恰好是2k-1。但一般来说,它会更大。例如,对于k=3,有15种将{a,b,c,d,e,f}划分为3个大小为2的部分的方法。你想要这15个划分的所有5个子集吗?还是只想要这15个划分? - ShreevatsaR
再往前迈进一步:对于k=4,有[105]种方法(https://oeis.org/A0011470)将列表{a,b,c,d,e,f,g,h}分成四个(无序)对。我认为你想要的只是105个分区的列表,并且你问题中提到的(2k-1)是一个错误。否则,如果你真的想要从这105个分区中得到所有大小为7的子集,那么它们的数量是[22760723700≈2.2×10^{10}](https://www.wolframalpha.com/input/?i=(105+choose+7))。你打算用它们做什么?还有就是忘了[k=5](https://www.wolframalpha.com/input/?i=(945+choose+9))。 - ShreevatsaR
2k-1是正确的,我认为。请注意,我不是在寻找您提到的大量组合,而这些只是我提出的算法中的临时候选项。最后一步会删除许多这些组合,因为它们的并集不能加起来等于输入列表的所有2组合。例如,如果k=3,则结果列表/集的数量为6,而不是15。 - siracusa
没关系,谢谢你的时间! - siracusa
我已经在回答中发布了我的代码,请告诉我您是否已经查看了它。在输入答案时,我意识到可以通过仅生成少量解并使用对称性来优化k=5的解决方案,但是没关系。顺便说一下,解的数量(1-因子分解的数量)恰好是组织轮流比赛的方式的数量。 - ShreevatsaR
显示剩余6条评论
2个回答

2
这是一种循环赛程安排方式:
  1. 一对是一场比赛,
  2. 一组列表是一轮比赛(每个团队都会与其他一些团队比赛),
  3. 一组列表的集合是整个锦标赛(每个团队与每个其他团队正好比赛一次)。
可以在这里查看更多信息。

1
那看起来像是一种替代算法,用于计算这样的调度。如何获取所有可能的调度(或者至少更多的结果,可以根据不同的指标选择最佳)? - siracusa

1
这是一个有趣的问题。在回答它的过程中(基本上是在编写下面包含的程序和查找OEIS上的序列之后),我了解到这个问题有一个名称和丰富的理论:你想要生成完全图K2k的所有1-因子分解

让我们首先用中文重新表述这个问题:

给定一个数字k和大小为2k的列表(集合)L。我们可以将L视为完全图K2k的顶点集。

  • 例如,当k=3时,L可以是{a, b, c, d, e, f}。

一个 1-factor (也称为 完美匹配 )是将L分成无序对(大小为2的集合)的一种方式。也就是说,它是一组k对,其不相交的并集为L。

例如,ab-cd-ef 是 L = {a, b, c, d, e, f} 的一个 1-因子。这意味着 ab 匹配,cd 匹配,ef 匹配。这样,L 被分成了三个集合 {a, b},{c, d} 和 {e, f},它们的并集为 L。

令S(在问题中称为C)表示L的所有元素对的集合。(就完全图而言,如果L是其顶点集,则S是其边集。)请注意,S包含(2k choose 2)= k(2k-1)对。因此,对于k = 0, 1, 2, 3, 4, 5, 6…,S的大小为0, 1, 6, 15, 28, 45, 66…

  • 例如,对于我们上面的L(k = 3,因此| S | = k(2k-1)= 15),S = {ab, ac, ad, ae, af, bc, bd, be, bf, cd, ce, cf, de, df, ef}。
1-因子分解是将S划分为集合的过程,其中每个集合本身都是一个1-因子(完美匹配)。注意,由于这些匹配中的每一个都有k对,而S的大小为k(2k-1),所以该分区的大小为2k-1(即由2k-1个匹配组成)。
例如,这是一个1-因子分解:{ab-cd-ef, ac-be-df, ad-bf-ce, ae-bd-cf, af-bc-de}。
换句话说,S的每个元素(每个对)恰好出现在1-因子分解的一个元素中,并且L的每个元素在1-因子分解的每个元素中恰好出现一次。
该问题要求生成所有的1-因子分解。

令M表示L的所有1-因子(完美匹配)的集合。很容易证明M包含(2k)!/(k!2^k) = 1×3×5×…×(2k-1)个匹配。对于k = 0, 1, 2, 3, 4, 5, 6…,M的大小为1, 1, 3, 15, 105, 945, 10395…

例如,对于我们的L来说,M = {ab-cd-ef, ab-ce-df, ab-cf-de, ac-bd-ef, ac-be-df, ac-bf-de, ad-bc-ef, ad-be-cf, ad-bf-ce, ae-bc-df, ae-bd-cf, ae-bf-cd, af-bc-de, af-bd-ce, af-be-cd}(对于k=3,这个数字15与配对数相同,但这只是一个巧合,因为你可以从其他数字中看出,这个数字增长得比配对数快得多。)

M很容易生成:

def perfect_matchings(l):
    if len(l) == 0:
        yield []
    for i in range(1, len(l)):
        first_pair = l[0] + l[i]
        for matching in perfect_matchings(l[1:i] + l[i+1:]):
            yield [first_pair] + matching

例如,调用perfect_matchings('abcdef')会产生15个元素['ab', 'cd', 'ef'], ['ab', 'ce', 'df'], ['ab', 'cf', 'de'], ['ac', 'bd', 'ef'], ['ac', 'be', 'df'], ['ac', 'bf', 'de'], ['ad', 'bc', 'ef'], ['ad', 'be', 'cf'], ['ad', 'bf', 'ce'], ['ae', 'bc', 'df'], ['ae', 'bd', 'cf'], ['ae', 'bf', 'cd'], ['af', 'bc', 'de'], ['af', 'bd', 'ce'], ['af', 'be', 'cd'],如预期所示。
根据定义,1-因子分解是S中来自M的元素的分区。或者等价地说,任何(2k-1)个不相交的M元素形成一个1-因子分解。这适用于简单的回溯算法:
  • 从空列表开始(部分因式分解)
  • 对于列表中的每个完美匹配,尝试将其添加到当前的部分因式分解中,即检查它是否不相交(它不应该包含任何已经使用的对)
    • 如果没问题,将其添加到部分因式分解中,并尝试扩展

在代码中:

matching_list = []
pair_used = defaultdict(lambda: False)
known_matchings = []  # Populate this list using perfect_matchings()
def extend_matching_list(r, need):
    """Finds ways of extending the matching list by `need`, using matchings r onwards."""
    if need == 0:
        use_result(matching_list)
        return
    for i in range(r, len(known_matchings)):
        matching = known_matchings[i]
        conflict = any(pair_used[pair] for pair in matching)
        if conflict:
            continue  # Can't use this matching. Some of its pairs have already appeared.
        # Else, use this matching in the current matching list.
        for pair in matching:
            pair_used[pair] = True
        matching_list.append(matching)
        extend_matching_list(i + 1, need - 1)
        matching_list.pop()
        for pair in matching:
            pair_used[pair] = False

如果你在填充了known_matchings之后,使用extend_matching_list(0, len(l) - 1)调用它,它将生成所有的1-因子分解。这里是一个完整的程序here。对于k=4(具体来说,列表'abcdefgh'),它输出6240个1-因子分解; 完整的输出在here
在这一点上,我将序列1、6、6240输入OEIS,并发现OEIS A000438,序列1、1、6、6240、1225566720、252282619805368320,...。它显示对于k=6,解的数量≈2.5×1017,这意味着我们放弃生成所有解的希望。即使对于k=5,约1十亿个解(回想一下,我们试图从|M|=945个匹配中找到2k-1=9个不相交的集合)也需要一些经过精心优化的程序。
第一个优化(令人尴尬的是,我只是通过查看k=4的跟踪输出后才意识到)是,在分区中选择的第一个匹配的索引(在自然字典编码下)不能大于k-1的匹配数。这是因为S的字典编码顺序(如“ab”)仅出现在这些匹配中,如果我们开始晚于这个位置,我们将永远不会在任何其他匹配中再次找到它。
第二个优化来自于回溯程序的瓶颈通常是测试当前候选项是否可行。我们需要有效地测试不相交性:即给定匹配(在我们的部分因数分解中)是否与所有先前匹配的并集不相交。(是否有任何k对是早期匹配已经覆盖的一对。)对于k = 5,S的大小为(2k选择2)= 45小于64,因此我们可以用64位整型紧凑地表示匹配(毕竟匹配是S的子集)。如果我们将这些对从0到44编号,则任何匹配都可以由具有在其包含元素对应位置处为1的整数表示。然后,测试不相交性就是整数的简单按位操作:我们只需检查当前候选匹配和我们部分因数分解中先前匹配的累积并(按位OR)的按位AND是否为零。
一段能够实现此功能的C++程序在这里,只有回溯部分(专为k=5定制)不需要任何C++特性,因此将其提取为C程序。 在我的笔记本电脑上运行大约4-5小时,并找到所有1225566720个1-因子化。
另一种看待这个问题的方法是说,如果它们相交(有一个共同的S元素对),则M的两个元素之间存在边缘,并且我们正在寻找M中的所有最大独立集。同样,解决这个问题的最简单方法可能仍然是回溯(我们将编写相同的程序)。
我们的程序可以通过利用问题中的对称性来提高效率:例如,我们可以选择任何匹配作为1-因子分解中的第一个1-因子(然后通过重新标记生成其余部分,注意不要避免重复)。这就是计算K12(当前记录)的1-因子分解数量的方法。

关于生成所有解的智慧

在《计算机程序设计艺术》第4A卷的7.2.1.2节“生成所有排列”结尾处,Knuth提出了这个重要的建议:

在进行排列之前三思。我们在本节中看到了几种有吸引力的排列生成算法,但是已知许多算法可以找到特定目的下最优的排列,而无需运行所有可能性。例如,[…] 在顺序存储上排列记录的最佳方法[…]只需要O(n log n)步。[…] 赋值问题,它询问如何重新排列方阵的列,使对角线元素的和最大[…]可以在不超过O(n3)次操作中解决,因此除非n非常小,否则使用n!的方法是愚蠢的。即使在像旅行推销员问题这样没有有效算法的情况下,我们通常也可以找到比检查每个可能解决方案更好的方法。当有充分理由单独查看每个排列时,最好使用排列生成。

这是似乎发生在这里的事情(根据下面问题的评论):
我想计算运行不同属性度量的所有解,并找到一个可选匹配[...]。由于结果数量似乎比预期增长得更快,这是不切实际的。
通常,如果您正在尝试“生成所有解决方案”,并且没有非常好的原因来查看每个解决方案(几乎从来没有),则有许多其他方法是首选的,从直接尝试解决优化问题,到生成随机解决方案并查看它们,或从某个子集生成解决方案(这似乎是您所做的)。

进一步阅读

跟进OEIS的参考文献可以了解到丰富的历史和理论。

  • 完全图的1-因子分解及其与轮流比赛时间表的关系,Gelling (M. A. Thesis),1973年

  • 完全图的1-因子分解数量,Charles C Lindner,Eric Mendelsohn,Alexander Rosa(1974年?)-- 这表明K2n非同构的1-因子分解数量随着n趋近于无穷大而趋近于无穷大。

  • E. Mendelsohn和A. Rosa。关于完全图的一些1-因子分解性质。Congr. Numer, 24 (1979): 739–752

  • E. Mendelsohn和A. Rosa。完全图的一个因子分解:一项调查。Journal of Graph Theory, 9 (1985): 43–65(早在1985年,这个确切的问题就已经被研究得足够好了,需要进行调查!)

  • 通过Dinitiz的论文

    • D. K. Garnick和J. H. Dinitz,12点完全图上的1-因子分解数量,Congressus Numerantium,94(1993),pp. 159-168。他们宣布正在计算K12的非同构1-因子分解数量。他们的算法基本上是回溯。
    • Jeffrey H. Dinitz,David K. Garnick,Brendan D. McKay:K12有526,915,620个非同构一因子分解(也可以在这里),组合设计杂志2(1994),pp. 273 - 285:他们完成了计算,并报告了他们发现的K12的数字(526,915,620个非同构,252,282,619,805,368,320个总数)。
  • Gopal、Kothapalli、Venkaiah、Subramanian(2007年)的各种完全图1-因子分解。这篇文章与此问题相关,并具有许多有用的参考资料。

  • W. D. Wallis,《组合设计导论》,第二版(2007)。第10章是“一个因子分解”,第11章是“一个因子分解的应用”。两者都非常相关,并且具有许多有用的参考资料。

  • Charles J. Colbourn和Jeffrey H. Dinitz,《组合设计手册》,第二版(2007)。 宝藏。请参阅第VI.3平衡锦标赛设计,VI.51调度锦标赛,VII.5图的因子分解(包括其5.4枚举和表格,5.5完全图的一些1-因子分解),VII.6设计理论中的计算方法(6.2穷举搜索)。这最后一章引用:

    • [715] 如何计算K12(“有序算法”),回溯--上面提到的Dinitz-Garnick-McKay论文
    • [725] “包含许多与因子分解相关的主题的快速算法,可用于查找K2n的1-因子分解。”(“房间平方及其相关设计”,J. H. Dinitz和S. R. Stinson)
    • [1270](P. Kaski和P. R. J. Östergård,12阶正则图形
      一些其他的东西:

在谷歌上搜索98758655816833727741338583040(目前为止计算出的最大值)会给出一些相关结果,例如计算它的论文 - ShreevatsaR

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接