生成集合的所有划分

10

对于一个形如A = {1, 2, 3, ..., n}的集合。如果存在一个大小为k<=n的集合,满足以下定理,则称其为集合A的一个划分:

a) 所有划分的并集等于原集合A

b) 任意两个划分的交集为空集(它们不能拥有共同元素)。

例如,对于集合A = {1, 2,... n},我们可以得到以下划分:

{1, 2, 3}
{1, 2} {3}
{1, 3} {2}
{2, 3} {1}
{1} {2} {3}

这些理论概念在我的算法教材中介绍(顺便说一下,这个子章节是“回溯”章节的一部分)。我需要找到一个算法来生成给定集合的所有划分。我整天都在苦苦思考这个问题,但是我找不到解决方案。你能否向我解释一下这个算法是如何工作的?同时,你能给我提供一个伪代码草图吗?


3
假设你已经拥有了 {1, ..., n-1} 的所有划分集合。其中,一个划分集合中的各个子集通常被称为“部分”。那么,是否有一种方法可以通过将元素 n 注入到每个划分集合中来生成 {1, ..., n} 的每个划分呢?这样做会确保每个 {1, ..., n} 的划分都能够被恰好生成一次吗? - j_random_hacker
1
是的,到目前为止你说得对。有两件事情需要记住:1. 当一个分区包含多个部分(比如你例子中的{1} {2}),你需要尝试将新元素添加到每个部分 -- 这样你就会为每个部分创建一个全新的分区。2. 你还需要创建包含新数字单独在一个部分中的分区。 - j_random_hacker
1
我在你编辑评论之前就回复了它。你只需要考虑如何从先前的分区生成当前分区;要生成先前的分区,你只需调用自己(递归)。 (你还需要处理一个基本情况——当集合为空时。)如果你展开执行,这将在4之前插入3。 - j_random_hacker
2
@cristid9:你提到将分区重写为数组的观察非常重要;这导致了一个简单的迭代解决方案,我已经提交了答案,并提供了一般的枚举策略。 - rici
3
正如我在答案中所说的,将其转换成O(1)需要维护最大值而不是重新计算。在您的递归解决方案中,您可以通过将最大值作为第五个参数传递来实现这一点。在对printPartitions的递归调用中,您传递i > maximum ? i : maximum。这可以通过观察到,在除了最后一个递归调用之外的所有递归调用中,您只需传递最大值,在最后一个递归调用中,您传递最大值加一来使其更有效率。这会使代码变得简单些。请参见https://ideone.com/6vaXaN。 - rici
显示剩余11条评论
3个回答

33

有一个重要的观察结果(在注释中),即一个包含n个元素的集合的划分可以表示为形如[p1, … pn]的整数序列,其中pi是元素i的划分号。为了使这样的序列有效,必须遵守规则:p1等于1,并且对于每个1 < jnj,存在某个 i < j满足pjpi+1。换句话说,在序列的任何前缀中,都不会跳过任何整数。

现在,有一种标准算法,可以按词典顺序列举有限制的序列,该算法包括以下步骤:

  • 从最小序列开始。
  • 要找到下一个按字典顺序的序列:
    • 向后扫描序列,找到最右边的“可递增”元素。(可递增的元素是这样一个元素,即有一些更大的元素可以替换该元素,并且在该点之前产生的子序列是至少一个有效序列的前缀。)
    • 将该元素更改为下一个可行的更大元素(即导致一个有效前缀的下一个元素,如上所述),然后用最小可能的值填充其右侧的剩余元素(如果有)。
    • 如果没有可递增的元素,则枚举已终止。

这个算法有几个关于寻找可增量元素的约定,最坏情况为O(n),当要增加的元素通常接近序列末尾时通常为O(1)。(例如,使用此算法枚举排列是O(1),只要您可以在O(1)中找到“下一个元素”)。

为了将此算法应用于分区案例,我们观察以下内容:

  1. 最小可能的序列是[1,… 1]
  2. 如果满足以下条件,则元素pi是可增量的:
    • pi<n
    • 存在某个j<i,使得pi=pj
  3. 可行前缀的最小后缀是[1,… 1]

陈述观察2中的条件的另一种方式是,除非元素的值为n或它是具有其值的序列中的第一个元素,否则该元素是可增量的。如果我们还维护序列[m1,… mn],其中m1为0,mimi-1pi-1的最大值。维护m很容易,它允许我们将增量条件简单地重新编写为pimi

很容易看出,Next Partition 可以在O(n)时间内实现,但事实上它的分摊时间也是O(1)。大致来说,这是因为在大多数情况下,序列的最后一个元素是可以递增的。


这个算法会生成“同构”分区吗?例如,它会同时生成{1, 1, 1, 2}和{2, 2, 2, 1}(它们定义了相同的分区)吗? - Zack
1
@zack:不会的。有效性规则(之前是错误的,但现在已经修复)保证只生成每个同构分组的一个。 - rici
7
这个回答是一颗被大大低估的宝石。我希望我能给它更多赞。 - kjo
@rici,你能帮我看一下这个问题吗?https://dev59.com/-qrka4cB1Zd3GeqPXw1V - famedoro
1
我同意这种方法要好得多。此外,对于一个包含16个元素的集合,其集合划分数约为$10^{10}$ - 因此生成更大集合的划分是非常值得怀疑的... - Yauhen Yakimenka

3
如果您所处理的集合不太大(否则请使用堆栈),可以尝试递归解答:
原则如下,您需要一个函数来返回以下内容:
rec_func(SET) = List of List of Set

并按照以下方式进行工作:
rec_func(SET) =
  if SET = {empty}:
    // if no element, easy to give the answer
    return([[]])
  else:
    // 1. Remove one element from the set : 'a' to this set
    a = SET.pop()
    // 2. Call rec_func :
    list_of_list_of_set = rec_func(SET\'a')  
    response = []
    // 3. For every possibilities given by the function add the element 'a' :
    For every list_of_set in list_of_list_of_set  :
       // Case 1, you add 'a' to list_of_set
       response.push( [{'a'} + list_of_set] )
       // Case 2, for every set, you create a copy where you add 'a'
       for every set in list_of_set:
           response.push( [{set,'a'} + list_of_set\set] )

    // The function return the list of list of set created.        
    return(response)

@cristid9 抱歉,操作错误,我正在写答案... - Emmanuel Jay
如果您需要更具体的实现,请告诉我您熟悉的编程语言,以便我编写该函数。 - Emmanuel Jay
我熟悉C语言,但我只需要了解一般的概念。 - cristid9
@EmmanuelJay,你能帮我解决https://dev59.com/-qrka4cB1Zd3GeqPXw1V这个问题吗? - famedoro
什么情况会导致您进入不同的情况?为什么要执行情况1而不是情况2? - Cuhrazatee

-2
我正在研究一种高效的算法,根据@rici定义的关键字方法,生成所有集合的分区。下面这个用Python编写的算法可以实现这一目标,并且仍有优化的空间。正如你所知,这个问题是NP完全问题!由于优化,可能会出现一些奇怪的符号,比如try/except。然而,n和k变量用于通过n定义集合中有多少不同的元素,k是允许集合具有的不同类别的数量。信息:该算法生成所有分区,直到不同类别的数量,而不是仅限于这些类别!!!
def partitions():
        global n
        global k
        codeword = [1 for digitIndex in range(0, n)]
        while True:
                print codeword
                startIndex = n - 1
                while startIndex >= 0:
                        maxValue = max(codeword[0 : startIndex])
                        if codeword[startIndex] > maxValue or maxValue > k or codeword[startIndex] >= k:
                                codeword[startIndex] = 1
                                startIndex -= 1
                        else:
                                codeword[startIndex] += 1
                                break

n = 12
k = 2
try:
        partitions()
except:
        pass

有趣的,-1。 - Erhard Dinhobl

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