问题 使用基于范围的for迭代图的边缘


我有一个图表的代表作为 std::vector<std::unordered_set<unsigned>> neighbors也就是说,顶点是整数,对于每个顶点,我们保留一组邻居。因此,走路所有的边缘,我会做的事情

for (unsigned u = 0; u < neighbors.size(); ++u)
    for (unsigned v : neighbors[u])
        if (u <= v)
            std::cout << u << ' ' << v << std::endl;

现在,我希望能够从中获得同样的效果

for (auto e: g.edges())
    std::cout << e.first << ' ' << e.second << std::endl;

哪里 g 来自封装的类 neighbors 向量。

但是,我尝试的一切看起来都非常复杂,我能想到的最好的版本有50行,而且很难看出它是正确的。有一个简单的方法吗?

这是我丑陋的版本:

#include <iostream>
#include <unordered_set>
#include <vector>
typedef unsigned Vertex;
class Graph {
public:
    typedef std::unordered_set<Vertex> Neighbors;
    std::size_t numVertices() const { return neighbors_.size(); }
    Graph(std::size_t n = 0) : neighbors_(n) { }
    void addEdge(Vertex u, Vertex v) {
        neighbors_[u].insert(v);
        neighbors_[v].insert(u);
    }
    class EdgeIter {
        friend Graph;
    public:
        bool operator!=(const EdgeIter& other) { return u_ != other.u_; }
        void operator++() {
            do {
                ++it_;
                while (it_ == it_end_) {
                    u_++;
                    if (u_ >= neighbors_.size())
                        break;
                    it_     = neighbors_[u_].cbegin();
                    it_end_ = neighbors_[u_].cend();
                }
            } while (u_ < neighbors_.size() && *it_ < u_);
        }
        std::pair<Vertex, Vertex> operator*() { return {u_, *it_}; }
    private:
        EdgeIter(const std::vector<std::unordered_set<Vertex> >& neighbors, size_t u)
            : u_(u), neighbors_(neighbors) {
            if (u < neighbors_.size()) {
                it_     = neighbors_[u_].cbegin();
                it_end_ = neighbors_[u_].cend();
                while (it_ == it_end_) {
                    u_++;
                    if (u_ >= neighbors_.size())
                        break;
                    it_     = neighbors_[u_].cbegin();
                    it_end_ = neighbors_[u_].cend();
                }
            }
        }
        Vertex u_;
        const std::vector<std::unordered_set<Vertex> >& neighbors_;
        std::unordered_set<Vertex>::const_iterator it_, it_end_;
    };
    EdgeIter edgesBegin() const { return EdgeIter(neighbors_, 0); }
    EdgeIter edgesEnd()   const { return EdgeIter(neighbors_, neighbors_.size()); }
    class Edges {
    public:
        Edges(const Graph& g) : g_(g) { }
        EdgeIter begin() const { return g_.edgesBegin(); }
        EdgeIter end  () const { return g_.edgesEnd();   }
    private:
        const Graph& g_;
    };
    Edges edges() { return Edges(*this); }
    std::vector<Neighbors> neighbors_;
};
int main() {
    Graph g(5);
    g.addEdge(1, 2);
    g.addEdge(2, 3);
    g.addEdge(1, 3);    
    for (unsigned u = 0; u < g.numVertices(); ++u)
        for (unsigned v : g.neighbors_[u])
            if (u <= v)
                std::cout << u << ' ' << v << std::endl;
    for (auto e: g.edges())
        std::cout << e.first << ' ' << e.second << std::endl;
}

6228
2018-04-04 13:27


起源

好问题。我不知道一个简单的方法。 - Petter
bool operator!=(const EdgeIter& other) { return u_ != other.u_; }?不应该两个 EdgeIter 当他们实际指向不同时,被认为是不平等的 边缘? - Zeta
我假设你想要迭代器是懒惰的;填写一个 vector 结果,然后迭代,这是不可接受的,对吧? - svick
你没有考虑孤立的节点, cbegin() 将返回 cend() 在这种情况下。 - Zeta
2天运行,3个答案,但没有评论,upvote / downvote或接受你。任何答案都有问题吗?如果是,请说明如何改进,我将编辑或撤消。 - TemplateRex


答案:


我强烈建议使用 Boost.Graph 用于此类计算的库。主要原因是 图是复杂的数据结构 你可以运行更多 复杂的算法。即使您自己的手工数据结构正常工作,它也可能无法有效运行(在空间/时间复杂性方面),并且可能不支持您的应用程序所需的算法。

作为这个库的可访问性的一个指示:我之前没有使用过Boost.Graph的经验,但是花了大约30分钟来完成以下30行完全重现你的例子的代码。

#include <iostream>
#include <iterator>
#include <boost/graph/adjacency_list.hpp>

typedef unsigned V;
typedef std::pair<V, V> E;

// store neighbors in a std::set, vertices in a std::vector
typedef boost::adjacency_list<boost::setS, boost::vecS> Graph;

int main()
{
   // construct the graph
   E e[] = { E(1,2), E(2,3), E(1,3) };
   Graph g(std::begin(e), std::end(e), 5);

   std::cout << "iterate over vertices, then over its neighbors\n";
   auto vs = boost::vertices(g);
   for (auto vit = vs.first; vit != vs.second; ++vit) {
       auto neighbors = boost::adjacent_vertices(*vit, g);
       for (auto nit = neighbors.first; nit != neighbors.second; ++nit)
           std::cout << *vit << ' ' << *nit << std::endl;
   }

   std::cout << "iterate directly over edges\n";
   auto es = boost::edges(g);
   for (auto eit = es.first; eit != es.second; ++eit) {
       std::cout << boost::source(*eit, g) << ' ' << boost::target(*eit, g) << std::endl;
   }
}

输出 LiveWorksSpace

因为,因为 boost::edges 返回一个 std::pair,你 不能使用基于范围的 在边缘,但这是唯一的语法糖,你可以尝试通过定义自己的开始/结束功能来修复。重要的是你可以直接迭代边缘。

请注意 boost_adjacency_list 数据结构为您提供了边缘和顶点操作 明确的时间和空间复杂性。上面的代码只是重现你的例子,而不知道你真正想要什么样的操作。更改底层容器允许您根据应用程序进行权衡。


11
2018-04-05 08:06



我之前看过boost图。虽然你只想使用图算法就可以(但不是很好),但我发现编写新算法非常困难。例如,参见 Dijkstra的继承人实施 (一个非常简单的算法),它对我来说似乎完全不透明。无论如何,我的主要兴趣在于如何实现迭代器,理想情况下是基于范围的。我将研究如何在boost中完成它。 - Falk Hüffner
使用C ++ 11,您可以使用类似的东西 for( auto e : boost::edges(g) ) 如果你第一次实施 std::begin(const std::pair<T,T>& eItPair) 回来 eItPair.first 和 std::end(const std::pair<T,T>& eItPair) 回来 eItPair.second。 - Ace7k3


一个无耻插头的机会!我有一个项目 LINQ-CPP 将.NET LINQ功能引入C ++ 11,这是一个非常好的例子。

使用它,您可以编写如下函数:

TEnumerable<std::pair<int, int>> EnumerateEdges(std::vector<std::unordered_set<int>>& neighbors)
{
    return Enumerable::FromRange(neighbors)
        .SelectManyIndexed([](std::unordered_set<int>& bNodes, int aNode)
        {
            return Enumerable::FromRange(bNodes)
                .Select([=](int bNode){ return std::make_pair(aNode, bNode); });
        });
}

然后像这样使用它:

EnumerateEdges(neighbors).ForEach([](std::pair<int, int> edge)
{
    /* your code */
});

或者像这样:

auto edges = EnumerateEdges(neighbors).ToVector();

1
2018-04-04 22:57



这看起来很不错。是否可以适应以某种方式使用基于范围的语法?另外,我没有看到只实现与aNode <bNode产生对的条件,可以添加吗? - Falk Hüffner
因此,虽然它可以适应基于范围的语法,但我故意没有这样做。那将是一个迭代器/枚举器;这是一个可枚举的。在自述文件中讨论了这种区别。要仅使用<b获得边缘,您只需添加一个调用 Where 查询中的函数。 github自述文件讨论了所有这些内容。 - Timothy Shields
诸如“从节点X执行BFS”的其他操作可以类似地以非常简单和可重复使用的方式编写。然后你可以做类似的事情 BFS(myNode).Where(/* some predicate on nodes */).Take(10) 意思是“让10个节点最接近 myNode 满足一些谓词。“ - Timothy Shields


我相信你的图形的内部表示, std::vector<std::unordered_set<Vertex>>,是什么使代码难以写/读。也许是另一种表现形式 std::set<std::pair<Vertex, Vertex>>)会使你的代码更简单。但是,由于我们不确切知道什么是要求,因此很难说 Graph

无论如何,正如所指出的那样 泽塔 有一个错误 EdgeIter::operator !=()。例如,下面的代码:

int main() {

    Graph g(5);
    g.addEdge(0, 1);
    g.addEdge(0, 2);

    auto i1 = g.edges().begin();
    auto i2 = i1;
    ++i2;
    std::cout << std::boolalpha;
    std::cout << (i1 != i2) << std::endl;
}

输出 false。因此,代码考虑到了这一点 i1 和 i2 他们显然是没有什么不同。

更新:

这可能是显而易见的,但这里是一个更简单的版本,它使用了不同的图形表示。但是,我强调这一点 这可能不太令人满意根据您的要求 Graph (我不知道):

#include <set>
#include <stdexcept>
#include <iostream>

typedef unsigned Vertex;

class Graph {

public:

    typedef std::pair<Vertex, Vertex> Edge;
    typedef std::set<Edge> Edges;

    void addEdge(Vertex u, Vertex v) {
      edges_.insert({u, v});
    }

    const Edges& edges() { return edges_; }

private:

    Edges edges_;
};

int main() {

    Graph g;

    g.addEdge(1, 2);
    g.addEdge(2, 3);
    g.addEdge(1, 3);    

    for (auto e: g.edges())
        std::cout << e.first << ' ' << e.second << std::endl;
}

1
2018-04-04 22:44



这是一个有趣的选择。但是,我经常需要遍历顶点的所有邻居。在这里如何做到这一点并不明显。另请注意,在插入之前,您需要将边缘标准化为u <v,因为我希望{u,v}和{v,u}相同 - Falk Hüffner