diff --git a/benchmarks/suites/hg_b_bfs.cpp b/benchmarks/suites/hg_b_bfs.cpp index e19f118c..7f12e53f 100644 --- a/benchmarks/suites/hg_b_bfs.cpp +++ b/benchmarks/suites/hg_b_bfs.cpp @@ -69,8 +69,7 @@ void bm_hgl_backward_bfs(benchmark::State& state) { auto hg = gen_bf_overlapping_chain_hypergraph(n_hedges, layer_width, stride); - auto roots = std::views::iota(id_type{0}, static_cast(layer_width)) - | std::ranges::to>(); + auto roots = std::views::iota(id_type{0}, static_cast(layer_width)); for (auto _ : state) { auto search_tree = hgl::algorithm::backward_bfs(hg, roots); @@ -100,7 +99,7 @@ bool incidence_backward_bfs( auto root_nodes = roots | std::views::transform([](const id_type root_id) { - return gl::algorithm::search_node>{root_id}; + return gl::algorithm::root_node(root_id); }) | std::ranges::to(); diff --git a/docs/gl/algorithms/templates.md b/docs/gl/algorithms/templates.md index 3cdf46e2..84a60dfb 100644 --- a/docs/gl/algorithms/templates.md +++ b/docs/gl/algorithms/templates.md @@ -17,12 +17,12 @@ The library provides four primary traversal engines. The true power of the generic templates lies in their callback/predicate hooks. Every iteration of the engine loop rigidly follows a defined sequence. By injecting custom callbacks (or omitting them via the [**empty_callback**](../../cpp-gl/structgl_1_1algorithm_1_1empty__callback.md)), you dictate the algorithm's behavior. -### Execution Flowchart +### Standard Execution Flow (`bfs`, `pfs`, and stateless `dfs`) -For a single popped node in `bfs`, `dfs`, or `pfs`, the execution flow looks exactly like this: +For a single popped node in standard traversal templates, the execution flow looks exactly like this: 1. **`visit_vertex_pred(node)`** - Evaluated immediately after popping the node. If it returns `false`, the node is skipped entirely, and the loop moves to the next node. *(Commonly used for late-rejection of stale elements in Priority Queues).* + Evaluated immediately after popping the node. If it returns `false`, the node is skipped entirely, and the loop moves to the next node. *(Commonly used for late-rejection of stale elements in Priority Queues or filtering already-visited vertices).* 2. **`pre_visit(vertex_id)`** A state-modification hook executed right before the vertex is officially marked as "visited". @@ -45,9 +45,37 @@ For a single popped node in `bfs`, `dfs`, or `pfs`, the execution flow looks exa If the target was accepted, this hook allows you to construct a custom object to push into the search frontier. -5. **`post_visit(vertex_id)`** +5. **`post_visit(vertex_id)`** *(BFS/PFS only)* Executed after all adjacent edges have been evaluated and processed. +### True Post-Order Execution (Iterative `dfs`) + +In a standard stack-based DFS, nodes are popped and discarded *before* their children are pushed. This makes executing a true post-order callback (after a node's entire subtree has been exhaustively explored) impossible with a naive implementation. + +The CPP-GL `dfs` template solves this using a zero-cost abstraction: + +- **Stateless Fast-Path:** If you pass an `empty_callback` for the `post_visit` hook, the engine compiles down to the standard execution flow described above, maximizing performance. +- **Stateful Stack-Frame:** If a valid `post_visit` callback is provided, the engine implicitly wraps the search nodes with a `dfs_extension` payload containing an `expanded` boolean flag. + +When utilizing the stateful stack, the execution loop shifts to a two-phase lifecycle: + +1. **Phase 1 (First Encounter):** The node is popped. Because `expanded == false`, the engine executes `visit_vertex_pred`, `pre_visit`, and `visit`. It then **marks the node as expanded and pushes it back onto the stack**, followed by pushing all of its valid children on top. +2. **Phase 2 (Subtree Exhausted):** Because the parent was pushed beneath its children, it surfaces again only after its entire subtree has been popped and processed. The engine pops it, sees `expanded == true`, and executes the `post_visit` callback. + +### Recursive Execution (`r_dfs`) + +The recursive DFS template (`r_dfs`) avoids standard container wrappers entirely and maps the generic callback sequence directly to the C++ call stack. Because of the nature of function calls, `r_dfs` achieves true post-order execution naturally without requiring stateful wrapper nodes. + +Its execution flow operates as follows: + +1. **Entry:** `visit_vertex_pred`, `pre_visit`, and `visit` are executed immediately upon entering the function. +2. **Recurse:** The engine iterates over outgoing edges. If `enqueue_node_pred` accepts a target, the engine immediately calls `r_dfs` nested within the current loop. +3. **Exit:** After the edge loop completes (meaning all recursive child calls have unwound), `post_visit` is naturally executed before the current function frame returns to its caller. + +> [!WARNING] Aborting Recursive Searches +> +> The generic generic `abort` mechanisms (like returning `false` from `visit`) do not work the same way in `r_dfs`. Returning from a nested recursive call only unwinds a single stack frame. If you need to instantly terminate a deep `r_dfs` traversal, you must utilize external state (e.g., throwing a custom exception or checking a global cancellation flag in your predicates). + ## Custom Node Injection (PFS) While BFS and DFS templates strictly operate on the lightweight [**gl::algorithm::search_node**](../../cpp-gl/structgl_1_1algorithm_1_1search__node.md), the Priority-First Search template often requires tracking dynamic state alongside the vertex ID. @@ -93,18 +121,3 @@ gl::algorithm::pfs( // (5)! return path_node{target_id, source_id, new_dist}; } ); -``` - -1. Define a custom stateful node tracking the distance accumulated so far. -2. Initialize a global distance map with "infinity", setting the start vertex distance to 0. -3. Define the priority comparator for a distance-based Min-Heap. -4. Setup the initial range containing the root node. -5. Run the Priority-First Search engine. -6. Define an empty vertex visit predicate and vertex visit callback. -7. Define the node enqueue predicate to only enqueue nodes that could yield paths shorter than those already discovered. -8. Define the callback which constructs a stateful node for the algorithm queue. -9. Update the global distance map to reflect the newly discovered shorter path. - -> [!NOTE] Algorithm Desing -> -> The example above is very similar, though not the same, to how the Dijkstra's algorithm implementation is designed within the library. diff --git a/docs/hgl/algorithms/templates.md b/docs/hgl/algorithms/templates.md index c4443e83..11a78d26 100644 --- a/docs/hgl/algorithms/templates.md +++ b/docs/hgl/algorithms/templates.md @@ -42,9 +42,9 @@ Because **BF-directed hypergraphs** distinguish between *tail* (source) and *hea The true power of the generic templates lies in their callback and predicate hooks. Every iteration of the engine loop rigidly follows a defined sequence. By injecting custom callbacks (or omitting them via the imported [**empty_callback**](../../cpp-gl/group__HGL-Algorithm.md#typedef-empty_callback)), you dictate the algorithm's exact behavior. -### Execution Flowchart +### Standard Execution Flow (`bfs` and stateless `dfs`) -For a single popped `curr_node` in the `bfs` or `dfs` templates, the execution flow looks exactly like this: +For a single popped node in the standard traversal templates, the execution flow looks exactly like this: 1. **`visit_pred(curr_node)`** Evaluated immediately after popping the node. If it returns `false`, the node is skipped entirely, and the loop moves to the next node in the queue or stack. @@ -59,7 +59,7 @@ For a single popped `curr_node` in the `bfs` or `dfs` templates, the execution f The engine queries the `traversal_policy` for the target hyperedges. For each `he_id`: - **`traverse_he_pred(he_id, curr_node.vertex_id)`** - Evaluates whether the hyperedge should be traversed. Returns a `decision`: + Evaluates whether the hyperedge should be traversed. Returns a [**decision**](../../cpp-gl/structgl_1_1algorithm_1_1decision.md): - `abort`: Kills the entire algorithm. - `reject`: Ignores this hyperedge and moves to the next. - `accept`: Proceeds to evaluate the hyperedge's vertices. @@ -68,17 +68,31 @@ For a single popped `curr_node` in the `bfs` or `dfs` templates, the execution f If the hyperedge was accepted, the engine queries the `traversal_policy` for the target vertices within that hyperedge. For each `target_id` (skipping the one we just came from): - **`enqueue_pred(tgt_node)`** - Evaluates whether the newly constructed `tgt_node` (containing the `target_id`, `curr_node.vertex_id`, and `he_id`) should be pushed to the active container. Returns a `decision`: + Evaluates whether the newly constructed `tgt_node` (containing the `target_id`, `curr_node.vertex_id`, and `he_id`) should be pushed to the active container. Returns a [**decision**](../../cpp-gl/structgl_1_1algorithm_1_1decision.md): - `abort`: Kills the entire algorithm. - `reject`: Ignores this specific target vertex. - `accept`: Pushes the `tgt_node` to the queue or stack. -6. **`post_visit(curr_node)`** +6. **`post_visit(curr_node)`** *(BFS only)* Executed after all adjacent hyperedges and their target vertices have been evaluated and processed. +### True Post-Order Execution (Iterative `dfs`) + +In a standard stack-based DFS, nodes are popped and discarded *before* their children are pushed. This makes executing a true post-order callback (firing only after a node's entire subtree has been exhaustively explored) impossible with a naive implementation. + +The HGL `dfs` template solves this using a zero-cost abstraction: + +- **Stateless Fast-Path:** If you pass an `empty_callback` for the `post_visit` hook, the engine compiles down to the standard execution flow described above, maximizing performance. +- **Stateful Stack-Frame:** If a valid `post_visit` callback is provided, the engine implicitly wraps the search nodes with a `dfs_extension` payload containing an `expanded` boolean flag. + +When utilizing the stateful stack, the execution loop shifts to a two-phase lifecycle: + +1. **Phase 1 (First Encounter):** The node is popped. Because `expanded == false`, the engine executes `visit_pred`, `pre_visit`, and `visit`. It then **marks the node as expanded and pushes it back onto the stack**, followed by executing the two-step hyperedge/vertex expansion to push all of its valid children on top. +2. **Phase 2 (Subtree Exhausted):** Because the parent was pushed beneath its children, it surfaces again only after its entire subtree has been popped and processed. The engine pops it, sees `expanded == true`, slices it back to its stateless base representation, and executes the `post_visit` callback. + ## Customizing the Traversal -By wiring up these 6 hooks, you can build highly specific reachability algorithms. For instance, to ensure we do not get stuck in infinite loops, we need to track both visited vertices and visited hyperedges. +By wiring up these hooks, you can build highly specific reachability algorithms. For instance, to ensure we do not get stuck in infinite loops, we need to track both visited vertices and visited hyperedges. ### Example: Custom Forward BFS Engine @@ -91,12 +105,9 @@ By wiring up these 6 hooks, you can build highly specific reachability algorithm std::vector visited_v(hg.n_vertices(), false); // (1)! std::vector visited_he(hg.n_hyperedges(), false); -using search_node = hgl::algorithm::search_node; -std::vector init_nodes = {search_node{start_id}}; // (2)! - -bool success = hgl::algorithm::bfs( // (3)! +bool success = hgl::algorithm::bfs( // (2)! hg, - init_nodes, + std::array{hgl::algorithm::root_node(start_id)}, // (3)! [&](const auto& node) { return not visited_v[node.vertex_id]; }, // (4)! [&](const auto& node) { // (5)! visited_v[node.vertex_id] = true; @@ -116,8 +127,8 @@ bool success = hgl::algorithm::bfs( // (3)! ``` 1. Initialize state-tracking vectors for both vertices and hyperedges. -2. Set up the initial queue range with a root node. -3. Explicitly invoke the template with `traversal_direction::forward` (the default, but explicitly shown here for clarity). +2. Explicitly invoke the template with `traversal_direction::forward` (the default, but explicitly shown here for clarity). +3. Set up the initial queue range with a root node. 4. **`visit_pred`**: Reject nodes in the queue if they were already visited by an earlier, faster branch. 5. **`visit`**: Mark the vertex as visited. 6. **`traverse_he_pred`**: Check if the hyperedge was already traversed. If not, mark it traversed and `accept` it. Returning a `decision` type here is required by the generic engines. diff --git a/docs/hgl/quick_start.md b/docs/hgl/quick_start.md index 1c61912d..fe1a7950 100644 --- a/docs/hgl/quick_start.md +++ b/docs/hgl/quick_start.md @@ -37,14 +37,14 @@ int main() { hg.add_hyperedge({v1, v2}, {v3})->weight = 5.0; std::vector roots = {0u}; - auto search_tree = hgl::algorithm::backward_bfs(hg, roots); // (7)! + auto search_tree = hgl::algorithm::backward_bfs(hg, roots); // (6)! - if (hgl::algorithm::is_reachable(search_tree, 3u)) // (8)! + if (search_tree.is_reachable(3u)) // (7)! std::cout << "Target is B-reachable from the Source.\n\n"; else std::cout << "Target is NOT B-reachable from the Source.\n\n"; - std::cout << hgl::io::verbose << hgl::io::with_properties; // (9)! + std::cout << hgl::io::verbose << hgl::io::with_properties; // (8)! std::cout << "Hypergraph Topology:\n" << hg << '\n'; return 0; @@ -57,7 +57,7 @@ int main() { 4. Retrieve stable vertex descriptors using `.vertex(id)` to assign property payloads safely. 5. BF-directed hyperedges connect a set of *tail* vertices to a set of *head* vertices. An edge can be added using vertex IDs or descriptors.
**NOTE:** You can alternatively instantiate the hypergraph with a given number of hyperedges and simply *bind* vertices to them using the dedicated binding methods.
**IMPORTANT:** Property Access Safety
Assigning properties inline via the returned descriptor is safe here because the temporary descriptor is immediately discarded, meaning no dangling references are kept. The same bahaviour can be achieved using the `add_hyperedge_with` methods. 6. Execute a Breadth-First Backward Search (B-BFS) to compute B-reachability semantics from the source vertex. -7. Query the resulting search tree to validate if the Target vertex was successfully reached. +7. Query the resulting search tree to validate if the target vertex was successfully reached. 8. Standard GL stream manipulators inject persistent formatting state to output the hypergraph's structure in a verbose format, including the element properties. **Output:** diff --git a/include/gl/algorithm/core.hpp b/include/gl/algorithm/core.hpp index 8fc434ec..943cb779 100644 --- a/include/gl/algorithm/core.hpp +++ b/include/gl/algorithm/core.hpp @@ -30,6 +30,12 @@ namespace gl::algorithm { /// > and speeds up compilation times. struct empty_callback {}; +/// @ingroup GL-Algorithm +/// @brief A tag type used to explicitly indicate the absence of a state-tracking extension in a search node. +/// ### See Also +/// - @ref gl::algorithm::search_node "search_node" : For the definition of the algorithm search node type. +struct empty_extension {}; + /// @ingroup GL-Algorithm GL-Types /// @brief Represents a generic tri-state decision for control flow. /// @@ -127,32 +133,48 @@ using predecessors_map = std::vector>; /// @ingroup GL-Algorithm /// @brief Represents an active node in a search container (e.g., a BFS queue or DFS stack). -/// @tparam GraphType The type of the graph being searched. -template +/// @tparam G The type of the graph being searched. +/// @tparam Extension An optional payload type attached to the node for state tracking (must satisfy `std::semiregular`). +/// ### See Also +/// - @ref gl::algorithm::root_node "gl::algorithm::root_node" : For the full definition of the root search node builder function. +template struct search_node { + /// @brief The underlying value type of the graph being searched. + using graph_type = val_t; + /// @brief The integral type used to identify vertices in the graph. using id_type = id_t; + /// @brief The type of the custom state-tracking payload attached to this node. + using extension_type = Extension; - /// @brief Constructs a search node acting as a root (predecessor is itself). - /// @param vertex_id The ID of the vertex. - search_node(id_type vertex_id) : vertex_id(vertex_id), pred_id(vertex_id) {} - - /// @brief Constructs a search node with an explicit predecessor. - /// @param vertex_id The ID of the vertex. - /// @param pred_id The ID of the vertex's predecessor. - search_node(id_type vertex_id, id_type pred_id) : vertex_id(vertex_id), pred_id(pred_id) {} + id_type vertex_id = invalid_id; ///< The ID of the vertex currently being searched. + id_type pred_id = invalid_id; ///< The ID of the predecessor from which this vertex was reached. + [[no_unique_address]] extension_type ext = {}; ///< Custom state-tracking payload. /// @brief Checks if this node is the root of a search tree. - /// @return `true` if the node is valid and its predecessor is itself, `false` otherwise. + /// @return `true` if the node is valid and `vertex_id == pred_id`, `false` otherwise. [[nodiscard]] gl_attr_force_inline bool is_root() const noexcept { return this->vertex_id != invalid_id and this->vertex_id == this->pred_id; } - - /// @brief The ID of the vertex currently being searched. - id_type vertex_id; - /// @brief The ID of the predecessor from which this vertex was reached. - id_type pred_id; }; +/// @ingroup GL-Algorithm +/// @brief Free function builder that creates a search node acting as the root of a search tree. +/// +/// This utility provides clean, unambiguous aggregate initialization semantics for root nodes +/// (where the vertex is strictly its own predecessor) at algorithmic call sites. +/// +/// @tparam G The type of the graph being searched. +/// @tparam Extension The type of the custom state-tracking payload attached to the node. +/// @param root_id The ID of the root vertex. +/// @param ext An optional state-tracking extension payload. +/// @return A fully initialized @ref gl::algorithm::search_node "search_node" acting as a root. +template +[[nodiscard]] gl_attr_force_inline search_node, Extension> root_node( + id_t root_id, Extension ext = {} +) { + return search_node, Extension>{root_id, root_id, std::move(ext)}; +} + // --- constants --- /// @ingroup GL-Algorithm diff --git a/include/gl/algorithm/pathfinding/dijkstra.hpp b/include/gl/algorithm/pathfinding/dijkstra.hpp index 7917ca0d..a980145e 100644 --- a/include/gl/algorithm/pathfinding/dijkstra.hpp +++ b/include/gl/algorithm/pathfinding/dijkstra.hpp @@ -55,32 +55,13 @@ template return paths_descriptor_type{graph.n_vertices()}; } -/// @ingroup GL-Algorithm -/// @brief Internal node structure for Dijkstra's algorithm to snapshot distances and preserve heap invariants. -/// -/// This structure is used in the @ref gl::algorithm::dijkstra_shortest_paths "dijkstra_shortest_paths" algorithm -/// to capture the state of a vertex at the moment it is enqueued, ensuring that the priority queue remains stable -/// even if the global distance map is updated during traversal. -/// -/// @tparam G The type of the graph. Must satisfy the [**c_graph**](gl_concepts.md#gl-traits-c-graph) concept. -template -struct dijkstra_search_node { - /// @brief The type of the vertex ID. - using id_type = id_t; - - id_type vertex_id; ///< @brief The ID of the vertex represented by this node. - id_type pred_id; ///< The ID of the predecessor vertex used to reach this node. - vertex_distance_t - distance; ///< The accumulated distance from the source to this vertex at the time of enqueueing. -}; - /// @ingroup GL-Algorithm /// @brief Computes the shortest paths from a single source vertex to all reachable vertices using Dijkstra's algorithm. /// -/// This algorithm utilizes the generic @ref gl::algorithm::pfs "pfs" template using the dedicated -/// @ref gl::algorithm::dijkstra_search_node "serch node type" to perform a priority-first search based -/// on accumulated edge weights. It strictly requires non-negative edge weights; if a negative weight is -/// encountered during traversal, the algorithm immediately throws an exception. +/// This algorithm utilizes the generic @ref gl::algorithm::pfs "pfs" template using a state-extended +/// @ref gl::algorithm::search_node "search node" to snapshot accumulated edge weights upon enqueuing. +/// It strictly requires non-negative edge weights; if a negative weight is encountered during traversal, +/// the algorithm immediately throws an exception. /// /// ### Example Usage /// ```cpp @@ -135,6 +116,12 @@ template < using edge_type = edge_t; using distance_type = vertex_distance_t; + struct dijkstra_ext { + distance_type distance{}; + }; + + using node_type = search_node, dijkstra_ext>; + auto paths = make_paths_descriptor(graph); paths.predecessors[source_id] = source_id; @@ -143,19 +130,16 @@ template < std::optional negative_edge; // Seed the queue with the custom snapshot node - std::vector> init_queue{ - {source_id, source_id, distance_type{}} - }; + std::vector init_queue{root_node(source_id)}; pfs( graph, - [](const dijkstra_search_node& lhs, const dijkstra_search_node& rhs - ) { // pq comparator - return lhs.distance > rhs.distance; + [](const node_type& lhs, const node_type& rhs) { // pq comparator + return lhs.ext.distance > rhs.ext.distance; }, init_queue, - [&paths](const dijkstra_search_node& node) { // visit_vertex_pred (stale node rejection) - return node.distance <= paths.distances[to_idx(node.vertex_id)]; + [&paths](const node_type& node) { // visit_vertex_pred (stale node rejection) + return node.ext.distance <= paths.distances[to_idx(node.vertex_id)]; }, empty_callback{}, // visit callback [&paths, &negative_edge](id_type vertex_id, const edge_type& in_edge) @@ -181,7 +165,7 @@ template < return false; }, [&paths](id_type target_id, id_type pred_id, const edge_type&) { // make_node callback - return dijkstra_search_node{target_id, pred_id, paths.distances[to_idx(target_id)]}; + return node_type{target_id, pred_id, {paths.distances[to_idx(target_id)]}}; }, pre_visit, post_visit diff --git a/include/gl/algorithm/templates/bfs.hpp b/include/gl/algorithm/templates/bfs.hpp index f7958334..dfc18b21 100644 --- a/include/gl/algorithm/templates/bfs.hpp +++ b/include/gl/algorithm/templates/bfs.hpp @@ -29,7 +29,7 @@ namespace gl::algorithm { /// /// bool completed = gl::algorithm::bfs( /// graph, -/// gl::algorithm::init_node_range(start_id), // (2)! +/// std::array{gl::algorithm::root_node(start_vertex_id)}, // (2)! /// gl::algorithm::default_visit_vertex_predicate(visited), // (3)! /// [&](auto v, auto p) { // (4)! /// std::cout << "Visited vertex " << v << '\n'; @@ -53,7 +53,7 @@ namespace gl::algorithm { /// | Parameter | Description | Constraint | /// | :-------- | :--- | :--- | /// | G | The type of the graph being traversed. | Must satisfy the [**c_graph**](gl_concepts.md#gl-traits-c-graph) concept. | -/// | InitQueueRangeType | The type of the container providing the initial roots to enqueue. | Must be a *forward range* of @ref gl::algorithm::search_node "search nodes". | +/// | InitNodeRngType | The type of the container providing the initial roots to enqueue. | Must be a *forward range* of @ref gl::algorithm::search_node "search nodes". | /// | VisitVertexPredicate | Type of the callable deciding if a popped vertex should be processed. | Must be one of:
- An `(id_type) -> bool` callable
- An @ref gl::algorithm::empty_callback "empty_callback" | /// | VisitCallback | Type of the callable executed when a vertex is officially visited. | Must be one of:
- An `(id_type, id_type) -> bool` callable
- An @ref gl::algorithm::empty_callback "empty_callback" | /// | EnqueueNodePred | Type of the callable deciding if a node corresponding to an adjacent vertex should be pushed to the queue. | Must be one of:
- An `(id_type, const edge_type&) -> decision` callable
- An @ref gl::algorithm::empty_callback "empty_callback" | @@ -61,7 +61,7 @@ namespace gl::algorithm { /// | PostVisitCallback | Type of the callable executed after all adjacent edges are evaluated. | Must be one of:
- An `(id_type) -> void` callable
- An @ref gl::algorithm::empty_callback "empty_callback" | /// /// @param graph The graph to traverse. -/// @param initial_queue_content A range of initial @ref gl::algorithm::search_node "search nodes" to seed the BFS queue. +/// @param initial_nodes A range of initial @ref gl::algorithm::search_node "search nodes" to seed the BFS queue. /// @param visit_vertex_pred Predicate evaluated immediately after popping a vertex. If it returns `false`, the vertex is skipped. /// @param visit Callback invoked when a vertex is officially visited. If it returns `false`, the entire BFS immediately aborts. /// @param enqueue_node_pred Predicate evaluated for each outgoing edge. Returns a @ref gl::algorithm::decision "decision": @@ -74,7 +74,7 @@ namespace gl::algorithm { /// @hideparams template < traits::c_graph G, - traits::c_forward_range_of>> InitQueueRangeType = + traits::c_forward_range_of>> InitNodeRngType = std::vector>>, traits::c_optional_predicate> VisitVertexPredicate = empty_callback, traits::c_optional_predicate, id_t> VisitCallback = empty_callback, @@ -83,19 +83,19 @@ template < traits::c_optional_callback> PostVisitCallback = empty_callback> bool bfs( G&& graph, - const InitQueueRangeType& initial_queue_content, + const InitNodeRngType& initial_nodes, VisitVertexPredicate visit_vertex_pred = {}, VisitCallback visit = {}, EnqueueNodePred enqueue_node_pred = {}, PreVisitCallback pre_visit = {}, PostVisitCallback post_visit = {} ) { - if (std::ranges::empty(initial_queue_content)) + if (std::ranges::empty(initial_nodes)) return false; // prepare the node queue std::queue>> q; - for (const auto& node : initial_queue_content) + for (const auto& node : initial_nodes) q.push(node); // search the graph diff --git a/include/gl/algorithm/templates/dfs.hpp b/include/gl/algorithm/templates/dfs.hpp index 2bfda1eb..f1d29bdf 100644 --- a/include/gl/algorithm/templates/dfs.hpp +++ b/include/gl/algorithm/templates/dfs.hpp @@ -22,13 +22,19 @@ namespace gl::algorithm { /// Concrete algorithms (like cycle detection or topological sorting) are built by injecting /// specific logic into the provided callback hooks. /// +/// > [!NOTE] True Post-Order Traversal +/// > If a non-empty `PostVisitCallback` is provided, this engine automatically utilizes a stateful +/// > stack frame to guarantee a true post-order traversal (the callback fires only after the entire +/// > subtree of a node has been fully explored). If the callback is omitted (using `empty_callback`), +/// > the engine bypasses frame tracking entirely for maximum performance. +/// /// ### Example Usage /// ```cpp /// std::vector visited(graph.n_vertices(), false); // (1)! /// /// bool completed = gl::algorithm::dfs( /// graph, -/// gl::algorithm::init_node_range(start_id), // (2)! +/// std::array{gl::algorithm::root_node(start_vertex_id)}, // (2)! /// gl::algorithm::default_visit_vertex_predicate(visited), // (3)! /// [&](auto v, auto p) { // (4)! /// std::cout << "Visited vertex " << v << '\n'; @@ -52,15 +58,15 @@ namespace gl::algorithm { /// | Parameter | Description | Constraint | /// | :-------- | :--- | :--- | /// | G | The type of the graph being traversed. | Must satisfy the [**c_graph**](gl_concepts.md#gl-traits-c-graph) concept. | -/// | InitStackRangeType | The type of the container providing the initial roots to push to the stack. | Must be a *forward range* of @ref gl::algorithm::search_node "search nodes". | +/// | InitNodeRngType | The type of the container providing the initial roots to push to the stack. | Must be a *forward range* of @ref gl::algorithm::search_node "search nodes". | /// | VisitVertexPredicate | Type of the callable deciding if a popped vertex should be processed. | Must be one of:
- An `(id_type) -> bool` callable
- An @ref gl::algorithm::empty_callback "empty_callback" | /// | VisitCallback | Type of the callable executed when a vertex is officially visited. | Must be one of:
- An `(id_type, id_type) -> bool` callable
- An @ref gl::algorithm::empty_callback "empty_callback" | /// | EnqueueNodePred | Type of the callable deciding if a node corresponding to an adjacent vertex should be pushed to the stack. | Must be one of:
- An `(id_type, const edge_type&) -> decision` callable
- An @ref gl::algorithm::empty_callback "empty_callback" | /// | PreVisitCallback | Type of the callable executed immediately before `VisitCallback`. | Must be one of:
- An `(id_type) -> void` callable
- An @ref gl::algorithm::empty_callback "empty_callback" | -/// | PostVisitCallback | Type of the callable executed after all adjacent edges are evaluated. | Must be one of:
- An `(id_type) -> void` callable
- An @ref gl::algorithm::empty_callback "empty_callback" | +/// | PostVisitCallback | Type of the callable executed after a node's subtree is fully explored. | Must be one of:
- An `(id_type) -> void` callable
- An @ref gl::algorithm::empty_callback "empty_callback" | /// /// @param graph The graph to traverse. -/// @param initial_stack_content A range of initial @ref gl::algorithm::search_node "search nodes" to seed the DFS stack. +/// @param initial_nodes A range of initial @ref gl::algorithm::search_node "search nodes" to seed the DFS stack. /// @param visit_vertex_pred Predicate evaluated immediately after popping a vertex. If it returns `false`, the vertex is skipped. /// @param visit Callback invoked when a vertex is officially visited. If it returns `false`, the entire DFS immediately aborts. /// @param enqueue_node_pred Predicate evaluated for each outgoing edge. Returns a @ref gl::algorithm::decision "decision": @@ -68,12 +74,12 @@ namespace gl::algorithm { /// - `reject` to skip, /// - `abort` to terminate the DFS entirely. /// @param pre_visit Hook executed immediately before the `visit` callback. -/// @param post_visit Hook executed after all adjacent edges of the current vertex have been evaluated. +/// @param post_visit Hook executed after the current vertex's children have been exhaustively processed (true post-order). /// @return `true` if the stack was exhausted naturally, `false` if the search was aborted early. /// @hideparams template < traits::c_graph G, - traits::c_forward_range_of>> InitStackRangeType = + traits::c_forward_range_of>> InitNodeRngType = std::vector>>, traits::c_optional_predicate> VisitVertexPredicate = empty_callback, traits::c_optional_predicate, id_t> VisitCallback = empty_callback, @@ -82,48 +88,91 @@ template < traits::c_optional_callback> PostVisitCallback = empty_callback> bool dfs( G&& graph, - const InitStackRangeType& initial_stack_content, + const InitNodeRngType& initial_nodes, VisitVertexPredicate visit_vertex_pred = {}, VisitCallback visit = {}, EnqueueNodePred enqueue_node_pred = {}, PreVisitCallback pre_visit = {}, PostVisitCallback post_visit = {} ) { - if (std::ranges::empty(initial_stack_content)) + if (std::ranges::empty(initial_nodes)) return false; - // prepare the node stack - std::stack>> s; - for (const auto& node : initial_stack_content) - s.push(node); - - // search the graph - while (not s.empty()) { - const auto node = s.top(); - s.pop(); - - if constexpr (not traits::c_empty_callback) - if (not visit_vertex_pred(node.vertex_id)) - continue; - - if constexpr (not traits::c_empty_callback) - pre_visit(node.vertex_id); - - if constexpr (not traits::c_empty_callback) - if (not visit(node.vertex_id, node.pred_id)) - return false; - - for (const auto& edge : graph.out_edges(node.vertex_id)) { - const auto target_vertex_id = edge.other(node.vertex_id); - const auto enqueue = enqueue_node_pred(target_vertex_id, edge); - if (enqueue == decision::abort) - return false; - if (enqueue) - s.emplace(target_vertex_id, node.vertex_id); + if constexpr (traits::c_empty_callback) { // stateless stack + std::stack>> s; + for (const auto& node : initial_nodes) + s.push(node); + + while (not s.empty()) { + const auto node = s.top(); + s.pop(); + + if constexpr (not traits::c_empty_callback) + if (not visit_vertex_pred(node.vertex_id)) + continue; + + if constexpr (not traits::c_empty_callback) + pre_visit(node.vertex_id); + + if constexpr (not traits::c_empty_callback) + if (not visit(node.vertex_id, node.pred_id)) + return false; + + for (const auto& edge : graph.out_edges(node.vertex_id)) { + const auto target_vertex_id = edge.other(node.vertex_id); + const auto enqueue = enqueue_node_pred(target_vertex_id, edge); + if (enqueue == decision::abort) + return false; + if (enqueue) + s.emplace(target_vertex_id, node.vertex_id); + } } + } + else { // statefull stack + + struct dfs_extension { + bool expanded = false; // Indicated if all of the node's children have been visited + }; - if constexpr (not traits::c_empty_callback) - post_visit(node.vertex_id); + using stateful_node_t = search_node, dfs_extension>; + std::stack s; + + for (const auto& node : initial_nodes) + s.emplace(node.vertex_id, node.pred_id); // Initialize as unexpanded + + while (not s.empty()) { + auto curr_node = s.top(); + s.pop(); + + if (curr_node.ext.expanded) { + post_visit(curr_node.vertex_id); + } + else { + if constexpr (not traits::c_empty_callback) + if (not visit_vertex_pred(curr_node.vertex_id)) + continue; + + if constexpr (not traits::c_empty_callback) + pre_visit(curr_node.vertex_id); + + if constexpr (not traits::c_empty_callback) + if (not visit(curr_node.vertex_id, curr_node.pred_id)) + return false; + + // Push parent back marked as expanded to wait for children + curr_node.ext.expanded = true; + s.push(curr_node); + + for (const auto& edge : graph.out_edges(curr_node.vertex_id)) { + const auto target_vertex_id = edge.other(curr_node.vertex_id); + const auto enqueue = enqueue_node_pred(target_vertex_id, edge); + if (enqueue == decision::abort) + return false; + if (enqueue) + s.emplace(target_vertex_id, curr_node.vertex_id); + } + } + } } return true; diff --git a/include/gl/algorithm/templates/pfs.hpp b/include/gl/algorithm/templates/pfs.hpp index 7e02bb63..88466875 100644 --- a/include/gl/algorithm/templates/pfs.hpp +++ b/include/gl/algorithm/templates/pfs.hpp @@ -34,8 +34,8 @@ namespace gl::algorithm { /// [](const auto& lhs, const auto& rhs) { // (2)! /// return lhs.vertex_id > rhs.vertex_id; /// }, -/// gl::algorithm::init_node_range(start_id), // (3)! -/// gl::algorithm::default_visit_vertex_predicate(visited // (4)! +/// std::array{gl::algorithm::root_node(start_vertex_id)}, // (3)! +/// gl::algorithm::default_visit_vertex_predicate(visited), // (4)! /// [&](auto v, auto p) { // (5)! /// std::cout << "Priority visited vertex " << v << '\n'; /// return true; // Continue search @@ -61,8 +61,8 @@ namespace gl::algorithm { /// | :-------- | :--- | :--- | /// | G | The type of the graph being traversed. | Must satisfy the [**c_graph**](gl_concepts.md#gl-traits-c-graph) concept. | /// | PQCmp | The comparator used to order elements within the priority queue. | Must be a `(NodeType, NodeType) -> bool` callable. | -/// | InitQueueRangeType | The container providing the initial roots to enqueue. | Must satisfy `std::ranges::forward_range`. | -/// | NodeType | The type of the node stored in the priority queue. | Extracted implicitly. Must be constructible from `(id_type, id_type)` unless `MakeNodeCallback` is provided. | +/// | InitNodeRngType | The container providing the initial roots to enqueue. | Must satisfy `std::ranges::forward_range` and yield a @ref gl::algorithm::search_node "search_node". | +/// | NodeType | The exact search node type extracted implicitly from the range. | Must strictly match `search_node, Extension>`. | /// | VisitVertexPredicate | Decides if a popped node should be processed. | Must be one of:
- `(NodeType) -> bool` callable
- An @ref gl::algorithm::empty_callback "empty_callback" | /// | VisitCallback | Executed when a vertex is officially visited. | Must be one of:
- `(id_type, id_type) -> bool` callable
- An @ref gl::algorithm::empty_callback "empty_callback" | /// | EnqueueNodePred | Decides if a node corresponding to an adjacent vertex should be pushed to the queue. | Must be one of:
- `(id_type, const edge_type&) -> decision` callable
- An @ref gl::algorithm::empty_callback "empty_callback" | @@ -72,14 +72,14 @@ namespace gl::algorithm { /// /// @param graph The graph to traverse. /// @param pq_cmp The comparator instance used to determine priority (highest priority is popped first). -/// @param initial_queue_content A range of initial nodes to seed the priority queue. +/// @param initial_nodes A range of initial @ref gl::algorithm::search_node "search nodes" to seed the priority queue. /// @param visit_vertex_pred Predicate evaluated immediately after popping a node. If it returns `false`, the node is skipped (often used for late-rejection in Dijkstra). /// @param visit Callback invoked when a vertex is officially visited. If it returns `false`, the entire PFS immediately aborts. /// @param enqueue_node_pred Predicate evaluated for each outgoing edge. Returns a @ref gl::algorithm::decision "decision": /// - `accept` to enqueue, /// - `reject` to skip, /// - `abort` to terminate the PFS entirely. -/// @param make_node Factory callback to construct a custom `NodeType` prior to enqueueing. Defaults to invoking the `NodeType(target_id, pred_id)` constructor. +/// @param make_node Factory callback to construct a custom node prior to enqueueing (useful for computing extensions like cumulative weights). Defaults to injecting a default-extended `search_node`. /// @param pre_visit Hook executed immediately before the `visit` callback. /// @param post_visit Hook executed after all adjacent edges of the current vertex have been evaluated. /// @return `true` if the queue was exhausted naturally, `false` if the search was aborted early by a callback or predicate. @@ -87,8 +87,8 @@ namespace gl::algorithm { template < traits::c_graph G, typename PQCmp, - typename InitQueueRangeType = std::vector>>, - typename NodeType = std::ranges::range_value_t, + typename InitNodeRngType = std::vector>>, + typename NodeType = std::ranges::range_value_t, traits::c_optional_predicate VisitVertexPredicate = empty_callback, traits::c_optional_predicate, id_t> VisitCallback = empty_callback, traits::c_decision_predicate, const edge_t&> EnqueueNodePred = empty_callback, @@ -96,11 +96,11 @@ template < empty_callback, traits::c_optional_callback> PreVisitCallback = empty_callback, traits::c_optional_callback> PostVisitCallback = empty_callback> -requires traits::c_predicate +requires(traits::c_predicate and traits::c_instantiation_of and std::same_as>) bool pfs( G&& graph, const PQCmp& pq_cmp, - const InitQueueRangeType& initial_queue_content, + const InitNodeRngType& initial_nodes, VisitVertexPredicate visit_vertex_pred = {}, VisitCallback visit = {}, EnqueueNodePred enqueue_node_pred = {}, @@ -108,14 +108,14 @@ bool pfs( PreVisitCallback pre_visit = {}, PostVisitCallback post_visit = {} ) { - if (std::ranges::empty(initial_queue_content)) + if (std::ranges::empty(initial_nodes)) return false; // prepare the node queue using queue_type = std::priority_queue, PQCmp>; queue_type q(pq_cmp); - for (const auto& node : initial_queue_content) + for (const auto& node : initial_nodes) q.push(node); // search the graph @@ -142,18 +142,10 @@ bool pfs( return false; if (enqueue) { - if constexpr (not traits::c_empty_callback) { + if constexpr (not traits::c_empty_callback) q.push(make_node(target_vertex_id, node.vertex_id, edge)); - } - else { - static_assert( - std::constructible_from, id_t>, - "[gl::algorithm::pfs] Custom NodeType provided without a MakeNodeCallback. " - "The NodeType must be constructible from (target_id, pred_id), or you must " - "provide a MakeNodeCallback!" - ); - q.emplace(target_vertex_id, node.vertex_id); - } + else + q.push(NodeType{target_vertex_id, node.vertex_id}); } } diff --git a/include/gl/algorithm/topology/coloring.hpp b/include/gl/algorithm/topology/coloring.hpp index bc8f5af7..5f68b445 100644 --- a/include/gl/algorithm/topology/coloring.hpp +++ b/include/gl/algorithm/topology/coloring.hpp @@ -77,7 +77,7 @@ template < const bool is_bipartite = bfs( graph, - init_node_range(root_id), + std::array{gl::algorithm::root_node(root_id)}, empty_callback{}, // visit predicate empty_callback{}, // visit callback [&coloring](id_t vertex_id, const edge_t& in_edge) diff --git a/include/gl/algorithm/traversal/breadth_first_search.hpp b/include/gl/algorithm/traversal/breadth_first_search.hpp index 2e30d72b..87d53b5c 100644 --- a/include/gl/algorithm/traversal/breadth_first_search.hpp +++ b/include/gl/algorithm/traversal/breadth_first_search.hpp @@ -88,7 +88,7 @@ result_type> breadth_first_search( if (root_vertex_id != no_root) { bfs( graph, - init_node_range(root_vertex_id), + std::array{gl::algorithm::root_node(root_vertex_id)}, default_visit_vertex_predicate(visited), default_visit_callback(visited, pred_map), default_enqueue_node_predicate(visited), @@ -100,7 +100,7 @@ result_type> breadth_first_search( for (const auto root_id : graph.vertex_ids()) bfs( graph, - init_node_range(root_id), + std::array{gl::algorithm::root_node(root_id)}, default_visit_vertex_predicate(visited), default_visit_callback(visited, pred_map), default_enqueue_node_predicate(visited), diff --git a/include/gl/algorithm/traversal/depth_first_search.hpp b/include/gl/algorithm/traversal/depth_first_search.hpp index d181b725..09214df7 100644 --- a/include/gl/algorithm/traversal/depth_first_search.hpp +++ b/include/gl/algorithm/traversal/depth_first_search.hpp @@ -96,7 +96,7 @@ result_type> depth_first_search( if (root_vertex_id != no_root) { dfs( graph, - init_node_range(root_vertex_id), + std::array{gl::algorithm::root_node(root_vertex_id)}, default_visit_vertex_predicate(visited), default_visit_callback(visited, pred_map), default_enqueue_node_predicate(visited), @@ -108,7 +108,7 @@ result_type> depth_first_search( for (const auto root_id : graph.vertex_ids()) dfs( graph, - init_node_range(root_id), + std::array{gl::algorithm::root_node(root_id)}, default_visit_vertex_predicate(visited), default_visit_callback(visited, pred_map), default_enqueue_node_predicate(visited), diff --git a/include/gl/algorithm/util.hpp b/include/gl/algorithm/util.hpp index f115aa74..758c7291 100644 --- a/include/gl/algorithm/util.hpp +++ b/include/gl/algorithm/util.hpp @@ -44,20 +44,6 @@ template return pred_map[to_idx(vertex_id)] != invalid_id; } -/// @ingroup GL-Algorithm -/// @brief Initializes a search container with the starting root vertex. -/// @tparam G The type of the graph. -/// @tparam InitRangeType The underlying container type for the container. -/// @param root_vertex_id The ID of the starting vertex. -/// @return A container initialized with a single @ref search_node for the root vertex. -template < - traits::c_graph G, - traits::c_forward_range_of>> InitRangeType = - std::vector>>> -[[nodiscard]] gl_attr_force_inline InitRangeType init_node_range(id_t root_vertex_id) { - return InitRangeType{search_node>{root_vertex_id}}; -} - /// @ingroup GL-Algorithm /// @brief Generates a default lambda predicate that checks if a vertex has not yet been visited. /// @param visited A reference to the boolean array tracking visited vertices. diff --git a/include/hgl/algorithm/core.hpp b/include/hgl/algorithm/core.hpp index 648c9fc4..b4e38109 100644 --- a/include/hgl/algorithm/core.hpp +++ b/include/hgl/algorithm/core.hpp @@ -29,6 +29,13 @@ namespace algorithm { /// - @ref gl::algorithm::empty_callback : For a more detailed description. using empty_callback = gl::algorithm::empty_callback; +/// @ingroup HGL-Algorithm +/// @copybrief gl::algorithm::empty_extension +/// ### See Also +/// - @ref gl::algorithm::empty_extension : For the original GL module's type documentation. +/// - @ref hgl::algorithm::search_node : For the definition of the algorithm search node type. +using empty_extension = gl::algorithm::empty_extension; + /// @ingroup HGL-Algorithm /// @copybrief gl::algorithm::decision /// ### See Also @@ -78,25 +85,26 @@ inline constexpr no_root_t no_root = gl::algorithm::no_root; /// @ingroup HGL-Algorithm /// @brief Represents an active node in a search container (e.g., a BFS queue or DFS stack) for hypergraph traversals. /// @tparam H The type of the hypergraph being searched. Must satisfy [**c_hypergraph**](hgl_concepts.md#hgl-traits-c-hypergraph). -template +/// @tparam Extension An optional payload type attached to the node for state tracking (must satisfy `std::semiregular`). +/// ### See Also +/// - @ref hgl::algorithm::root_node "hgl::algorithm::root_node" : For the full definition of the root search node builder function. +template struct search_node { + /// @brief The underlying value type of the hypergraph being searched. + using hypergraph_type = val_t; /// @brief The identifier type of the hypergraph elements. using id_type = id_t; + /// @brief The type of the custom state-tracking payload attached to this node. + using extension_type = Extension; - /// @brief Default constructor creates an invalid node. - search_node() = default; - - /// @brief Constructs a *root* search node (predecessor is itself, no incident hyperedge). - /// @param vertex_id The ID of the root vertex. - search_node(id_type vertex_id) - : vertex_id(vertex_id), pred_id(vertex_id), hyperedge_id(invalid_id) {} - - /// @brief Constructs a search node with an explicit predecessor vertex and the connecting hyperedge. - /// @param vertex_id The ID of the currently reached vertex. - /// @param pred_id The ID of the predecessor vertex from which this vertex was reached. - /// @param hyperedge_id The ID of the hyperedge connecting the predecessor to this vertex. - search_node(id_type vertex_id, id_type pred_id, id_type hyperedge_id) - : vertex_id(vertex_id), pred_id(pred_id), hyperedge_id(hyperedge_id) {} + /// @brief The ID of the current vertex. + id_type vertex_id = invalid_id; + /// @brief The ID of the predecessor from which this vertex was reached. + id_type pred_id = invalid_id; + /// @brief The ID of the hyperedge via which this vertex was reached from the predecessor. + id_type hyperedge_id = invalid_id; + /// @brief Custom state-tracking payload. + [[no_unique_address]] extension_type ext = {}; /// @brief Checks if this node is the root of a search tree. /// @return `true` if the node is valid and its predecessor is itself, `false` otherwise. @@ -104,24 +112,85 @@ struct search_node { return this->vertex_id != invalid_id and this->vertex_id == this->pred_id; } - /// @brief The ID of the current vertex. - id_type vertex_id = invalid_id; - /// @brief The ID of the predecessor from which this vertex was reached. - id_type pred_id = invalid_id; - /// @brief The ID of the hyperedge via which this vertex was reached from the predecessor. - id_type hyperedge_id = invalid_id; + /// @brief Explicitly converts this node to a search node with a different extension type. + /// + /// This allows for safe, seamless slicing and up-casting between stateful and stateless + /// search nodes during algorithm execution. The new extension is default-initialized. + /// + /// @tparam OtherExt The target extension type. + /// @return A new search node preserving the topology but with the target extension type. + template + requires(not std::same_as) + [[nodiscard]] gl_attr_force_inline explicit operator search_node() const noexcept { + return search_node{this->vertex_id, this->pred_id, this->hyperedge_id}; + } }; /// @ingroup HGL-Algorithm -/// @brief A flat, index-mapped representation of a hypergraph search tree. +/// @brief Free function builder that creates a search node acting as the root of a search tree. /// -/// The $i$-th element corresponds to the vertex with `id == i`. The tree topology is formed implicitly, -/// as each @ref hgl::algorithm::search_node "search_node" stores the ID of its predecessor and the -/// connecting hyperedge, enabling \f$O(1)\f$ lookups and and \f$O(\vert V \vert)\f$ path reconstruction. +/// This utility provides clean, unambiguous aggregate initialization semantics for root nodes +/// (where the vertex is strictly its own predecessor, and the connecting hyperedge is invalid) +/// at algorithmic call sites. /// /// @tparam H The type of the hypergraph being searched. +/// @tparam Extension The type of the custom state-tracking payload attached to the node. +/// @param root_id The ID of the root vertex. +/// @param ext An optional state-tracking extension payload. +/// @return A fully initialized @ref hgl::algorithm::search_node "search_node" acting as a root. +template +[[nodiscard]] gl_attr_force_inline search_node, Extension> root_node( + id_t root_id, Extension ext = {} +) { + return search_node, Extension>{root_id, root_id, invalid_id, std::move(ext)}; +} + +/// @ingroup HGL-Algorithm +/// @brief A flat, index-mapped representation of a hypergraph search tree. +/// +/// This structure is a simple wrapper around a `std::vector` of nodes, storing the +/// resulting topology of a hypergraph traversal. The $i$-th element in the `nodes` +/// vector implicitly corresponds to the vertex with `id == i`. +/// +/// @tparam H The type of the hypergraph being searched. Must satisfy [**c_hypergraph**](hgl_concepts.md#hgl-traits-c-hypergraph). template -using search_tree = std::vector>>; +struct search_tree { + /// @brief The underlying hypergraph type. + using hypergraph_type = val_t; + /// @brief The identifier type of the hypergraph elements. + using id_type = id_t; + + /// @brief Represents a static link in the traversal tree. + struct node { + id_type pred_id = invalid_id; ///< The ID of the predecessor vertex. + id_type hyperedge_id = invalid_id; ///< The ID of the connecting hyperedge. + }; + + /// @brief Default constructor creating an empty search tree. + search_tree() = default; + + /// @brief Constructs a search tree allocated for a specific number of vertices. + /// @param n_vertices The total number of vertices in the hypergraph. + explicit search_tree(const std::size_t n_vertices) : nodes(n_vertices) {} + + /// @brief Checks if a specific vertex was reached during the traversal. + /// @param vertex_id The ID of the vertex to check. + /// @return `true` if the vertex has a valid assigned predecessor, `false` otherwise. + [[nodiscard]] gl_attr_force_inline bool is_reachable(const id_type vertex_id) const noexcept { + return this->nodes[vertex_id].pred_id != invalid_id; + } + + /// @brief Checks if a specific vertex acts as a root in the search tree. + /// @param vertex_id The ID of the vertex to check. + /// @return `true` if the vertex is its own predecessor, `false` otherwise. + [[nodiscard]] gl_attr_force_inline bool is_root(const id_type vertex_id) const noexcept { + const auto pred = this->nodes[vertex_id].pred_id; + return pred != invalid_id and pred == vertex_id; + } + + /// @brief The underlying container mapping vertex IDs to their traversal tree nodes. + std::vector nodes; +}; } // namespace algorithm @@ -169,14 +238,6 @@ using gl::traits::c_decision_predicate; /// - [**c_optional_decision_predicate**](gl_concepts.md#gl-traits-c-optional-decision-predicate) : For the full concept documentation in the GL module. using gl::traits::c_optional_decision_predicate; -/// @ingroup HGL-Traits -/// @brief Validates if a type is a valid hypergraph search tree (a random access range of @ref hgl::algorithm::search_node "search_node"s). -/// @tparam T The type to evaluate against the concept. -template -concept c_search_tree = - c_random_access_range - and c_instantiation_of, algorithm::search_node>; - } // namespace traits namespace algorithm { diff --git a/include/hgl/algorithm/templates/bfs.hpp b/include/hgl/algorithm/templates/bfs.hpp index 9ceb9cef..fda47b56 100644 --- a/include/hgl/algorithm/templates/bfs.hpp +++ b/include/hgl/algorithm/templates/bfs.hpp @@ -28,7 +28,7 @@ namespace hgl::algorithm { /// /// bool completed = hgl::algorithm::bfs( /// hypergraph, -/// hgl::algorithm::init_node_range(start_id), // (2)! +/// std::array{hgl::algorithm::root_node(start_id)}, // (2)! /// [&](const auto& node) { return not visited[node.vertex_id]; }, // (3)! /// [&](const auto& node) { // (4)! /// visited[node.vertex_id] = true; @@ -57,7 +57,7 @@ namespace hgl::algorithm { /// | :-------- | :--- | :--- | /// | Dir | The @ref hgl::algorithm::traversal_direction "traversal direction" (i.e., `forward` or `backward`). Relevant only for BF-directed hypergraphs. | Defaults to `forward`. | /// | H | The type of the hypergraph being searched. | Must satisfy the [**c_hypergraph**](hgl_concepts.md#hgl-traits-c-hypergraph) concept. | -/// | InitQueueRangeType | A forward range of `search_node>` used to prime the BFS queue. | Must be a *forward range* of @ref hgl::algorithm::search_node "search nodes". | +/// | InitNodeRngType | A forward range of `search_node>` used to prime the BFS queue. | Must be a *forward range* of @ref hgl::algorithm::search_node "search nodes". | /// | VisitPredicate | Type of the callable deciding if a popped node should be processed. | Must be one of:
- A `(const search_node>&) -> bool` callable
- An @ref hgl::algorithm::empty_callback "empty_callback" | /// | VisitCallback | Type of the callable executed when a vertex is officially visited. | Must be one of:
- A `(const search_node>&) -> bool` callable
- An @ref hgl::algorithm::empty_callback "empty_callback" | /// | TraverseHePredicate | Type of the callable deciding if an incident hyperedge should be traversed. | Must be one of:
- An `(id_type, id_type) -> decision` callable
- An @ref hgl::algorithm::empty_callback "empty_callback" | @@ -66,7 +66,7 @@ namespace hgl::algorithm { /// | PostVisitCallback | Type of the callable executed after all adjacent elements are evaluated. | Must be one of:
- A `(const search_node>&) -> void` callable
- An @ref hgl::algorithm::empty_callback "empty_callback" | /// /// @param hypergraph The hypergraph to traverse. -/// @param initial_queue_content The initial set of search nodes to begin the traversal from. +/// @param init_nodes The initial set of search nodes to begin the traversal from. /// @param visit_pred Predicate to filter nodes immediately after popping them from the queue. /// @param visit Primary callback for node processing. /// @param traverse_he_pred Predicate to determine if an incident hyperedge should be traversed. Returns a @ref hgl::algorithm::decision "decision": @@ -84,7 +84,7 @@ namespace hgl::algorithm { template < traversal_direction Dir = traversal_direction::forward, traits::c_hypergraph H, - traits::c_forward_range_of>> InitQueueRangeType = + traits::c_forward_range_of>> InitNodeRngType = std::vector>>, traits::c_optional_predicate>&> VisitPredicate = empty_callback, traits::c_optional_predicate>&> VisitCallback = empty_callback, @@ -96,7 +96,7 @@ template < empty_callback> bool bfs( H&& hypergraph, - const InitQueueRangeType& initial_queue_content, + const InitNodeRngType& init_nodes, const VisitPredicate& visit_pred = {}, const VisitCallback& visit = {}, const TraverseHePredicate& traverse_he_pred = {}, @@ -106,11 +106,11 @@ bool bfs( ) { using policy = traversal_policy; - if (std::ranges::empty(initial_queue_content)) + if (std::ranges::empty(init_nodes)) return false; std::queue>> q; - for (const auto& node : initial_queue_content) + for (const auto& node : init_nodes) q.push(node); while (not q.empty()) { diff --git a/include/hgl/algorithm/templates/dfs.hpp b/include/hgl/algorithm/templates/dfs.hpp index 29e1a0c0..88e29c60 100644 --- a/include/hgl/algorithm/templates/dfs.hpp +++ b/include/hgl/algorithm/templates/dfs.hpp @@ -22,13 +22,19 @@ namespace hgl::algorithm { /// engine exposes specific hooks for both steps. Concrete algorithms are constructed by injecting /// logic into the provided callback and predicate hooks. /// +/// > [!NOTE] True Post-Order Traversal +/// > If a `PostVisitCallback` is provided, this engine automatically utilizes a stateful stack +/// > frame to guarantee a true post-order traversal (the callback fires only after the entire +/// > subtree of a node has been fully explored). If the callback is omitted (using `empty_callback`), +/// > the engine bypasses frame tracking entirely for maximum performance. +/// /// ### Example Usage /// ```cpp /// std::vector visited(hypergraph.n_vertices(), false); // (1)! /// /// bool completed = hgl::algorithm::dfs( /// hypergraph, -/// hgl::algorithm::init_node_range(start_id), // (2)! +/// std::array{hgl::algorithm::root_node(start_id)}, // (2)! /// [&](const auto& node) { return not visited[node.vertex_id]; }, // (3)! /// [&](const auto& node) { // (4)! /// visited[node.vertex_id] = true; @@ -57,7 +63,7 @@ namespace hgl::algorithm { /// | :-------- | :--- | :--- | /// | Dir | The @ref hgl::algorithm::traversal_direction "traversal direction" (i.e., `forward` or `backward`). Relevant only for BF-directed hypergraphs. | Defaults to `forward`. | /// | H | The type of the hypergraph being searched. | Must satisfy the [**c_hypergraph**](hgl_concepts.md#hgl-traits-c-hypergraph) concept. | -/// | InitQueueRangeType | A forward range of `search_node>` used to prime the DFS stack. | Must be a *forward range* of @ref hgl::algorithm::search_node "search nodes". | +/// | InitNodeRngType | A forward range of `search_node>` used to prime the DFS stack. | Must be a *forward range* of @ref hgl::algorithm::search_node "search nodes". | /// | VisitPredicate | Type of the callable deciding if a popped node should be processed. | Must be one of:
- A `(const search_node>&) -> bool` callable
- An @ref hgl::algorithm::empty_callback "empty_callback" | /// | VisitCallback | Type of the callable executed when a vertex is officially visited. | Must be one of:
- A `(const search_node>&) -> bool` callable
- An @ref hgl::algorithm::empty_callback "empty_callback" | /// | TraverseHePredicate | Type of the callable deciding if an incident hyperedge should be traversed. | Must be one of:
- An `(id_type, id_type) -> decision` callable
- An @ref hgl::algorithm::empty_callback "empty_callback" | @@ -66,7 +72,7 @@ namespace hgl::algorithm { /// | PostVisitCallback | Type of the callable executed after all adjacent elements are evaluated. | Must be one of:
- A `(const search_node>&) -> void` callable
- An @ref hgl::algorithm::empty_callback "empty_callback" | /// /// @param hypergraph The hypergraph to traverse. -/// @param initial_queue_content The initial set of search nodes to begin the traversal from. +/// @param init_nodes The initial set of search nodes to begin the traversal from. /// @param visit_pred Predicate to filter nodes immediately after popping them from the stack. /// @param visit Primary callback for node processing. /// @param traverse_he_pred Predicate to determine if an incident hyperedge should be traversed. Returns a @ref hgl::algorithm::decision "decision": @@ -78,13 +84,13 @@ namespace hgl::algorithm { /// - `reject` to skip the node, /// - `abort` to terminate the DFS entirely. /// @param pre_visit Callback executed prior to the primary visit logic. -/// @param post_visit Callback executed after all adjacent elements of the current node have been processed. +/// @param post_visit Callback executed after all adjacent elements of the current node have been processed (true post-order). /// @return `true` if the search completed normally, `false` if it was explicitly aborted via a callback. /// @hideparams template < traversal_direction Dir = traversal_direction::forward, traits::c_hypergraph H, - traits::c_forward_range_of>> InitQueueRangeType = + traits::c_forward_range_of>> InitNodeRngType = std::vector>>, traits::c_optional_predicate>&> VisitPredicate = empty_callback, traits::c_optional_predicate>&> VisitCallback = empty_callback, @@ -96,7 +102,7 @@ template < empty_callback> bool dfs( H&& hypergraph, - const InitQueueRangeType& initial_queue_content, + const InitNodeRngType& init_nodes, const VisitPredicate& visit_pred = {}, const VisitCallback& visit = {}, const TraverseHePredicate& traverse_he_pred = {}, @@ -105,53 +111,118 @@ bool dfs( const PostVisitCallback& post_visit = {} ) { using policy = traversal_policy; + using stateless_node_t = search_node>; - if (std::ranges::empty(initial_queue_content)) + if (std::ranges::empty(init_nodes)) return false; - std::stack>> s; - for (const auto& node : initial_queue_content) - s.push(node); - - while (not s.empty()) { - const search_node curr_node = s.top(); - s.pop(); + if constexpr (traits::c_empty_callback) { // stateless stack + std::stack s; + for (const auto& node : init_nodes) + s.push(node); - if constexpr (not traits::c_empty_callback) - if (not visit_pred(curr_node)) - continue; + while (not s.empty()) { + const search_node curr_node = s.top(); + s.pop(); - if constexpr (not traits::c_empty_callback) - pre_visit(curr_node); + if constexpr (not traits::c_empty_callback) + if (not visit_pred(curr_node)) + continue; - if constexpr (not traits::c_empty_callback) - if (not visit(curr_node)) - return false; + if constexpr (not traits::c_empty_callback) + pre_visit(curr_node); - for (const auto he_id : policy::target_hyperedges(hypergraph, curr_node.vertex_id)) { - if constexpr (not traits::c_empty_callback) { - const auto traverse = traverse_he_pred(he_id, curr_node.vertex_id); - if (traverse == decision::abort) + if constexpr (not traits::c_empty_callback) + if (not visit(curr_node)) return false; - if (traverse == decision::reject) - continue; + + for (const auto he_id : policy::target_hyperedges(hypergraph, curr_node.vertex_id)) { + if constexpr (not traits::c_empty_callback) { + const auto traverse = traverse_he_pred(he_id, curr_node.vertex_id); + if (traverse == decision::abort) + return false; + if (traverse == decision::reject) + continue; + } + + for (const auto target_id : policy::target_vertices(hypergraph, he_id)) { + if (target_id == curr_node.vertex_id) + continue; + + search_node> tgt_node{target_id, curr_node.vertex_id, he_id}; + const auto enqueue = enqueue_pred(tgt_node); + + if (enqueue == decision::abort) + return false; + if (enqueue) + s.push(tgt_node); + } } + } + } + else { // stateful stack - for (const auto target_id : policy::target_vertices(hypergraph, he_id)) { - if (target_id == curr_node.vertex_id) - continue; + struct dfs_extension { + bool expanded = false; + }; - search_node> tgt_node{target_id, curr_node.vertex_id, he_id}; - const auto enqueue = enqueue_pred(tgt_node); - if (enqueue == decision::abort) - return false; - if (enqueue) - s.push(tgt_node); + using stateful_node_t = search_node, dfs_extension>; + std::stack s; + + for (const auto& node : init_nodes) + s.push(stateful_node_t(node)); + + while (not s.empty()) { + auto curr_node = s.top(); + s.pop(); + + // Reconstruct the stateless base node to safely satisfy the callback concepts + const stateless_node_t base_node(curr_node); + + if (curr_node.ext.expanded) { + post_visit(base_node); + } + else { + if constexpr (not traits::c_empty_callback) + if (not visit_pred(base_node)) + continue; + + if constexpr (not traits::c_empty_callback) + pre_visit(base_node); + + if constexpr (not traits::c_empty_callback) + if (not visit(base_node)) + return false; + + // Push parent back marked as expanded to wait for children + curr_node.ext.expanded = true; + s.push(curr_node); + + for (const auto he_id : + policy::target_hyperedges(hypergraph, curr_node.vertex_id)) { + if constexpr (not traits::c_empty_callback) { + const auto traverse = traverse_he_pred(he_id, curr_node.vertex_id); + if (traverse == decision::abort) + return false; + if (traverse == decision::reject) + continue; + } + + for (const auto target_id : policy::target_vertices(hypergraph, he_id)) { + if (target_id == curr_node.vertex_id) + continue; + + stateless_node_t tgt_base{target_id, curr_node.vertex_id, he_id}; + const auto enqueue = enqueue_pred(tgt_base); + + if (enqueue == decision::abort) + return false; + if (enqueue) + s.push(stateful_node_t(tgt_base)); + } + } } } - - if constexpr (not traits::c_empty_callback) - post_visit(curr_node); } return true; diff --git a/include/hgl/algorithm/traversal/backward_search.hpp b/include/hgl/algorithm/traversal/backward_search.hpp index 646fdac1..779f4a20 100644 --- a/include/hgl/algorithm/traversal/backward_search.hpp +++ b/include/hgl/algorithm/traversal/backward_search.hpp @@ -63,7 +63,7 @@ template < empty_callback, traits::c_optional_callback>&> PostVisitCallback = empty_callback> -result_type> backward_bfs( +result_type>> backward_bfs( H&& hypergraph, const RootRange& root_vertices, const PreVisitCallback& pre_visit = {}, @@ -74,9 +74,8 @@ result_type> backward_bfs( auto stree = init_search_tree(hypergraph); auto root_queue = - root_vertices | std::views::transform([](const id_t root_id) { - return search_node>{root_id}; - }); + root_vertices + | std::views::transform([](const id_t root_id) { return root_node(root_id); }); // clang-format off @@ -145,7 +144,7 @@ template < empty_callback, traits::c_optional_callback>&> PostVisitCallback = empty_callback> -result_type> backward_dfs( +result_type>> backward_dfs( H&& hypergraph, const RootRange& root_vertices, const PreVisitCallback& pre_visit = {}, @@ -156,9 +155,8 @@ result_type> backward_dfs( auto stree = init_search_tree(hypergraph); auto root_queue = - root_vertices | std::views::transform([](const id_t root_id) { - return search_node>{root_id}; - }); + root_vertices + | std::views::transform([](const id_t root_id) { return root_node(root_id); }); // clang-format off diff --git a/include/hgl/algorithm/traversal/breadth_first_search.hpp b/include/hgl/algorithm/traversal/breadth_first_search.hpp index 1b886b1e..6cf81e08 100644 --- a/include/hgl/algorithm/traversal/breadth_first_search.hpp +++ b/include/hgl/algorithm/traversal/breadth_first_search.hpp @@ -62,7 +62,7 @@ template < empty_callback, traits::c_optional_callback>&> PostVisitCallback = empty_callback> -result_type> breadth_first_search( +result_type>> breadth_first_search( H&& hypergraph, const id_t root_vertex_id = no_root, const PreVisitCallback& pre_visit = {}, @@ -78,7 +78,7 @@ result_type> breadth_first_search( if (root_vertex_id != no_root) { bfs( hypergraph, - init_node_range(root_vertex_id), + std::array{root_node(root_vertex_id)}, default_visit_predicate(visited_vertices), default_visit_callback(visited_vertices, stree), default_traverse_hyperedge_predicate(visited_hyperedges), @@ -91,7 +91,7 @@ result_type> breadth_first_search( for (const auto root_id : hypergraph.vertex_ids()) bfs( hypergraph, - init_node_range(root_id), + std::array{root_node(root_id)}, default_visit_predicate(visited_vertices), default_visit_callback(visited_vertices, stree), default_traverse_hyperedge_predicate(visited_hyperedges), diff --git a/include/hgl/algorithm/traversal/depth_first_search.hpp b/include/hgl/algorithm/traversal/depth_first_search.hpp index be7b66b4..21370411 100644 --- a/include/hgl/algorithm/traversal/depth_first_search.hpp +++ b/include/hgl/algorithm/traversal/depth_first_search.hpp @@ -62,7 +62,7 @@ template < empty_callback, traits::c_optional_callback>&> PostVisitCallback = empty_callback> -result_type> depth_first_search( +result_type>> depth_first_search( H&& hypergraph, const id_t root_vertex_id = no_root, const PreVisitCallback& pre_visit = {}, @@ -78,7 +78,7 @@ result_type> depth_first_search( if (root_vertex_id != no_root) { dfs( hypergraph, - init_node_range(root_vertex_id), + std::array{root_node(root_vertex_id)}, default_visit_predicate(visited_vertices), default_visit_callback(visited_vertices, stree), default_traverse_hyperedge_predicate(visited_hyperedges), @@ -91,7 +91,7 @@ result_type> depth_first_search( for (const auto root_id : hypergraph.vertex_ids()) dfs( hypergraph, - init_node_range(root_id), + std::array{root_node(root_id)}, default_visit_predicate(visited_vertices), default_visit_callback(visited_vertices, stree), default_traverse_hyperedge_predicate(visited_hyperedges), diff --git a/include/hgl/algorithm/traversal/forward_search.hpp b/include/hgl/algorithm/traversal/forward_search.hpp index 72a6ab10..b2cf88c0 100644 --- a/include/hgl/algorithm/traversal/forward_search.hpp +++ b/include/hgl/algorithm/traversal/forward_search.hpp @@ -63,7 +63,7 @@ template < empty_callback, traits::c_optional_callback>&> PostVisitCallback = empty_callback> -result_type> forward_bfs( +result_type>> forward_bfs( H&& hypergraph, const RootRange& root_vertices, const PreVisitCallback& pre_visit = {}, @@ -74,9 +74,8 @@ result_type> forward_bfs( auto stree = init_search_tree(hypergraph); auto root_queue = - root_vertices | std::views::transform([](const id_t root_id) { - return search_node>{root_id}; - }); + root_vertices + | std::views::transform([](const id_t root_id) { return root_node(root_id); }); // clang-format off @@ -145,7 +144,7 @@ template < empty_callback, traits::c_optional_callback>&> PostVisitCallback = empty_callback> -result_type> forward_dfs( +result_type>> forward_dfs( H&& hypergraph, const RootRange& root_vertices, const PreVisitCallback& pre_visit = {}, @@ -156,9 +155,8 @@ result_type> forward_dfs( auto stree = init_search_tree(hypergraph); auto root_queue = - root_vertices | std::views::transform([](const id_t root_id) { - return search_node>{root_id}; - }); + root_vertices + | std::views::transform([](const id_t root_id) { return root_node(root_id); }); // clang-format off diff --git a/include/hgl/algorithm/util.hpp b/include/hgl/algorithm/util.hpp index 0301a404..70925c15 100644 --- a/include/hgl/algorithm/util.hpp +++ b/include/hgl/algorithm/util.hpp @@ -23,32 +23,9 @@ template init_search_tree(H&& hypergraph) { using return_t = non_void_result_type>>; if constexpr (Result == ret) - return return_t(hypergraph.n_vertices()); + return return_t{hypergraph.n_vertices()}; else - return return_t(); -} - -/// @ingroup HGL-Algorithm -/// @brief Checks if a specific vertex was reached during the traversal. -/// @param tree The computed search tree resulting from a traversal. -/// @param vertex_id The identifier of the vertex to check. -/// @return `true` if the vertex has a valid predecessor in the tree, `false` otherwise. -[[nodiscard]] gl_attr_force_inline bool is_reachable( - const traits::c_search_tree auto& tree, traits::c_id_type auto vertex_id -) noexcept { - return tree[to_idx(vertex_id)].pred_id != invalid_id; -} - -/// @ingroup HGL-Algorithm -/// @brief Initializes a container with a starting set of root search nodes. -/// @tparam H The type of the hypergraph. -/// @param root_vertex_id The ID of the starting vertex. -/// @return A `std::vector` containing a single root @ref hgl::algorithm::search_node "search_node". -template -[[nodiscard]] gl_attr_force_inline std::vector>> init_node_range( - id_t root_vertex_id -) { - return {search_node>{root_vertex_id}}; + return return_t{}; } /// @ingroup HGL-Algorithm @@ -71,18 +48,17 @@ template /// @tparam H The type of the hypergraph. /// @tparam Result The compilation tag dictating whether to populate the search tree. /// @param visited_v A reference to the boolean array tracking visited vertices. -/// @param pred_map A reference to the search tree being populated (or a dummy if `Result == noret`). +/// @param stree A reference to the search tree being populated (or a dummy if `Result == noret`). /// @return A callable callback returning `true` to unconditionally continue the traversal. /// @hideparams template [[nodiscard]] gl_attr_force_inline auto default_visit_callback( - std::vector& visited_v, non_void_result_type>>& pred_map + std::vector& visited_v, non_void_result_type>>& stree ) { return [&](const search_node>& node) { - const auto vertex_idx = to_idx(node.vertex_id); - visited_v[vertex_idx] = true; + visited_v[node.vertex_id] = true; if constexpr (Result == ret) - pred_map[vertex_idx] = node; + stree.nodes[node.vertex_id] = {node.pred_id, node.hyperedge_id}; return true; }; } diff --git a/tests/source/gl/test_alg_dfs.cpp b/tests/source/gl/test_alg_dfs.cpp index 1131912c..9c3e5618 100644 --- a/tests/source/gl/test_alg_dfs.cpp +++ b/tests/source/gl/test_alg_dfs.cpp @@ -21,29 +21,39 @@ TEST_CASE_TEMPLATE_DEFINE( using vertex_type = typename GraphType::vertex_type; graph_type graph; - std::deque expected_previsit_order; + std::vector expected_previsit_order, expected_postvisit_order; SUBCASE("empty graph") { graph = gl::topology::clique(0uz); expected_previsit_order = {}; + expected_postvisit_order = {}; } SUBCASE("single vertex graph") { graph = gl::topology::clique(1uz); expected_previsit_order = {0}; + expected_postvisit_order = {0}; } SUBCASE("clique") { graph = gl::topology::clique(constants::n_elements_alg); - for (auto id = constants::v2_id; id < constants::n_elements_alg; id++) - expected_previsit_order.push_front(id); - expected_previsit_order.push_front(constants::v1_id); + + expected_previsit_order.push_back(constants::v1_id); + for (auto i = constants::n_elements_alg; i > constants::v2_id; i--) + expected_previsit_order.push_back(static_cast(i - 1)); + + expected_postvisit_order = expected_previsit_order; + std::ranges::reverse(expected_postvisit_order); } SUBCASE("path graph") { graph = gl::topology::bidirectional_path(constants::n_elements_alg); + for (auto id = constants::v1_id; id < constants::n_elements_alg; id++) expected_previsit_order.push_back(id); + + expected_postvisit_order = expected_previsit_order; + std::ranges::reverse(expected_postvisit_order); } SUBCASE("biclique") { @@ -58,12 +68,27 @@ TEST_CASE_TEMPLATE_DEFINE( */ graph = gl::topology::biclique(3uz, 2uz); expected_previsit_order = {0, 4, 2, 3, 1}; + expected_postvisit_order = {1, 3, 2, 4, 0}; + } + + SUBCASE("regular binary tree") { + /* + Depth = 3 (7 vertices: 0 to 6) + [s: ] + -> root = 0 -> connected to left (1), right (2). [s: 2 1] + -> pop 2 -> connected to left (5), right (6). [s: 6 5 1] + -> pop 6 (leaf) -> pop 5 (leaf) + -> pop 1 -> connected to left (3), right (4). [s: 4 3] + -> pop 4 (leaf) -> pop 3 (leaf) + */ + graph = gl::topology::regular_binary_tree(3uz); + expected_previsit_order = {0, 2, 6, 5, 1, 4, 3}; + expected_postvisit_order = {6, 5, 2, 4, 3, 1, 0}; } CAPTURE(graph); CAPTURE(expected_previsit_order); - - std::deque expected_postvisit_order = expected_previsit_order; + CAPTURE(expected_postvisit_order); std::vector previsit_order, postvisit_order; const auto vertex_properties = graph.vertex_properties_map(); @@ -122,30 +147,41 @@ TEST_CASE_TEMPLATE_DEFINE( graph_type graph; id_type root_vertex_id; - std::deque expected_previsit_order; + std::vector expected_previsit_order, expected_postvisit_order; SUBCASE("single vertex graph") { graph = gl::topology::clique(1uz); root_vertex_id = constants::v1_id; expected_previsit_order = {0}; + expected_postvisit_order = {0}; } SUBCASE("clique") { graph = gl::topology::clique(constants::n_elements_alg); root_vertex_id = constants::v3_id; - for (auto id = constants::v1_id; id < constants::n_elements_alg; id++) { + expected_previsit_order.push_back(constants::v3_id); + for (auto i = constants::n_elements_alg; i > constants::v1_id; i--) { + const auto id = static_cast(i - 1); if (id != constants::v3_id) - expected_previsit_order.push_front(id); + expected_previsit_order.push_back(id); } - expected_previsit_order.push_front(constants::v3_id); + + expected_postvisit_order = expected_previsit_order; + std::ranges::reverse(expected_postvisit_order); + } + + SUBCASE("regular binary tree") { + graph = gl::topology::regular_binary_tree(3uz); + root_vertex_id = 0; + expected_previsit_order = {0, 2, 6, 5, 1, 4, 3}; + expected_postvisit_order = {6, 5, 2, 4, 3, 1, 0}; } CAPTURE(graph); CAPTURE(root_vertex_id); CAPTURE(expected_previsit_order); - - std::deque expected_postvisit_order = expected_previsit_order; + CAPTURE(expected_postvisit_order); std::vector previsit_order, postvisit_order; gl::algorithm::depth_first_search( @@ -214,28 +250,36 @@ TEST_CASE_TEMPLATE_DEFINE( using vertex_type = typename GraphType::vertex_type; graph_type graph; - std::vector expected_previsit_order; + std::vector expected_previsit_order, expected_postvisit_order; SUBCASE("empty graph") { graph = gl::topology::clique(0uz); expected_previsit_order = {}; + expected_postvisit_order = {}; } SUBCASE("single vertex graph") { graph = gl::topology::clique(1uz); expected_previsit_order = {0}; + expected_postvisit_order = {0}; } SUBCASE("clique") { graph = gl::topology::clique(constants::n_elements_alg); for (auto id = constants::v1_id; id < constants::n_elements_alg; id++) expected_previsit_order.push_back(id); + + expected_postvisit_order = expected_previsit_order; + std::ranges::reverse(expected_postvisit_order); } SUBCASE("path graph") { graph = gl::topology::bidirectional_path(constants::n_elements_alg); for (auto id = constants::v1_id; id < constants::n_elements_alg; id++) expected_previsit_order.push_back(id); + + expected_postvisit_order = expected_previsit_order; + std::ranges::reverse(expected_postvisit_order); } SUBCASE("biclique") { @@ -251,17 +295,27 @@ TEST_CASE_TEMPLATE_DEFINE( */ graph = gl::topology::biclique(3uz, 2uz); expected_previsit_order = {0, 3, 1, 4, 2}; + expected_postvisit_order = {2, 4, 1, 3, 0}; + } + + SUBCASE("regular binary tree") { + /* + Depth = 3 (7 vertices: 0 to 6) + -> root = 0 -> recursively calls left child (1) + -> 1 -> recursively calls left child (3) + -> 3 is leaf -> returns -> 1 calls right child (4) + -> 4 is leaf -> returns -> 1 returns -> 0 calls right child (2) + -> 2 -> recursively calls left child (5) + -> 5 is leaf -> returns -> 2 calls right child (6) + */ + graph = gl::topology::regular_binary_tree(3uz); + expected_previsit_order = {0, 1, 3, 4, 2, 5, 6}; + expected_postvisit_order = {3, 4, 1, 5, 6, 2, 0}; } CAPTURE(graph); CAPTURE(expected_previsit_order); - - /* - post visit order should be reverse of pre visit order - because the algorithm will search the graph recursively and call - post visit after return from the recursive call - */ - const auto expected_postvisit_order = std::views::reverse(expected_previsit_order); + CAPTURE(expected_postvisit_order); std::vector previsit_order, postvisit_order; const auto vertex_properties = graph.vertex_properties_map(); @@ -320,12 +374,13 @@ TEST_CASE_TEMPLATE_DEFINE( graph_type graph; id_type root_vertex_id = gl::invalid_id; - std::vector expected_previsit_order; + std::vector expected_previsit_order, expected_postvisit_order; SUBCASE("single vertex graph") { graph = gl::topology::clique(1uz); root_vertex_id = constants::v1_id; expected_previsit_order = {0}; + expected_postvisit_order = {0}; } SUBCASE("clique") { @@ -337,18 +392,22 @@ TEST_CASE_TEMPLATE_DEFINE( if (id != constants::v3_id) expected_previsit_order.push_back(id); } + + expected_postvisit_order = expected_previsit_order; + std::ranges::reverse(expected_postvisit_order); + } + + SUBCASE("regular binary tree") { + graph = gl::topology::regular_binary_tree(3uz); + root_vertex_id = 0; + expected_previsit_order = {0, 1, 3, 4, 2, 5, 6}; + expected_postvisit_order = {3, 4, 1, 5, 6, 2, 0}; } CAPTURE(graph); CAPTURE(root_vertex_id); CAPTURE(expected_previsit_order); - - /* - post visit order should be reverse of pre visit order - because the algorithm will search the graph recursively and call - post visit after return from the recursive call - */ - const auto expected_postvisit_order = std::views::reverse(expected_previsit_order); + CAPTURE(expected_postvisit_order); std::vector previsit_order, postvisit_order; gl::algorithm::recursive_depth_first_search( diff --git a/tests/source/hgl/test_alg_backward_search.cpp b/tests/source/hgl/test_alg_backward_search.cpp index 3b287a46..52f14623 100644 --- a/tests/source/hgl/test_alg_backward_search.cpp +++ b/tests/source/hgl/test_alg_backward_search.cpp @@ -16,7 +16,8 @@ TEST_CASE_TEMPLATE_DEFINE( ) { using hypergraph_type = hgl::hypergraph; using id_type = typename hypergraph_type::id_type; - using node_type = hgl::algorithm::search_node; + using tree_type = hgl::algorithm::search_tree; + using tree_node_type = typename tree_type::node; hypergraph_type hypergraph; std::vector root_vertices; @@ -99,18 +100,19 @@ TEST_CASE_TEMPLATE_DEFINE( hgl::algorithm::backward_bfs(hypergraph, root_vertices); const auto ret_pred_map = - search_tree | std::views::transform(&node_type::pred_id) | std::ranges::to(); + search_tree.nodes | std::views::transform(&tree_node_type::pred_id) + | std::ranges::to(); const auto ret_in_hyperedges = - search_tree | std::views::transform(&node_type::hyperedge_id) + search_tree.nodes | std::views::transform(&tree_node_type::hyperedge_id) | std::ranges::to(); CHECK_EQ(ret_pred_map, expected_pred_map); CHECK_EQ(ret_in_hyperedges, expected_in_hyperedges); CHECK(std::ranges::all_of(expected_visit_order, [&search_tree](const auto v_id) { - return hgl::algorithm::is_reachable(search_tree, v_id); + return search_tree.is_reachable(v_id); })); CHECK(std::ranges::all_of(unreachable_vertices, [&search_tree](const auto v_id) { - return not hgl::algorithm::is_reachable(search_tree, v_id); + return not search_tree.is_reachable(v_id); })); } @@ -155,12 +157,14 @@ TEST_CASE_TEMPLATE_DEFINE( ) { using hypergraph_type = hgl::hypergraph; using id_type = typename hypergraph_type::id_type; - using node_type = hgl::algorithm::search_node; + using tree_type = hgl::algorithm::search_tree; + using tree_node_type = typename tree_type::node; hypergraph_type hypergraph; std::vector root_vertices; - std::vector expected_visit_order; + std::vector expected_previsit_order; + std::vector expected_postvisit_order; std::vector expected_pred_map; std::vector expected_in_hyperedges; std::vector unreachable_vertices; @@ -186,7 +190,8 @@ TEST_CASE_TEMPLATE_DEFINE( SUBCASE("single root v0 (immediate halt)") { root_vertices = {0u}; - expected_visit_order = {0u}; + expected_previsit_order = {0u}; + expected_postvisit_order = {0u}; expected_pred_map.resize(n_vertices, hgl::invalid_id); expected_pred_map[0uz] = 0u; expected_in_hyperedges.resize(n_vertices, hgl::invalid_id); @@ -203,7 +208,8 @@ TEST_CASE_TEMPLATE_DEFINE( // Pop v0 -> unlocks e0 -> pushes v2. // Pop v2 -> unlocks e1 -> pushes v3, v4(visited). // Pop v3 -> no outgoing. - expected_visit_order = {1u, 4u, 0u, 2u, 3u}; + expected_previsit_order = {1u, 4u, 0u, 2u, 3u}; + expected_postvisit_order = {4u, 1u, 3u, 2u, 0u}; // v0->v0(root), v1->v1(root), v2->v0 (via e0), v3->v2 (via e1), v4->v1 (via e2) expected_pred_map = {0u, 1u, 0u, 2u, 1u}; @@ -214,7 +220,8 @@ TEST_CASE_TEMPLATE_DEFINE( CAPTURE(hypergraph); CAPTURE(root_vertices); - CAPTURE(expected_visit_order); + CAPTURE(expected_previsit_order); + CAPTURE(expected_postvisit_order); CAPTURE(expected_pred_map); CAPTURE(expected_in_hyperedges); CAPTURE(unreachable_vertices); @@ -237,8 +244,8 @@ TEST_CASE_TEMPLATE_DEFINE( [&](const auto& node) { postvisit_order.push_back(node.vertex_id); } ); - CHECK_EQ(previsit_order, expected_visit_order); - CHECK_EQ(postvisit_order, expected_visit_order); + CHECK_EQ(previsit_order, expected_previsit_order); + CHECK_EQ(postvisit_order, expected_postvisit_order); CHECK_EQ(noret_pred_map, expected_pred_map); CHECK_EQ(noret_in_hyperedges, expected_in_hyperedges); @@ -248,18 +255,19 @@ TEST_CASE_TEMPLATE_DEFINE( hgl::algorithm::backward_dfs(hypergraph, root_vertices); const auto ret_pred_map = - search_tree | std::views::transform(&node_type::pred_id) | std::ranges::to(); + search_tree.nodes | std::views::transform(&tree_node_type::pred_id) + | std::ranges::to(); const auto ret_in_hyperedges = - search_tree | std::views::transform(&node_type::hyperedge_id) + search_tree.nodes | std::views::transform(&tree_node_type::hyperedge_id) | std::ranges::to(); CHECK_EQ(ret_pred_map, expected_pred_map); CHECK_EQ(ret_in_hyperedges, expected_in_hyperedges); - CHECK(std::ranges::all_of(expected_visit_order, [&search_tree](const auto v_id) { - return hgl::algorithm::is_reachable(search_tree, v_id); + CHECK(std::ranges::all_of(expected_previsit_order, [&search_tree](const auto v_id) { + return search_tree.is_reachable(v_id); })); CHECK(std::ranges::all_of(unreachable_vertices, [&search_tree](const auto v_id) { - return not hgl::algorithm::is_reachable(search_tree, v_id); + return not search_tree.is_reachable(v_id); })); } diff --git a/tests/source/hgl/test_alg_bfs.cpp b/tests/source/hgl/test_alg_bfs.cpp index 85940792..a38c9cd3 100644 --- a/tests/source/hgl/test_alg_bfs.cpp +++ b/tests/source/hgl/test_alg_bfs.cpp @@ -19,7 +19,8 @@ TEST_CASE_TEMPLATE_DEFINE( ) { using hypergraph_type = hgl::hypergraph; using id_type = typename hypergraph_type::id_type; - using node_type = hgl::algorithm::search_node; + using tree_type = hgl::algorithm::search_tree; + using tree_node_type = typename tree_type::node; hypergraph_type hypergraph; id_type root_vertex_id; @@ -137,18 +138,19 @@ TEST_CASE_TEMPLATE_DEFINE( hgl::algorithm::breadth_first_search(hypergraph, root_vertex_id); const auto ret_pred_map = - search_tree | std::views::transform(&node_type::pred_id) | std::ranges::to(); + search_tree.nodes | std::views::transform(&tree_node_type::pred_id) + | std::ranges::to(); const auto ret_in_hyperedges = - search_tree | std::views::transform(&node_type::hyperedge_id) + search_tree.nodes | std::views::transform(&tree_node_type::hyperedge_id) | std::ranges::to(); CHECK(std::ranges::equal(ret_pred_map, expected_pred_map)); CHECK(std::ranges::equal(ret_in_hyperedges, expected_in_hyperedges)); CHECK(std::ranges::all_of(expected_visit_order, [&search_tree](const auto v_id) { - return hgl::algorithm::is_reachable(search_tree, v_id); + return search_tree.is_reachable(v_id); })); CHECK(std::ranges::all_of(unreachable_vertices, [&search_tree](const auto v_id) { - return not hgl::algorithm::is_reachable(search_tree, v_id); + return not search_tree.is_reachable(v_id); })); } @@ -194,7 +196,8 @@ TEST_CASE_TEMPLATE_DEFINE( ) { using hypergraph_type = hgl::hypergraph; using id_type = typename hypergraph_type::id_type; - using node_type = hgl::algorithm::search_node; + using tree_type = hgl::algorithm::search_tree; + using tree_node_type = typename tree_type::node; hypergraph_type hypergraph; id_type root_vertex_id; @@ -313,18 +316,19 @@ TEST_CASE_TEMPLATE_DEFINE( hgl::algorithm::breadth_first_search(hypergraph, root_vertex_id); const auto ret_pred_map = - search_tree | std::views::transform(&node_type::pred_id) | std::ranges::to(); + search_tree.nodes | std::views::transform(&tree_node_type::pred_id) + | std::ranges::to(); const auto ret_in_hyperedges = - search_tree | std::views::transform(&node_type::hyperedge_id) + search_tree.nodes | std::views::transform(&tree_node_type::hyperedge_id) | std::ranges::to(); CHECK(std::ranges::equal(ret_pred_map, expected_pred_map)); CHECK(std::ranges::equal(ret_in_hyperedges, expected_in_hyperedges)); CHECK(std::ranges::all_of(expected_visit_order, [&search_tree](const auto v_id) { - return hgl::algorithm::is_reachable(search_tree, v_id); + return search_tree.is_reachable(v_id); })); CHECK(std::ranges::all_of(unreachable_vertices, [&search_tree](const auto v_id) { - return not hgl::algorithm::is_reachable(search_tree, v_id); + return not search_tree.is_reachable(v_id); })); } diff --git a/tests/source/hgl/test_alg_dfs.cpp b/tests/source/hgl/test_alg_dfs.cpp index 7375d45a..54c5855e 100644 --- a/tests/source/hgl/test_alg_dfs.cpp +++ b/tests/source/hgl/test_alg_dfs.cpp @@ -16,12 +16,14 @@ TEST_CASE_TEMPLATE_DEFINE( ) { using hypergraph_type = hgl::hypergraph; using id_type = typename hypergraph_type::id_type; - using node_type = hgl::algorithm::search_node; + using tree_type = hgl::algorithm::search_tree; + using tree_node_type = typename tree_type::node; hypergraph_type hypergraph; id_type root_vertex_id; - std::vector expected_visit_order; + std::vector expected_previsit_order; + std::vector expected_postvisit_order; std::vector expected_pred_map; std::vector expected_in_hyperedges; std::vector unreachable_vertices; @@ -32,7 +34,8 @@ TEST_CASE_TEMPLATE_DEFINE( root_vertex_id = hgl::initial_id; - expected_visit_order = {0u, 3u, 2u, 1u}; + expected_previsit_order = {0u, 3u, 2u, 1u}; + expected_postvisit_order = {3u, 2u, 1u, 0u}; expected_pred_map = {0u, 0u, 0u, 0u}; expected_in_hyperedges = {hgl::invalid_id, e, e, e}; unreachable_vertices = {}; @@ -48,7 +51,8 @@ TEST_CASE_TEMPLATE_DEFINE( root_vertex_id = hgl::initial_id; // 0->1; 1->2,3; 3->4,5; 5; 4; 2 - expected_visit_order = {0u, 1u, 3u, 5u, 4u, 2u}; + expected_previsit_order = {0u, 1u, 3u, 5u, 4u, 2u}; + expected_postvisit_order = {5u, 4u, 3u, 2u, 1u, 0u}; expected_pred_map = {0u, 0u, 1u, 1u, 3u, 3u}; expected_in_hyperedges = {hgl::invalid_id, e0, e1, e1, e2, e2}; unreachable_vertices = {}; @@ -65,7 +69,8 @@ TEST_CASE_TEMPLATE_DEFINE( root_vertex_id = hgl::initial_id; // 0->1,3; 3->1,2,4; 4; 2; 1 - expected_visit_order = {0u, 3u, 4u, 2u, 1u}; + expected_previsit_order = {0u, 3u, 4u, 2u, 1u}; + expected_postvisit_order = {4u, 2u, 1u, 3u, 0u}; expected_pred_map = {0u, 3u, 3u, 0u, 3u}; expected_in_hyperedges = {hgl::invalid_id, e1, e1, e2, e3}; unreachable_vertices = {}; @@ -80,7 +85,8 @@ TEST_CASE_TEMPLATE_DEFINE( root_vertex_id = hgl::initial_id; // 0->1,2; 2; 1 - expected_visit_order = {0u, 2u, 1u}; + expected_previsit_order = {0u, 2u, 1u}; + expected_postvisit_order = {2u, 1u, 0u}; expected_pred_map = {0u, 0u, 0u, hgl::invalid_id, hgl::invalid_id}; expected_in_hyperedges = {hgl::invalid_id, e0, e0, hgl::invalid_id, hgl::invalid_id}; unreachable_vertices = {3u, 4u}; @@ -95,7 +101,8 @@ TEST_CASE_TEMPLATE_DEFINE( root_vertex_id = hgl::algorithm::no_root; // 0->1,2; 2; 1; 3->4; 4 - expected_visit_order = {0u, 2u, 1u, 3u, 4u}; + expected_previsit_order = {0u, 2u, 1u, 3u, 4u}; + expected_postvisit_order = {2u, 1u, 0u, 4u, 3u}; expected_pred_map = {0u, 0u, 0u, 3u, 3u}; expected_in_hyperedges = {hgl::invalid_id, e0, e0, hgl::invalid_id, e1}; unreachable_vertices = {}; @@ -103,7 +110,8 @@ TEST_CASE_TEMPLATE_DEFINE( CAPTURE(hypergraph); CAPTURE(root_vertex_id); - CAPTURE(expected_visit_order); + CAPTURE(expected_previsit_order); + CAPTURE(expected_postvisit_order); CAPTURE(expected_pred_map); CAPTURE(expected_in_hyperedges); CAPTURE(unreachable_vertices); @@ -126,8 +134,8 @@ TEST_CASE_TEMPLATE_DEFINE( [&](const auto& node) { postvisit_order.push_back(node.vertex_id); } ); - CHECK_EQ(previsit_order, expected_visit_order); - CHECK_EQ(postvisit_order, expected_visit_order); + CHECK_EQ(previsit_order, expected_previsit_order); + CHECK_EQ(postvisit_order, expected_postvisit_order); CHECK_EQ(noret_pred_map, expected_pred_map); CHECK_EQ(noret_in_hyperedges, expected_in_hyperedges); @@ -137,18 +145,19 @@ TEST_CASE_TEMPLATE_DEFINE( hgl::algorithm::depth_first_search(hypergraph, root_vertex_id); const auto ret_pred_map = - search_tree | std::views::transform(&node_type::pred_id) | std::ranges::to(); + search_tree.nodes | std::views::transform(&tree_node_type::pred_id) + | std::ranges::to(); const auto ret_in_hyperedges = - search_tree | std::views::transform(&node_type::hyperedge_id) + search_tree.nodes | std::views::transform(&tree_node_type::hyperedge_id) | std::ranges::to(); CHECK_EQ(ret_pred_map, expected_pred_map); CHECK_EQ(ret_in_hyperedges, expected_in_hyperedges); - CHECK(std::ranges::all_of(expected_visit_order, [&search_tree](const auto v_id) { - return hgl::algorithm::is_reachable(search_tree, v_id); + CHECK(std::ranges::all_of(expected_previsit_order, [&search_tree](const auto v_id) { + return search_tree.is_reachable(v_id); })); CHECK(std::ranges::all_of(unreachable_vertices, [&search_tree](const auto v_id) { - return not hgl::algorithm::is_reachable(search_tree, v_id); + return not search_tree.is_reachable(v_id); })); } @@ -194,12 +203,14 @@ TEST_CASE_TEMPLATE_DEFINE( ) { using hypergraph_type = hgl::hypergraph; using id_type = typename hypergraph_type::id_type; - using node_type = hgl::algorithm::search_node; + using tree_type = hgl::algorithm::search_tree; + using tree_node_type = typename tree_type::node; hypergraph_type hypergraph; id_type root_vertex_id; - std::vector expected_visit_order; + std::vector expected_previsit_order; + std::vector expected_postvisit_order; std::vector expected_pred_map; std::vector expected_in_hyperedges; std::vector unreachable_vertices; @@ -211,7 +222,8 @@ TEST_CASE_TEMPLATE_DEFINE( root_vertex_id = hgl::initial_id; - expected_visit_order = {0u, 3u, 2u, 1u}; + expected_previsit_order = {0u, 3u, 2u, 1u}; + expected_postvisit_order = {3u, 2u, 1u, 0u}; expected_pred_map = {0u, 0u, 0u, 0u}; expected_in_hyperedges = {hgl::invalid_id, e, e, e}; unreachable_vertices = {}; @@ -227,7 +239,8 @@ TEST_CASE_TEMPLATE_DEFINE( root_vertex_id = hgl::initial_id; // 0->1; 1->2,3; 3->4,5; 5; 4; 2 - expected_visit_order = {0u, 1u, 3u, 5u, 4u, 2u}; + expected_previsit_order = {0u, 1u, 3u, 5u, 4u, 2u}; + expected_postvisit_order = {5u, 4u, 3u, 2u, 1u, 0u}; expected_pred_map = {0u, 0u, 1u, 1u, 3u, 3u}; expected_in_hyperedges = {hgl::invalid_id, e0, e1, e1, e2, e2}; unreachable_vertices = {}; @@ -244,7 +257,8 @@ TEST_CASE_TEMPLATE_DEFINE( root_vertex_id = hgl::initial_id; // 0->1,3; 3->4; 4; 1->2; 2 - expected_visit_order = {0u, 3u, 4u, 1u, 2u}; + expected_previsit_order = {0u, 3u, 4u, 1u, 2u}; + expected_postvisit_order = {4u, 3u, 2u, 1u, 0u}; expected_pred_map = {0u, 0u, 1u, 0u, 3u}; expected_in_hyperedges = {hgl::invalid_id, e0, e1, e2, e3}; unreachable_vertices = {}; @@ -259,7 +273,8 @@ TEST_CASE_TEMPLATE_DEFINE( root_vertex_id = hgl::initial_id; // 0->1,2; 2; 1 - expected_visit_order = {0u, 2u, 1u}; + expected_previsit_order = {0u, 2u, 1u}; + expected_postvisit_order = {2u, 1u, 0u}; expected_pred_map = {0u, 0u, 0u, hgl::invalid_id, hgl::invalid_id}; expected_in_hyperedges = {hgl::invalid_id, e0, e0, hgl::invalid_id, hgl::invalid_id}; unreachable_vertices = {3u, 4u}; @@ -274,7 +289,8 @@ TEST_CASE_TEMPLATE_DEFINE( root_vertex_id = hgl::algorithm::no_root; // 0->1,2; 2; 1; 3->4; 4 - expected_visit_order = {0u, 2u, 1u, 3u, 4u}; + expected_previsit_order = {0u, 2u, 1u, 3u, 4u}; + expected_postvisit_order = {2u, 1u, 0u, 4u, 3u}; expected_pred_map = {0u, 0u, 0u, 3u, 3u}; expected_in_hyperedges = {hgl::invalid_id, e0, e0, hgl::invalid_id, e1}; unreachable_vertices = {}; @@ -282,7 +298,8 @@ TEST_CASE_TEMPLATE_DEFINE( CAPTURE(hypergraph); CAPTURE(root_vertex_id); - CAPTURE(expected_visit_order); + CAPTURE(expected_previsit_order); + CAPTURE(expected_postvisit_order); CAPTURE(expected_pred_map); CAPTURE(expected_in_hyperedges); CAPTURE(unreachable_vertices); @@ -305,8 +322,8 @@ TEST_CASE_TEMPLATE_DEFINE( [&](const auto& node) { postvisit_order.push_back(node.vertex_id); } ); - CHECK(std::ranges::equal(previsit_order, expected_visit_order)); - CHECK(std::ranges::equal(postvisit_order, expected_visit_order)); + CHECK(std::ranges::equal(previsit_order, expected_previsit_order)); + CHECK(std::ranges::equal(postvisit_order, expected_postvisit_order)); CHECK(std::ranges::equal(noret_pred_map, expected_pred_map)); CHECK(std::ranges::equal(noret_in_hyperedges, expected_in_hyperedges)); @@ -316,18 +333,19 @@ TEST_CASE_TEMPLATE_DEFINE( hgl::algorithm::depth_first_search(hypergraph, root_vertex_id); const auto ret_pred_map = - search_tree | std::views::transform(&node_type::pred_id) | std::ranges::to(); + search_tree.nodes | std::views::transform(&tree_node_type::pred_id) + | std::ranges::to(); const auto ret_in_hyperedges = - search_tree | std::views::transform(&node_type::hyperedge_id) + search_tree.nodes | std::views::transform(&tree_node_type::hyperedge_id) | std::ranges::to(); CHECK(std::ranges::equal(ret_pred_map, expected_pred_map)); CHECK(std::ranges::equal(ret_in_hyperedges, expected_in_hyperedges)); - CHECK(std::ranges::all_of(expected_visit_order, [&search_tree](const auto v_id) { - return hgl::algorithm::is_reachable(search_tree, v_id); + CHECK(std::ranges::all_of(expected_previsit_order, [&search_tree](const auto v_id) { + return search_tree.is_reachable(v_id); })); CHECK(std::ranges::all_of(unreachable_vertices, [&search_tree](const auto v_id) { - return not hgl::algorithm::is_reachable(search_tree, v_id); + return not search_tree.is_reachable(v_id); })); } diff --git a/tests/source/hgl/test_alg_forward_search.cpp b/tests/source/hgl/test_alg_forward_search.cpp index 1f510d8d..00bf51e4 100644 --- a/tests/source/hgl/test_alg_forward_search.cpp +++ b/tests/source/hgl/test_alg_forward_search.cpp @@ -16,7 +16,8 @@ TEST_CASE_TEMPLATE_DEFINE( ) { using hypergraph_type = hgl::hypergraph; using id_type = typename hypergraph_type::id_type; - using node_type = hgl::algorithm::search_node; + using tree_type = hgl::algorithm::search_tree; + using tree_node_type = typename tree_type::node; hypergraph_type hypergraph; std::vector root_vertices; @@ -132,18 +133,19 @@ TEST_CASE_TEMPLATE_DEFINE( hgl::algorithm::forward_bfs(hypergraph, root_vertices); const auto ret_pred_map = - search_tree | std::views::transform(&node_type::pred_id) | std::ranges::to(); + search_tree.nodes | std::views::transform(&tree_node_type::pred_id) + | std::ranges::to(); const auto ret_in_hyperedges = - search_tree | std::views::transform(&node_type::hyperedge_id) + search_tree.nodes | std::views::transform(&tree_node_type::hyperedge_id) | std::ranges::to(); CHECK_EQ(ret_pred_map, expected_pred_map); CHECK_EQ(ret_in_hyperedges, expected_in_hyperedges); CHECK(std::ranges::all_of(expected_visit_order, [&search_tree](const auto v_id) { - return hgl::algorithm::is_reachable(search_tree, v_id); + return search_tree.is_reachable(v_id); })); CHECK(std::ranges::all_of(unreachable_vertices, [&search_tree](const auto v_id) { - return not hgl::algorithm::is_reachable(search_tree, v_id); + return not search_tree.is_reachable(v_id); })); } @@ -188,12 +190,14 @@ TEST_CASE_TEMPLATE_DEFINE( ) { using hypergraph_type = hgl::hypergraph; using id_type = typename hypergraph_type::id_type; - using node_type = hgl::algorithm::search_node; + using tree_type = hgl::algorithm::search_tree; + using tree_node_type = typename tree_type::node; hypergraph_type hypergraph; std::vector root_vertices; - std::vector expected_visit_order; + std::vector expected_previsit_order; + std::vector expected_postvisit_order; std::vector expected_pred_map; std::vector expected_in_hyperedges; std::vector unreachable_vertices; @@ -220,7 +224,8 @@ TEST_CASE_TEMPLATE_DEFINE( SUBCASE("single root v4 (partial backward traversal)") { root_vertices = {4u}; - expected_visit_order = {4u, 1u}; + expected_previsit_order = {4u, 1u}; + expected_postvisit_order = {1u, 4u}; expected_pred_map.resize(n_vertices, hgl::invalid_id); expected_pred_map[4uz] = 4u; // Root @@ -242,7 +247,8 @@ TEST_CASE_TEMPLATE_DEFINE( // Pop v3 -> e1 unlocked -> pushes v2. // Pop v2 -> e0 unlocked -> pushes v0, v1(visited). // Pop v0 -> no incoming edges. - expected_visit_order = {4u, 1u, 3u, 2u, 0u}; + expected_previsit_order = {4u, 1u, 3u, 2u, 0u}; + expected_postvisit_order = {1u, 4u, 0u, 2u, 3u}; expected_pred_map = { 2u, // v0 reached backward from v2 via e0 @@ -265,7 +271,8 @@ TEST_CASE_TEMPLATE_DEFINE( CAPTURE(hypergraph); CAPTURE(root_vertices); - CAPTURE(expected_visit_order); + CAPTURE(expected_previsit_order); + CAPTURE(expected_postvisit_order); CAPTURE(expected_pred_map); CAPTURE(expected_in_hyperedges); CAPTURE(unreachable_vertices); @@ -288,8 +295,8 @@ TEST_CASE_TEMPLATE_DEFINE( [&](const auto& node) { postvisit_order.push_back(node.vertex_id); } ); - CHECK_EQ(previsit_order, expected_visit_order); - CHECK_EQ(postvisit_order, expected_visit_order); + CHECK_EQ(previsit_order, expected_previsit_order); + CHECK_EQ(postvisit_order, expected_postvisit_order); CHECK_EQ(noret_pred_map, expected_pred_map); CHECK_EQ(noret_in_hyperedges, expected_in_hyperedges); @@ -299,18 +306,19 @@ TEST_CASE_TEMPLATE_DEFINE( hgl::algorithm::forward_dfs(hypergraph, root_vertices); const auto ret_pred_map = - search_tree | std::views::transform(&node_type::pred_id) | std::ranges::to(); + search_tree.nodes | std::views::transform(&tree_node_type::pred_id) + | std::ranges::to(); const auto ret_in_hyperedges = - search_tree | std::views::transform(&node_type::hyperedge_id) + search_tree.nodes | std::views::transform(&tree_node_type::hyperedge_id) | std::ranges::to(); CHECK_EQ(ret_pred_map, expected_pred_map); CHECK_EQ(ret_in_hyperedges, expected_in_hyperedges); - CHECK(std::ranges::all_of(expected_visit_order, [&search_tree](const auto v_id) { - return hgl::algorithm::is_reachable(search_tree, v_id); + CHECK(std::ranges::all_of(expected_previsit_order, [&search_tree](const auto v_id) { + return search_tree.is_reachable(v_id); })); CHECK(std::ranges::all_of(unreachable_vertices, [&search_tree](const auto v_id) { - return not hgl::algorithm::is_reachable(search_tree, v_id); + return not search_tree.is_reachable(v_id); })); }