从一组对中生成组合而无需重复元素


28

我有一对。每对都具有(x,y)的形式,使得x,y属于范围内的整数[0,n)

因此,如果n为4,那么我有以下几对:

(0,1) (0,2) (0,3)
(1,2) (1,3) 
(2,3) 

我已经有一对了。现在,我必须使用n/2对构建一个组合,这样就不会重复任何整数(换句话说,每个整数在最终组合中至少出现一次)。以下是正确和不正确组合以更好地理解的示例

 1. (0,1)(1,2) [Invalid as 3 does not occur anywhere]
 2. (0,2)(1,3) [Correct]
 3. (1,3)(0,2) [Same as 2]

一旦我有了配对,有人可以建议我一种生成所有可能组合的方法。


也许通过使用2d数组来表示您的对。有效组合对应于n个数组单元的选择,因此每一行和每一列正好包含1个所选单元。
2012年

4
您是说输入是所有对的集合吗?如果是这样,那么您应该只说输入为ñ
rgrig 2012年

2
总是偶数?如果不是,则语句​​“不重复整数”和“每个整数在最终组合中至少出现一次”是矛盾的。n
Dmytro Korduban 2012年

1
与@rgrig相同的问题:输入是否全部为无序对,或者是任意可能的对集?如果是所有对,则只需说输入为,就无需给出列表。ñ
卡夫

1
您有兴趣在由初始对对定义的n个点上生成图形的所有完美匹配。此外,似乎您认为该图是这些点上的完整图。如果您提到这一点,您的问题将会更清楚。有n 1 )个= 1 × 3 × 5 × × n 1 这样的匹配。n(n1)!!:=1×3×5××(n1
马克·范·吕文

Answers:


14

一种直接的方法是递归过程,该过程在每次调用时执行以下操作。该过程的输入是已选择的对的列表和所有对的列表。

  1. 计算输入列表中尚未涵盖的最小数字。对于第一次调用,当然将为0,因为尚未选择任何对。
  2. 如果所有数字均已覆盖,则您的组合正确,将其打印出来并返回上一步。否则,发现的最小数量就是我们要追求的目标。
  3. 搜索对,寻找覆盖目标号码的方法。如果没有,则只需返回上一级递归。
  4. 如果有一种方法可以覆盖目标编号,请选择第一种方法,然后再次递归调用整个过程,将刚刚选择的对添加到所选对的列表中。
  5. 当返回时,寻找用一对覆盖目标号码而不重叠先前选择的对的一种方法。如果找到一个,请选择它,然后再次递归调用下一个过程。
  6. 继续执行步骤4和5,直到没有其他方法可以覆盖目标号码为止。浏览整个配对对。如果没有其他正确的选择,请返回上一级递归。

可视化此算法的方法是使用一棵树,该树的路径是不重叠对的序列。树的第一级包含所有包含0的对。对于上面的示例,树为

           根
             |
     ----------------
     | | |
   (0,1)(0,2)(0,3)
     | | |
   (2,3)(1,3)(1,2)

在此示例中,穿过树的所有路径实际上都给出了正确的集合,但是例如,如果我们省略对(1,2),则最右边的路径将只有一个节点,并且对应于步骤3中的搜索失败。

可以针对枚举特定类型的所有对象的许多类似问题开发这种类型的搜索算法。


有人建议说,OP可能意味着所有对都在输入中,而不仅仅是问题中提到的一组。在那种情况下,该算法要容易得多,因为不再需要检查允许的配对。甚至没有必要生成所有对的集合。以下伪代码将执行OP要求的操作。这里是输入数字,“ list”开始是一个空列表,“ covered”是长度为n的数组,初始化为0。可以提高效率,但这不是我的近期目标。ññ

sub cover {
  i = 0;
  while ( (i < n) && (covered[i] == 1 )) {
   i++;
  }
  if ( i == n ) { print list; return;}
  covered[i] = 1;
  for ( j = 0; j < n; j++ ) {
    if ( covered[j] == 0 ) {
      covered[j] = 1;
      push list, [i,j];
      cover();
      pop list;
      covered[j] = 0;
    }
  }
  covered[i] = 0;
}

这应该可行,但这可能不是最有效的方法。
2012年

2
最后,重点在于以某种方式枚举该树的路径。如果输入列表中的对数远远小于可能的对数,则这种算法将非常有效,特别是如果使用一些哈希表来帮助记住每个步骤已经覆盖了哪些数,那么可以在固定时间内查询。
卡尔·默默特

如果列表使用指针,那么Knuth的Dancing Links值得一看。当您从表单返回时,必须恢复列表的先前状态。
乌里2012年

10

您可以迭代解决。假设您具有[ 0 n 范围的所有解。然后,您可以轻松地从S n构造解S n + 2。随着n的增加,大小会非常快地增长,因此编写生成器而不是将所有集合保存在内存中可能是个好主意,请参见下面的Python示例。Sn[0,n)Sn+2Snn

def pairs(n):
    if (n%2==1 or n<2):
        print("no solution")
        return
    if (n==2):
        yield(  [[0,1]]  )
    else:
        Sn_2 = pairs(n-2) 
        for s in Sn_2:
            yield( s + [[n-2,n-1]] )
            for i in range(n/2-1):
                sn = list(s)
                sn.remove(s[i])
                yield( sn + [ [s[i][0], n-2] , [s[i][1], n-1] ] )
                yield( sn + [ [s[i][1], n-2] , [s[i][0], n-1] ] )

您可以通过致电列出所有对

for x in pairs(6):
   print(x)

6

更新:我之前的答案涉及二部图,OP并未对此提出要求。我暂时保留它,作为相关信息。但更相关的信息与非二分图中的完美匹配有关。

在这方面,Propp有一份不错的调查,概述了进展情况(直到1999年)。该文章中的某些想法以及相关链接可能被证明是有用的。TL; DR是-这很棘手:)

---旧答案的开始

请注意,您要执行的操作是在二分图上枚举所有可能的完美匹配。有许多不同的算法可以做到这一点,尤其是最近的一种算法来自ISAAC 2001

基本思想是通过使用网络流找到一个完美的匹配,然后通过使用交替循环来反复修改此匹配(有关更多信息,请参阅有关网络流的任何算法教科书章节)。


二部图由具有给定标签[0,n]的两个集合组成,并且当且仅当(i!= j)时存在边(i,j)
Joe

ñķñ

2
永久物计算答案。但OP想一一列举
苏雷什

由于图的结构,它们都是同构的,因此考虑应用置换可能是一个好主意(但问题是它将创建重复项)。
卡夫2012年

4

您选择的每一对都消除了您无法再选择的两行。这个想法可用于设置递归算法(在Scala中):

def combine(pairs : Seq[(Int,Int)]) : Seq[Seq[(Int, Int)]] = pairs match {
  case Seq() => Seq()
  case Seq(p) => Seq(Seq(p))
  case _ => {
    val combinations = pairs map { case (a,b) => {
      val others = combine(pairs filter { case (c,d) =>
        a != c && a != d && b != c && b != d
      })

      others map { s => ((a,b) +: s) }
    }}

    combinations.flatten map { _.sorted } distinct
  }
}

当然,这可以更有效地表达。特别是,对的调用未使用不必考虑整个行进行组合的想法filter


这是否还会返回不包含每个数字但不能扩展的组合,因为原始序列中没有对可以扩展它们?如果是这样,则需要过滤掉这些组合。
卡尔·默默特

ñ2ñ

01个ñ=4

是。但是正如我说的,我的答案涉及OP提出的方案,即不是任意输入。
拉斐尔

当我阅读原始问题时,它是关于任意对的集合,OP从未说过所有对都是可能的。但是,我同意,OP对此可能会更加清楚。
卡尔·默默特

4

尽管这个问题已经有很多可爱的答案了,但我认为指出它们背后的基本,一般技巧是很好的。

如果您可以对要组合的元素进行总排序,则生成唯一组合会容易得多。这样,如果我们仅允许排序的组合,则可以保证唯一性。也不难生成排序后的组合-只需执行通常的蛮力枚举搜索,但是在每个步骤中仅选择比在每个步骤中已选择的元素更大的元素。

在这个特定问题中,额外的复杂之处是只希望获得长度为n / 2(最大长度)的组合。如果我们决定一个好的分类策略,这并不难。例如,正如卡尔·穆梅特(Carl Mummet)的答案所指出的那样,如果我们考虑字典顺序的排序(问题图中,图的自上而下,从左到右),我们将得出始终采用下一个元素以使其第一个数字为最小的未使用数量。

如果我们要生成其他长度的序列,也可以扩展此策略。只需记住,只要选择下一个元素的第一个数字不是可用的最小元素,我们就会排除一排或多排元素出现在排序后的子序列上,因此预置换的最大长度会相应减少。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.