在组合对之间查找共享元素的最佳方法

时间:2014-02-12 17:29:12

标签: c++ algorithm combinations

我有一个A类订购商品的清单,每个商品都包含商品B列表中的一个子集。对于A中的每一对商品,我想找到他们共享的商品B的数量(相交) 。

例如,如果我有这些数据:

A1 : B1  
A2 : B1 B2 B3  
A3 : B1  

然后我会得到以下结果:

A1, A2 : 1  
A1, A3 : 1  
A2, A3 : 1  

我遇到的问题是使算法有效。我的数据集大小约为8.4K类型的项目。这意味着8.4K选择2 = 35275800组合。我正在使用的算法只是通过每个组合对并进行集合交集。

到目前为止,我所掌握的要点如下。我将计数存储为地图中的键,其值为A对的向量。我正在使用图形数据结构来存储数据,但我正在使用的唯一“图形”操作是get_neighbors(),它返回A中项目的B子集。我碰巧知道图中的元素是从索引0到8.4K排序。

void get_overlap(Graph& g, map<int, vector<A_pair> >& overlap) {

map<int, vector<A_pair> >::iterator it;

EdgeList el_i, el_j;
set<int> intersect;

size_t i, j;

VertexList vl = g.vertices();

for (i = 0; i < vl.size()-1; i++) {
    el_i = g.get_neighbors(i);

    for (j = i+1; j < vl.size(); j++) {
        el_j = g.get_neighbors(j);

        set_intersection(el_i.begin(), el_i.end(), el_j.begin(), el_j.end(), inserter(intersect, intersect.begin()));
        int num_overlap = intersect.size();

        it = overlap.find(num_overlap);
        if (it == overlap.end()) {
            vector<A_pair> temp;
            temp.push_back(A_pair(i, j));
            overlap.insert(pair<int, vector<A_pair> >(num_overlap, temp));
        }
        else {
            vector<A_pair> temp = it->second;
            temp.push_back(A_pair(i, j));
            overlap[num_overlap] = temp;
        }
    }
}

}

我已经运行了这个程序将近24小时,并且for循环中的第i个元素已经达到迭代250(我将每个i打印到日志文件中)。当然,这距离8.4K还有很长的路要走(尽管我知道随着迭代的进行,比较次数将会缩短,因为j = i + 1)。有更优化的方法吗?

编辑:要明确,这里的目标是最终找到前k个重叠对。

编辑2:感谢@Beta和其他人指出优化。特别是,直接更新地图(而不是复制其内容并重置地图值)大大提高了性能。现在它可以在几秒钟内完成。

1 个答案:

答案 0 :(得分:1)

我认为你可以通过预先计算反向(边到顶点)地图来加快速度。这将允许您避免set_intersection调用,该调用执行一系列昂贵的集合插入。我缺少一些声明来制作功能齐全的代码,但希望你能得到这个想法。我假设EdgeList是某种int向量:

void get_overlap(Graph& g, map<int, vector<A_pair> >& overlap) {

map<int, vector<A_pair> >::iterator it;



EdgeList el_i, el_j;
set<int> intersect;

size_t i, j;

VertexList vl = g.vertices();

// compute reverse map
map<int, set<int>> reverseMap;
for (i = 0; i < vl.size()-1; i++) {
    el_i = g.get_neighbors(i);
    for (auto e : el_i) {
        const auto findIt = reverseMap.find(e);
        if (end(reverseMap) == findIt) {
            reverseMap.emplace(e, set<int>({i})));
        } else {
            findIt->second.insert(i);
        }
    }
}

for (i = 0; i < vl.size()-1; i++) {
    el_i = g.get_neighbors(i);

    for (j = i+1; j < vl.size(); j++) {
        el_j = g.get_neighbors(j);

        int num_overlap = 0;
        for (auto e: el_i) {
            auto findIt = reverseMap.find(e);
            if (end(reverseMap) != findIt) {
                if (findIt->second.count(j) > 0) {
                    ++num_overlap;
                }
            }
        }

        it = overlap.find(num_overlap);
        if (it == overlap.end()) {
            overlap.emplace(num_overlap, vector<A_pair>({ A_pair(i, j) }));
        }
        else {
            it->second.push_back(A_pair(i,j));
        }
    }
}

我没有进行精确的性能分析,但是在双循环中,你用“N * log(M)* log(E)比较”替换“最多4N比较”+一些昂贵的设置插入(来自set_intersection),其中N是每个顶点的平均边数,M是每个边的平均顶点数,E是边数,因此根据您的数据集可能是有益的。 此外,如果边缘索引是紧凑的,那么您可以使用简化向量而不是地图来表示反向映射,从而消除了日志(E)性能成本。

但有一个问题。既然你在谈论顶点和边缘,那么你是否还有边缘总是有2个顶点的附加约束?这可以简化一些计算。