【问题标题】:Fastest way to split a list into a minimum amount of sets, enumerating all possible solutions将列表拆分为最少数量的集合的最快方法,枚举所有可能的解决方案
【发布时间】:2021-08-28 18:33:15
【问题描述】:

假设我有一个包含复制者的数字列表。

import random
lst = [0, 0, 1, 2, 2, 2, 3, 3, 4, 4, 4, 4, 5, 6, 7, 7, 8, 8, 8, 9]
random.shuffle(lst)

我想将列表分成最少数量的子“集”,其中包含所有唯一数字,不丢弃任何数字。我设法写了下面的代码,但我觉得这是硬编码的,所以应该有更快更通用的解决方案。

from collections import Counter

counter = Counter(lst)
maxcount = counter.most_common(1)[0][1]
res = []
while maxcount > 0:
    res.append(set(x for x in lst if counter[x] >= maxcount))
    maxcount -= 1
assert len([x for st in res for x in st]) == len(lst)
print(res)

输出:

[{4}, {8, 2, 4}, {0, 2, 3, 4, 7, 8}, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}]

显然,这只是其中一种解决方案。另一种解决方案可能是

[{4, 9}, {8, 2, 4}, {0, 2, 3, 4, 7, 8}, {0, 1, 2, 3, 4, 5, 6, 7, 8}]

我想用最少的子“集”(在本例中为 4 个)找到所有可能的解决方案。请注意,相同的数字是无法区分的,例如对于[1, 2, 1] 的列表,[{1}, {1, 2}][{1, 2}, {1}] 的解决方案相同。

有什么建议吗?

【问题讨论】:

  • 为什么[{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}] 不是解决方案?我在您写的内容中找不到任何可以解释为什么结果必须包含 4 个集合的原因。
  • 我说的是没有丢弃任何数字
  • 您是否真的需要枚举所有可能的解决方案(如标题所述),或者可能解决方案的数量是否足够?
  • @BuddyBob 实际上没有烤过。句子模棱两可。 “任何数字”可以是“不丢弃唯一数字(数字)”,也可以表示“原始列表的每个成员都必须恰好在一个结果集中”。英语表达的精确性使一切变得不同。此外,正如 TimPeters 所提到的,没有明确要求产生四组。
  • 我想要所有可能的解决方案,这是最难的部分。

标签: python list algorithm set subset


【解决方案1】:

这种方式花费的时间与列表元素的数量呈线性关系,并且无论输入列表的顺序如何,它的输出都是相同的(相同的集合,相同的顺序)。它基本上是您代码的更“急切”的变体:

    def split(xs):
        from collections import defaultdict
        x2count = defaultdict(int)
        result = []
        for x in xs:
            x2count[x] += 1
            count = x2count[x]
            if count > len(result):
                result.append(set())
            result[count - 1].add(x)
        return result

然后,例如,

    xs = [0, 0, 1, 2, 2, 2, 3, 3, 4, 4, 4, 4, 5, 6, 7, 7, 8, 8, 8, 9]
    import random
    random.shuffle(xs)
    print(split(xs))

展示

[{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, {0, 2, 3, 4, 7, 8}, {8, 2, 4}, {4}]

找到所有答案肯定很烦人 ;-) 但足够简单。一旦你知道结果中有 4 个集合,那么你就有了一种毛茸茸的笛卡尔积来计算。你知道,例如,7 出现了两次,所以有comb(4, 2) == 6 方法可以选择两个结果集 7 进入。对于这些方法中的每一种,你知道,例如,8 出现 3 次,所以有comb(4, 3) == 4 方法可以选择三个结果集 8 进入。现在我们最多有 6 * 4 = 24 个部分结果。对所有其他原始整数类似地重复。 itertools.combinations() 可以用来做选择。

不清楚:考虑输入[1, 1, 2]。这里的输出是[{1, 2}, {1}]。您是否认为这与[{1}, {1, 2}] 相同?也就是说,您认为输出是一组集合(在这种情况下它们是不同的),还是作为一组集合(在这种情况下它们是相同的)?一个简单的笛卡尔积采用“它是一个序列”的观点。

找到所有的他们

这是一种方法。如图所示,它计算在所需输出集的数量上分布每个元素的所有方式的笛卡尔积。但不是为此使用itertools.product(),而是递归地执行它,一次一个元素。这允许它检查到目前为止的部分结果的同构,并拒绝扩展任何与它已经扩展的部分解决方案同构的部分解决方案。

为此,它将部分解决方案视为一组集合。出于技术原因,Python 需要使用 frozenset 来表示将依次用作集合元素的集合。

注意:这个生成器每次都会产生 same result 对象。那是为了效率。如果你不喜欢这样,你可以,例如,替换

            yield result

            yield result[:]

改为。

编辑:注意我替换了行

            sig = frozenset(map(frozenset, result))

            sig = frozenset(Counter(map(frozenset, result)).items())

这是因为您实际上并没有将结果视为一组集合,而是将结果视为一组多组(给定的集合可以在结果中出现多次,并且出现的次数很重要)。在比这里给出的更高级的测试用例中,这可以产生真正的影响。

Counter 是 Python 最接近内置多重集类型的东西,但没有类似于 freezesets 的“冻结”Counter。因此,我们将Counter 转换为 2 元组序列,并将这些元组放入 freezeset。通过使用(set, count) 对,这使我们能够说明一个集合在结果中出现的次数是显着的。

def allsplit(xs):
    from collections import Counter
    from itertools import combinations
    c = Counter(xs)
    n = max(c.values())
    result = [set() for i in range(n)]
    pairs = list(c.items())
    pin = len(pairs)

    def inner(pi):
        if pi >= pin:
            yield result
            return
        elt, count = pairs[pi]
        seen = set()
        for ixs in combinations(range(n), count):
            for i in ixs:
                result[i].add(elt)
            sig = frozenset(Counter(map(frozenset, result)).items())
            if sig not in seen:
                yield from inner(pi + 1)
                seen.add(sig)
            for i in ixs:
                result[i].remove(elt)

    return inner(0)

例子:

>>> for x in allsplit([1, 1, 2, 3, 8, 4, 4]):
...     print(x)
    
[{1, 2, 3, 4, 8}, {1, 4}]
[{1, 2, 3, 4}, {8, 1, 4}]
[{1, 2, 4, 8}, {1, 3, 4}]
[{1, 2, 4}, {1, 3, 4, 8}]

对于您的原始示例,它找到了 36992 种对输入进行分区的独特方法。

【讨论】:

  • 是的,你是对的。我应该说清楚。我更新了问题。无论它们在列表中的顺序如何,相同的数字都是相同的。
  • 我想知道这些是否可能是计算(多)项目集的混乱的构建块github.com/sympy/sympy/issues/6662
  • @smichr。我希望通过查看在二分图中有效枚举完美匹配的算法(一组节点对应于多重集的成员,另一组对应于紊乱中的序数位置,以及在当且仅当允许多集成员在给定位置结束时,一个集合才连接到另一个节点。
  • 谢谢@TimPeters。如果你已经签署了节点-有序-你的我认为这不会更好地确认你的身份:-)
  • 哈哈 ;-) 顺便说一句,再想一想,寻找完美二分匹配的算法可能有助于找到 a 多集紊乱(或确定是否可以扩展部分紊乱一个完整的),但毕竟我没有看到使用它们进行枚举的明确方法。例如,如果有元素 xk 实例,则直接枚举将产生这些元素的 k! 排列。
【解决方案2】:

我的简单解决方案,从最大到最小返回集合:

# Problem definition
import random
lst = [0, 0, 1, 2, 2, 2, 3, 3, 4, 4, 4, 4, 5, 6, 7, 7, 8, 8, 8, 9]
random.shuffle(lst)

# Divide in sets, biggest first
l = lst.copy()
sets = []
while l:
    sets.append(set(l))
    for item in sets[-1]:
        l.remove(item)

现在最难的部分是组合。我建议从类似和扩展的东西开始,接下来的内容只是一个概念证明,除了琐碎的“移动整个差异集”之外,避免重复。真正的实现将涵盖 set -> set 传输的所有组合(只需添加另一个 itertools.combinations 级别),但我不知道如何以一种巧妙的方式并行处理不同集合的移动元素。

import itertools
more_sets = [sets]
diff_0_1 = sets[0] - sets[1]
for comb_size in range(1, len(diff_0_1)):
    for comb in itertools.combinations(diff_0_1, comb_size):
        s0 = sets[0] - set(comb)
        s1 = sets[1] | set(comb)
        more_sets.append([s0, s1] + sets[2:])

for some_sets in more_sets:
    print(some_sets)

上面的代码返回这个:

~ python3.8 tmp.py
[{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, {0, 2, 3, 4, 7, 8}, {8, 2, 4}, {4}]
[{0, 2, 3, 4, 5, 6, 7, 8, 9}, {0, 1, 2, 3, 4, 7, 8}, {8, 2, 4}, {4}]
[{0, 1, 2, 3, 4, 6, 7, 8, 9}, {0, 2, 3, 4, 5, 7, 8}, {8, 2, 4}, {4}]
[{0, 1, 2, 3, 4, 5, 7, 8, 9}, {0, 2, 3, 4, 6, 7, 8}, {8, 2, 4}, {4}]
[{0, 1, 2, 3, 4, 5, 6, 7, 8}, {0, 2, 3, 4, 7, 8, 9}, {8, 2, 4}, {4}]
[{0, 2, 3, 4, 6, 7, 8, 9}, {0, 1, 2, 3, 4, 5, 7, 8}, {8, 2, 4}, {4}]
[{0, 2, 3, 4, 5, 7, 8, 9}, {0, 1, 2, 3, 4, 6, 7, 8}, {8, 2, 4}, {4}]
[{0, 2, 3, 4, 5, 6, 7, 8}, {0, 1, 2, 3, 4, 7, 8, 9}, {8, 2, 4}, {4}]
[{0, 1, 2, 3, 4, 7, 8, 9}, {0, 2, 3, 4, 5, 6, 7, 8}, {8, 2, 4}, {4}]
[{0, 1, 2, 3, 4, 6, 7, 8}, {0, 2, 3, 4, 5, 7, 8, 9}, {8, 2, 4}, {4}]
[{0, 1, 2, 3, 4, 5, 7, 8}, {0, 2, 3, 4, 6, 7, 8, 9}, {8, 2, 4}, {4}]
[{0, 2, 3, 4, 7, 8, 9}, {0, 1, 2, 3, 4, 5, 6, 7, 8}, {8, 2, 4}, {4}]
[{0, 2, 3, 4, 6, 7, 8}, {0, 1, 2, 3, 4, 5, 7, 8, 9}, {8, 2, 4}, {4}]
[{0, 2, 3, 4, 5, 7, 8}, {0, 1, 2, 3, 4, 6, 7, 8, 9}, {8, 2, 4}, {4}]
[{0, 1, 2, 3, 4, 7, 8}, {0, 2, 3, 4, 5, 6, 7, 8, 9}, {8, 2, 4}, {4}]

【讨论】:

    【解决方案3】:

    我建议使用预填充列表,然后将每个值存储在单独的存储桶中

    import random
    from collections import Counter
    
    lst = [0, 0, 1, 2, 2, 2, 3, 3, 4, 4, 4, 4, 5, 6, 7, 7, 8, 8, 8, 9]
    random.shuffle(lst)
    
    c = Counter(lst)
    maxcount = c.most_common(1)[0][1]
    result = [set() for _ in range(maxcount)]
    for k, v in c.items():
        for i in range(v):
            result[i].add(k)
    
    print(result)
    

    也可以通过defaultdict实现

    c = Counter(lst)
    result = defaultdict(set)
    for k, v in c.items():
        for i in range(v):
            result[i].add(k)
    result = list(result.values())
    print(result)
    

    性能说明

    from timeit import timeit
    import numpy as np
    lst = list(np.random.randint(0, 100, 10000))
    nb = 1000
    print(timeit(lambda: prefilled_list(lst), number=nb))   # 2.144
    print(timeit(lambda: default_dict_set(lst), number=nb)) # 1.903
    print(timeit(lambda: op_while_loop(lst), number=nb))    # 318.2
    

    【讨论】:

    • 只是测试你的第一个答案:它只找到一个解决方案,而 OP 想要所有解决方案。
    猜你喜欢
    • 2022-01-25
    • 1970-01-01
    • 1970-01-01
    • 2011-01-13
    • 1970-01-01
    • 2015-04-14
    • 1970-01-01
    • 2019-09-24
    • 1970-01-01
    相关资源
    最近更新 更多