Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions benchmarks/suites/hg_b_bfs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ bool incidence_backward_bfs(
const gl::size_type original_n_vertices
) {
using id_type = gl::id_t<IncidenceGraph>;
using node_type = gl::algorithm::search_node<gl::val_t<IncidenceGraph>>;

std::vector<bool> visited_v(original_n_vertices, false);
auto tail_unvisited =
Expand All @@ -103,30 +104,29 @@ bool incidence_backward_bfs(
})
| std::ranges::to<std::vector>();

auto visit_vertex_pred = [&](id_type v) {
if (v < original_n_vertices)
return not visited_v[gl::to_idx(v)];
auto visit_pred = [&](node_type node) {
if (node.vertex_id < original_n_vertices)
return not visited_v[node.vertex_id];
return true;
};

auto visit = [&](id_type v, id_type /*p*/) {
if (v < original_n_vertices)
visited_v[gl::to_idx(v)] = true;
auto visit = [&](node_type node) {
if (node.vertex_id < original_n_vertices)
visited_v[node.vertex_id] = true;
return true;
};

auto enqueue_node_pred =
[&](id_type target_id, const auto& /*edge*/) -> gl::algorithm::decision {
if (target_id >= original_n_vertices) {
const auto he_idx = target_id - original_n_vertices;
return --tail_unvisited[gl::to_idx(he_idx)] == 0uz;
auto enqueue_pred = [&](node_type tgt_node, const auto& /*edge*/) -> gl::algorithm::decision {
if (tgt_node.vertex_id >= original_n_vertices) {
const auto he_idx = tgt_node.vertex_id - original_n_vertices;
return --tail_unvisited[he_idx] == 0uz;
}
else {
return not visited_v[gl::to_idx(target_id)];
return not visited_v[tgt_node.vertex_id];
}
};

return gl::algorithm::bfs(ig, root_nodes, visit_vertex_pred, visit, enqueue_node_pred);
return gl::algorithm::bfs(ig, root_nodes, visit_pred, visit, enqueue_pred);
}

// --- GL Incidence Graph Backward BFS Benchmark ---
Expand Down
2 changes: 1 addition & 1 deletion docs/gl/algorithms/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ By default, the active container of a search engine stores [**gl::algorithm::sea
1. `vertex_id`: The vertex currently being visited.
2. `pred_id`: The vertex from which this current vertex was reached (its parent in the traversal tree).

If a vertex is the starting point of a search, its `pred_id` is set to itself, making it a "root" node. The library provides the [**gl::algorithm::no_root**](../../cpp-gl/group__GL-Algorithm.md#variable-no_root) tag to explicitly identify states where a node lacks a predecessor.
If a vertex is the starting point of a search, its `pred_id` is set to itself, making it a "root" node.

### The Result Discriminator

Expand Down
51 changes: 33 additions & 18 deletions docs/gl/algorithms/templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The library provides four primary traversal engines.

- [**`bfs` (Breadth-First Search)**](../../cpp-gl/group__GL-Algorithm.md#function-bfs): Uses a `std::queue`. Explores the graph level by level, expanding uniformly outward from the initial range.
- [**`dfs` (Depth-First Search)**](../../cpp-gl/group__GL-Algorithm.md#function-dfs): Uses a `std::stack`. Dives as deeply as possible along a branch before backtracking.
- [**`r_dfs` (Recursive DFS)**](../../cpp-gl/group__GL-Algorithm.md#function-r_dfs): Uses the C++ call stack. Instead of an initial range, it is initiated with a specific starting vertex ID. It operates identically to `dfs` but requires external logic to manage abort signals, as returning from the recursion only unwinds one level.
- [**`r_dfs` (Recursive DFS)**](../../cpp-gl/group__GL-Algorithm.md#function-r_dfs): Uses the C++ call stack. Instead of an initial range, it is initiated with a specific starting search node. It operates analogously to `dfs` but requires external logic to manage abort signals, as returning from the recursion only unwinds one level.
- [**`pfs` (Priority-First Search)**](../../cpp-gl/group__GL-Algorithm.md#function-pfs): Uses a `std::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
Expand All @@ -21,31 +21,31 @@ The true power of the generic templates lies in their callback/predicate hooks.

For a single popped node in standard traversal templates, the execution flow looks exactly like this:

1. **`visit_vertex_pred(node)`**
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. *(Commonly used for late-rejection of stale elements in Priority Queues or filtering already-visited vertices).*

2. **`pre_visit(vertex_id)`**
2. **`pre_visit(curr_node)`**
A state-modification hook executed right before the vertex is officially marked as "visited".

3. **`visit(vertex_id, pred_id)`**
3. **`visit(curr_node)`**
The primary callback. If this returns `false`, the entire search is immediately aborted.

4. **Edge Iteration**
The engine iterates over every outgoing edge connected to the `vertex_id`. For each edge it calls:
The engine iterates over every outgoing edge connected to `curr_node.vertex_id`. For each edge it constructs a basic target search node (`tgt_node`) and calls:

- **`enqueue_node_pred(target_id, edge)`**
- **`enqueue_pred(tgt_node, edge)`**

Evaluates whether a new search node should be created for the target. Returns a [**decision**](../../cpp-gl/structgl_1_1algorithm_1_1decision.md):
Evaluates whether the target 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 edge and moves to the next.
- `accept`: Approves the target for enqueueing.

- **`make_node(target_id, vertex_id, edge)`** *(PFS Only)*
- **`make_node(target_id, source_id, edge)`** *(PFS Only)*

If the target was accepted, this hook allows you to construct a custom object to push into the search frontier.
If the target was accepted, this hook allows you to construct a custom, stateful `NodeType` to push into the priority search frontier.

5. **`post_visit(vertex_id)`** *(BFS/PFS only)*
5. **`post_visit(curr_node)`** *(BFS/PFS only)*
Executed after all adjacent edges have been evaluated and processed.

### True Post-Order Execution (Iterative `dfs`)
Expand All @@ -59,30 +59,30 @@ The CPP-GL `dfs` template solves this using a zero-cost abstraction:

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.
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 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`, safely reconstructs the stateless base node, 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.
1. **Entry:** `visit_pred`, `pre_visit`, and `visit` are executed immediately upon entering the function using the current logical node.
2. **Recurse:** The engine iterates over outgoing edges. If `enqueue_pred` accepts a target node, the engine immediately constructs it and 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).
> The 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.

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.
`pfs` solves this by automatically inferring the `NodeType` from the initial queue range container. If your `NodeType` requires more than just topological IDs to construct, you must provide `MakeNodeCallback` which is a `(target_id, source_id, edge) -> NodeType` factory callback.

### Example: PFS Stateful Nodes

Expand Down Expand Up @@ -112,12 +112,27 @@ gl::algorithm::pfs( // (5)!
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;
[&](const auto& tgt_node, const auto& edge) { // (7)!
return distance_map[tgt_node.vertex_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};
}
);
```

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 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.
6 changes: 3 additions & 3 deletions docs/gl/algorithms/traversal.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ auto pred_map = gl::algorithm::breadth_first_search(graph, start_id); // (1)!
gl::algorithm::breadth_first_search<gl::algorithm::noret>( // (2)!
graph,
gl::algorithm::no_root, // (3)!
[](auto v) { std::cout << "Discovered: " << v << '\n'; } // (4)!
[](auto node) { std::cout << "Discovered: " << node.vertex_id << '\n'; } // (4)!
);
```

Expand Down Expand Up @@ -75,8 +75,8 @@ The [**recursive_depth_first_search**](../../cpp-gl/group__GL-Algorithm.md#funct
gl::algorithm::recursive_depth_first_search<gl::algorithm::noret>(
graph,
start_id,
[](auto v) { std::cout << "Entering subtree of: " << v << '\n'; }, // (1)!
[](auto v) { std::cout << "Exiting subtree of: " << v << '\n'; } // (2)!
[](auto node) { std::cout << "Entering subtree of: " << node.vertex_id << '\n'; }, // (1)!
[](auto node) { std::cout << "Exiting subtree of: " << node.vertex_id << '\n'; } // (2)!
);
```

Expand Down
13 changes: 13 additions & 0 deletions include/gl/algorithm/core.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,19 @@ struct search_node {
[[nodiscard]] gl_attr_force_inline bool is_root() const noexcept {
return this->vertex_id != invalid_id and this->vertex_id == this->pred_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 <std::semiregular OtherExt>
requires(not std::same_as<Extension, OtherExt>)
[[nodiscard]] gl_attr_force_inline explicit operator search_node<G, OtherExt>() const noexcept {
return search_node<G, OtherExt>{this->vertex_id, this->pred_id};
}
};

/// @ingroup GL-Algorithm
Expand Down
19 changes: 9 additions & 10 deletions include/gl/algorithm/pathfinding/dijkstra.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ template <traits::c_graph G>
/// | Parameter | Description | Constraint |
/// | :-------- | :--- | :--- |
/// | G | The type of the graph being traversed. Must define a valid distance/weight property. | Must satisfy the [**c_graph**](gl_concepts.md#gl-traits-c-graph) concept. |
/// | PreVisitCallback | Type of the callable executed immediately before a vertex is officially visited. | Must be one of:<br/>- `(id_type) -> void` callable<br/>- An @ref gl::algorithm::empty_callback "empty_callback" |
/// | PostVisitCallback | Type of the callable executed after all adjacent edges of a vertex are evaluated. | Must be one of:<br/>- `(id_type) -> void` callable<br/>- An @ref gl::algorithm::empty_callback "empty_callback" |
/// | PreVisitCallback | Type of the callable executed immediately before `VisitCallback`. | Must be one of:<br/>- A `(search_node<val_t<G>>) -> void` callable<br/>- An @ref gl::algorithm::empty_callback "empty_callback" |
/// | PostVisitCallback | Type of the callable executed after all adjacent edges are evaluated. | Must be one of:<br/>- A `(search_node<val_t<G>>) -> void` callable<br/>- An @ref gl::algorithm::empty_callback "empty_callback" |
///
/// @param graph The graph to evaluate.
/// @param source_id The starting vertex ID for the shortest path calculation.
Expand All @@ -107,8 +107,8 @@ template <traits::c_graph G>
/// @hideparams
template <
traits::c_graph G,
traits::c_optional_callback<void, id_t<G>> PreVisitCallback = empty_callback,
traits::c_optional_callback<void, id_t<G>> PostVisitCallback = empty_callback>
traits::c_optional_callback<void, search_node<val_t<G>>> PreVisitCallback = empty_callback,
traits::c_optional_callback<void, search_node<val_t<G>>> PostVisitCallback = empty_callback>
[[nodiscard]] paths_descriptor_type<G> dijkstra_shortest_paths(
G&& graph, id_t<G> source_id, PreVisitCallback pre_visit = {}, PostVisitCallback post_visit = {}
) {
Expand Down Expand Up @@ -142,23 +142,22 @@ template <
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)
[&paths, &negative_edge](search_node<val_t<G>> node, const edge_type& in_edge)
-> decision { // enqueue predicate
const auto pred_id = in_edge.other(vertex_id);
const auto edge_weight = get_weight<G>(in_edge);

if (edge_weight < 0) {
negative_edge.emplace(in_edge);
return decision::abort;
}

const auto new_distance = paths.distances[to_idx(pred_id)] + edge_weight;
auto& v_pred = paths.predecessors[to_idx(vertex_id)];
auto& v_dist = paths.distances[to_idx(vertex_id)];
const auto new_distance = paths.distances[node.pred_id] + edge_weight;
auto& v_pred = paths.predecessors[node.vertex_id];
auto& v_dist = paths.distances[node.vertex_id];

if (v_pred == invalid_id or new_distance < v_dist) {
v_dist = new_distance;
v_pred = pred_id;
v_pred = node.pred_id;
return true;
}

Expand Down
Loading
Loading