Search code examples
c++boostboost-graphboost-multi-index

Reversing a graph while maintaining access to edges sorted by source or target vertex


Note: I changed the title of the question because it turns out the following text and minimal example reflect my original problem poorly. The original title was "Interchanging two similar indices in a boost multi-index container"


I'm implementing directed graphs (with loops and multiple edges) whose vertices are numbered and where we can view the edges sorted by source vertex or target vertex. To do this, I'm using a boost::multi_index_container that stores each edge, with two ordered, non-unique, member key extractors: source and target. (I don't think this is possible directly with BGL, but please let me know if it is!)

In addition, I would like to be able to take a graph and reverse all of its edges (or create a new graph with the reverse edges). I can do this by iterating over the original edges and adding each one's reverse to a new container. However, this means that boost has to recompute everything for every edge for at least one of the indices, which is quasilinear in the number of edges. Is there a way to tell boost that it can reuse the knowledge it already has about the source and target of the original edges in order to place the reversed edges?

Here is a minimal example:

#include <iostream>
#include <vector>

#include <boost/multi_index_container.hpp>
#include <boost/multi_index/ordered_index.hpp>
#include <boost/multi_index/member.hpp>

// A directed edge
class Edge {
 public:
  Edge(int s, int t) :
    source(s),
    target(t)
  { }
  
  int source;
  int target;
  
  // For display purposes
  friend std::ostream& operator<<(std::ostream& os, const Edge& edge) {
    os << "(" << edge.source << "," << edge.target << ")" << std::flush;
    return os;
  }
};

// Tags for the multi-index container
struct Source { };
struct Target { };

// Directed graphs with loops and multiple edges, with the possibility of
// viewing the edges sorted by source or by target.
using Directed_graph = boost::multi_index_container<
  Edge,
  boost::multi_index::indexed_by<
    boost::multi_index::ordered_non_unique<
      boost::multi_index::tag< Source >,
      boost::multi_index::member< Edge, int, &Edge::source >
    >,
    boost::multi_index::ordered_non_unique<
      boost::multi_index::tag< Target >,
      boost::multi_index::member< Edge, int, &Edge::target >
    >
  >
>;

// Reverse all the edges of the graph, either in place or creating a copy
//
// QUESTION: Is there a better way to do this?
//
Directed_graph reverse_graph(Directed_graph& graph) {
  Directed_graph reversed_graph;
  for (const auto& edge : graph) {
    reversed_graph.insert(Edge(edge.target, edge.source));
  }
  return reversed_graph;
}

// Print every edge of the graph
void output(const Directed_graph& graph) {
  for (const auto& edge : graph) {
    std::cout << edge << " " << std::flush;
  }
  std::cout << std::endl;
}

int main() {
  Directed_graph G;
  
  G.insert(Edge(0, 1));
  G.insert(Edge(1, 2));
  G.insert(Edge(1, 3));
  G.insert(Edge(3, 0));
  
  std::cout << "Directed graph:" << std::endl;
  output(G);
  
  std::cout << "Reversed directed graph:" << std::endl;
  Directed_graph rG = reverse_graph(G);
  output(rG);
  
  return 0;
}

Compiling with gcc -std=c++11, I get the output

Directed graph:
(0,1) (1,2) (1,3) (3,0) 
Reversed directed graph:
(0,3) (1,0) (2,1) (3,1) 

To sum up, is there a way to write the function reverse_graph with less than quasilinear complexity? Ideally, this would be a constant time operation.

One potential direction would be the ability to insert elements with simultaneous hints for multiple indices, but I haven't found a function like that either (and even then, I don't see how to get to constant time).

Note: Just a technicality, but Directed_graph doesn't completely encode a directed graph, since we would also need to know how many vertices there are in total. This isn't an issue in the real code, and I hope it doesn't obstruct the above example!


Solution

  • First let me propose some simplifications: Live On Coliru

    Your graphs are edge-lists. You can already directly model these in BGL: https://www.boost.org/doc/libs/1_77_0/libs/graph/doc/edge_list.html

    So you can use:

    Live On Coliru

    #include <iostream>
    #include <list>
    
    #include <boost/graph/edge_list.hpp>
    
    using Vertex = int;
    using Edge   = std::pair<Vertex, Vertex>;
    using Edges  = std::list<Edge>; // iterator stability like multi-index
    
    using Directed_graph = boost::edge_list<Edges::iterator>;
    
    // Print every edge of the graph
    template <typename Graph>
    void output(const Graph& g)
    {
        for (auto e : boost::make_iterator_range(edges(g))) {
            std::cout << "(" << source(e, g) << "," << target(e, g) << ") ";
        }
        std::cout << std::endl;
    }
    
    int main()
    {
        Edges EE{{0, 1}, {1, 2}, {1, 3}, {3, 0}};
        Directed_graph G(begin(EE), end(EE));
    
        std::cout << "Directed graph:" << std::endl;
        output(G);
    }
    

    Printing

    Directed graph:
    (0,1) (1,2) (1,3) (3,0)
    

    Hybrid With Multi-Index

    Assuming you might want to work on top of the existing container for some reason, you can with little changes (though std::pair conpatibility is required):

    Live On Coliru

    #include <boost/graph/edge_list.hpp>
    #include <boost/multi_index/member.hpp>
    #include <boost/multi_index/ordered_index.hpp>
    #include <boost/multi_index_container.hpp>
    #include <iostream>
    
    using Vertex  = int;
    using VertexP = std::pair<Vertex, Vertex>;
    
    // A directed edge
    struct Edge : VertexP {
        using VertexP::VertexP;
        /*
         * // For display purposes
         * friend std::ostream& operator<<(std::ostream& os, const Edge& e) {
         *     return os << "(" << e.first << "," << e.second << ")";
         * }
         */
    };
    
    // Tags for the multi-index container
    
    // Directed graphs with loops and multiple edges, with the possibility of
    // viewing the edges sorted by source or by target.
    using Edges = boost::multi_index_container<
        Edge,
        boost::multi_index::indexed_by<
            boost::multi_index::ordered_non_unique<
                boost::multi_index::tag<struct Source>,
                boost::multi_index::member<VertexP, Vertex, &Edge::first>>,
            boost::multi_index::ordered_non_unique<
                boost::multi_index::tag<struct Target>,
                boost::multi_index::member<VertexP, Vertex, &Edge::second>>>>;
    
    using Directed_graph = boost::edge_list<Edges::iterator>;
    
    // Print every edge of the graph
    template <typename Graph>
    void output(const Graph& g)
    {
        for (auto e : boost::make_iterator_range(edges(g))) {
            std::cout << "(" << source(e, g) << "," << target(e, g) << ") ";
        }
        std::cout << std::endl;
    }
    
    int main()
    {
        Edges EE{{0, 1}, {1, 2}, {1, 3}, {3, 0}};
        Directed_graph G(begin(EE), end(EE));
    
        std::cout << "Directed graph:" << std::endl;
        output(G);
    }
    

    In Reverse

    However, I think you might have thought of the multi-indexes mainly/only because of the reversability. BGL has reversed_graph adaptors. This allows you to drop the - generally inefficient/inflexible - model of an EdgeList for storage.

    The most popular BGL model is the AdjacencyList, which can be configured for bidirectional access, so the reverse adaptor is optimal:

    The construction of the reverse_graph is constant time, providing a highly efficient way to obtain a transposed view of a graph.

    Live On Compiler Explorer

    #include <boost/graph/adjacency_list.hpp>
    #include <boost/graph/reverse_graph.hpp>
    #include <boost/graph/graph_utility.hpp>
    #include <iostream>
    
    using Directed_graph =
        boost::adjacency_list<boost::vecS, boost::vecS, boost::bidirectionalS>;
    
    int main()
    {
        Directed_graph g;
        for (auto [s, t] : {std::pair{0, 1}, {1, 2}, {1, 3}, {3, 0}})
            add_edge(s, t, g);
    
        print_graph(g, std::cout << "Directed graph:\n" );
        print_graph(make_reverse_graph(g), std::cout << "Reversed graph:\n" );
    }
    

    Prints

    Directed graph:
    0 --> 1
    1 --> 2 3
    2 -->
    3 --> 0
    Reversed graph:
    0 --> 3
    1 --> 0
    2 --> 1
    3 --> 1
    

    Of course, the above output template function still works with this graph model.

    BONUS: Bimaps

    I just realized that there is another container that you might like: Boost Bimap

    See it Live On Coliru

    #include <boost/bimap.hpp>
    #include <boost/bimap/multiset_of.hpp>
    #include <boost/graph/edge_list.hpp>
    #include <iostream>
    
    namespace bb = boost::bimaps;
    using Vertex = int;
    using Edges  = bb::bimap<bb::multiset_of<int>, bb::multiset_of<int>>;
    
    using Directed_graph = boost::edge_list<Edges::left_map::iterator>;
    using Reverse_graph = boost::edge_list<Edges::right_map::iterator>;
    
    // Print every edge of the graph
    template <typename Graph>
    void output(const Graph& g)
    {
        for (auto e : boost::make_iterator_range(edges(g))) {
            std::cout << "(" << source(e, g) << "," << target(e, g) << ") ";
        }
        std::cout << std::endl;
    }
    
    int main()
    {
        Edges ee{};
        for (auto [s, t] : {std::pair{0, 1}, {1, 2}, {1, 3}, {3, 0}})
            ee.insert({s, t});
    
        {
            auto& vw = ee.left;
            Directed_graph g(vw.begin(), vw.end());
    
            std::cout << "Directed graph:" << std::endl;
            output(g);
        }
    
        {
            auto& vw = ee.right;
            Reverse_graph r(vw.begin(), vw.end());
    
            std::cout << "Reversed graph:" << std::endl;
            output(r);
        }
    }
    

    Which also prints

    Directed graph:
    (0,1) (1,2) (1,3) (3,0) 
    Reversed graph:
    (0,3) (1,0) (2,1) (3,1)