Skip to content

Commit 2a967a7

Browse files
committed
Add lazy traversal edge views
1 parent 0d62e06 commit 2a967a7

3 files changed

Lines changed: 286 additions & 0 deletions

File tree

include/nxpp/graph.hpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1546,6 +1546,14 @@ class Graph {
15461546
* @throws std::runtime_error If @p start is not present in the graph.
15471547
*/
15481548
[[nodiscard]] auto bfs_edges(const NodeID& start) const;
1549+
/**
1550+
* @brief Returns a lazy input range over breadth-first-search tree edges.
1551+
*
1552+
* @param start Node ID used as the BFS root.
1553+
* @return A non-owning input range yielding `(parent, child)` tree-edge pairs.
1554+
* @throws std::runtime_error If @p start is not present in the graph.
1555+
*/
1556+
[[nodiscard]] auto bfs_edges_view(const NodeID& start) const;
15491557
/**
15501558
* @brief Materializes the breadth-first-search tree rooted at @p start.
15511559
*
@@ -1607,6 +1615,14 @@ class Graph {
16071615
* @throws std::runtime_error If @p start is not present in the graph.
16081616
*/
16091617
[[nodiscard]] auto dfs_edges(const NodeID& start) const;
1618+
/**
1619+
* @brief Returns a lazy input range over depth-first-search tree edges.
1620+
*
1621+
* @param start Node ID used as the DFS root.
1622+
* @return A non-owning input range yielding `(parent, child)` tree-edge pairs.
1623+
* @throws std::runtime_error If @p start is not present in the graph.
1624+
*/
1625+
[[nodiscard]] auto dfs_edges_view(const NodeID& start) const;
16101626
/**
16111627
* @brief Materializes the depth-first-search tree rooted at @p start.
16121628
*

include/nxpp/traversal.hpp

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,197 @@
1111
#include <boost/graph/breadth_first_search.hpp>
1212
#include <boost/graph/depth_first_search.hpp>
1313

14+
#include <iterator>
15+
#include <memory>
16+
#include <optional>
17+
#include <queue>
18+
#include <vector>
19+
1420
#include "graph.hpp"
1521

1622
namespace nxpp {
1723

1824
namespace detail {
1925

26+
enum class traversal_order {
27+
bfs,
28+
dfs
29+
};
30+
31+
template <typename GraphWrapper, traversal_order Order>
32+
class traversal_edges_view {
33+
public:
34+
using NodeID = typename GraphWrapper::NodeType;
35+
using value_type = std::pair<NodeID, NodeID>;
36+
37+
traversal_edges_view(const GraphWrapper& graph, const NodeID& start) : graph(&graph), start(start) {}
38+
39+
class iterator {
40+
public:
41+
using iterator_category = std::input_iterator_tag;
42+
using value_type = traversal_edges_view::value_type;
43+
using difference_type = std::ptrdiff_t;
44+
using reference = const value_type&;
45+
46+
iterator() = default;
47+
48+
iterator(const GraphWrapper& graph, const NodeID& start)
49+
: state(std::make_shared<traversal_state>(graph, start)) {
50+
advance();
51+
}
52+
53+
reference operator*() const {
54+
return *state->current;
55+
}
56+
57+
const value_type* operator->() const {
58+
return &*state->current;
59+
}
60+
61+
iterator& operator++() {
62+
advance();
63+
return *this;
64+
}
65+
66+
void operator++(int) {
67+
++(*this);
68+
}
69+
70+
friend bool operator==(const iterator& it, std::default_sentinel_t) {
71+
return !it.state || it.state->done;
72+
}
73+
74+
friend bool operator==(std::default_sentinel_t sentinel, const iterator& it) {
75+
return it == sentinel;
76+
}
77+
78+
private:
79+
using GraphType = typename GraphWrapper::GraphType;
80+
using VertexDesc = typename GraphWrapper::VertexDesc;
81+
using OutEdgeIterator = typename boost::graph_traits<GraphType>::out_edge_iterator;
82+
83+
struct edge_cursor {
84+
VertexDesc vertex;
85+
OutEdgeIterator current;
86+
OutEdgeIterator end;
87+
};
88+
89+
struct traversal_state {
90+
traversal_state(const GraphWrapper& graph, const NodeID& start)
91+
: graph(&graph),
92+
colors(boost::num_vertices(graph.get_impl()), boost::white_color),
93+
current(std::nullopt),
94+
done(false) {
95+
const auto start_vertex = graph.get_id_to_bgl_map().at(start);
96+
colors[graph.get_vertex_index(start_vertex)] = boost::gray_color;
97+
if constexpr (Order == traversal_order::bfs) {
98+
bfs_queue.push(start_vertex);
99+
} else {
100+
push_dfs_vertex(start_vertex);
101+
}
102+
}
103+
104+
void push_dfs_vertex(VertexDesc vertex) {
105+
auto [edge_it, edge_end] = boost::out_edges(vertex, graph->get_impl());
106+
dfs_stack.push_back(edge_cursor{vertex, edge_it, edge_end});
107+
}
108+
109+
const GraphWrapper* graph;
110+
std::vector<boost::default_color_type> colors;
111+
std::queue<VertexDesc> bfs_queue;
112+
std::optional<edge_cursor> bfs_cursor;
113+
std::vector<edge_cursor> dfs_stack;
114+
std::optional<value_type> current;
115+
bool done;
116+
};
117+
118+
void advance() {
119+
if (!state || state->done) {
120+
return;
121+
}
122+
123+
state->current.reset();
124+
if constexpr (Order == traversal_order::bfs) {
125+
advance_bfs();
126+
} else {
127+
advance_dfs();
128+
}
129+
}
130+
131+
void advance_bfs() {
132+
while (!state->bfs_queue.empty()) {
133+
if (!state->bfs_cursor.has_value()) {
134+
const auto vertex = state->bfs_queue.front();
135+
auto [edge_it, edge_end] = boost::out_edges(vertex, state->graph->get_impl());
136+
state->bfs_cursor = edge_cursor{vertex, edge_it, edge_end};
137+
}
138+
139+
while (state->bfs_cursor->current != state->bfs_cursor->end) {
140+
const auto edge = *state->bfs_cursor->current;
141+
++state->bfs_cursor->current;
142+
const auto child = boost::target(edge, state->graph->get_impl());
143+
const auto child_index = state->graph->get_vertex_index(child);
144+
if (state->colors[child_index] == boost::white_color) {
145+
state->colors[child_index] = boost::gray_color;
146+
state->bfs_queue.push(child);
147+
state->current = value_type{
148+
state->graph->get_node_id(state->bfs_cursor->vertex),
149+
state->graph->get_node_id(child)
150+
};
151+
return;
152+
}
153+
}
154+
155+
state->colors[state->graph->get_vertex_index(state->bfs_cursor->vertex)] = boost::black_color;
156+
state->bfs_queue.pop();
157+
state->bfs_cursor.reset();
158+
}
159+
160+
state->done = true;
161+
}
162+
163+
void advance_dfs() {
164+
while (!state->dfs_stack.empty()) {
165+
auto& cursor = state->dfs_stack.back();
166+
while (cursor.current != cursor.end) {
167+
const auto edge = *cursor.current;
168+
++cursor.current;
169+
const auto child = boost::target(edge, state->graph->get_impl());
170+
const auto child_index = state->graph->get_vertex_index(child);
171+
if (state->colors[child_index] == boost::white_color) {
172+
state->colors[child_index] = boost::gray_color;
173+
state->current = value_type{
174+
state->graph->get_node_id(cursor.vertex),
175+
state->graph->get_node_id(child)
176+
};
177+
state->push_dfs_vertex(child);
178+
return;
179+
}
180+
}
181+
182+
state->colors[state->graph->get_vertex_index(cursor.vertex)] = boost::black_color;
183+
state->dfs_stack.pop_back();
184+
}
185+
186+
state->done = true;
187+
}
188+
189+
std::shared_ptr<traversal_state> state;
190+
};
191+
192+
iterator begin() const {
193+
return iterator(*graph, start);
194+
}
195+
196+
std::default_sentinel_t end() const {
197+
return {};
198+
}
199+
200+
private:
201+
const GraphWrapper* graph;
202+
NodeID start;
203+
};
204+
20205
template <typename NodeID, typename EdgeWeight, bool Directed, typename EdgeRange>
21206
Graph<NodeID, EdgeWeight, Directed> build_tree_from_edges(const NodeID& root, const EdgeRange& edges) {
22207
Graph<NodeID, EdgeWeight, Directed> tree;
@@ -381,6 +566,15 @@ auto Graph<NodeID, EdgeWeight, Directed, Multi, Weighted, OutEdgeSelector, Verte
381566
return edges;
382567
}
383568

569+
template <typename NodeID, typename EdgeWeight, bool Directed, bool Multi, bool Weighted, typename OutEdgeSelector, typename VertexSelector>
570+
auto Graph<NodeID, EdgeWeight, Directed, Multi, Weighted, OutEdgeSelector, VertexSelector>::bfs_edges_view(const NodeID& start) const {
571+
if (!has_node(start)) {
572+
throw std::runtime_error("Traversal failed: start node not found.");
573+
}
574+
575+
return detail::traversal_edges_view<Graph, detail::traversal_order::bfs>(*this, start);
576+
}
577+
384578
template <typename NodeID, typename EdgeWeight, bool Directed, bool Multi, bool Weighted, typename OutEdgeSelector, typename VertexSelector>
385579
auto Graph<NodeID, EdgeWeight, Directed, Multi, Weighted, OutEdgeSelector, VertexSelector>::bfs_tree(const NodeID& start) const {
386580
if (!has_node(start)) {
@@ -455,6 +649,15 @@ auto Graph<NodeID, EdgeWeight, Directed, Multi, Weighted, OutEdgeSelector, Verte
455649
return edges;
456650
}
457651

652+
template <typename NodeID, typename EdgeWeight, bool Directed, bool Multi, bool Weighted, typename OutEdgeSelector, typename VertexSelector>
653+
auto Graph<NodeID, EdgeWeight, Directed, Multi, Weighted, OutEdgeSelector, VertexSelector>::dfs_edges_view(const NodeID& start) const {
654+
if (!has_node(start)) {
655+
throw std::runtime_error("Traversal failed: start node not found.");
656+
}
657+
658+
return detail::traversal_edges_view<Graph, detail::traversal_order::dfs>(*this, start);
659+
}
660+
458661
template <typename NodeID, typename EdgeWeight, bool Directed, bool Multi, bool Weighted, typename OutEdgeSelector, typename VertexSelector>
459662
auto Graph<NodeID, EdgeWeight, Directed, Multi, Weighted, OutEdgeSelector, VertexSelector>::dfs_tree(const NodeID& start) const {
460663
if (!has_node(start)) {

tests/test_edge_cases.cpp

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <iostream>
44
#include <iterator>
55
#include <limits>
6+
#include <ranges>
67
#include <stdexcept>
78
#include <string>
89
#include <type_traits>
@@ -78,12 +79,77 @@ void test_missing_node_operations_throw() {
7879
[&] { (void)graph.bfs_edges("Milan"); },
7980
"Traversal failed: start node not found.",
8081
"bfs_edges() on a missing start node should report the standardized traversal error");
82+
expect_runtime_error_message(
83+
[&] { (void)graph.bfs_edges_view("Milan"); },
84+
"Traversal failed: start node not found.",
85+
"bfs_edges_view() on a missing start node should report the standardized traversal error");
86+
expect_runtime_error_message(
87+
[&] { (void)graph.dfs_edges_view("Milan"); },
88+
"Traversal failed: start node not found.",
89+
"dfs_edges_view() on a missing start node should report the standardized traversal error");
8190
expect_runtime_error_message(
8291
[&] { (void)graph.shortest_path("Rome", "Milan"); },
8392
"Shortest-path lookup failed: source or target node not found.",
8493
"shortest_path() with a missing target should report the standardized shortest-path lookup error");
8594
}
8695

96+
void test_traversal_edge_views_match_eager_edges() {
97+
nxpp::DiGraph graph;
98+
graph.add_edge("A", "B");
99+
graph.add_edge("A", "C");
100+
graph.add_edge("B", "D");
101+
graph.add_edge("C", "E");
102+
103+
auto bfs_view = graph.bfs_edges_view("A");
104+
auto dfs_view = graph.dfs_edges_view("A");
105+
106+
static_assert(std::ranges::input_range<decltype(bfs_view)>);
107+
static_assert(std::ranges::input_range<decltype(dfs_view)>);
108+
109+
std::vector<std::pair<std::string, std::string>> bfs_edges;
110+
for (const auto& edge : bfs_view) {
111+
bfs_edges.push_back(edge);
112+
}
113+
114+
std::vector<std::pair<std::string, std::string>> dfs_edges;
115+
for (const auto& edge : dfs_view) {
116+
dfs_edges.push_back(edge);
117+
}
118+
119+
expect(bfs_edges == graph.bfs_edges("A"), "bfs_edges_view should yield BFS tree edges in eager order");
120+
expect(dfs_edges == graph.dfs_edges("A"), "dfs_edges_view should yield DFS tree edges in eager order");
121+
122+
std::vector<std::pair<std::string, std::string>> first_two_bfs;
123+
for (const auto& edge : graph.bfs_edges_view("A") | std::views::take(2)) {
124+
first_two_bfs.push_back(edge);
125+
}
126+
127+
const auto eager_bfs = graph.bfs_edges("A");
128+
expect(first_two_bfs.size() == 2, "bfs_edges_view should compose with std::views::take");
129+
expect(first_two_bfs[0] == eager_bfs[0] && first_two_bfs[1] == eager_bfs[1],
130+
"taken BFS view edges should match the eager prefix");
131+
132+
nxpp::Graph<> undirected_graph;
133+
undirected_graph.add_edge("A", "B");
134+
undirected_graph.add_edge("A", "C");
135+
undirected_graph.add_edge("B", "D");
136+
137+
std::vector<std::pair<std::string, std::string>> undirected_bfs_edges;
138+
for (const auto& edge : undirected_graph.bfs_edges_view("A")) {
139+
undirected_bfs_edges.push_back(edge);
140+
}
141+
142+
std::vector<std::pair<std::string, std::string>> undirected_dfs_edges;
143+
for (const auto& edge : undirected_graph.dfs_edges_view("A")) {
144+
undirected_dfs_edges.push_back(edge);
145+
}
146+
147+
expect(undirected_bfs_edges == undirected_graph.bfs_edges("A"),
148+
"undirected bfs_edges_view should yield tree edges in eager order");
149+
expect(undirected_dfs_edges == undirected_graph.dfs_edges("A"),
150+
"undirected dfs_edges_view should yield tree edges in eager order");
151+
}
152+
87153
void test_disconnected_shortest_paths_preserve_unreachable_state() {
88154
nxpp::WeightedGraphStr graph;
89155
graph.add_edge("A", "B", 2.0);
@@ -438,6 +504,7 @@ int main() {
438504
{"empty graph reports empty collections", test_empty_graph_reports_empty_collections},
439505
{"singleton graph has no neighbors or traversal edges", test_singleton_graph_has_no_neighbors_or_traversal_edges},
440506
{"missing node operations throw", test_missing_node_operations_throw},
507+
{"traversal edge views match eager edges", test_traversal_edge_views_match_eager_edges},
441508
{"disconnected shortest paths preserve unreachable state", test_disconnected_shortest_paths_preserve_unreachable_state},
442509
{"shortest path WeightMode options", test_shortest_path_weight_mode_options},
443510
{"2-SAT satisfiable and unsatisfiable formulas", test_two_sat_satisfiable_and_unsatisfiable_formulas},

0 commit comments

Comments
 (0)