首页 > 解决方案 > 在 Python 中的某些约束下生成集合的最大子集

问题描述

我有一组属性A= {a1, a2, ...an}和一组集群C = {c1, c2, ... ck},我有一组对应关系COR,它们是A x C和的子集|COR|<< A x C。这是一组对应的样本

COR = {(a1, c1), (a1, c2), (a2, c1), (a3, c3), (a4, c4)}

现在,我想生成这样的所有子集,COR使得子集中的每一对都代表一个从 setA到 set的单射函数C。让我们称每个这样的子集为一个映射,那么来自上述集合的有效映射COR将是有趣的
m1 = {(a1, c1), (a3, c3), (a4, c4)}m2 = {(a1, c2), (a2, c1), (a3, c3), (a4, c4)}
m1因为添加任何剩余的元素 from要么违反函数的定义,要么违反作为单射函数的COR条件m1. 例如,如果我们将对添加(a1,c2)m1,m1将不再是一个函数,如果我们添加(a2,c1)m1,它将不再是单射函数。因此,我对一些可用于生成所有此类映射的代码片段或算法感兴趣。这是我迄今为止在 python import collections import itertools 中尝试过的

corr = set({('a1', 'c1'), ('a1', 'c2'), ('a2', 'c1'), ('a3', 'c3'), ('a4', 'c4')})
clusters = [c[1] for c in corr]
attribs = [a[0] for a in corr]
rep_clusters = [item for item, count in collections.Counter(clusters).items() if count>1]
rep_attribs = [item for item, count in collections.Counter(attribs).items() if count>1]
conflicting_sets = []
for c in rep_clusters:
    conflicting_sets.append([p for p in corr if p[1] == c])
for a in rep_attribs:
    conflicting_sets.append([p for p in corr if p[0] == a])
non_conflicting  = corr
for s in conflicting_sets:
    non_conflicting = non_conflicting - set(s)
m = set()
for p in itertools.product(*conflicting_sets):
    print(p, 'product', len(p))
    p_attribs = set([k[0] for k in p])
    p_clusters = set([k[1] for k in p])
    print(len(p_attribs), len(p_clusters))
    if len(p) == len(p_attribs) and len(p) == len(p_clusters):
        m.add(frozenset(set(p).union(non_conflicting)))
print(m)

正如预期的那样,代码会产生m2但不是m1因为m1不会从itertools.product. 有人可以指导我吗?我还想要一些关于性能的指导,因为实际的集合会比COR这里使用的集合大,并且可能包含更多冲突的集合。

标签: pythonsetsubsetitertoolsinjective-function

解决方案


您的要求的一个更简单的定义是:

  • 你有一组独特的元组。
  • 您想要生成所有子集:
    • 元组的所有第一个元素都是唯一的(以确保功能);
    • 并且所有第二个元素都是唯一的(以确保注入性)。
  • 您的标题表明您只想要最大子集,即在不破坏其他要求的情况下,必须不可能从原始集合中添加任何其他元素。

我还假设任何a<x>orc<y>是唯一的。

这是一个解决方案:

def get_maximal_subsets(corr):
    def is_injective_function(f):
        if not f:
            return False
        f_domain, f_range = zip(*f)
        return len(set(f_domain)) - len(f_domain) + len(set(f_range)) - len(f_range) == 0

    def generate_from(f):
        if is_injective_function(f):
            for r in corr - f:
                if is_injective_function(f | {r}):
                    break
            else:
                yield f
        else:
            for c in f:
                yield from generate_from(f - {c})

    return list(map(set, set(map(frozenset, generate_from(corr)))))


# representing a's and c's as strings, as their actual value doesn't matter, as long as they are unique
print(get_maximal_subsets(corr={('a1', 'c1'), ('a1', 'c2'), ('a2', 'c1'), ('a3', 'c3'), ('a4', 'c4')}))

该测试通过从函数的域和范围中获取所有值并检查两者是否仅包含唯一值来is_injective_function检查提供的集合是否表示有效的单射函数。f

生成器接受一个f,如果它代表一个有效的单射函数,它会检查从原始corr到达的元素中没有一个f可以被添加回来,同时仍然让它代表一个有效的单射函数。如果是这种情况,它会产生f一个有效的结果。

如果f不是一个有效的单射函数,它将尝试f依次删除每个元素并从每个子集中生成任何有效的单射函数。

最后,整个函数从生成的生成器中删除重复项,并将其作为唯一集列表返回。

输出:

[{('a1', 'c1'), ('a3', 'c3'), ('a4', 'c4')}, {('a2', 'c1'), ('a3', 'c3'), ('a4', 'c4'), ('a1', 'c2')}]

请注意,有几种方法可以对非哈希值列表进行重复数据删除,但这种方法将列表中的所有集合转换为 afrozenset以使其可哈希,然后将列表转换为集合以删除重复项,然后再次将内容转换为集合并将结果作为列表返回。

您可以通过跟踪已尝试删除的子集来防止最后删除重复项,这可能会根据您的实际数据集表现更好:

def get_maximal_subsets(corr):
    def is_injective_function(f):
        if not f:
            return False
        f_domain, f_range = zip(*f)
        return len(set(f_domain)) - len(f_domain) + len(set(f_range)) - len(f_range) == 0

    previously_removed = []

    def generate_from(f, removed: set = None):
        previously_removed.append(removed)
        if removed is None:
            removed = set()
        if is_injective_function(f):
            for r in removed:
                if is_injective_function(f | {r}):
                    break
            else:
                yield f
        else:
            for c in f:
                if removed | {c} not in previously_removed:
                    yield from generate_from(f - {c}, removed | {c})

    return list(generate_from(corr))

这可能是一个通常性能更好的解决方案,但我更喜欢第一个的干净算法来更好地解释。

在评论询问它是否可以扩展到 100 个元素和约 15 个冲突(它会运行很多分钟来解决它)之后,我对上述解决方案的缓慢感到恼火,所以这里有一个更快的解决方案,100 个元素在 1 秒内运行有 15 个冲突,虽然执行时间仍然呈指数增长,所以它有其限制):

def injective_function_conflicts(f):
    if not f:
        return {}
    conflicts = defaultdict(set)
    # loop over the product f x f
    for x in f:
        for y in f:
            # for each x and y that have a conflict in any position
            if x != y and any(a == b for a, b in zip(x, y)):
                # add x to y's entry and y to x's entry
                conflicts[y].add(x)
                conflicts[x].add(y)
    return conflicts


def get_maximal_partial_subsets(conflicts, off_limits: set = None):
    if off_limits is None:
        off_limits = set()

    while True and conflicts:
        # pop elements from the conflicts, using them now, or discarding them if off-limits
        k, vs = conflicts.popitem()
        if k not in off_limits:
            break
    else:
        # nothing left in conflicts that's not off-limits
        yield set()
        return

    # generate each possible result from the rest of the conflicts, adding the conflicts vs for k to off_limits
    for sub_result in get_maximal_partial_subsets(dict(conflicts), off_limits | vs):
        # these results can have k added to them, as all the conflicts with k were off-limits
        yield sub_result | {k}
    # also generated each possible result from the rest of the conflicts without k's conflicts
    for sub_result in get_maximal_partial_subsets(conflicts, off_limits):
        # but only yield as a result if adding k itself to it would actually cause a conflict, avoiding duplicates
        if sub_result and injective_function_conflicts(sub_result | {k}):
            yield sub_result


def efficient_get_maximal_subsets(corr):
    conflicts = injective_function_conflicts(corr)
    final_result = list((corr - set(conflicts.keys())) | result
                        for result in get_maximal_partial_subsets(dict(conflicts)))
    print(f'size of result and conflict: {len(final_result)}, {len(conflicts)}')
    return final_result


print(efficient_get_maximal_subsets(corr={('a1', 'c1'), ('a1', 'c2'), ('a2', 'c1'), ('a3', 'c3'), ('a4', 'c4')}))

推荐阅读