1

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!

Isaac Ren
  • 113
  • 5

1 Answers1

2

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) 

sehe
  • 374,641
  • 47
  • 450
  • 633
  • I just realized that there is another container that you might like: [Boost Bimap](https://www.boost.org/doc/libs/1_77_0/libs/bimap/doc/html/index.html). See it **[Live On Coliru](http://coliru.stacked-crooked.com/a/34bcd4ab02bd10b3)** – sehe Nov 08 '21 at 16:34
  • 1
    I think the ideal solution would be to use Boost.Bimap directly (without `edge_list` or any other BGL construct): `left` is the direct view and `right` the reverse view, no need to adapt or construct the reverse graph. – Joaquín M López Muñoz Nov 08 '21 at 17:17
  • Thank you @sehe for the very extensive answer! I appreciate the detail you went into to show functionalities of the BGL. Unfortunately it appears that I haven't communicated my problem very well, but that's my fault when designing the minimal example. In particular, I wanted to apply the same (not written) modifying functions to the regular graph and the reversed graph, and be able to pass from the regular to reversed graph and back after modifications. This might be possible (maybe with bimap?), but I'll have to think some more about my problem. – Isaac Ren Nov 08 '21 at 21:57
  • 1
    You can do that with both the bimap approach and the reverse_graph adaptor. I want to highlight that if you want the option of running the graph algorithms in BGL on your graph, then using the adjacency-list is greatly increasing your options over a strict edge list. Also, you can add properties to your edge in case your struct contained extra data: [live example](https://godbolt.org/z/3nr6x6ev8) – sehe Nov 09 '21 at 00:05
  • 1
    I don't actually need much of BGL's algorithms, which is part of the reason I didn't think of using it originally. In the end, I think it's the bimaps and Joaquín's suggestion that work best for me. I will still accept this answer and change the question title because I think this does answer the question I ended up asking! – Isaac Ren Nov 09 '21 at 09:04