pub mod binary;
pub mod dial;
pub mod fib;
pub mod fib_unsafe;
mod heap_trait;
pub mod pairing;
pub mod radix;
use std::collections::HashMap;
use heap_trait::PriorityQueue;
pub use binary::dijkstra_binary;
pub use dial::dijkstra_dial;
pub use fib::dijkstra_fibonacci;
pub use fib_unsafe::dijkstra_fibonacci_unsafe;
pub use pairing::dijkstra_pairing;
pub use radix::dijkstra_radix;
pub type DijkstraFn = fn(usize, usize, &[Vec<(usize, u32)>]) -> (Vec<usize>, Option<u32>);
pub struct ParsedGraph<'a> {
pub graph: Vec<Vec<(usize, u32)>>,
pub nodes_reverse: HashMap<usize, &'a str>,
}
pub fn parse_graph<'a>(
lines: &'a [&'a str],
bidirectional: bool,
) -> Result<ParsedGraph<'a>, String> {
if lines.is_empty() {
return Err("No input lines provided".to_string());
}
let num_nodes = lines[0].parse::<u32>().map_err(|_| {
format!(
"Invalid number of nodes: '{}' (expected a positive integer)",
lines[0]
)
})? as usize;
if lines.len() < 1 + num_nodes {
return Err(format!(
"Not enough lines: expected {} node names, but only {} lines provided",
num_nodes,
lines.len().saturating_sub(1)
));
}
let mut nodes = HashMap::new();
let mut nodes_reverse = HashMap::new();
let mut seen_nodes = std::collections::HashSet::new();
for (i, &item) in lines.iter().enumerate().skip(1usize).take(num_nodes) {
let node_name = item.trim();
if node_name.is_empty() {
return Err(format!("Empty node name at line {}", i + 1));
}
if !seen_nodes.insert(node_name) {
return Err(format!("Duplicate node name: '{}'", node_name));
}
nodes.insert(node_name, i - 1); nodes_reverse.insert(i - 1, node_name); }
let mut graph = vec![Vec::new(); num_nodes];
for (line_num, line) in lines.iter().skip(1 + num_nodes).enumerate() {
let line = line.trim();
if line.is_empty() {
continue; }
let parts: Vec<&str> = line.split('|').collect();
if parts.len() != 3 {
return Err(format!(
"Invalid edge format at line {}: '{}' (expected format: node1|node2|weight)",
line_num + 1 + num_nodes + 1,
line
));
}
let node_1 = parts[0].trim();
let node_2 = parts[1].trim();
let weight_str = parts[2].trim();
let node_1_index = nodes.get(node_1).ok_or_else(|| {
format!(
"Node '{}' in edge definition not found in node list",
node_1
)
})?;
let node_2_index = nodes.get(node_2).ok_or_else(|| {
format!(
"Node '{}' in edge definition not found in node list",
node_2
)
})?;
let weight = weight_str.parse::<u32>().map_err(|_| {
format!(
"Invalid weight '{}' in edge '{}|{}|{}' (expected a positive integer)",
weight_str, node_1, node_2, weight_str
)
})?;
if node_1_index == node_2_index {
return Err(format!(
"Self-loop detected: node '{}' connected to itself",
node_1
));
}
graph[*node_1_index].push((*node_2_index, weight));
if bidirectional {
graph[*node_2_index].push((*node_1_index, weight));
}
}
Ok(ParsedGraph {
graph,
nodes_reverse,
})
}
pub fn dijkstra<Q: PriorityQueue>(
start: usize,
end: usize,
graph: &[Vec<(usize, u32)>],
mut heap: Q,
) -> (Vec<usize>, Option<u32>) {
let mut distances = vec![u32::MAX; graph.len()];
distances[start] = 0;
let mut previous = vec![None; graph.len()];
let supports_decrease_key = heap.supports_decrease_key();
let mut handles: Vec<Option<Q::Handle>> = if supports_decrease_key {
vec![None; graph.len()]
} else {
Vec::new() };
let handle = heap.insert(0, start);
if supports_decrease_key {
handles[start] = Some(handle);
}
while let Some((current_distance, current_node)) = heap.extract_min() {
if distances[current_node] < current_distance {
continue;
}
if current_node == end {
break;
}
for &(neighbor, weight) in &graph[current_node] {
let new_distance = current_distance + weight;
if new_distance < distances[neighbor] {
distances[neighbor] = new_distance;
previous[neighbor] = Some(current_node);
if supports_decrease_key {
if let Some(ref handle) = handles[neighbor] {
heap.decrease_key(handle, new_distance);
} else {
let handle = heap.insert(new_distance, neighbor);
handles[neighbor] = Some(handle);
}
} else {
heap.insert(new_distance, neighbor);
}
}
}
}
let mut path = Vec::new();
let mut current = end;
if distances[end] == u32::MAX {
return (path, None); }
while let Some(prev) = previous[current] {
path.push(current);
current = prev;
}
path.push(start);
path.reverse();
(path, Some(distances[end]))
}
fn find_shortest_path_with(
lines: Vec<&str>,
bidirectional: bool,
dijkstra_impl: DijkstraFn,
) -> Result<(String, Option<u32>), String> {
if lines.is_empty() {
return Ok(("-1".to_string(), None));
}
let parsed = parse_graph(&lines, bidirectional)?;
let num_nodes = parsed.graph.len();
if num_nodes == 0 {
return Ok(("-1".to_string(), None));
}
if num_nodes == 1 {
return Ok((
parsed
.nodes_reverse
.get(&0)
.ok_or_else(|| "Internal error: single node not found in reverse map".to_string())?
.to_string(),
Some(0),
));
}
let (path, distance) = dijkstra_impl(0, num_nodes - 1, &parsed.graph);
if path.len() <= 1 {
return Ok(("-1".to_string(), None));
}
let mut path_parts = Vec::with_capacity(path.len());
for node_id in path {
let node = parsed.nodes_reverse.get(&node_id).ok_or_else(|| {
format!(
"Internal error: node ID {} not found in reverse map",
node_id
)
})?;
path_parts.push(*node);
}
Ok((path_parts.join("-"), distance))
}
pub fn find_shortest_path(lines: Vec<&str>) -> Result<(String, Option<u32>), String> {
find_shortest_path_with(lines, true, dijkstra_binary) }
pub fn find_shortest_path_fibonacci(lines: Vec<&str>) -> Result<(String, Option<u32>), String> {
find_shortest_path_with(lines, true, dijkstra_fibonacci)
}
pub fn find_shortest_path_fibonacci_unsafe(
lines: Vec<&str>,
) -> Result<(String, Option<u32>), String> {
find_shortest_path_with(lines, true, dijkstra_fibonacci_unsafe)
}
pub fn find_shortest_path_pairing(lines: Vec<&str>) -> Result<(String, Option<u32>), String> {
find_shortest_path_with(lines, true, dijkstra_pairing)
}
pub fn find_shortest_path_radix(lines: Vec<&str>) -> Result<(String, Option<u32>), String> {
find_shortest_path_with(lines, true, dijkstra_radix)
}
pub fn find_shortest_path_dial(lines: Vec<&str>) -> Result<(String, Option<u32>), String> {
find_shortest_path_with(lines, true, dijkstra_dial)
}
#[allow(clippy::doc_overindented_list_items)]
pub fn find_shortest_path_directed(
lines: Vec<&str>,
bidirectional: bool,
) -> Result<(String, Option<u32>), String> {
find_shortest_path_with(lines, bidirectional, dijkstra_binary)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_input() {
let result = find_shortest_path(vec![]);
assert_eq!(result, Ok(("-1".to_string(), None)));
}
#[test]
fn test_single_node() {
let input = vec!["1", "A"];
let result = find_shortest_path(input);
assert_eq!(result, Ok(("A".to_string(), Some(0))));
}
#[test]
fn test_two_nodes_connected() {
let input = vec!["2", "A", "B", "A|B|5"];
let result = find_shortest_path(input);
assert_eq!(result, Ok(("A-B".to_string(), Some(5))));
}
#[test]
fn test_two_nodes_disconnected() {
let input = vec!["2", "A", "B"];
let result = find_shortest_path(input);
assert_eq!(result, Ok(("-1".to_string(), None)));
}
#[test]
fn test_simple_path() {
let input = vec!["3", "A", "B", "C", "A|B|2", "B|C|3"];
let result = find_shortest_path(input);
assert_eq!(result, Ok(("A-B-C".to_string(), Some(5))));
}
#[test]
fn test_shortest_path_through_intermediate() {
let input = vec![
"4", "A", "B", "C", "D", "A|B|1", "B|C|1", "C|D|1", "A|D|100",
];
let result = find_shortest_path(input);
assert_eq!(result, Ok(("A-B-C-D".to_string(), Some(3))));
}
#[test]
fn test_invalid_node_count() {
let input = vec!["abc"];
let result = find_shortest_path(input);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Invalid number of nodes"));
}
#[test]
fn test_not_enough_nodes() {
let input = vec!["3", "A", "B"];
let result = find_shortest_path(input);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Not enough lines"));
}
#[test]
fn test_duplicate_node_names() {
let input = vec!["2", "A", "A", "A|A|5"];
let result = find_shortest_path(input);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Duplicate node name"));
}
#[test]
fn test_invalid_edge_format() {
let input = vec!["2", "A", "B", "A|B"];
let result = find_shortest_path(input);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Invalid edge format"));
}
#[test]
fn test_node_not_in_list() {
let input = vec!["2", "A", "B", "A|C|5"];
let result = find_shortest_path(input);
assert!(result.is_err());
assert!(result.unwrap_err().contains("not found in node list"));
}
#[test]
fn test_invalid_weight() {
let input = vec!["2", "A", "B", "A|B|abc"];
let result = find_shortest_path(input);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Invalid weight"));
}
#[test]
fn test_self_loop() {
let input = vec!["2", "A", "B", "A|A|5"];
let result = find_shortest_path(input);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Self-loop detected"));
}
#[test]
fn test_empty_node_name() {
let input = vec!["2", "", "B", "A|B|5"];
let result = find_shortest_path(input);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Empty node name"));
}
#[test]
fn test_zero_nodes() {
let input = vec!["0"];
let result = find_shortest_path(input);
assert_eq!(result, Ok(("-1".to_string(), None)));
}
#[test]
fn test_fibonacci_heap_simple() {
let graph = vec![
vec![(1, 2), (2, 4)], vec![(0, 2), (3, 1)], vec![(0, 4), (3, 5)], vec![(1, 1), (2, 5)], ];
let (path_binary, dist_binary) = dijkstra_binary(0, 3, &graph);
let (path_fib, dist_fib) = dijkstra_fibonacci(0, 3, &graph);
assert_eq!(dist_binary, dist_fib);
assert_eq!(path_binary, path_fib);
assert_eq!(path_binary, vec![0, 1, 3]);
}
#[test]
fn test_fibonacci_heap_comprehensive() {
let test_cases = vec![
(
vec![vec![(1, 1)], vec![(2, 1)], vec![]],
0,
2,
vec![0, 1, 2],
),
(vec![vec![], vec![]], 0, 1, vec![]),
(vec![vec![]], 0, 0, vec![0]),
(
vec![
vec![(1, 4), (2, 2)], vec![(2, 1), (3, 5)], vec![(1, 1), (3, 8), (4, 10)], vec![(4, 2)], vec![], ],
0,
4,
vec![0, 2, 1, 3, 4],
),
(
vec![
vec![(1, 1), (2, 5)], vec![(2, 1), (3, 2)], vec![(3, 3)], vec![], ],
0,
3,
vec![0, 1, 3],
),
];
for (graph, start, end, expected_path) in test_cases {
let (path_binary, dist_binary) = dijkstra_binary(start, end, &graph);
let (_path_fib, dist_fib) = dijkstra_fibonacci(start, end, &graph);
assert_eq!(
dist_binary, dist_fib,
"Distance mismatch for graph: start={}, end={}, binary={:?}, fib={:?}",
start, end, dist_binary, dist_fib
);
if !expected_path.is_empty() {
assert_eq!(
path_binary, expected_path,
"Path doesn't match expected: got {:?}, expected {:?}",
path_binary, expected_path
);
}
}
}
#[test]
fn test_empty_lines_in_edges() {
let input = vec!["2", "A", "B", "A|B|5", "", "A|B|3"];
let result = find_shortest_path(input);
assert_eq!(result, Ok(("A-B".to_string(), Some(3))));
}
#[test]
fn test_valid_inputs_from_files() {
use std::fs;
use std::path::Path;
let testdata_dir = Path::new("testdata");
if !testdata_dir.exists() {
return;
}
for i in 0..=18 {
let input_file = testdata_dir.join(format!("input{}.txt", i));
let output_file = testdata_dir.join(format!("output{}.txt", i));
if !input_file.exists() || !output_file.exists() {
continue;
}
let input_content = fs::read_to_string(&input_file)
.unwrap_or_else(|_| panic!("Failed to read {:?}", input_file));
let input_lines: Vec<&str> = input_content.lines().collect();
let expected_output = fs::read_to_string(&output_file)
.unwrap_or_else(|_| panic!("Failed to read {:?}", output_file))
.trim()
.to_string();
let result = find_shortest_path(input_lines);
match result {
Ok((actual, _distance)) => {
assert_eq!(
actual, expected_output,
"Mismatch for input{}.txt: expected '{}', got '{}'",
i, expected_output, actual
);
}
Err(e) => {
panic!("input{}.txt should succeed but got error: {}", i, e);
}
}
}
}
#[test]
fn test_invalid_inputs_from_files() {
use std::fs;
use std::path::Path;
let testdata_dir = Path::new("testdata");
if !testdata_dir.exists() {
return;
}
for i in 1..=6 {
let input_file = testdata_dir.join(format!("invalid{}.txt", i));
let error_file = testdata_dir.join(format!("error_invalid{}.txt", i));
if !input_file.exists() || !error_file.exists() {
continue;
}
let input_content = fs::read_to_string(&input_file)
.unwrap_or_else(|_| panic!("Failed to read {:?}", input_file));
let input_lines: Vec<&str> = input_content.lines().collect();
let expected_error_full = fs::read_to_string(&error_file)
.unwrap_or_else(|_| panic!("Failed to read {:?}", error_file))
.trim()
.to_string();
let expected_error = expected_error_full
.strip_prefix("Graph processing error: ")
.unwrap_or(&expected_error_full)
.to_string();
let result = find_shortest_path(input_lines);
match result {
Ok(_) => {
panic!(
"invalid{}.txt should fail but succeeded. Expected error: {}",
i, expected_error
);
}
Err(actual_error) => {
assert_eq!(
actual_error, expected_error,
"Error message mismatch for invalid{}.txt:\n Expected: {}\n Got: {}",
i, expected_error, actual_error
);
}
}
}
}
}