The Generic Templates
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.
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:
- Expand from the current vertex to its incident hyperedges.
- Expand from those hyperedges to their target vertices.
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.
The Core Engines
The library provides two primary traversal engines for hypergraphs:
bfs(Breadth-First Search): Uses astd::queue. Explores the hypergraph level by level, expanding uniformly outward from the initial range.dfs(Depth-First Search): Uses astd::stack. Dives as deeply as possible along a branch before backtracking.
Traversal Directions & Policies
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 template parameter (which defaults to forward) and resolved by an internal traversal_policy.
API Note
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.
-
forward:- Retrieves hyperedges originating from the current vertex (the forward star /
out_hyperedge_ids). - Retrieves the vertices targeted by those hyperedges (the head nodes /
head_ids).
- Retrieves hyperedges originating from the current vertex (the forward star /
-
backward:- Retrieves hyperedges entering the current vertex (the backward star /
in_hyperedge_ids). - Retrieves the vertices originating those hyperedges (the tail nodes /
tail_ids).
- Retrieves hyperedges entering the current vertex (the backward star /
Undirected Hypergraphs
For undirected hypergraphs, the traversal policy seamlessly falls back to retrieving all incident_hyperedge_ids and incident_vertex_ids regardless of the traversal direction.
The Callback Sequence
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), you dictate the algorithm's exact behavior.
Execution Flowchart
For a single popped curr_node in the bfs or dfs templates, the execution flow looks exactly like this:
-
visit_pred(curr_node)Evaluated immediately after popping the node. If it returnsfalse, the node is skipped entirely, and the loop moves to the next node in the queue or stack. -
pre_visit(curr_node)A state-modification hook executed right before the vertex is officially processed. -
visit(curr_node)The primary callback. If this returnsfalse, the entire search is immediately aborted. -
Hyperedge Expansion (Step 1) The engine queries the
traversal_policyfor the target hyperedges. For eachhe_id: -
traverse_he_pred(he_id, curr_node.vertex_id)Evaluates whether the hyperedge should be traversed. Returns adecision:abort: Kills the entire algorithm.reject: Ignores this hyperedge and moves to the next.accept: Proceeds to evaluate the hyperedge's vertices.
-
Vertex Expansion (Step 2) If the hyperedge was accepted, the engine queries the
traversal_policyfor the target vertices within that hyperedge. For eachtarget_id(skipping the one we just came from): -
enqueue_pred(tgt_node)Evaluates whether the newly constructedtgt_node(containing thetarget_id,curr_node.vertex_id, andhe_id) should be pushed to the active container. Returns adecision:abort: Kills the entire algorithm.reject: Ignores this specific target vertex.accept: Pushes thetgt_nodeto the queue or stack.
-
post_visit(curr_node)Executed after all adjacent hyperedges and their target vertices have been evaluated and processed.
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.
Example: Custom Forward BFS Engine
#include <hgl/algorithm/templates/bfs.hpp>
#include <hgl/algorithm/core.hpp>
#include <vector>
// Assume 'hg' is a populated BF-directed hypergraph and 'start_id' is valid.
std::vector<bool> visited_v(hg.n_vertices(), false); // (1)!
std::vector<bool> visited_he(hg.n_hyperedges(), false);
using search_node = hgl::algorithm::search_node<decltype(hg)>;
std::vector<search_node> init_nodes = {search_node{start_id}}; // (2)!
bool success = hgl::algorithm::bfs<hgl::algorithm::forward>( // (3)!
hg,
init_nodes,
[&](const auto& node) { return not visited_v[node.vertex_id]; }, // (4)!
[&](const auto& node) { // (5)!
visited_v[node.vertex_id] = true;
return true; // continue search
},
[&](auto he_id, auto /*source_id*/) -> hgl::algorithm::decision { // (6)!
if (visited_he[he_id])
return hgl::algorithm::decision::reject;
visited_he[he_id] = true;
return hgl::algorithm::decision::accept;
},
[&](const auto& tgt_node) -> hgl::algorithm::decision { // (7)!
return !visited_v[tgt_node.vertex_id];
}
);
- Initialize state-tracking vectors for both vertices and hyperedges.
- Set up the initial queue range with a root node.
- Explicitly invoke the template with
traversal_direction::forward(the default, but explicitly shown here for clarity). visit_pred: Reject nodes in the queue if they were already visited by an earlier, faster branch.visit: Mark the vertex as visited.traverse_he_pred: Check if the hyperedge was already traversed. If not, mark it traversed andacceptit. Returning adecisiontype here is required by the generic engines.enqueue_pred: Only enqueue adjacent vertices that haven't been visited yet (implicitly casts the boolean condition to adecision::accept/decision::reject).