匹配集的数据结构

时间:2010-08-03 01:03:17

标签: c++ c algorithm data-structures

我有一个应用程序,我有很多套。一套可能是
{4,7,12,18}
唯一的数字,都不到50。

然后我有几个数据项:
1 {1,2,4,7,8,12,18,23,29} 2 {3,4,6,7,15,23,34,38}
3 {4,7,12,18} 4 {1,4,7,12,13,14,15,16,17,18} 5 {2,4,6,7,13,15}

数据项1,3和4与集合匹配,因为它们包含集合中的所有项目。

我需要设计一个超快速的数据结构来识别数据项是否是集合的成员包括属于集合的所有成员(因此数据项是该集的超集)。我目前最好的估计表明将会少于50,000套。

我当前的实现将我的集合和数据作为无符号64位整数和存储在列表中的集合。然后检查一个数据项我遍历列表做((set& data)== set)比较。它的工作原理和节省空间但速度很慢(O(n))而且我很乐意用一些内存来换取一些性能。有没有人对如何组织这个有更好的想法?

修改: 非常感谢所有答案。看起来我需要提供有关该问题的更多信息。我首先得到集合,然后逐个获取数据项。我需要检查数据项是否与其中一组相匹配 这些集很可能是“块状的”,例如对于给定的问题,1,3和9可能包含在95%的集合中;我可以提前预测到这一点(但不是很好)。

对于那些建议记忆的人:这就是memoized函数的数据结构。这些集代表已经计算过的一般解决方案,数据项是函数的新输入。通过将数据项与一般解决方案相匹配,我可以避免大量处理。

13 个答案:

答案 0 :(得分:8)

我看到另一个对你来说是双重的解决方案(即,针对每个集合测试一个数据项),并且使用二叉树,其中每个节点测试是否包含特定项目。

例如,如果您有集合A = {2,3}且B = {4}且C = {1,3},则您将拥有以下树

                      _NOT_HAVE_[1]___HAVE____
                      |                      |            
                _____[2]_____          _____[2]_____
                |           |          |           |
             __[3]__     __[3]__    __[3]__     __[3]__
             |     |     |     |    |     |     |     |
            [4]   [4]   [4]   [4]  [4]   [4]   [4]   [4]
            / \   / \   / \   / \  / \   / \   / \   / \
           .   B .   B .   B .   B    B C   B A   A A   A
                                            C     B C   B
                                                        C

制作完树后,您只需进行50次比较---或者您可以在一组中进行多少项目。

例如,对于{1,4},你通过树分支:右(集合有1),左(没有2),左,右,你得到[B],意思是只设置B包括在{1,4}中。

这基本上称为“二元决策图”。如果你被节点中的冗余所冒犯(因为你应该这样,因为2 ^ 50是很多节点......)那么你应该考虑简化形式,这称为“简化的有序二进制决策图”和是一种常用的数据结构。在这个版本中,节点在冗余时合并,你不再拥有二叉树,而是一个有向无环图。

Wikipedia page on ROBBDs可以为您提供更多信息,以及指向实现各种语言数据结构的库的链接。

答案 1 :(得分:2)

我无法证明这一点,但我相当确定没有可以轻易击败O(n)界限的解决方案。你的问题“太笼统”了:每一组都有 m = 50 属性(即属性k是它包含数字k),关键是所有这些属性独立于每个属性其他。没有任何聪明的属性组合可以预测其他属性的存在。排序不起作用,因为问题非常对称,50个数字的任何排列都会产生同样的问题,但搞砸了任何类型的排序。除非你的输入有隐藏结构,否则你运气不好。

但是,速度/内存存在一定的折衷空间。也就是说,您可以预先计算小查询的答案。让Q成为查询集,supersets(Q)是包含Q的集合的集合,即问题的解决方案。然后,您的问题具有以下关键属性

Q ⊆ P  =>  supersets(Q) ⊇ supersets(P)

换句话说,P = {1,3,4}的结果是Q = {1,3}的结果的子集合。

现在,预先计算小查询的所有答案。为了演示,让我们采取大小< = 3的所有查询。你将获得一个表

supersets({1})
supersets({2})
...
supersets({50})
supersets({1,2})
supersets({2,3})
...
supersets({1,2,3})
supersets({1,2,4})
...

supersets({48,49,50})

使用O(m ^ 3)个条目。要计算supersets({1,2,3,4}),请查找superset({1,2,3})并在此集合上运行线性算法。关键是,superset({1,2,3})平均不会包含完整的 n = 50,000 元素,但只有 n / 2 ^ 3 = 6250 的那些元素,使速度提高8倍。

(这是其他答案建议的“反向索引”方法的概括。)

根据您的数据集,内存使用会相当糟糕。但是,您可以省略某些行或加快算法速度,方法是注意{1,2,3,4}之类的查询可以从几个不同的预先计算的答案中计算出来,例如supersets({1,2,3})supersets({1,2,4}),以及您将使用其中最小的一个。

答案 2 :(得分:1)

如果你要提高性能,你将不得不做一些花哨的事情来减少你设定的比较次数。

也许您可以对数据项进行分区,以便您拥有1中一个组中最小元素的所有项,以及其中2是另一个组中最小项的所有项,依此类推。

在搜索时,您会在搜索集中找到最小值,并查看该值所在的组。

或者,或许,将它们分组为50个组'此数据项包含N',N = 1..50。

在搜索时,您会找到包含该组每个元素的每个组的大小,然后只搜索最小的组。

对此的关注 - 尤其是后者 - 是减少搜索时间的开销可能超过缩小搜索空间的性能优势。

答案 3 :(得分:1)

分割位图列表的一种可能方法是创建一个(编译的半字节指示符)

的数组

假设您的64位位图之一设置了位0到位8 在十六进制中,我们可以将其视为0x000000000000001F

现在,让我们将其转换为更简单和更小的表示。 每个4位半字节,或者至少有一个位设置或不设置。 如果是,我们将其表示为1,如果不是,我们将其表示为0。

因此十六进制值减少到位模式0000000000000011,因为右手2个半字节具有唯一具有位的位。创建一个包含65536个值的数组,并将它们用作链接列表或大型数组的头部....

将每个位图编译成紧凑的 CNI 。将其添加到正确的列表中,直到所有列表都已编译完毕。

然后拿针。将其编译为 CNI 表单。使用它来值,下标到列表的头部。该列表中的所有位图都有可能成为匹配项。 其他列表中的所有位图都无法匹配。

这是一种将它们分开的方法。

现在在实践中,我怀疑链表是否符合您的性能要求。

如果您编写一个函数来将位图编译为 CNI ,您可以使用它作为基础,通过 CNI 对数组进行排序。然后让你的数组为65536个头,只需下标到原始数组作为范围的开始。

另一种技术是编译64位位图的一部分,因此你的头数较少。分析你的模式应该可以让你了解哪些半字节最有效的分区。

祝你好运,请告诉我们你最终会做什么。

答案 4 :(得分:1)

您可以使用数据项的倒排索引。对于你的例子

1 {1, 2, 4, 7, 8, 12, 18, 23, 29}
2 {3, 4, 6, 7, 15, 23, 34, 38}
3 {4, 7, 12, 18}
4 {1, 4, 7, 12, 13, 14, 15, 16, 17, 18}
5 {2, 4, 6, 7, 13, 15}

倒排索引将是

1: {1, 4}
2: {1, 5}
3: {2}
4: {1, 2, 3, 4, 5}
5: {}
6: {2, 5}
...

因此,对于任何特定集合{x_0,x_1,...,x_i},您需要交叉x_0,x_1和其他集合。例如,对于集合{2,3,4},您需要将{1,5}{2}{1,2,3,4,5}相交。因为您可以对倒排索引中的所有集合进行排序,所以可以将要交叉的集合的长度与最小集合相交。

如果您有非常“热门”的项目(在我们的示例中为4)并且索引设置很大,那么这可能是一个问题。

关于相交的一些话。您可以在倒排索引中使用排序列表,并成对交叉集合(以递增的长度顺序)。或者,由于您的项目不超过50K,您可以使用压缩位集(每个数字约为6Kb,稀疏位集较少,约50个数字,不那么贪婪),并且按位设置比特。对于有效的稀疏位集,我认为。

答案 5 :(得分:1)

与搜索条件匹配的集合的索引类似于集合本身。我们使用小于50000的唯一索引,而不是使用小于50的唯一索引。由于您不介意使用一些内存,因此可以预先计算50个500位整数数组中的匹配集。然后你索引到预先计算的匹配,基本上只做你的((set& data)== set)但是在代表匹配集的50000位数上。这就是我的意思。

#include <iostream>

enum
{
    max_sets = 50000, // should be >= 64
    num_boxes = max_sets / 64 + 1,
    max_entry = 50
};

uint64_t sets_containing[max_entry][num_boxes];

#define _(x) (uint64_t(1) << x)

uint64_t sets[] =
{
    _(1) | _(2) | _(4) | _(7) | _(8) | _(12) | _(18) | _(23) | _(29),
    _(3) | _(4) | _(6) | _(7) | _(15) | _(23) | _(34) | _(38),
    _(4) | _(7) | _(12) | _(18),
    _(1) | _(4) | _(7) | _(12) | _(13) | _(14) | _(15) | _(16) | _(17) | _(18),
    _(2) | _(4) | _(6) | _(7) | _(13) | _(15),
    0,
};

void big_and_equals(uint64_t lhs[num_boxes], uint64_t rhs[num_boxes])
{
    static int comparison_counter = 0;
    for (int i = 0; i < num_boxes; ++i, ++comparison_counter)
    {
        lhs[i] &= rhs[i];
    }
    std::cout
        << "performed "
        << comparison_counter
        << " comparisons"
        << std::endl;
}

int main()
{
    // Precompute matches
    memset(sets_containing, 0, sizeof(uint64_t) * max_entry * num_boxes);

    int set_number = 0;
    for (uint64_t* p = &sets[0]; *p; ++p, ++set_number)
    {
        int entry = 0;
        for (uint64_t set = *p; set; set >>= 1, ++entry)
        {
            if (set & 1)
            {
                std::cout
                    << "sets_containing["
                    << entry
                    << "]["
                    << (set_number / 64)
                    << "] gets bit "
                    << set_number % 64
                    << std::endl;

                uint64_t& flag_location =
                    sets_containing[entry][set_number / 64];

                flag_location |= _(set_number % 64);
            }
        }
    }

    // Perform search for a key
    int key[] = {4, 7, 12, 18};
    uint64_t answer[num_boxes];
    memset(answer, 0xff, sizeof(uint64_t) * num_boxes);

    for (int i = 0; i < sizeof(key) / sizeof(key[0]); ++i)
    {
        big_and_equals(answer, sets_containing[key[i]]);
    }

    // Display the matches
    for (int set_number = 0; set_number < max_sets; ++set_number)
    {
        if (answer[set_number / 64] & _(set_number % 64))
        {
            std::cout
                << "set "
                << set_number
                << " matches"
                << std::endl;
        }
    }

    return 0;
}

运行此程序会产生:

sets_containing[1][0] gets bit 0
sets_containing[2][0] gets bit 0
sets_containing[4][0] gets bit 0
sets_containing[7][0] gets bit 0
sets_containing[8][0] gets bit 0
sets_containing[12][0] gets bit 0
sets_containing[18][0] gets bit 0
sets_containing[23][0] gets bit 0
sets_containing[29][0] gets bit 0
sets_containing[3][0] gets bit 1
sets_containing[4][0] gets bit 1
sets_containing[6][0] gets bit 1
sets_containing[7][0] gets bit 1
sets_containing[15][0] gets bit 1
sets_containing[23][0] gets bit 1
sets_containing[34][0] gets bit 1
sets_containing[38][0] gets bit 1
sets_containing[4][0] gets bit 2
sets_containing[7][0] gets bit 2
sets_containing[12][0] gets bit 2
sets_containing[18][0] gets bit 2
sets_containing[1][0] gets bit 3
sets_containing[4][0] gets bit 3
sets_containing[7][0] gets bit 3
sets_containing[12][0] gets bit 3
sets_containing[13][0] gets bit 3
sets_containing[14][0] gets bit 3
sets_containing[15][0] gets bit 3
sets_containing[16][0] gets bit 3
sets_containing[17][0] gets bit 3
sets_containing[18][0] gets bit 3
sets_containing[2][0] gets bit 4
sets_containing[4][0] gets bit 4
sets_containing[6][0] gets bit 4
sets_containing[7][0] gets bit 4
sets_containing[13][0] gets bit 4
sets_containing[15][0] gets bit 4
performed 782 comparisons
performed 1564 comparisons
performed 2346 comparisons
performed 3128 comparisons
set 0 matches
set 2 matches
set 3 matches

3128 uint64_t比较胜过50000比较,所以你赢了。即使在最糟糕的情况下,这将是一个包含所有50个项目的密钥,您只需要进行num_boxes * max_entry比较,在这种情况下为39100.仍然优于50000。

答案 6 :(得分:0)

您可以构建包含每个元素的“haystack”列表的反向索引:

std::set<int> needle;  // {4, 7, 12, 18}
std::vector<std::set<int>> haystacks;
// A list of your each of your data sets:
// 1 {1, 2, 4, 7, 8, 12, 18, 23, 29}
// 2 {3, 4, 6, 7, 15, 23, 34, 38}
// 3 {4, 7, 12, 18}
// 4 {1, 4, 7, 12, 13, 14, 15, 16, 17, 18}
// 5 {2, 4, 6, 7, 13, 

std::hash_map[int, set<int>>  element_haystacks;
// element_haystacks maps each integer to the sets that contain it
// (the key is the integers from the haystacks sets, and 
// the set values are the index into the 'haystacks' vector):
// 1 -> {1, 4}  Element 1 is in sets 1 and 4.
// 2 -> {1, 5}  Element 2 is in sets 2 and 4.
// 3 -> {2}  Element 3 is in set 3.
// 4 -> {1, 2, 3, 4, 5}  Element 4 is in sets 1 through 5.  
std::set<int> answer_sets;  // The list of haystack sets that contain your set.
for (set<int>::const_iterator it = needle.begin(); it != needle.end(); ++it) {
  const std::set<int> &new_answer = element_haystacks[i];
  std::set<int> existing_answer;
  std::swap(existing_answer, answer_sets);
  // Remove all answers that don't occur in the new element list.
  std::set_intersection(existing_answer.begin(), existing_answer.end(),
                        new_answer.begin(), new_answer.end(),
                        inserter(answer_sets, answer_sets.begin()));
  if (answer_sets.empty()) break;  // No matches :(
}

// answer_sets now lists the haystack_ids that include all your needle elements.
for (int i = 0; i < answer_sets.size(); ++i) {
  cout << "set: " << element_haystacks[answer_sets[i]];
}

如果我没弄错的话,这将有O(k*m)的最大运行时间,其中是整数所属的平均集合数,m是针集的平均大小(&lt; 50) 。不幸的是,由于构建反向映射(element_haystacks),它会产生大量内存开销。

如果您存储已排序的vectors而不是sets并且element_haystacks可能是50个元素vector而不是{{},我相信您可以改善这一点1}}。

答案 7 :(得分:0)

由于数字小于50,您可以使用64位整数构建一对一哈希,然后使用按位运算在O(1)时间内测试集合。哈希创建也将是O(1)。我认为要么是XOR,要么是测试零,要么是AND,然后是相等的测试就行了。 (如果我理解正确的问题。)

答案 8 :(得分:0)

将你的集合放入一个数组(不是链表)和SORT他们。排序标准可以是1)集合中的元素数量(集合表示中的1位数),或2)集合中的最低元素。例如,设A={7, 10, 16}B={11, 17}。然后在标准1)下B<A,在标准2)下A<B。排序是O(n log n),但我假设您可以承担一些预处理时间,即搜索结构是静态的。

当新数据项到达时,您可以使用二进制搜索(对数时间)来查找数组中的起始候选集。然后线性搜索数组并根据数组中的集合测试数据项,直到数据项变得“大于”集合。

您应该根据集合的范围选择排序标准。如果所有集合都将0作为最低元素,则不应选择标准2)。反之亦然,如果设定的基数分布不均匀,则不应选择标准1)。

另一个更强大的排序标准是计算每组中元素的范围,并根据它进行排序。例如,集合A中的最低元素是7,最高元素是16,因此您将其范围编码为0x1007;类似地,B的跨度为0x110B。根据“跨度代码”对集合进行排序,然后再次使用二进制搜索来查找与数据项目具有相同“跨度代码”的所有集合。

计算“跨度代码”在普通C中很慢,但是如果你求助于汇编它可以快速完成 - 大多数CPU都有指令找到最多/最不重要的设置位。

答案 9 :(得分:0)

这不是一个真正的答案更多的观察:这个问题看起来可以有效地并行化甚至分布,这至少会减少运行时间到O(n /核心数)

答案 10 :(得分:0)

我很惊讶没有人提到STL包含一种算法来处理这类事情。因此,您应该使用includes。正如它描述的那样,它最多执行 2 *(N + M)-1次比较,以获得O(M + N)的最差情况。

因此:

bool isContained = includes( myVector.begin(), myVector.end(), another.begin(), another.end() );

如果您需要O(log N)时间,我将不得不屈服于其他响应者。

答案 11 :(得分:0)

您有多少数据?它们真的都是独特的吗?您可以在运行之前缓存热门数据项,还是使用存储桶/基数排序将重复项组合在一起?

这是一种索引方法:

1)将50位字段分成例如10个5位子字段。如果你真的有50K套,那么3个17位的块可能更接近标记。

2)对于每个集合,选择一个子字段。一个很好的选择是子场,其中该集合的位数设置最多,几乎任意地断开关系 - 例如使用最左边的这样的子字段。

3)对于每个子字段中的每个可能的位模式,记下分配给该子字段的集合列表并匹配该模式,仅考虑子字段。

4)给定一个新数据项,将其划分为5位块,并在每个查找表中查找每个数据项以获取要测试的集合列表。如果您的数据是完全随机的,则可以获得两倍或更多的加速因子,具体取决于每组中最密集的子字段中设置的位数。如果对手可以为你编制随机数据,也许他们会发现几乎但不完全匹配集合的数据项,你根本就做得不好。

可能存在利用集合中任何结构的余地,通过对比特进行编号,使得集合在其最佳子字段中倾向于具有两个或更多个比特 - 例如,对比特进行聚类分析,如果它们倾向于一起出现,则将它们视为相似的。或者,如果您可以预测数据项中的模式,请在步骤(2)中将集合分配更改为子字段,以减少预期的错误匹配数。

增加: 需要多少个表来保证任何2位始终属于同一个表?如果你看一下http://en.wikipedia.org/wiki/Projective_plane中的组合定义,你可以看到有一种方法可以用57种不同的方式从57(= 1 + 7 + 49)位中提取7位的集合,这样任何两位都可以至少一个集合包含它们。可能不是很有用,但它仍然是一个答案。

答案 12 :(得分:0)

另一个想法是完全预先捕捉你的大象。

<强>设置

创建一个64位X 50,000元素位数组。

分析您的搜索集,并在每行中设置相应的位。

将位图保存到磁盘,因此可以根据需要重新加载。

<强>搜索

将元素位数组加载到内存中。

创建一个位图数组,1 X 50000.将所有值设置为1.这是搜索位数组

拿针,然后走过每个值。将它用作元素位数组的下标。取相应的位数组,然后将它转换为搜索数组。

对针中的所有值执行此操作,并且搜索位数组将保持1, 对于每个匹配的元素。

<强>重构

遍历搜索位数组,对于每个1,您可以使用元素位数组来重建原始值。