【问题标题】:How to generate a list of subsets with restrictions?如何生成有限制的子集列表?
【发布时间】:2010-12-04 01:01:52
【问题描述】:

我正在尝试找出一种有效的算法来获取项目列表并生成将列表拆分为恰好 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}}

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

感谢任何帮助。谢谢。

【问题讨论】:

    标签: algorithm combinatorics


    【解决方案1】:

    如果您要生成所有子集,您最终会为长度为 n 的列表生成 2n 个子集。一种常见的方法是遍历从 0 到 2n-1 的所有数字 i 并使用在i 来确定哪些项目在第 i 个子集中。这是有效的,因为任何项目要么存在或不存在于任何特定子集中,因此通过遍历 n 位的所有组合,您可以遍历 2n 子集。

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

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

    在您的问题中,您可以生成每个子集及其补集,以获得您的一对互斥子集。执行此操作时,每一对都会重复,因此您只需迭代最多 2n-1 - 1 然后停止。

    1 = 001b → (1) + (2, 3)
    2 = 010b → (2) + (1, 3)
    3 = 011b → (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)
    

    【讨论】:

    • 有趣的想法。基本上将补码操作简化为 XOR。我认为当列表中存在重复项时,这种方法仍然会弄乱排序唯一集合。也许我只是误解了你的答案。
    • 当我们有相等数字的输入列表时,这种方法效率很低。例如{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}。它将生成 2 ^(“1”的数量)。当然,将在 subsets.add((l, r)) 行中过滤重复项(对不起,如果我遗漏了什么,我不熟悉 python)。但无论如何,时间复杂度都会比可能的高得多。
    【解决方案2】:

    以下 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";
      }
    }
    

    【讨论】:

    • 将其转换为 C# 的好时机 :) 它像宣传的那样工作。谢谢。
    【解决方案3】:

    一点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}}李>

    【讨论】:

      【解决方案4】:

      我的建议是……

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

      遍历该数量的组合。

      在每个组合中,复制您的循环计数(作为 x),然后通过您的哈希表项​​开始一个内部循环。

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

      如果您担心组合的数量可能会溢出您的整数类型,那么这个问题是可以避免的。使用从零开始的每个项目(每个哈希图键一个)的数组,并通过将每个数组项目视为数字的组合“计数”(因此整个数组表示组合编号),但每个“数字”都有一个不同的基数(相应的计数)。也就是说,要“增加”数组,首先增加第 0 项。如果溢出(变为等于其计数),请将其设置为零并增加下一个数组项。重复溢出检查,直到如果溢出继续超过数组的末尾,您就完成了。

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-06-11
        • 1970-01-01
        • 2023-01-31
        • 1970-01-01
        • 2021-05-10
        • 2018-12-01
        相关资源
        最近更新 更多