如何生成有限制的子集列表?

时间:2009-10-05 18:15:25

标签: algorithm combinatorics

我试图找出一种有效的算法来获取项目列表并生成所有唯一的子集,这些子集是将列表拆分为2个子列表的结果。我确信有一个通用的方法来做到这一点,但我对一个特定的案例感兴趣。我的列表将被排序,并且可能存在重复的项目。

一些例子:

输入
{1,2,3}

输出
{{1},{2,3}}
{{2},{1,3}}
{{3},{1,2}}

输入
{1,2,3,4}

输出
{{1},{2,3,4}}
{{2},{1,3,4}}
{{3},{1,2,4}}
{{4},{1,2,3}}
{{1,2},{3,4}}
{{1,3},{2,4}}
{{1,4},{2,3}}

输入
{1,2,2,3}

输出
{{1},{2,2,3}}
{{2},{1,2,3}}
{{3},{1,2,2}}
{{1,2},{2,3}}
{{1,3},{2,2}}

我可以在纸上做到这一点,但我正在努力找出一种以编程方式完成它的简单方法。我只是在寻找一个如何执行此操作的快速伪代码描述,而不是任何特定的代码示例。

感谢任何帮助。感谢。

4 个答案:

答案 0 :(得分:2)

如果要生成所有子集,则最终会为长度为 n 的列表生成2个 n 子集。一种常见的方法是迭代所有数字 i 从0到2 n -1并使用设置的位 i 确定哪些项目在 i 子集中。这是有效的,因为任何项目在任何特定子集中都存在或不存在,因此通过迭代 n 位的所有组合,您将遍历2 n < / sup>子集。

例如,要生成(1,2,3)的子集,您将遍历数字0到7:

  

0 = 000 b →()
  1 = 001 b →(1)
  2 = 010 b →(2)
  3 = 011 b →(1,2)
  4 = 100 b →(3)
  5 = 101 b →(1,3)
  6 = 110 b →(2,3)
  7 = 111 b →(1,2,3)

在您的问题中,您可以生成每个子集及其补码,以获得您的互斥子集对。当你这样做时,每一对都会重复,所以你只需要迭代2 n -1 - 1然后停止。

  

1 = 001 b →(1)+(2,3)
  2 = 010 b →(2)+(1,3)
  3 = 011 b →(1,2)+(3)

要处理重复项,您可以生成列表索引的子集而不是列表项的子集。与列表(1,2,2,3)一样,生成列表的子集(0,1,2,3),然后使用这些数字作为(1,2,2,3)列表的索引。基本上添加一个间接级别。

这里有一些Python代码将它们放在一起。

#!/usr/bin/env python

def split_subsets(items):
    subsets = set()

    for n in xrange(1, 2 ** len(items) / 2):
        # Use ith index if ith bit of n is set.
        l_indices = [i for i in xrange(0, len(items)) if n & (1 << i) != 0]
        # Use the indices NOT present in l_indices.
        r_indices = [i for i in xrange(0, len(items)) if i not in l_indices]

        # Get the items corresponding to the indices above.
        l = tuple(items[i] for i in l_indices)
        r = tuple(items[i] for i in r_indices)

        # Swap l and r if they are reversed.
        if (len(l), l) > (len(r), r):
            l, r = r, l

        subsets.add((l, r))

    # Sort the subset pairs so the left items are in ascending order.
    return sorted(subsets, key = lambda (l, r): (len(l), l))

for l, r in split_subsets([1, 2, 2, 3]):
    print l, r

输出:

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

答案 1 :(得分:1)

一些Erlang代码,问题是当你有重复的元素时会产生重复,所以结果列表仍然需要被过滤......

do([E,F]) -> [{[E], [F]}];
do([H|T]) -> lists:flatten([{[H], T}] ++
                           [[{[H|L1],L2},{L1, [H|L2]}]  || {L1,L2} <- all(T)]).

filtered(L) ->
  lists:usort([case length(L1) < length(L2) of true -> {L1,L2};
                                               false -> {L2,L1} end
              || {L1,L2} <- do(L)]).

在伪代码中,这意味着:

  • 对于两个长列表{E,F},结果为{{E},{F}}
  • 对于较长的列表,取第一个元素H和列表T的其余部分并返回
    • {{H},{T}}(第一个元素作为单个元素列表,剩下的列表)
    • 也为T递归运行算法,对于结果列表中的每个{L1,L2}元素,返回{{H,L1},{L2}}和{{L1},{H,L2}}

答案 2 :(得分:1)

以下C ++函数完全符合您的需要,但顺序与示例中的顺序不同:

// input contains all input number with duplicates allowed
void generate(std::vector<int> input) {
  typedef std::map<int,int> Map;
  std::map<int,int> mp;
  for (size_t i = 0; i < input.size(); ++i) {
    mp[input[i]]++;
  }

  std::vector<int> numbers;
  std::vector<int> mult;
  for (Map::iterator it = mp.begin(); it != mp.end(); ++it) {
    numbers.push_back(it->first);
    mult.push_back(it->second);
  }

  std::vector<int> cur(mult.size());
  for (;;) {
    size_t i = 0;
    while (i < cur.size() && cur[i] == mult[i]) cur[i++] = 0;
    if (i == cur.size()) break;
    cur[i]++;
    std::vector<int> list1, list2;
    for (size_t i = 0; i < cur.size(); ++i) {
      list1.insert(list1.end(), cur[i], numbers[i]);
      list2.insert(list2.end(), mult[i] - cur[i], numbers[i]);
    }
    if (list1.size() == 0 || list2.size() == 0) continue;
    if (list1 > list2) continue;
    std::cout << "{{";
    for (size_t i = 0; i < list1.size(); ++i) {
      if (i > 0) std::cout << ",";
      std::cout << list1[i];
    }
    std::cout << "},{";
    for (size_t i = 0; i < list2.size(); ++i) {
      if (i > 0) std::cout << ",";
      std::cout << list2[i];
    }
    std::cout << "}\n";
  }
}

答案 3 :(得分:0)

我的建议是......

首先,计算您拥有的每个值的数量,可能在哈希表中。然后计算要考虑的组合总数 - 计数的乘积。

迭代这些组合。

在每个组合中,复制循环计数(作为x),然后通过哈希表项启动内部循环。

对于每个哈希表项,使用(x模数)作为第一个列表中哈希表键的实例数。在重复内循环之前将x除以计数。

如果您担心组合数量可能会溢出整数类型,则问题是可以避免的。对每个项目使用一个数组(每个hashmap键一个)从零开始,并通过组合'count'将每个数组项作为一个数字处理(所以整个数组代表组合数),但每个'digit'都有一个数字不同的基数(相应的计数)。也就是说,要“递增”数组,首先递增项目0.如果它溢出(变得等于其计数),则将其设置为零并递增下一个数组项目。重复溢出检查,直到溢出继续超过数组末尾,结束。

我认为sergdev对第二个使用非常类似的方法,但是使用std :: map而不是哈希表(std :: unordered_map应该可以工作)。对于大量项目,散列表应该更快,但不会以任何特定顺序为您提供值。通过哈希表中的键的每个循环的顺序应该是一致的,但除非你添加/删除键。