Skip to content

Commit c6bcaa2

Browse files
Added Dijkstra's algorithm to UltraGraph (#263)
* Updated docstring Signed-off-by: Marvin Hansen <[email protected]> * Fixed a memory leak in the ultragraph benchmark. Signed-off-by: Marvin Hansen <[email protected]> * Fixed typo in Rustdoc. Signed-off-by: Marvin Hansen <[email protected]> * Added Dijkstra's algorithm to UltraGraph. Signed-off-by: Marvin Hansen <[email protected]> * Removed problematic check in Dijkstra's algorithm Signed-off-by: Marvin Hansen <[email protected]> * Improved test coverage for Dijkstra's algorithm. Improved docstring. Signed-off-by: Marvin Hansen <[email protected]> --------- Signed-off-by: Marvin Hansen <[email protected]>
1 parent c3a58c4 commit c6bcaa2

File tree

4 files changed

+170
-0
lines changed

4 files changed

+170
-0
lines changed

ultragraph/src/traits/graph_algo.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,27 @@ pub trait GraphAlgorithms<N, W>: GraphView<N, W> {
5050
start_index: usize,
5151
stop_index: usize,
5252
) -> Result<Option<Vec<usize>>, GraphError>;
53+
54+
/// Finds the shortest path in a weighted graph using Dijkstra's algorithm.
55+
///
56+
/// The edge weight type `W` must support addition, comparison, and have a zero value.
57+
///
58+
/// # Returns
59+
/// A tuple containing the sequence of node indices in the path and the total cost of that path.
60+
/// Returns `None` if no path exists.
61+
fn shortest_weighted_path(
62+
&self,
63+
start_index: usize,
64+
stop_index: usize,
65+
) -> Result<Option<(Vec<usize>, W)>, GraphError>
66+
where
67+
W: Copy + Ord + Default + std::ops::Add<Output = W>;
68+
69+
// Implement later.
70+
// /// Finds all Strongly Connected Components in the graph using Tarjan's algorithm.
71+
// ///
72+
// /// # Returns
73+
// /// A vector of vectors, where each inner vector is a list of node indices
74+
// /// belonging to a single SCC.
75+
// fn strongly_connected_components(&self) -> Result<Vec<Vec<usize>>, GraphError>;
5376
}

ultragraph/src/types/storage/graph_csm/graph_csm_algo.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,4 +270,73 @@ where
270270
path.reverse();
271271
Ok(Some(path))
272272
}
273+
274+
/// Finds the shortest path between two nodes in a weighted graph using Dijkstra's algorithm.
275+
fn shortest_weighted_path(
276+
&self,
277+
start_index: usize,
278+
stop_index: usize,
279+
) -> Result<Option<(Vec<usize>, W)>, GraphError>
280+
where
281+
W: Copy + Ord + Default + std::ops::Add<Output = W>,
282+
{
283+
use std::cmp::Reverse;
284+
use std::collections::BinaryHeap;
285+
286+
if !self.contains_node(start_index) || !self.contains_node(stop_index) {
287+
return Ok(None);
288+
}
289+
290+
if start_index == stop_index {
291+
return Ok(Some((vec![start_index], W::default())));
292+
}
293+
294+
let num_nodes = self.number_nodes();
295+
let mut distances: Vec<Option<W>> = vec![None; num_nodes];
296+
let mut predecessors: Vec<Option<usize>> = vec![None; num_nodes];
297+
let mut pq: BinaryHeap<(Reverse<W>, usize)> = BinaryHeap::new();
298+
299+
distances[start_index] = Some(W::default());
300+
pq.push((Reverse(W::default()), start_index));
301+
302+
while let Some((Reverse(dist), u)) = pq.pop() {
303+
if u == stop_index {
304+
let mut path = Vec::new();
305+
let mut current = Some(stop_index);
306+
while let Some(node) = current {
307+
path.push(node);
308+
if node == start_index {
309+
break;
310+
}
311+
current = predecessors[node];
312+
}
313+
path.reverse();
314+
315+
return Ok(Some((path, dist)));
316+
}
317+
318+
if let Some(known_dist) = distances[u] {
319+
if dist > known_dist {
320+
continue;
321+
}
322+
}
323+
324+
let start_offset = self.forward_edges.offsets[u];
325+
let end_offset = self.forward_edges.offsets[u + 1];
326+
327+
for i in start_offset..end_offset {
328+
let v = self.forward_edges.targets[i];
329+
let weight = self.forward_edges.weights[i];
330+
let new_dist = dist + weight;
331+
332+
if distances[v].map_or(true, |d| new_dist < d) {
333+
distances[v] = Some(new_dist);
334+
predecessors[v] = Some(u);
335+
pq.push((Reverse(new_dist), v));
336+
}
337+
}
338+
}
339+
340+
Ok(None)
341+
}
273342
}

ultragraph/src/types/ultra_graph/graph_algo.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,32 @@ where
111111
GraphState::Dynamic(_) => Err(GraphError::GraphNotFrozen),
112112
}
113113
}
114+
115+
/// Finds the shortest path (as a sequence of node indices) between two nodes, considering edge weights.
116+
///
117+
/// # Preconditions
118+
/// This high-performance operation is only available when the graph is in a `Static` (frozen) state.
119+
///
120+
/// # Type Parameters
121+
/// - `W`: The weight type, which must implement `Copy`, `Ord`, `Default`, and `std::ops::Add`.
122+
///
123+
/// # Errors
124+
///
125+
/// - Returns `GraphError::GraphNotFrozen` if the graph is in a `Dynamic` state.
126+
/// - Returns an error if either node index is invalid.
127+
/// - Returns an error if the graph contains negative cycles (for algorithms like Dijkstra's).
128+
///
129+
fn shortest_weighted_path(
130+
&self,
131+
start_index: usize,
132+
stop_index: usize,
133+
) -> Result<Option<(Vec<usize>, W)>, GraphError>
134+
where
135+
W: Copy + Ord + Default + std::ops::Add<Output = W>,
136+
{
137+
match &self.state {
138+
GraphState::Static(g) => g.shortest_weighted_path(start_index, stop_index),
139+
GraphState::Dynamic(_) => Err(GraphError::GraphNotFrozen),
140+
}
141+
}
114142
}

ultragraph/tests/types/ultra_graph/graph_algo_tests.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,53 @@ fn test_shortest_path_on_static_graph() {
162162
assert_eq!(g.shortest_path(0, 3).unwrap(), Some(vec![0, 1, 2, 3]));
163163
assert_eq!(g.shortest_path(3, 0).unwrap(), None);
164164
}
165+
166+
#[test]
167+
fn test_shortest_weighted_path_on_static_graph() {
168+
let mut g = UltraGraphWeighted::new();
169+
g.add_node(0).unwrap();
170+
g.add_node(1).unwrap();
171+
g.add_node(2).unwrap();
172+
g.add_node(3).unwrap();
173+
g.add_node(4).unwrap();
174+
g.add_edge(0, 1, 1).unwrap();
175+
g.add_edge(0, 2, 4).unwrap();
176+
g.add_edge(1, 2, 2).unwrap();
177+
g.add_edge(1, 3, 5).unwrap();
178+
g.add_edge(2, 3, 1).unwrap();
179+
g.add_edge(3, 4, 1).unwrap();
180+
181+
let result = g.shortest_weighted_path(0, 4);
182+
// triggers GraphNotFrozen error.
183+
assert!(result.is_err());
184+
185+
// Freeze the graph to enable all algos.
186+
g.freeze();
187+
188+
// Path 0 -> 1 -> 2 -> 3 -> 4, weight 1 + 2 + 1 + 1 = 5
189+
let result = g.shortest_weighted_path(0, 4).unwrap().unwrap();
190+
assert_eq!(result.0, vec![0, 1, 2, 3, 4]);
191+
assert_eq!(result.1, 5);
192+
193+
// Path 0 -> 1 -> 2 -> 3, weight 1 + 2 + 1 = 4
194+
let result = g.shortest_weighted_path(0, 3).unwrap().unwrap();
195+
assert_eq!(result.0, vec![0, 1, 2, 3]);
196+
assert_eq!(result.1, 4);
197+
198+
// Path 1 -> 2 -> 3, weight 2 + 1 = 3
199+
let result = g.shortest_weighted_path(1, 3).unwrap().unwrap();
200+
assert_eq!(result.0, vec![1, 2, 3]);
201+
assert_eq!(result.1, 3);
202+
203+
// No path from 4 to 0
204+
assert!(g.shortest_weighted_path(4, 0).unwrap().is_none());
205+
206+
// Invalid nodes
207+
assert!(g.shortest_weighted_path(0, 99).unwrap().is_none());
208+
assert!(g.shortest_weighted_path(99, 0).unwrap().is_none());
209+
210+
// Start and stop are the same
211+
let result = g.shortest_weighted_path(0, 0).unwrap().unwrap();
212+
assert_eq!(result.0, vec![0]);
213+
assert_eq!(result.1, 0);
214+
}

0 commit comments

Comments
 (0)