The Generic Templates
The generic templates are the workhorse engines of the CPP-GL algorithm module. Rather than implementing distinct algorithms, they implement the strict structural iteration required by specific data structures.
All engines operate on a similar fundamental principle: pop a node from a frontier container, evaluate it, process its outgoing edges, and push valid targets back into the frontier.
The Core Engines
The library provides four primary traversal engines.
bfs(Breadth-First Search): Uses astd::queue. Explores the graph 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.r_dfs(Recursive DFS): Uses the C++ call stack. Instead of an initial range, it is initiated with a specific starting vertex ID. It operates identically todfsbut requires external logic to manage abort signals, as returning from the recursion only unwinds one level.pfs(Priority-First Search): Uses astd::priority_queue. Requires a custom comparator (PQCmp) to mathematically order the frontier. This is the underlying engine for algorithms like Dijkstra's shortest paths algorithm.
The Callback Sequence
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), you dictate the algorithm's behavior.
Execution Flowchart
For a single popped node in bfs, dfs, or pfs, the execution flow looks exactly like this:
-
visit_vertex_pred(node)Evaluated immediately after popping the node. If it returnsfalse, the node is skipped entirely, and the loop moves to the next node. (Commonly used for late-rejection of stale elements in Priority Queues). -
pre_visit(vertex_id)A state-modification hook executed right before the vertex is officially marked as "visited". -
visit(vertex_id, pred_id)The primary callback. If this returnsfalse, the entire search is immediately aborted. -
Edge Iteration The engine iterates over every outgoing edge connected to the
vertex_id. For each edge it calls:-
enqueue_node_pred(target_id, edge)Evaluates whether a new search node should be created for the target. Returns a decision:
abort: Kills the entire algorithm.reject: Ignores this edge and moves to the next.accept: Approves the target for enqueueing.
-
make_node(target_id, vertex_id, edge)(PFS Only)If the target was accepted, this hook allows you to construct a custom object to push into the search frontier.
-
-
post_visit(vertex_id)Executed after all adjacent edges have been evaluated and processed.
Custom Node Injection (PFS)
While BFS and DFS templates strictly operate on the lightweight gl::algorithm::search_node
For instance, in Dijkstra's algorithm, the priority queue must sort nodes based on their accumulated distance from the starting point. You cannot sort based purely on the vertex ID.
pfs solves this by automatically inferring the NodeType from the initial queue range container. If your NodeType requires more than just (target_id, pred_id) to construct, you must provide MakeNodeCallback which is a (vertex_id, pred_id, edge) -> NodeType callback.
Example: PFS Stateful Nodes
struct path_node { // (1)!
gl::default_id_type vertex_id;
gl::default_id_type pred_id;
int accumulated_distance;
};
std::vector<int> distance_map( // (2)!
graph.n_vertices(), std::numeric_limits<int>::max()
);
distance_map[start_id] = 0;
auto cmp = [](const path_node& a, const path_node& b) { // (3)!
return a.accumulated_distance > b.accumulated_distance;
};
std::vector<path_node> init_nodes = { // (4)!
path_node{start_id, start_id, 0}
};
gl::algorithm::pfs( // (5)!
graph,
cmp,
init_nodes,
gl::algorithm::empty_callback{}, // (6)!
gl::algorithm::empty_callback{},
[&](auto target_id, const auto& edge) { // (7)!
return distance_map[target_id] > distance_map[edge.source()] + edge.properties().weight;
},
[&](auto target_id, auto source_id, const auto& edge) { // (8)!
int new_dist = distance_map[source_id] + edge.properties().weight;
distance_map[target_id] = new_dist; // (9)!
return path_node{target_id, source_id, new_dist};
}
);
- Define a custom stateful node tracking the distance accumulated so far.
- Initialize a global distance map with "infinity", setting the start vertex distance to 0.
- Define the priority comparator for a distance-based Min-Heap.
- Setup the initial range containing the root node.
- Run the Priority-First Search engine.
- Define an empty vertex visit predicate and vertex visit callback.
- Define the node enqueue predicate to only enqueue nodes that could yield paths shorter than those already discovered.
- Define the callback which constructs a stateful node for the algorithm queue.
- Update the global distance map to reflect the newly discovered shorter path.
Algorithm Desing
The example above is very similar, though not the same, to how the Dijkstra's algorithm implementation is designed within the library.