用于查找所有最大子集的高效算法

时间:2012-12-31 21:17:02

标签: algorithm set

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

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_func只是element & existing == element,它在恒定时间内运行。

4 个答案:

答案 0 :(得分:15)

假设您标记了所有输入集。

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

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

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询问是否存在该算法的在线版本,即,当输入集一个接一个地到达时,可以递增地保持最大集合集合。答案似乎是肯定的。如果新集合是已经看到的子集,则中间集会快速告诉我们。但是如何快速判断它是否是超集?如果是这样,其中现有的最大集合?因为在这种情况下,那些最大集合不再是最大集合,必须用新的集合替换。

关键是要注意AB的超集iff A'B'的一个子集(tick'表示集补码)。

在这个灵感之后,我们像以前一样保持中间体。当新输入集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) )'

再次将tick'设置为补码。联合表单的计算速度可能会快一些。 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}

这告诉我们DF的子集,因此应在F添加时丢弃

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

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

我希望这有用。

摘要

这里的一般原则是一种强有力的想法,即经常在算法设计中的作物。这是反向地图。每当您发现自己进行线性搜索以查找具有给定属性的项目时,请考虑从属性到项目构建地图。构建此地图通常很便宜,并且大大缩短了搜索时间。最重要的示例是一个排列映射p[i],它告诉您在置换数组后i'元素占据的位置。如果您需要搜索最终位于指定位置a的项目,则必须在p搜索线性时间操作a。另一方面,反向地图pi使得pi[p[i]] == i不再需要计算p(因此其费用为“隐藏”),但是pi[a]会产生在恒定时间内获得理想的结果。

通过原始海报实施

import collections
import operator

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

答案 1 :(得分: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:子集测试和查找最大集的算法。 SODA 1992:386-392。

[2] Daniel M. Yellin,Charanjit S. Jutla:在不到二次的时间内寻找极值集。天道酬勤。处理。快报。 48(1):29-34(1993)。

答案 2 :(得分:2)

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

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

  1. 如果域名为空,请返回
  2. 如果sets为空或者sets的长度等于1,则返回
  3. 迭代案例:

    1. 从集合中删除所有空集
    2. 在域中选择元素D
    3. 从域中删除D
    4. 根据集合是否包含D
    5. 将集合分为两组(set1&amp; set2)
    6. 从集合中的每一组中删除D
    7. 设置result = union(helper(set1,domain),helper(set2,domain))
    8. 对于set1中的每个集合,添加D back
    9. 返回结果
    10. 请注意,运行时取决于使用的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());
          }
      
      
      }
      

      *新的一年是破坏性的

答案 3 :(得分:1)

预处理假设:

  • 输入集按降序长度排序
  • 每个集按值递增排序
  • 可以访问每组的总数和长度

    方法#2 - 使用桶方法

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

    半puedo

    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 ))......效率不够

    半puedo

    //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);
     }
    }
    

    这将采用一组集合,并遍历每一组。对于每一个,它将再次遍历集合中的每个集合。当嵌套迭代发生时,它将比较外部集合是否与嵌套集合(来自内部迭代)(如果是,不进行检查),它还将比较外部集合是否具有更大的集合比嵌套集(如果总数更大,那么外集不能是一个合适的子集),它将比较外集的项数是否少于嵌套集。

    完成这些检查后,它将从外部集的第一项开始,并将其与嵌套集的第一项进行比较。如果它们不相等,它将检查嵌套集的下一项。如果它们相等,那么它会向计数器添加一个,然后将外部集合的下一个项目与内部集合中的下一个项目进行比较。

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