|
Provide a one-paragraph overview of the algorithm:
- What problem does it solve?
- What is the primary use case?
- When should it be used vs alternatives?
Example:
Dijkstra's algorithm finds the shortest paths from a source vertex to all other vertices in a weighted graph with non-negative edge weights. It is optimal for dense graphs and provides both distance and predecessor information for path reconstruction.
| Case | Complexity | Notes |
|---|---|---|
| Best case | O(...) | When does this occur? |
| Average case | O(...) | Typical performance |
| Worst case | O(...) | Under what conditions? |
For graph algorithms, specify complexity in terms of:
V= number of verticesE= number of edges- Other relevant parameters (e.g., maximum edge weight)
Implementation-Specific Notes:
- Binary heap: O((V + E) log V)
- Fibonacci heap: O(E + V log V)
- Dense graphs (E ≈ V²): O(V²) with adjacency matrix
| Component | Space | Purpose |
|---|---|---|
| Distance array | O(V) | Store shortest distances |
| Predecessor array | O(V) | Store path information |
| Priority queue | O(V) | Pending vertices |
| Visited flags | O(V) | Track processed vertices |
| Total | O(V) | Excluding graph storage |
Notes:
- Specify whether space is auxiliary (temporary) or part of output
- Include recursion stack depth if applicable
- Mention if algorithm can operate in-place
Clearly state which graph types and configurations are supported:
- ✅ Directed graphs
- ✅ Undirected graphs
⚠️ Mixed (with caveats)
- ✅ Weighted edges (non-negative)
- ❌ Negative edge weights
- ✅ Uniform weights (can be optimized to BFS)
⚠️ Multi-edges: Uses minimum weight edge- ✅ Self-loops: Allowed (ignored in shortest path)
- ✅ Connected graphs
- ✅ Disconnected graphs (finds paths within components)
- ❌ Must be acyclic (DAG)
- ✅ May contain cycles
- Requires:
adjacency_listconcept - Requires:
forward_range<vertex_range_t<G>> - Requires:
integral<vertex_id_t<G>> - Works with: All
dynamic_graphcontainer combinations - Works with:
compressed_graph(when implemented) - Limitations: None known
template <adjacency_list G,
random_access_range Distance,
random_access_range Predecessor,
class WF = function<range_value_t<Distance>(edge_t<G>)>>
requires /* ... constraints ... */
void algorithm_name(
G&& g, // graph
const vertex_id_t<G>& source, // starting vertex
Distance& distance, // out: distances
Predecessor& predecessor, // out: predecessors
WF&& weight) // edge weight function- Requires:
adjacency_list<G> - Requires:
forward_range<vertex_range_t<G>> - Requires:
integral<vertex_id_t<G>> - Description: The graph type to operate on
- Requires:
random_access_range<Distance> - Requires:
is_arithmetic_v<range_value_t<Distance>> - Description: Container for storing vertex distances
- Typical:
std::vector<int>orstd::vector<double>
- Requires:
random_access_range<Predecessor> - Requires:
convertible_to<vertex_id_t<G>, range_value_t<Predecessor>> - Description: Container for storing predecessor information
- Typical:
std::vector<vertex_id_t<G>> - Special: Can use
null_predecessorsif path reconstruction not needed
- Requires:
edge_weight_function<G, WF> - Description: Callable that returns the weight of an edge
- Default: Returns 1 for all edges (unweighted graph)
- Signature:
range_value_t<Distance> operator()(const edge_t<G>&)
- Type:
G&&(forwarding reference) - Precondition:
num_vertices(g) > 0ifsourceis provided - Description: The graph to process
- Type:
vertex_id_t<G> - Precondition:
source < num_vertices(g)(for vector-based containers) - Precondition:
contains_vertex(g, source)(for map-based containers) - Description: Starting vertex for the algorithm
- Type:
Distance& - Precondition:
distance.size() >= num_vertices(g) - Postcondition:
distance[v]contains shortest distance from source to v - Description: Output container for distances
- Type:
Predecessor& - Precondition:
predecessor.size() >= num_vertices(g)(if not null) - Postcondition:
predecessor[v]contains predecessor of v in shortest path tree - Description: Output container for path reconstruction
- Special: Can pass
null_predecessorsto skip path tracking
- Type:
WF&&(forwarding reference) - Requires:
weight(uv)returns arithmetic type for any edgeuv - Description: Function to extract edge weight
- Default:
[](const edge_t<G>& uv) { return 1; }
Type: void (results stored in output parameters)
Effects:
- Modifies
distancerange: Setsdistance[v]for all vertices v - Modifies
predecessorrange: Setspredecessor[v]for all reachable vertices - Does not modify the graph
g
requires adjacency_list<G>
requires forward_range<vertex_range_t<G>>
requires integral<vertex_id_t<G>>
requires random_access_range<Distance>
requires is_arithmetic_v<range_value_t<Distance>>
requires convertible_to<vertex_id_t<G>, range_value_t<Predecessor>>
requires edge_weight_function<G, WF>These constraints are enforced via C++20 concepts and will produce a compilation error if not satisfied.
sourcemust be a valid vertex ID in the graphdistance.size() >= num_vertices(g)predecessor.size() >= num_vertices(g)(if not null_predecessors)- For weighted graphs: all edge weights must be non-negative
- Weight function must not throw exceptions
- Weight function must not modify graph state
Validation:
assert(source < num_vertices(g)); // vector-based containers
assert(contains_vertex(g, source)); // map-based containers
assert(distance.size() >= num_vertices(g));distance[source] == 0- For all vertices
vreachable fromsource:distance[v]contains the length of the shortest path fromsourcetovpredecessor[v]contains the predecessor ofvin the shortest path tree
- For all vertices
vunreachable fromsource:distance[v] == std::numeric_limits<weight_type>::max()predecessor[v]is unmodified (implementation-defined)
Guarantee: Basic exception safety
Throws:
- May throw
std::bad_allocif internal containers cannot allocate memory - May propagate exceptions from container operations (unlikely with standard containers)
- Assumes weight function is
noexcept(not enforced, but recommended)
State after exception:
- Graph
gremains unchanged distanceandpredecessormay be partially modified (indeterminate state)- Client must re-initialize output containers before retry
Recommendation: Use noexcept weight functions when possible for strong guarantee.
Include any additional information, clarifications, or important notes that don't fit in other sections:
- Implementation quirks or special behaviors
- Performance characteristics beyond complexity analysis
- Compatibility notes with other algorithms or graph types
- Historical context or design rationale
- Known limitations or caveats
#include <graph/graph.hpp>
#include <graph/algorithm/dijkstra_shortest_paths.hpp>
#include <vector>
#include <iostream>
using namespace graph;
int main() {
// Create a simple graph: 0 -> 1 (weight 4), 0 -> 2 (weight 2), 1 -> 2 (weight 1)
using Graph = container::dynamic_graph<
int, void, void, uint32_t, false,
container::vov_graph_traits<int, void, void, uint32_t, false>>;
Graph g({{0, 1, 4}, {0, 2, 2}, {1, 2, 1}, {1, 3, 5}, {2, 3, 8}});
std::vector<int> distance(num_vertices(g));
std::vector<uint32_t> predecessor(num_vertices(g));
dijkstra(g, 0, distance, predecessor,
[](const auto& uv) { return edge_value(uv); });
// Print results
for (uint32_t v = 0; v < num_vertices(g); ++v) {
std::cout << "Distance to " << v << ": " << distance[v] << "\n";
}
return 0;
}Output:
Distance to 0: 0
Distance to 1: 4
Distance to 2: 2
Distance to 3: 9
std::vector<uint32_t> reconstruct_path(
uint32_t source, uint32_t target,
const std::vector<uint32_t>& predecessor) {
std::vector<uint32_t> path;
uint32_t current = target;
while (current != source) {
path.push_back(current);
current = predecessor[current];
}
path.push_back(source);
std::ranges::reverse(path);
return path;
}// For unweighted graphs, Dijkstra becomes equivalent to BFS
Graph g({{0, 1}, {0, 2}, {1, 2}, {1, 3}});
std::vector<int> distance(num_vertices(g));
std::vector<uint32_t> predecessor(num_vertices(g));
// Default weight function returns 1 for all edges
dijkstra(g, 0, distance, predecessor);-
Initialization:
- Set
distance[source] = 0 - Set
distance[v] = ∞for all other vertices - Add source to priority queue
- Set
-
Main Loop:
- Extract minimum distance vertex
ufrom queue - For each edge
(u, v)with weightw:- If
distance[u] + w < distance[v]:- Update
distance[v] = distance[u] + w - Update
predecessor[v] = u - Add
vto priority queue
- Update
- If
- Extract minimum distance vertex
-
Termination:
- Queue becomes empty (all reachable vertices processed)
- Priority Queue: Uses
std::priority_queuewith max-heap (inverted comparison) - Visited Tracking: Implicit via distance comparison (no explicit visited set)
- Re-insertion: Allows vertices to be inserted multiple times (lazy deletion)
-
Why allow re-insertion instead of decrease-key?
std::priority_queuedoesn't support decrease-key- Re-insertion with lazy deletion is simpler and nearly as efficient
- Alternative: Use external heap library (Boost, etc.) for decrease-key
-
Why require random_access_range for distance/predecessor?
- Ensures O(1) lookup by vertex ID
- Essential for algorithm efficiency
- Could be relaxed for map-based containers (future enhancement)
-
Why separate weight function instead of requiring edge_value?
- Flexibility: Can compute weights dynamically
- Supports graphs without stored edge values
- Allows weight transformations (e.g., inverted, scaled)
- For dense graphs: Consider adjacency matrix + array-based heap
- For sparse graphs: Current implementation is near-optimal
- For uniform weights: Use BFS instead (O(V + E) without heap)
- For DAG: Use topological sort + relaxation (O(V + E))
- Dijkstra, E. W. (1959). "A note on two problems in connexion with graphs". Numerische Mathematik, 1(1), 269-271.
- Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2009). Introduction to Algorithms (3rd ed.). MIT Press. Section 24.3.
- Bellman-Ford: Handles negative weights, O(VE) time
- A Search:* Heuristic-guided variant for single target
- BFS: Unweighted shortest path, O(V + E) time
- Floyd-Warshall: All-pairs shortest paths, O(V³) time
-
Correctness Tests:
- Known result validation (e.g., CLRS example graph)
- Path reconstruction verification
- Multiple source vertices
- Disconnected graph components
-
Edge Cases:
- Empty graph
- Single vertex
- Single edge
- Self-loops
- No path to target
- All vertices reachable
- All vertices unreachable
-
Container Tests:
- Test with all major container types (vov, dov, vol, dol, etc.)
- Sparse vertex IDs (map-based containers)
- Different edge value types (int, double, float)
-
Performance Tests:
- Verify O((V + E) log V) complexity
- Compare with naive O(V²) implementation
- Benchmark on various graph sizes
- Memory usage validation
tests/algorithms/test_dijkstra.cpp
benchmark/algorithms/benchmark_shortest_path.cpp
- Add bidirectional Dijkstra for single source-target queries
- Support custom comparator for priority queue
- Add early termination when target is reached
- Optimize for graphs with small integer weights (dial's algorithm)
- Add parallel/concurrent version for large graphs
- Support for external memory graphs
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2026-02-03 | - | Initial implementation |