29

我有一组独特的集合(表示为位掩码),并希望消除作为另一个元素的真子集的所有元素。例如:

input = [{1, 2, 3}, {1, 2}, {2, 3}, {2, 4}, {}]
output = [{1, 2, 3}, {2, 4}]

我一直无法为此找到标准算法,甚至无法找到此问题的名称,因此我将其称为“最大子集”,因为没有其他任何内容。这是一个 O(n^2) 算法(在 Python 中具体来说),假设is_subset_func是 O(1): 1

def eliminate_subsets(a, cardinality_func, is_subset_func):
    out = []
    for element in sorted(a, reverse=True, key=cardinality_func):
        for existing in out:
            if is_subset_func(element, existing):
                break
        else:
            out.append(element)
    return out

是否有更有效的算法,希望 O(n log n) 或更好?


1 对于恒定大小的位掩码,在我的情况下是真的,is_subset_funcis just element & existing == element,它在恒定时间内运行。

4

4 回答 4

15

假设您标记所有输入集。

A={1, 2, 3}, B={1, 2}, C={2, 3}, D={2, 4}, E={}

现在构建中间集,宇宙中的每个元素一个,包含它出现的集合的标签:

1={A,B}
2={A,B,C,D}
3={A,C}
4={D}

现在对于每个输入集计算其元素的所有标签集的交集:

For A, {A,B} intesect {A,B,C,D} intersect {A,C} = {A}   (*)

如果交集包含集合本身以外的一些标签,则它是该集合的子集。这里没有其他元素,所以答案是否定的。但,

For C, {A,B,C,D} intersect {A,C} = {A,C}, which means that it's a subset of A.

这种方法的成本取决于集合的实现。假设位图(如您所暗示的)。假设有 n 个最大大小为 m 和 |U| 的输入集 宇宙中的物品。然后中间集构造产生|U| 大小为 n 位的集合,因此有 O(|U|n) 时间来初始化它们。设置这些位需要 O(nm) 时间。如上所述计算每个交点(*)需要 O(mn);全部为 O(mn^2)。

将所有这些放在一起,我们有 O(|U|n) + O(nm) +O(mn^2) = O(|U|n + mn^2)。使用相同的约定,您的“所有对”算法为 O(|U|^2 n^2)。由于 m <= |U|,该算法渐近地更快。它在实践中也可能更快,因为没有复杂的簿记来添加常数因子。

补充:在线版

OP 询问是否有该算法的在线版本,即当输入集一个接一个到达时,最大集合的集合可以增量维护。答案似乎是肯定的。中间集可以快速告诉我们一个新集是否是已经见过的集的子集。但是如何快速判断它是否是超集?如果是这样,哪些现有的最大集合?因为在这种情况下,那些最大集合不再是最大的,必须用新的集合来代替。

关键是要注意iffA的超集是(勾号'表示集合补码)的子集。BA'B'

遵循这个灵感,我们像以前一样保持中间集。当一个新的输入集S到达时,进行与上述相同的测试:设I(e)为输入元素的中间集e。那么这个测试是

For X = \intersect_{e \in S} . I(e), |X| > 0

(在这种情况下,它大于零而不是上面的一,因为它还S没有进入I。)如果测试成功,那么新集合是现有最大集合的(可能不正确的)子集,因此可以丢弃它。

否则,我们必须添加S一个新的最大集,但在此之前,计算:

Y = \intersect_{e \in S'} . I'(e) = ( \union_{e \in S'} . I(e) )'

再次将勾号'设置为补码。联合形式的计算速度可能会快一些。Y包含已被 取代的最大集合S。它们必须从最大集合和I. 最后添加S为最大集并I使用S' 元素进行更新。

让我们来看看我们的例子。到达时A,我们将其添加到I并拥有

1={A}  2={A}  3={A}

B到达时,我们找到了X = {A} intersect {A} = {A},所以扔掉B并继续。同样的情况发生在C. 到了D我们就找到X = {A} intersect {} = {}了,所以继续Y = I'(1) intersect I'(3) = {} intersect {}。这正确地告诉我们最大集合A不包含在 中D,因此没有什么可删除的。但它必须作为一个新的最大集合添加,并且I变为

1={A}  2={A,D}  3={A}  4={D}

到来的E原因没有改变。假设一个新集合的到来F={2, 3, 4, 5}。我们发现

X = {A} isect {A,D} isect {A} isect {D} isect {}

所以我们不能扔掉F。继续

Y = \intersect_{e in {1}} I'(e) = I'(1) = {D}

这告诉我们D是 的子集F,因此在添加时应该丢弃F,留下

1={A} 2={A,F} 3={A,F} 4={F} 5={F}

由于算法的在线性质,补码的计算既棘手又漂亮。输入补语的域只需要包括到目前为止看到的输入元素。中间集的全域仅包含当前最大集合中的集标签。对于许多输入流,该集合的大小将随着时间的推移而稳定或减小。

我希望这是有帮助的。

概括

这里起作用的一般原则是算法设计中经常出现的一个强大的想法。是反向地图。每当您发现自己在进行线性搜索以查找具有给定属性的项目时,请考虑构建从属性返回到项目的映射。构建此地图通常很便宜,并且大大减少了搜索时间。首要的例子是一个排列图p[i],它告诉您i排列数组后第 ' 个元素将占据什么位置。如果您需要搜索最终在给定位置的项目a,您必须搜索pa线性时间操作。另一方面,一个逆映射不需要比实际计算时间更长(pi因此它的成本是“隐藏的”),但是pi[p[i]] == ippi[a]在恒定时间内产生所需的结果。

由原始海报实施

import collections
import operator
from functools import reduce # only in Python 3

def is_power_of_two(n):
    """Returns True iff n is a power of two.  Assumes n > 0."""
    return (n & (n - 1)) == 0

def eliminate_subsets(sequence_of_sets):
    """Return a list of the elements of `sequence_of_sets`, removing all
    elements that are subsets of other elements.  Assumes that each
    element is a set or frozenset and that no element is repeated."""
    # The code below does not handle the case of a sequence containing
    # only the empty set, so let's just handle all easy cases now.
    if len(sequence_of_sets) <= 1:
        return list(sequence_of_sets)
    # We need an indexable sequence so that we can use a bitmap to
    # represent each set.
    if not isinstance(sequence_of_sets, collections.Sequence):
        sequence_of_sets = list(sequence_of_sets)
    # For each element, construct the list of all sets containing that
    # element.
    sets_containing_element = {}
    for i, s in enumerate(sequence_of_sets):
        for element in s:
            try:
                sets_containing_element[element] |= 1 << i
            except KeyError:
                sets_containing_element[element] = 1 << i
    # For each set, if the intersection of all of the lists in which it is
    # contained has length != 1, this set can be eliminated.
    out = [s for s in sequence_of_sets
           if s and is_power_of_two(reduce(
               operator.and_, (sets_containing_element[x] for x in s)))]
    return out
于 2013-01-01T04:18:00.840 回答
3

这个问题已经在文献中进行了研究。给定 S_1,...,S_k 是 {1,...,n} 的子集,Yellin [1] 给出了一种算法来在 O(kdm) 时间内找到 {S_1,...,S_k} 的最大子集其中 d 是 S_i 的平均大小,m 是 {S_1,...,S_k} 的最大子集的基数。后来 Yellin 和 Jutla [2] 将某些参数范围改进为 O((kd)^2/sqrt(log(kd)))。相信不存在针对该问题的真正的次二次算法。

[1] Daniel M. Yellin:子集测试和寻找最大集的算法。苏打水 1992:386-392。

[2] Daniel M. Yellin、Charanjit S. Jutla:在小于二次的时间内找到极值集。信息。过程。莱特。48(1):29-34(1993)。

于 2016-06-05T21:40:05.997 回答
2

在我的脑海中,有一个 O(D*N*log(N)) ,其中 D 是唯一数字的数量。

递归函数“helper”的工作原理如下:@arguments 是集合和域(集合中唯一数字的数量):基本情况:

  1. 如果域为空,则返回
  2. 如果 sets 为空或 sets 的长度等于 1,则返回

迭代案例:

  1. 从集合中删除所有空集合
  2. 在域中选择一个元素 D
  3. 从域中删除 D
  4. 根据集合是否包含 D 将集合分为两组(set1 和 set2)
  5. 从集合中的每个集合中删除 D
  6. 设置结果 = union ( helper(set1,domain), helper(set2,domain) )
  7. 对于 set1 中的每个集合,添加 D 回
  8. 返回结果

请注意,运行时取决于使用的 Set 实现。如果使用双向链表存储集合,则:

步骤 1-5,7 采用 O(N) 步骤 6 的并集是 O(N*log(N)) 通过排序然后合并

因此整体算法为 O(D*N*log(N))

这是执行以下操作的java代码

import java.util.*;

public class MyMain {

    public static Set<Set<Integer>> eliminate_subsets(Set<Set<Integer>> sets) throws Exception {
        Set<Integer> domain = new HashSet<Integer>();
        for (Set<Integer> set : sets) {
            for (Integer i : set) {
                domain.add(i);
            }
        }
        return helper(sets,domain);
    }

    public static Set<Set<Integer>> helper(Set<Set<Integer>> sets, Set<Integer> domain) throws Exception {
        if (domain.isEmpty()) { return sets; }
        if (sets.isEmpty()) { return sets; }
        else if (sets.size() == 1) { return sets; }

        sets.remove(new HashSet<Integer>());

        // Pop some value from domain
        Iterator<Integer> it = domain.iterator();
        Integer splitNum = it.next();
        it.remove();

        Set<Set<Integer>> set1 = new HashSet<Set<Integer>>(); 
        Set<Set<Integer>> set2 = new HashSet<Set<Integer>>();
        for (Set<Integer> set : sets) {
            if (set.contains(splitNum)) {
                set.remove(splitNum);
                set1.add(set);
            }
            else {
                set2.add(set);
            }
        }

        Set<Set<Integer>> ret = helper(set1,domain);
        ret.addAll(helper(set2,domain));

        for (Set<Integer> set : set1) {
            set.add(splitNum);
        }
        return ret;
    }

    /**
     * @param args
     * @throws Exception 
     */
    public static void main(String[] args) throws Exception {
        // TODO Auto-generated method stub
        Set<Set<Integer>> s=new HashSet<Set<Integer>>();
        Set<Integer> tmp = new HashSet<Integer>();
        tmp.add(new Integer(1)); tmp.add(new Integer(2)); tmp.add(new Integer(3));
        s.add(tmp);

        tmp = new HashSet<Integer>();
        tmp.add(new Integer(1)); tmp.add(new Integer(2));
        s.add(tmp);

        tmp = new HashSet<Integer>();
        tmp.add(new Integer(3)); tmp.add(new Integer(4));
        s.add(tmp);
        System.out.println(eliminate_subsets(s).toString());
    }


}

*新年是破坏性的

于 2012-12-31T22:06:26.893 回答
1

预处理假设:

  • 输入集按降序排列
  • 每个集合按值升序排序
  • 可以访问每组的总长度和长度

    方法 #2 - 使用存储桶方法

    相同的假设。可以假设唯一性吗?(即没有 {1,4,6},{1,4,6})否则,您可能需要在某个时候检查 distinct,可能是在创建存储桶后。

    半伪

    List<Set> Sets;//input
    List<Set> Output;
    List<List<Set>> Buckets;
    int length = Sets[0].length;//"by descending lengths"
    List<Set> Bucket = new List<Set>();//current bucket
    
    //Place each set with shared length in its own bucket
    for( Set set in Sets )
    {
     if( set.length == length )//current Bucket
     {
      Bucket.add(set);
     }else//new Bucket
     {
      length = set.length;
      Buckets.Add(Bucket);
      Bucket = new Bucket();
      Bucket.Add(set);
     }
    }
    Buckets.add(Bucket);
    
    
    
    //Based on the assumption of uniqueness, everything in the first bucket is
    //larger than every other set and since it is unique, they are not proper subsets
    Output.AddRange(Buckets[0]);
    
    //Iterate through the buckets
    for( int i = 1; i < Buckets.length; i++ )
    {
     List<Set> currentBucket = Buckets[i];
    
     //Iterate through the sets in the current bucket
     for( int a = 0; a < currentBucket.length; a++ )
     {
      Set currentSet = currentBucket[a];
      bool addSet = true;
      //Iterate through buckets with greater length
      for( int b = 0; b < i; b++ )
      {
       List<Set> testBucket = Buckets[b];
    
       //Iterate through the sets in testBucket
       for( int c = 0; c < testBucket.length; c++ )
       {
        Set testSet = testBucket[c];
        int testMatches = 0;
    
        //Iterate through the values in the current set
        for( int d = 0; d < currentSet.length; d++ )
        {
         int testIndex = 0;
    
         //Iterate through the values in the test set
         for( ; testIndex < testSet.length; testIndex++ )
         {
          if( currentSet[d] < testSet[testIndex] )
          {
           setClear = true;
           break;
          }
          if( currentSet[d] == testSet[testIndex] )
          {
           testMatches++;
           if( testMatches == currentSet.length )
           {
            addSet = false;
            setClear = true;
            break;
           }
          }
         }//testIndex
         if( setClear ) break;
        }//d
        if( !addSet ) break;
       }//c
       if( !addSet ) break;
      }//b
      if( addSet ) Output.Add( currentSet );
     }//a
    }//i
    

    方法#1 ( O( n(n+1)/2 )) ...效率不够

    半伪

    //input Sets
    List<Set> results;
    for( int current = 0; current < Sets.length; current++ )
    {
     bool addCurrent = true;
     Set currentSet = Sets[current];
     for( int other = 0; other < current; other++)
     {
      Set otherSet = Sets[other];
      //is current a subset of other?
      if( currentSet.total > otherSet.total 
       || currentSet.length >= otherSet.length) continue;
      int max = currentSet.length;
      int matches = 0;
      int otherIndex = 0, len = otherSet.length;
      for( int i = 0; i < max; i++ )
      {
       for( ; otherIndex < len; otherIndex++ )
       {
         if( currentSet[i] == otherSet[otherInex] )
         {
          matches++;
          break;
         }
       }
       if( matches == max )
       {
        addCurrent = false;
        break;
       }
      }
      if( addCurrent ) results.Add(currentSet);
     }
    }
    

    这将获取一组集合,并遍历每个集合。对于每一个,它将再次遍历集合中的每个集合。当嵌套迭代发生时,它将比较外部集是否与嵌套集相同(来自内部迭代)(如果它们相同,则不进行检查),它还将比较外部集是否具有更大的总数比嵌套集合(如果总数更大,则外部集合不能是适当的子集),然后将比较外部集合的项目数量是否小于嵌套集合。

    一旦这些检查完成,它将从外部集合的第一项开始,并将其与嵌套集合的第一项进行比较。如果它们不相等,它将检查嵌套集的下一项。如果它们相等,则将一个计数器添加到计数器,然后将外部集合的下一项与它在内部集合中停止的位置进行比较。

    如果它达到匹配比较的数量等于外部集合中的项目数的点,则外部集合已被发现是内部集合的适当子集。它被标记为被排除,并且比较被停止。

  • 于 2012-12-31T22:07:32.290 回答