From 724d8f3c9ad67bc87c1d81fe8f957bc15bf9cbf5 Mon Sep 17 00:00:00 2001 From: Joseph Malle Date: Sun, 7 Jun 2026 12:17:31 -0400 Subject: [PATCH] Implement topological_sort_levels --- doc/table_of_contents.html | 1 + doc/topological_sort_levels.html | 174 +++++++++++ .../boost/graph/topological_sort_levels.hpp | 195 ++++++++++++ test/Jamfile.v2 | 1 + test/topological_sort_levels_test.cpp | 285 ++++++++++++++++++ 5 files changed, 656 insertions(+) create mode 100644 doc/topological_sort_levels.html create mode 100644 include/boost/graph/topological_sort_levels.hpp create mode 100644 test/topological_sort_levels_test.cpp diff --git a/doc/table_of_contents.html b/doc/table_of_contents.html index 0bc75b89d..3c8357ebd 100644 --- a/doc/table_of_contents.html +++ b/doc/table_of_contents.html @@ -158,6 +158,7 @@

Table of Contents: the Boost Graph Library
  • Other Core Algorithms
    1. topological_sort +
    2. topological_sort_levels
    3. transitive_closure
    4. lengauer_tarjan_dominator_tree
    diff --git a/doc/topological_sort_levels.html b/doc/topological_sort_levels.html new file mode 100644 index 000000000..a02ef852b --- /dev/null +++ b/doc/topological_sort_levels.html @@ -0,0 +1,174 @@ + + + +Boost Graph Library: Topological Sort into Levels + +C++ Boost + +
    + + +

    +topological_sort_levels +

    + +
    +// Property-map form. Returns the number of levels.
    +template <typename VertexListGraph, typename LevelMap>
    +typename graph_traits<VertexListGraph>::vertices_size_type
    +topological_sort_levels(const VertexListGraph& g, LevelMap level);
    +
    +template <typename VertexListGraph, typename LevelMap,
    +          typename P, typename T, typename R>
    +typename graph_traits<VertexListGraph>::vertices_size_type
    +topological_sort_levels(const VertexListGraph& g, LevelMap level,
    +    const bgl_named_params<P, T, R>& params = all defaults);
    +
    +// Convenience form. Fills levels so that levels[k] contains
    +// every vertex assigned to level k.
    +template <typename VertexListGraph>
    +void topological_sort_levels(const VertexListGraph& g,
    +    std::vector<std::vector<
    +        typename graph_traits<VertexListGraph>::vertex_descriptor> >& levels);
    +
    +template <typename VertexListGraph, typename P, typename T, typename R>
    +void topological_sort_levels(const VertexListGraph& g,
    +    std::vector<std::vector<
    +        typename graph_traits<VertexListGraph>::vertex_descriptor> >& levels,
    +    const bgl_named_params<P, T, R>& params = all defaults);
    +
    + +

    +A variant of topological_sort +that groups vertices into levels rather than producing a single linear +ordering. Following BGL's edge convention, an edge (u, v) means u +must precede v; equivalently level(u) < level(v). Level 0 +contains every vertex with no incoming edges. For k > 0, level +k contains every vertex whose deepest predecessor lies at level +k - 1. +

    + +

    +The graph must be a directed acyclic graph (DAG). If it contains a cycle a +not_a_dag exception is +thrown. +

    + +

    +Note on direction: topological_sort +emits its output in reverse topological order (sinks first when read in +iteration order) and most users reverse the result to consume it forward. +topological_sort_levels writes levels forward natively: levels[0] +holds the sources and levels[n-1] the sinks. +

    + +

    +The implementation is Kahn's algorithm: it scans out-edges once to compute +in-degrees, then peels off zero-in-degree vertices in waves. +

    + +

    Where Defined:

    +boost/graph/topological_sort_levels.hpp + +

    Parameters

    + +IN: const VertexListGraph& g +
    + A directed acyclic graph (DAG). The graph type must be a model of + Vertex List Graph and + Incidence Graph. + If the graph is not a DAG, a + not_a_dag exception is + thrown and the contents of the output parameter are unspecified. +
    + +OUT: LevelMap level +
    + After the call, get(level, v) is the level of vertex v. + Must be a model of + Read/Write + Property Map whose key type is the graph's vertex descriptor and whose + value type is convertible from + graph_traits<VertexListGraph>::vertices_size_type. +
    + +OUT: std::vector<std::vector<vertex_descriptor> >& levels +
    + (Convenience overload.) Resized to hold one inner vector per level. On + return, levels[k] contains every vertex at level k. The + order of vertices within a level is unspecified. +
    + +

    Named Parameters

    + +IN: vertex_index_map(VertexIndexMap i_map) +
    + Maps each vertex to an integer in [0, num_vertices(g)), used + internally to store running in-degrees. The type must be a model of + Readable Property + Map whose key type is the graph's vertex descriptor and whose value + type is an integer.
    + Default: get(vertex_index, g). If you use this default, + make sure your graph has an internal vertex_index property + (e.g. adjacency_list<…, vecS, …>). +
    + +

    Return Value

    + +The property-map overloads return the number of levels (one more than the +longest path length in the DAG, or 0 if the graph is empty). + +

    Complexity

    + +O(V + E) time and O(V) auxiliary space. + +

    Example

    + +

    +A diamond DAG with edges 0 → 1, 0 → 2, 1 → 3, 2 → 3 +has three levels: {0}, {1, 2}, {3}. +

    + +
    +  typedef boost::adjacency_list<boost::vecS, boost::vecS, boost::directedS> Graph;
    +  typedef boost::graph_traits<Graph>::vertex_descriptor Vertex;
    +
    +  Graph g(4);
    +  add_edge(0, 1, g);
    +  add_edge(0, 2, g);
    +  add_edge(1, 3, g);
    +  add_edge(2, 3, g);
    +
    +  std::vector<std::vector<Vertex> > levels;
    +  boost::topological_sort_levels(g, levels);
    +
    +  for (std::size_t k = 0; k < levels.size(); ++k) {
    +    std::cout << "level " << k << ":";
    +    for (std::size_t i = 0; i < levels[k].size(); ++i)
    +      std::cout << ' ' << levels[k][i];
    +    std::cout << '\n';
    +  }
    +
    +The output is: +
    +  level 0: 0
    +  level 1: 1 2
    +  level 2: 3
    +
    + +

    +Motivation: +issue #240. +

    + +
    +
    + + + diff --git a/include/boost/graph/topological_sort_levels.hpp b/include/boost/graph/topological_sort_levels.hpp new file mode 100644 index 000000000..6df7be8d5 --- /dev/null +++ b/include/boost/graph/topological_sort_levels.hpp @@ -0,0 +1,195 @@ +// +//======================================================================= +// Distributed under the Boost Software License, Version 1.0. (See +// accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +//======================================================================= +// +#ifndef BOOST_GRAPH_TOPOLOGICAL_SORT_LEVELS_HPP +#define BOOST_GRAPH_TOPOLOGICAL_SORT_LEVELS_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost +{ + +// Topological Sort into Levels +// +// Like topological_sort, but groups vertices by "level" rather than producing +// a single linear ordering. Level 0 contains every vertex with no incoming +// edges; level k > 0 contains every vertex whose longest incoming path comes +// from a vertex at level k - 1. +// +// Edge convention follows the rest of BGL: an edge (u, v) means u must +// precede v, so level(u) < level(v). Level numbering runs forward, from +// sources at level 0 to sinks at the highest level. +// +// Implemented via Kahn's algorithm. Same concept requirements as +// topological_sort: VertexListGraph + IncidenceGraph. + +namespace detail +{ + + template < typename VertexListGraph, typename LevelMap, + typename VertexIndexMap > + typename graph_traits< VertexListGraph >::vertices_size_type + topological_sort_levels_impl( + const VertexListGraph& g, LevelMap level, VertexIndexMap index_map) + { + typedef typename graph_traits< VertexListGraph >::vertex_descriptor + Vertex; + typedef typename graph_traits< VertexListGraph >::vertices_size_type + size_type; + typedef typename graph_traits< VertexListGraph >::vertex_iterator + vertex_iter; + typedef typename graph_traits< VertexListGraph >::out_edge_iterator + out_edge_iter; + + const size_type n = num_vertices(g); + std::vector< size_type > in_degree(n, 0); + + vertex_iter vi, vi_end; + for (boost::tie(vi, vi_end) = vertices(g); vi != vi_end; ++vi) + { + out_edge_iter ei, ei_end; + for (boost::tie(ei, ei_end) = out_edges(*vi, g); ei != ei_end; + ++ei) + { + ++in_degree[get(index_map, target(*ei, g))]; + } + } + + std::vector< Vertex > current_level; + for (boost::tie(vi, vi_end) = vertices(g); vi != vi_end; ++vi) + { + if (in_degree[get(index_map, *vi)] == 0) + { + current_level.push_back(*vi); + put(level, *vi, size_type(0)); + } + } + + size_type total_emitted = current_level.size(); + size_type level_count = current_level.empty() ? size_type(0) + : size_type(1); + + std::vector< Vertex > next_level; + while (!current_level.empty()) + { + next_level.clear(); + for (typename std::vector< Vertex >::const_iterator it + = current_level.begin(); + it != current_level.end(); ++it) + { + out_edge_iter ei, ei_end; + for (boost::tie(ei, ei_end) = out_edges(*it, g); ei != ei_end; + ++ei) + { + Vertex v = target(*ei, g); + size_type vidx = get(index_map, v); + if (--in_degree[vidx] == 0) + { + next_level.push_back(v); + put(level, v, level_count); + } + } + } + if (!next_level.empty()) + { + ++level_count; + total_emitted += next_level.size(); + } + current_level.swap(next_level); + } + + if (total_emitted != n) + { + BOOST_THROW_EXCEPTION(not_a_dag()); + } + + return level_count; + } + + template < typename VertexListGraph, typename VertexIndexMap > + void topological_sort_levels_to_buckets(const VertexListGraph& g, + VertexIndexMap index_map, + std::vector< std::vector< typename graph_traits< + VertexListGraph >::vertex_descriptor > >& levels) + { + typedef typename graph_traits< VertexListGraph >::vertices_size_type + size_type; + + std::vector< size_type > level_of(num_vertices(g), size_type(0)); + + const size_type num_levels = topological_sort_levels_impl(g, + make_iterator_property_map(level_of.begin(), index_map), + index_map); + + levels.clear(); + levels.resize(num_levels); + + typename graph_traits< VertexListGraph >::vertex_iterator vi, vi_end; + for (boost::tie(vi, vi_end) = vertices(g); vi != vi_end; ++vi) + { + levels[level_of[get(index_map, *vi)]].push_back(*vi); + } + } + +} // namespace detail + +// Property-map form. Writes level[v] = k for every vertex v and returns the +// number of levels. LevelMap must be a writable property map keyed on the +// graph's vertex descriptor with a value type convertible from +// vertices_size_type. +template < typename VertexListGraph, typename LevelMap, typename P, typename T, + typename R > +typename graph_traits< VertexListGraph >::vertices_size_type +topological_sort_levels(const VertexListGraph& g, LevelMap level, + const bgl_named_params< P, T, R >& params) +{ + return detail::topological_sort_levels_impl(g, level, + choose_const_pmap(get_param(params, vertex_index), g, vertex_index)); +} + +template < typename VertexListGraph, typename LevelMap > +typename graph_traits< VertexListGraph >::vertices_size_type +topological_sort_levels(const VertexListGraph& g, LevelMap level) +{ + return topological_sort_levels( + g, level, bgl_named_params< int, buffer_param_t >(0)); +} + +// Convenience form. Resizes levels to hold one inner vector per +// level; on return, levels[k] contains every vertex assigned to +// level k. +template < typename VertexListGraph, typename P, typename T, typename R > +void topological_sort_levels(const VertexListGraph& g, + std::vector< std::vector< typename graph_traits< + VertexListGraph >::vertex_descriptor > >& levels, + const bgl_named_params< P, T, R >& params) +{ + detail::topological_sort_levels_to_buckets(g, + choose_const_pmap(get_param(params, vertex_index), g, vertex_index), + levels); +} + +template < typename VertexListGraph > +void topological_sort_levels(const VertexListGraph& g, + std::vector< std::vector< typename graph_traits< + VertexListGraph >::vertex_descriptor > >& levels) +{ + topological_sort_levels( + g, levels, bgl_named_params< int, buffer_param_t >(0)); +} + +} // namespace boost + +#endif // BOOST_GRAPH_TOPOLOGICAL_SORT_LEVELS_HPP diff --git a/test/Jamfile.v2 b/test/Jamfile.v2 index d05a0a7ef..a77097956 100644 --- a/test/Jamfile.v2 +++ b/test/Jamfile.v2 @@ -174,6 +174,7 @@ alias graph_test_regular : [ run delete_edge.cpp ] [ run johnson-test.cpp ] [ run lvalue_pmap.cpp ] + [ run topological_sort_levels_test.cpp ] ; alias graph_test_with_filesystem : : diff --git a/test/topological_sort_levels_test.cpp b/test/topological_sort_levels_test.cpp new file mode 100644 index 000000000..c1e1f5823 --- /dev/null +++ b/test/topological_sort_levels_test.cpp @@ -0,0 +1,285 @@ +// Distributed under the Boost Software License, Version 1.0. (See +// accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) + +#include +#include +#include + +#include +#include +#include +#include +#include + +typedef boost::adjacency_list< boost::vecS, boost::vecS, boost::directedS > + Graph; +typedef boost::graph_traits< Graph >::vertex_descriptor Vertex; + +// Check that the level assignment is consistent with the graph: for every +// edge (u, v) the level of u must be strictly less than the level of v, +// and levels are tight (any vertex above level 0 has a predecessor exactly +// one level below). +template < typename G, typename Levels > +static void check_levels_are_tight(const G& g, const Levels& levels) +{ + std::vector< std::size_t > level_of(num_vertices(g), 0); + for (std::size_t k = 0; k < levels.size(); ++k) + { + for (std::size_t i = 0; i < levels[k].size(); ++i) + { + level_of[levels[k][i]] = k; + } + } + + std::size_t total = 0; + for (std::size_t k = 0; k < levels.size(); ++k) + total += levels[k].size(); + BOOST_TEST_EQ(total, num_vertices(g)); + + typename boost::graph_traits< G >::edge_iterator ei, ei_end; + for (boost::tie(ei, ei_end) = edges(g); ei != ei_end; ++ei) + { + const std::size_t su = level_of[source(*ei, g)]; + const std::size_t sv = level_of[target(*ei, g)]; + BOOST_TEST_LT(su, sv); + } + + for (std::size_t k = 1; k < levels.size(); ++k) + { + for (std::size_t i = 0; i < levels[k].size(); ++i) + { + const Vertex v = levels[k][i]; + bool has_predecessor_at_prev_level = false; + typename boost::graph_traits< G >::edge_iterator e, e_end; + for (boost::tie(e, e_end) = edges(g); e != e_end; ++e) + { + if (target(*e, g) == v && level_of[source(*e, g)] == k - 1) + { + has_predecessor_at_prev_level = true; + break; + } + } + BOOST_TEST(has_predecessor_at_prev_level); + } + } +} + +// Vertices 0..3 with edges 0->1, 0->2, 1->3, 2->3 form a diamond. +// Following BGL's edge convention (u -> v means u precedes v in topological +// order), the expected levels are {0}, {1, 2}, {3}. +static void test_diamond() +{ + Graph g(4); + add_edge(0, 1, g); + add_edge(0, 2, g); + add_edge(1, 3, g); + add_edge(2, 3, g); + + std::vector< std::vector< Vertex > > levels; + boost::topological_sort_levels(g, levels); + + BOOST_TEST_EQ(levels.size(), std::size_t(3)); + BOOST_TEST_EQ(levels[0].size(), std::size_t(1)); + BOOST_TEST_EQ(levels[0][0], Vertex(0)); + BOOST_TEST_EQ(levels[1].size(), std::size_t(2)); + BOOST_TEST(std::find(levels[1].begin(), levels[1].end(), Vertex(1)) + != levels[1].end()); + BOOST_TEST(std::find(levels[1].begin(), levels[1].end(), Vertex(2)) + != levels[1].end()); + BOOST_TEST_EQ(levels[2].size(), std::size_t(1)); + BOOST_TEST_EQ(levels[2][0], Vertex(3)); + + check_levels_are_tight(g, levels); +} + +static void test_empty() +{ + Graph g(0); + std::vector< std::vector< Vertex > > levels; + boost::topological_sort_levels(g, levels); + BOOST_TEST_EQ(levels.size(), std::size_t(0)); +} + +static void test_single_vertex() +{ + Graph g(1); + std::vector< std::vector< Vertex > > levels; + boost::topological_sort_levels(g, levels); + BOOST_TEST_EQ(levels.size(), std::size_t(1)); + BOOST_TEST_EQ(levels[0].size(), std::size_t(1)); + BOOST_TEST_EQ(levels[0][0], Vertex(0)); +} + +static void test_isolated_vertices() +{ + Graph g(5); + std::vector< std::vector< Vertex > > levels; + boost::topological_sort_levels(g, levels); + BOOST_TEST_EQ(levels.size(), std::size_t(1)); + BOOST_TEST_EQ(levels[0].size(), std::size_t(5)); +} + +static void test_chain() +{ + Graph g(5); + add_edge(0, 1, g); + add_edge(1, 2, g); + add_edge(2, 3, g); + add_edge(3, 4, g); + + std::vector< std::vector< Vertex > > levels; + boost::topological_sort_levels(g, levels); + + BOOST_TEST_EQ(levels.size(), std::size_t(5)); + for (std::size_t k = 0; k < levels.size(); ++k) + { + BOOST_TEST_EQ(levels[k].size(), std::size_t(1)); + BOOST_TEST_EQ(levels[k][0], Vertex(k)); + } +} + +static void test_disconnected_components() +{ + // Two independent chains: 0->1->2 and 3->4 + Graph g(5); + add_edge(0, 1, g); + add_edge(1, 2, g); + add_edge(3, 4, g); + + std::vector< std::vector< Vertex > > levels; + boost::topological_sort_levels(g, levels); + + BOOST_TEST_EQ(levels.size(), std::size_t(3)); + BOOST_TEST_EQ(levels[0].size(), std::size_t(2)); + BOOST_TEST_EQ(levels[1].size(), std::size_t(2)); + BOOST_TEST_EQ(levels[2].size(), std::size_t(1)); + BOOST_TEST_EQ(levels[2][0], Vertex(2)); + + check_levels_are_tight(g, levels); +} + +static void test_long_edge_skips_levels() +{ + // 0->1, 0->2, 1->2: vertex 2 has predecessors at both level 0 and level 1, + // so it must land at level 2, not level 1. + Graph g(3); + add_edge(0, 1, g); + add_edge(0, 2, g); + add_edge(1, 2, g); + + std::vector< std::vector< Vertex > > levels; + boost::topological_sort_levels(g, levels); + + BOOST_TEST_EQ(levels.size(), std::size_t(3)); + BOOST_TEST_EQ(levels[0][0], Vertex(0)); + BOOST_TEST_EQ(levels[1][0], Vertex(1)); + BOOST_TEST_EQ(levels[2][0], Vertex(2)); +} + +static void test_cycle_throws() +{ + Graph g(3); + add_edge(0, 1, g); + add_edge(1, 2, g); + add_edge(2, 0, g); + + std::vector< std::vector< Vertex > > levels; + bool threw = false; + try + { + boost::topological_sort_levels(g, levels); + } + catch (const boost::not_a_dag&) + { + threw = true; + } + BOOST_TEST(threw); +} + +static void test_self_loop_throws() +{ + Graph g(1); + add_edge(0, 0, g); + + std::vector< std::vector< Vertex > > levels; + bool threw = false; + try + { + boost::topological_sort_levels(g, levels); + } + catch (const boost::not_a_dag&) + { + threw = true; + } + BOOST_TEST(threw); +} + +static void test_property_map_form() +{ + Graph g(4); + add_edge(0, 1, g); + add_edge(0, 2, g); + add_edge(1, 3, g); + add_edge(2, 3, g); + + boost::vector_property_map< std::size_t > level_map(num_vertices(g)); + const std::size_t num_levels = boost::topological_sort_levels(g, level_map); + + BOOST_TEST_EQ(num_levels, std::size_t(3)); + BOOST_TEST_EQ(level_map[0], std::size_t(0)); + BOOST_TEST_EQ(level_map[1], std::size_t(1)); + BOOST_TEST_EQ(level_map[2], std::size_t(1)); + BOOST_TEST_EQ(level_map[3], std::size_t(2)); +} + +static void test_named_param_vertex_index() +{ + Graph g(4); + add_edge(0, 1, g); + add_edge(0, 2, g); + add_edge(1, 3, g); + add_edge(2, 3, g); + + boost::vector_property_map< std::size_t > level_map(num_vertices(g)); + const std::size_t num_levels = boost::topological_sort_levels(g, level_map, + boost::vertex_index_map(get(boost::vertex_index, g))); + + BOOST_TEST_EQ(num_levels, std::size_t(3)); + BOOST_TEST_EQ(level_map[0], std::size_t(0)); + BOOST_TEST_EQ(level_map[3], std::size_t(2)); +} + +static void test_named_param_convenience_form() +{ + Graph g(4); + add_edge(0, 1, g); + add_edge(0, 2, g); + add_edge(1, 3, g); + add_edge(2, 3, g); + + std::vector< std::vector< Vertex > > levels; + boost::topological_sort_levels(g, levels, + boost::vertex_index_map(get(boost::vertex_index, g))); + + BOOST_TEST_EQ(levels.size(), std::size_t(3)); + BOOST_TEST_EQ(levels[0][0], Vertex(0)); + BOOST_TEST_EQ(levels[2][0], Vertex(3)); +} + +int main(int, char*[]) +{ + test_diamond(); + test_empty(); + test_single_vertex(); + test_isolated_vertices(); + test_chain(); + test_disconnected_components(); + test_long_edge_skips_levels(); + test_cycle_throws(); + test_self_loop_throws(); + test_property_map_form(); + test_named_param_vertex_index(); + test_named_param_convenience_form(); + return boost::report_errors(); +}