|
1 | 1 | # The Generic Templates |
| 2 | + |
| 3 | +The generic templates are the workhorse engines of the HGL algorithm module. Rather than implementing distinct algorithms, they implement the strict structural iteration required to safely navigate a hypergraph's topology. |
| 4 | + |
| 5 | +Traversing a hypergraph is fundamentally more complex than traversing a standard graph. Instead of moving directly from vertex to vertex, a hypergraph traversal requires a **two-step expansion**: |
| 6 | + |
| 7 | +1. Expand from the current vertex to its incident hyperedges. |
| 8 | +2. Expand from those hyperedges to their *target* vertices. |
| 9 | + |
| 10 | +All engines operate on this principle: pop a search node from a pending container, evaluate it, execute the two-step expansion across valid hyperedges and vertices, and push the discovered targets back into the container. |
| 11 | + |
| 12 | +## The Core Engines |
| 13 | + |
| 14 | +The library provides two primary traversal engines for hypergraphs: |
| 15 | + |
| 16 | +- [**`bfs` (Breadth-First Search)**](../../cpp-gl/group__HGL-Algorithm.md#function-bfs): Uses a `std::queue`. Explores the hypergraph level by level, expanding uniformly outward from the initial range. |
| 17 | +- [**`dfs` (Depth-First Search)**](../../cpp-gl/group__HGL-Algorithm.md#function-dfs): Uses a `std::stack`. Dives as deeply as possible along a branch before backtracking. |
| 18 | + |
| 19 | +## Traversal Directions & Policies |
| 20 | + |
| 21 | +Because **BF-directed hypergraphs** distinguish between *tail* (source) and *head* (destination) vertices, the generic engines must know how to flow through them. This is controlled via the [**traversal_direction**](../../cpp-gl/group__HGL-Algorithm.md#enum-traversal_direction) template parameter (which defaults to `forward`) and resolved by an internal `traversal_policy`. |
| 22 | + |
| 23 | +> [!NOTE] API Note |
| 24 | +> |
| 25 | +> The library utilizes C++20's `using enum` feature for the `traversal_direction` enum type, allowing you to access these tags directly via `hgl::algorithm::forward` and `hgl::algorithm::backward`. |
| 26 | +
|
| 27 | +- **`forward`**: |
| 28 | + |
| 29 | + - Retrieves hyperedges originating from the current vertex (the **forward star** / `out_hyperedge_ids`). |
| 30 | + - Retrieves the vertices targeted by those hyperedges (the **head nodes** / `head_ids`). |
| 31 | + |
| 32 | +- **`backward`**: |
| 33 | + |
| 34 | + - Retrieves hyperedges entering the current vertex (the **backward star** / `in_hyperedge_ids`). |
| 35 | + - Retrieves the vertices originating those hyperedges (the **tail nodes** / `tail_ids`). |
| 36 | + |
| 37 | +> [!NOTE] Undirected Hypergraphs |
| 38 | +> |
| 39 | +> For undirected hypergraphs, the traversal policy seamlessly falls back to retrieving all `incident_hyperedge_ids` and `incident_vertex_ids` regardless of the traversal direction. |
| 40 | +
|
| 41 | +## The Callback Sequence |
| 42 | + |
| 43 | +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. |
| 44 | + |
| 45 | +### Execution Flowchart |
| 46 | + |
| 47 | +For a single popped `curr_node` in the `bfs` or `dfs` templates, the execution flow looks exactly like this: |
| 48 | + |
| 49 | +1. **`visit_pred(curr_node)`** |
| 50 | + 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. |
| 51 | + |
| 52 | +2. **`pre_visit(curr_node)`** |
| 53 | + A state-modification hook executed right before the vertex is officially processed. |
| 54 | + |
| 55 | +3. **`visit(curr_node)`** |
| 56 | + The primary callback. If this returns `false`, the entire search is immediately aborted. |
| 57 | + |
| 58 | +4. **Hyperedge Expansion (Step 1)** |
| 59 | + The engine queries the `traversal_policy` for the target hyperedges. For each `he_id`: |
| 60 | + |
| 61 | + - **`traverse_he_pred(he_id, curr_node.vertex_id)`** |
| 62 | + Evaluates whether the hyperedge should be traversed. Returns a `decision`: |
| 63 | + - `abort`: Kills the entire algorithm. |
| 64 | + - `reject`: Ignores this hyperedge and moves to the next. |
| 65 | + - `accept`: Proceeds to evaluate the hyperedge's vertices. |
| 66 | + |
| 67 | +5. **Vertex Expansion (Step 2)** |
| 68 | + 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): |
| 69 | + |
| 70 | + - **`enqueue_pred(tgt_node)`** |
| 71 | + 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`: |
| 72 | + - `abort`: Kills the entire algorithm. |
| 73 | + - `reject`: Ignores this specific target vertex. |
| 74 | + - `accept`: Pushes the `tgt_node` to the queue or stack. |
| 75 | + |
| 76 | +6. **`post_visit(curr_node)`** |
| 77 | + Executed after all adjacent hyperedges and their target vertices have been evaluated and processed. |
| 78 | + |
| 79 | +## Customizing the Traversal |
| 80 | + |
| 81 | +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. |
| 82 | + |
| 83 | +### Example: Custom Forward BFS Engine |
| 84 | + |
| 85 | +```cpp |
| 86 | +#include <hgl/algorithm/templates/bfs.hpp> |
| 87 | +#include <hgl/algorithm/core.hpp> |
| 88 | +#include <vector> |
| 89 | + |
| 90 | +// Assume 'hg' is a populated BF-directed hypergraph and 'start_id' is valid. |
| 91 | +std::vector<bool> visited_v(hg.n_vertices(), false); // (1)! |
| 92 | +std::vector<bool> visited_he(hg.n_hyperedges(), false); |
| 93 | + |
| 94 | +using search_node = hgl::algorithm::search_node<decltype(hg)>; |
| 95 | +std::vector<search_node> init_nodes = {search_node{start_id}}; // (2)! |
| 96 | + |
| 97 | +bool success = hgl::algorithm::bfs<hgl::algorithm::forward>( // (3)! |
| 98 | + hg, |
| 99 | + init_nodes, |
| 100 | + [&](const auto& node) { return not visited_v[node.vertex_id]; }, // (4)! |
| 101 | + [&](const auto& node) { // (5)! |
| 102 | + visited_v[node.vertex_id] = true; |
| 103 | + return true; // continue search |
| 104 | + }, |
| 105 | + [&](auto he_id, auto /*source_id*/) -> hgl::algorithm::decision { // (6)! |
| 106 | + if (visited_he[he_id]) |
| 107 | + return hgl::algorithm::decision::reject; |
| 108 | + |
| 109 | + visited_he[he_id] = true; |
| 110 | + return hgl::algorithm::decision::accept; |
| 111 | + }, |
| 112 | + [&](const auto& tgt_node) -> hgl::algorithm::decision { // (7)! |
| 113 | + return !visited_v[tgt_node.vertex_id]; |
| 114 | + } |
| 115 | +); |
| 116 | +``` |
| 117 | + |
| 118 | +1. Initialize state-tracking vectors for both vertices and hyperedges. |
| 119 | +2. Set up the initial queue range with a root node. |
| 120 | +3. Explicitly invoke the template with `traversal_direction::forward` (the default, but explicitly shown here for clarity). |
| 121 | +4. **`visit_pred`**: Reject nodes in the queue if they were already visited by an earlier, faster branch. |
| 122 | +5. **`visit`**: Mark the vertex as visited. |
| 123 | +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. |
| 124 | +7. **`enqueue_pred`**: Only enqueue adjacent vertices that haven't been visited yet (implicitly casts the boolean condition to a `decision::accept` / `decision::reject`). |
0 commit comments