rssn/symbolic/
graph_algorithms.rs

1//! # Comprehensive Graph Algorithms
2//!
3//! This module provides a comprehensive suite of graph algorithms for various tasks,
4//! including graph traversal (DFS, BFS), connectivity analysis (connected components,
5//! strongly connected components), cycle detection, minimum spanning trees (Kruskal's,
6//! Prim's), network flow (Edmonds-Karp, Dinic's), shortest paths (Dijkstra's,
7//! Bellman-Ford, Floyd-Warshall), and topological sorting.
8
9use crate::prelude::simplify;
10use crate::symbolic::core::Expr;
11use crate::symbolic::graph::Graph;
12use crate::symbolic::simplify::as_f64;
13use ordered_float::OrderedFloat;
14use std::collections::{HashMap, HashSet, VecDeque};
15use std::fmt::Debug;
16use std::hash::Hash;
17
18// =====================================================================================
19// region: Graph Traversal
20// =====================================================================================
21
22/// Performs a Depth-First Search (DFS) traversal on a graph.
23///
24/// DFS explores as far as possible along each branch before backtracking.
25///
26/// # Arguments
27/// * `graph` - The graph to traverse.
28/// * `start_node` - The index of the starting node.
29///
30/// # Returns
31/// A `Vec<usize>` containing the node IDs in the order they were visited.
32pub fn dfs<V>(graph: &Graph<V>, start_node: usize) -> Vec<usize>
33where
34    V: Eq + Hash + Clone + std::fmt::Debug,
35{
36    let mut visited = HashSet::new();
37    let mut result = Vec::new();
38    dfs_recursive(graph, start_node, &mut visited, &mut result);
39    result
40}
41
42pub(crate) fn dfs_recursive<V>(
43    graph: &Graph<V>,
44    u: usize,
45    visited: &mut HashSet<usize>,
46    result: &mut Vec<usize>,
47) where
48    V: Eq + Hash + Clone + std::fmt::Debug,
49{
50    visited.insert(u);
51    result.push(u);
52    if let Some(neighbors) = graph.adj.get(u) {
53        for &(v, _) in neighbors {
54            if !visited.contains(&v) {
55                dfs_recursive(graph, v, visited, result);
56            }
57        }
58    }
59}
60
61/// Performs a Breadth-First Search (BFS) traversal on a graph.
62///
63/// BFS explores all of the neighbor nodes at the present depth prior to moving on to nodes at the next depth level.
64///
65/// # Arguments
66/// * `graph` - The graph to traverse.
67/// * `start_node` - The index of the starting node.
68///
69/// # Returns
70/// A `Vec<usize>` containing the node IDs in the order they were visited.
71pub fn bfs<V>(graph: &Graph<V>, start_node: usize) -> Vec<usize>
72where
73    V: Eq + Hash + Clone + std::fmt::Debug,
74{
75    let mut visited = HashSet::new();
76    let mut result = Vec::new();
77    let mut queue = VecDeque::new();
78
79    visited.insert(start_node);
80    queue.push_back(start_node);
81
82    while let Some(u) = queue.pop_front() {
83        result.push(u);
84        if let Some(neighbors) = graph.adj.get(u) {
85            for &(v, _) in neighbors {
86                if !visited.contains(&v) {
87                    visited.insert(v);
88                    queue.push_back(v);
89                }
90            }
91        }
92    }
93    result
94}
95
96// =====================================================================================
97// region: Connectivity & Components
98// =====================================================================================
99
100/// Finds all connected components of an undirected graph.
101///
102/// A connected component is a subgraph in which any two vertices are connected to each other
103/// by paths, and which is connected to no additional vertices in the supergraph.
104///
105/// # Arguments
106/// * `graph` - The graph to analyze.
107///
108/// # Returns
109/// A `Vec<Vec<usize>>` where each inner `Vec` represents a connected component.
110pub fn connected_components<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(
111    graph: &Graph<V>,
112) -> Vec<Vec<usize>> {
113    let mut visited = HashSet::new();
114    let mut components = Vec::new();
115    for node_id in 0..graph.nodes.len() {
116        if !visited.contains(&node_id) {
117            let component = bfs(graph, node_id);
118            for &visited_node in &component {
119                visited.insert(visited_node);
120            }
121            components.push(component);
122        }
123    }
124    components
125}
126
127/// Checks if the graph is connected.
128///
129/// An undirected graph is connected if for every pair of vertices `(u, v)`,
130/// there is a path from `u` to `v`.
131///
132/// # Arguments
133/// * `graph` - The graph to check.
134///
135/// # Returns
136/// `true` if the graph is connected, `false` otherwise.
137pub fn is_connected<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(graph: &Graph<V>) -> bool {
138    connected_components(graph).len() == 1
139}
140
141/// Finds all strongly connected components (SCCs) of a directed graph using Tarjan's algorithm.
142///
143/// An SCC is a subgraph where every vertex is reachable from every other vertex within that subgraph.
144/// Tarjan's algorithm is an efficient DFS-based method for finding SCCs.
145///
146/// # Arguments
147/// * `graph` - The directed graph to analyze.
148///
149/// # Returns
150/// A `Vec<Vec<usize>>` where each inner `Vec` represents a strongly connected component.
151pub fn strongly_connected_components<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(
152    graph: &Graph<V>,
153) -> Vec<Vec<usize>> {
154    let mut scc = Vec::new();
155    let mut stack = Vec::new();
156    let mut on_stack = HashSet::new();
157    let mut discovery_times = HashMap::new();
158    let mut low_link = HashMap::new();
159    let mut time = 0;
160    for node_id in 0..graph.nodes.len() {
161        tarjan_scc_util(
162            graph,
163            node_id,
164            &mut time,
165            &mut discovery_times,
166            &mut low_link,
167            &mut stack,
168            &mut on_stack,
169            &mut scc,
170        );
171    }
172    scc
173}
174
175pub(crate) fn tarjan_scc_util<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(
176    graph: &Graph<V>,
177    u: usize,
178    time: &mut usize,
179    disc: &mut HashMap<usize, usize>,
180    low: &mut HashMap<usize, usize>,
181    stack: &mut Vec<usize>,
182    on_stack: &mut HashSet<usize>,
183    scc: &mut Vec<Vec<usize>>,
184) {
185    disc.insert(u, *time);
186    low.insert(u, *time);
187    *time += 1;
188    stack.push(u);
189    on_stack.insert(u);
190
191    if let Some(neighbors) = graph.adj.get(u) {
192        for &(v, _) in neighbors {
193            if !disc.contains_key(&v) {
194                tarjan_scc_util(graph, v, time, disc, low, stack, on_stack, scc);
195                let low_u = *low.get(&u).unwrap();
196                let low_v = *low.get(&v).unwrap();
197                low.insert(u, low_u.min(low_v));
198            } else if on_stack.contains(&v) {
199                let low_u = *low.get(&u).unwrap();
200                let disc_v = *disc.get(&v).unwrap();
201                low.insert(u, low_u.min(disc_v));
202            }
203        }
204    }
205
206    if low.get(&u) == disc.get(&u) {
207        let mut component = Vec::new();
208        while let Some(top) = stack.pop() {
209            on_stack.remove(&top);
210            component.push(top);
211            if top == u {
212                break;
213            }
214        }
215        scc.push(component);
216    }
217}
218
219// =====================================================================================
220// region: Cycles, Bridges, Articulation Points
221// =====================================================================================
222
223/// Detects if a cycle exists in the graph.
224///
225/// For directed graphs, it uses a DFS-based approach with a recursion stack.
226/// For undirected graphs, it uses a DFS-based approach that checks for back-edges.
227///
228/// # Arguments
229/// * `graph` - The graph to check.
230///
231/// # Returns
232/// `true` if a cycle is found, `false` otherwise.
233pub fn has_cycle<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(graph: &Graph<V>) -> bool {
234    let mut visited = HashSet::new();
235    if graph.is_directed {
236        let mut recursion_stack = HashSet::new();
237        for node_id in 0..graph.nodes.len() {
238            if !visited.contains(&node_id) {
239                if has_cycle_directed_util(graph, node_id, &mut visited, &mut recursion_stack) {
240                    return true;
241                }
242            }
243        }
244    } else {
245        for node_id in 0..graph.nodes.len() {
246            if !visited.contains(&node_id) {
247                if has_cycle_undirected_util(graph, node_id, &mut visited, None) {
248                    return true;
249                }
250            }
251        }
252    }
253    false
254}
255
256pub(crate) fn has_cycle_directed_util<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(
257    graph: &Graph<V>,
258    u: usize,
259    visited: &mut HashSet<usize>,
260    rec_stack: &mut HashSet<usize>,
261) -> bool {
262    visited.insert(u);
263    rec_stack.insert(u);
264    if let Some(neighbors) = graph.adj.get(u) {
265        for &(v, _) in neighbors {
266            if !visited.contains(&v) {
267                if has_cycle_directed_util(graph, v, visited, rec_stack) {
268                    return true;
269                }
270            } else if rec_stack.contains(&v) {
271                return true;
272            }
273        }
274    }
275    rec_stack.remove(&u);
276    false
277}
278
279pub(crate) fn has_cycle_undirected_util<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(
280    graph: &Graph<V>,
281    u: usize,
282    visited: &mut HashSet<usize>,
283    parent: Option<usize>,
284) -> bool {
285    visited.insert(u);
286    if let Some(neighbors) = graph.adj.get(u) {
287        for &(v, _) in neighbors {
288            if !visited.contains(&v) {
289                if has_cycle_undirected_util(graph, v, visited, Some(u)) {
290                    return true;
291                }
292            } else if Some(v) != parent {
293                return true;
294            }
295        }
296    }
297    false
298}
299
300/// Finds all bridges and articulation points (cut vertices) in a graph using Tarjan's algorithm.
301///
302/// A bridge is an edge whose removal increases the number of connected components.
303/// An articulation point is a vertex whose removal increases the number of connected components.
304/// Tarjan's algorithm is an efficient DFS-based method for finding these.
305///
306/// # Arguments
307/// * `graph` - The graph to analyze.
308///
309/// # Returns
310/// A tuple `(bridges, articulation_points)` where `bridges` is a `Vec<(usize, usize)>`
311/// and `articulation_points` is a `Vec<usize>`.
312pub fn find_bridges_and_articulation_points<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(
313    graph: &Graph<V>,
314) -> (Vec<(usize, usize)>, Vec<usize>) {
315    let mut bridges = Vec::new();
316    let mut articulation_points = HashSet::new();
317    let mut visited = HashSet::new();
318    let mut discovery_times = HashMap::new();
319    let mut low_link = HashMap::new();
320    let mut time = 0;
321
322    for node_id in 0..graph.nodes.len() {
323        if !visited.contains(&node_id) {
324            b_and_ap_util(
325                graph,
326                node_id,
327                None,
328                &mut time,
329                &mut visited,
330                &mut discovery_times,
331                &mut low_link,
332                &mut bridges,
333                &mut articulation_points,
334            );
335        }
336    }
337    (bridges, articulation_points.into_iter().collect())
338}
339
340pub(crate) fn b_and_ap_util<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(
341    graph: &Graph<V>,
342    u: usize,
343    parent: Option<usize>,
344    time: &mut usize,
345    visited: &mut HashSet<usize>,
346    disc: &mut HashMap<usize, usize>,
347    low: &mut HashMap<usize, usize>,
348    bridges: &mut Vec<(usize, usize)>,
349    ap: &mut HashSet<usize>,
350) {
351    visited.insert(u);
352    disc.insert(u, *time);
353    low.insert(u, *time);
354    *time += 1;
355    let mut children = 0;
356
357    if let Some(neighbors) = graph.adj.get(u) {
358        for &(v, _) in neighbors {
359            if Some(v) == parent {
360                continue;
361            }
362            if visited.contains(&v) {
363                low.insert(u, (*low.get(&u).unwrap()).min(*disc.get(&v).unwrap()));
364            } else {
365                children += 1;
366                b_and_ap_util(graph, v, Some(u), time, visited, disc, low, bridges, ap);
367                low.insert(u, (*low.get(&u).unwrap()).min(*low.get(&v).unwrap()));
368
369                if parent.is_some() && low.get(&v).unwrap() >= disc.get(&u).unwrap() {
370                    ap.insert(u);
371                }
372                if low.get(&v).unwrap() > disc.get(&u).unwrap() {
373                    bridges.push((u, v));
374                }
375            }
376        }
377    }
378
379    if parent.is_none() && children > 1 {
380        ap.insert(u);
381    }
382}
383
384// =====================================================================================
385// region: Minimum Spanning Tree
386// =====================================================================================
387
388/// A Disjoint Set Union (DSU) data structure for Kruskal's algorithm.
389pub struct DSU {
390    parent: Vec<usize>,
391}
392
393impl DSU {
394    pub(crate) fn new(n: usize) -> Self {
395        DSU {
396            parent: (0..n).collect(),
397        }
398    }
399
400    pub(crate) fn find(&mut self, i: usize) -> usize {
401        if self.parent[i] == i {
402            return i;
403        }
404        self.parent[i] = self.find(self.parent[i]);
405        self.parent[i]
406    }
407
408    pub(crate) fn union(&mut self, i: usize, j: usize) {
409        let root_i = self.find(i);
410        let root_j = self.find(j);
411        if root_i != root_j {
412            self.parent[root_i] = root_j;
413        }
414    }
415}
416
417/// Finds the Minimum Spanning Tree (MST) of a graph using Kruskal's algorithm.
418///
419/// Kruskal's algorithm is a greedy algorithm that finds an MST for a connected,
420/// undirected graph. It works by adding edges in increasing order of weight,
421/// as long as they do not form a cycle.
422///
423/// # Arguments
424/// * `graph` - The graph to analyze.
425///
426/// # Returns
427/// A `Vec<(usize, usize, Expr)>` representing the edges `(u, v, weight)` that form the MST.
428pub fn kruskal_mst<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(
429    graph: &Graph<V>,
430) -> Vec<(usize, usize, Expr)> {
431    let mut edges = graph.get_edges();
432
433    edges.sort_by(|a, b| {
434        let weight_a = as_f64(&a.2).unwrap_or(f64::INFINITY);
435        let weight_b = as_f64(&b.2).unwrap_or(f64::INFINITY);
436        weight_a.partial_cmp(&weight_b).unwrap()
437    });
438
439    let mut dsu = DSU::new(graph.nodes.len());
440    let mut mst = Vec::new();
441
442    for (u, v, weight) in edges {
443        if dsu.find(u) != dsu.find(v) {
444            dsu.union(u, v);
445            mst.push((u, v, weight));
446        }
447    }
448    mst
449}
450
451// =====================================================================================
452// region: Network Flow
453// =====================================================================================
454
455/// Finds the maximum flow from a source `s` to a sink `t` in a flow network
456/// using the Edmonds-Karp algorithm.
457///
458/// Edmonds-Karp is an implementation of the Ford-Fulkerson method that uses BFS
459/// to find augmenting paths in the residual graph. It guarantees to find the maximum flow.
460///
461/// # Arguments
462/// * `capacity_graph` - A graph where edge weights represent capacities.
463/// * `s` - The source node index.
464/// * `t` - The sink node index.
465///
466/// # Returns
467/// The maximum flow value as an `f64`.
468pub fn edmonds_karp_max_flow<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(
469    capacity_graph: &Graph<V>,
470    s: usize,
471    t: usize,
472) -> f64 {
473    let n = capacity_graph.nodes.len();
474    let mut residual_capacity = vec![vec![0.0; n]; n];
475    for u in 0..n {
476        if let Some(neighbors) = capacity_graph.adj.get(u) {
477            for &(v, ref cap) in neighbors {
478                residual_capacity[u][v] = as_f64(cap).unwrap_or(0.0);
479            }
480        }
481    }
482
483    let mut max_flow = 0.0;
484
485    loop {
486        let (parent, path_flow) = bfs_for_augmenting_path(&residual_capacity, s, t);
487
488        if path_flow == 0.0 {
489            break; // No more augmenting paths
490        }
491
492        max_flow += path_flow;
493        let mut v = t;
494        while v != s {
495            let u = parent[v].unwrap();
496            residual_capacity[u][v] -= path_flow;
497            residual_capacity[v][u] += path_flow;
498            v = u;
499        }
500    }
501
502    max_flow
503}
504
505/// Helper BFS to find an augmenting path in the residual graph.
506pub(crate) fn bfs_for_augmenting_path(
507    capacity: &Vec<Vec<f64>>,
508    s: usize,
509    t: usize,
510) -> (Vec<Option<usize>>, f64) {
511    let n = capacity.len();
512    let mut parent = vec![None; n];
513    let mut queue = VecDeque::new();
514    let mut path_flow = vec![f64::INFINITY; n];
515
516    queue.push_back(s);
517
518    while let Some(u) = queue.pop_front() {
519        for v in 0..n {
520            if parent[v].is_none() && v != s && capacity[u][v] > 0.0 {
521                parent[v] = Some(u);
522                path_flow[v] = path_flow[u].min(capacity[u][v]);
523                if v == t {
524                    return (parent, path_flow[t]);
525                }
526                queue.push_back(v);
527            }
528        }
529    }
530
531    (parent, 0.0)
532}
533
534/// Finds the maximum flow from a source `s` to a sink `t` in a flow network
535/// using Dinic's algorithm.
536///
537/// Dinic's algorithm is a more efficient algorithm for solving the maximum flow problem
538/// compared to Edmonds-Karp, especially for dense graphs. It uses a level graph
539/// and blocking flows to find augmenting paths.
540///
541/// # Arguments
542/// * `capacity_graph` - A graph where edge weights represent capacities.
543/// * `s` - The source node index.
544/// * `t` - The sink node index.
545///
546/// # Returns
547/// The maximum flow value as an `f64`.
548pub fn dinic_max_flow<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(
549    capacity_graph: &Graph<V>,
550    s: usize,
551    t: usize,
552) -> f64 {
553    let n = capacity_graph.nodes.len();
554    let mut residual_capacity = vec![vec![0.0; n]; n];
555    for u in 0..n {
556        if let Some(neighbors) = capacity_graph.adj.get(u) {
557            for &(v, ref cap) in neighbors {
558                residual_capacity[u][v] = as_f64(cap).unwrap_or(0.0);
559            }
560        }
561    }
562
563    let mut max_flow = 0.0;
564    let mut level = vec![0; n];
565
566    while dinic_bfs(&residual_capacity, s, t, &mut level) {
567        let mut ptr = vec![0; n];
568        while {
569            let pushed = dinic_dfs(
570                &mut residual_capacity,
571                s,
572                t,
573                f64::INFINITY,
574                &level,
575                &mut ptr,
576            );
577            if pushed > 0.0 {
578                max_flow += pushed;
579                true
580            } else {
581                false
582            }
583        } {}
584    }
585
586    max_flow
587}
588
589pub(crate) fn dinic_bfs(
590    capacity: &Vec<Vec<f64>>,
591    s: usize,
592    t: usize,
593    level: &mut Vec<i32>,
594) -> bool {
595    level.iter_mut().for_each(|l| *l = -1);
596    level[s] = 0;
597    let mut q = VecDeque::new();
598    q.push_back(s);
599
600    while let Some(u) = q.pop_front() {
601        for v in 0..capacity.len() {
602            if level[v] < 0 && capacity[u][v] > 0.0 {
603                level[v] = level[u] + 1;
604                q.push_back(v);
605            }
606        }
607    }
608    level[t] != -1
609}
610
611pub(crate) fn dinic_dfs(
612    cap: &mut Vec<Vec<f64>>,
613    u: usize,
614    t: usize,
615    pushed: f64,
616    level: &Vec<i32>,
617    ptr: &mut Vec<usize>,
618) -> f64 {
619    if pushed == 0.0 {
620        return 0.0;
621    }
622    if u == t {
623        return pushed;
624    }
625
626    while ptr[u] < cap.len() {
627        let v = ptr[u];
628        if level[v] != level[u] + 1 || cap[u][v] == 0.0 {
629            ptr[u] += 1;
630            continue;
631        }
632        let tr = dinic_dfs(cap, v, t, pushed.min(cap[u][v]), level, ptr);
633        if tr == 0.0 {
634            ptr[u] += 1;
635            continue;
636        }
637        cap[u][v] -= tr;
638        cap[v][u] += tr;
639        return tr;
640    }
641    0.0
642}
643
644/// Finds the shortest paths from a single source in a graph with possible negative edge weights.
645///
646/// Bellman-Ford algorithm is capable of handling graphs where some edge weights are negative,
647/// unlike Dijkstra's algorithm. It can also detect negative-weight cycles.
648///
649/// # Arguments
650/// * `graph` - The graph to analyze.
651/// * `start_node` - The index of the starting node.
652///
653/// # Returns
654/// A `Result` containing a tuple `(distances, predecessors)`.
655/// `distances` is a `HashMap` from node ID to shortest distance.
656/// `predecessors` is a `HashMap` from node ID to its predecessor on the shortest path.
657/// Returns an error string if a negative-weight cycle is detected.
658pub fn bellman_ford<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(
659    graph: &Graph<V>,
660    start_node: usize,
661) -> Result<(HashMap<usize, f64>, HashMap<usize, Option<usize>>), String> {
662    let n = graph.nodes.len();
663    let mut dist = HashMap::new();
664    let mut prev = HashMap::new();
665
666    for node_id in 0..graph.nodes.len() {
667        dist.insert(node_id, f64::INFINITY);
668    }
669    dist.insert(start_node, 0.0);
670
671    for _ in 1..n {
672        for u in 0..n {
673            if let Some(neighbors) = graph.adj.get(u) {
674                for &(v, ref weight) in neighbors {
675                    let w = as_f64(weight).unwrap_or(f64::INFINITY);
676                    if dist[&u] != f64::INFINITY && dist[&u] + w < dist[&v] {
677                        dist.insert(v, dist[&u] + w);
678                        prev.insert(v, Some(u));
679                    }
680                }
681            }
682        }
683    }
684
685    // Check for negative-weight cycles
686    for u in 0..n {
687        if let Some(neighbors) = graph.adj.get(u) {
688            for &(v, ref weight) in neighbors {
689                let w = as_f64(weight).unwrap_or(f64::INFINITY);
690                if dist[&u] != f64::INFINITY && dist[&u] + w < dist[&v] {
691                    return Err("Graph contains a negative-weight cycle.".to_string());
692                }
693            }
694        }
695    }
696
697    Ok((dist, prev))
698}
699
700/// Solves the Minimum-Cost Maximum-Flow problem using the successive shortest path algorithm with Bellman-Ford.
701///
702/// This algorithm finds the maximum flow through a network while minimizing the total cost of the flow.
703/// It repeatedly finds the shortest augmenting path in the residual graph, where edge weights are costs.
704/// Assumes edge weights are given as a tuple `(capacity, cost)`.
705///
706/// # Arguments
707/// * `graph` - The graph where edge weights are `Expr::Tuple(capacity, cost)`.
708/// * `s` - The source node index.
709/// * `t` - The sink node index.
710///
711/// # Returns
712/// A tuple `(max_flow, min_cost)` as `f64`.
713#[allow(unused_variables)]
714pub fn min_cost_max_flow<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(
715    graph: &Graph<V>,
716    s: usize,
717    t: usize,
718) -> (f64, f64) {
719    let n = graph.nodes.len();
720    let mut capacity = vec![vec![0.0; n]; n];
721    let mut cost = vec![vec![0.0; n]; n];
722
723    for u in 0..n {
724        if let Some(neighbors) = graph.adj.get(u) {
725            for &(v, ref weight) in neighbors {
726                if let Expr::Tuple(t) = weight {
727                    if t.len() == 2 {
728                        capacity[u][v] = as_f64(&t[0]).unwrap_or(0.0);
729                        cost[u][v] = as_f64(&t[1]).unwrap_or(0.0);
730                    }
731                }
732            }
733        }
734    }
735
736    let mut flow = 0.0;
737    let mut total_cost = 0.0;
738
739    loop {
740        // Find shortest path in residual graph using costs as weights
741        let mut dist = vec![f64::INFINITY; n];
742        let mut parent = vec![None; n];
743        dist[s] = 0.0;
744
745        for _ in 1..n {
746            for u in 0..n {
747                for v in 0..n {
748                    if capacity[u][v] > 0.0
749                        && dist[u] != f64::INFINITY
750                        && dist[u] + cost[u][v] < dist[v]
751                    {
752                        dist[v] = dist[u] + cost[u][v];
753                        parent[v] = Some(u);
754                    }
755                }
756            }
757        }
758
759        if dist[t] == f64::INFINITY {
760            break; // No more augmenting paths
761        }
762
763        // Find path flow
764        let mut path_flow = f64::INFINITY;
765        let mut curr = t;
766        while let Some(prev) = parent[curr] {
767            path_flow = path_flow.min(capacity[prev][curr]);
768            curr = prev;
769        }
770
771        // Augment flow
772        flow += path_flow;
773        total_cost += path_flow * dist[t];
774        let mut v = t;
775        while let Some(u) = parent[v] {
776            capacity[u][v] -= path_flow;
777            capacity[v][u] += path_flow;
778            // Note: cost[v][u] should be -cost[u][v], this simple matrix representation doesn't handle that well.
779            // A full implementation would use a proper residual graph struct.
780            v = u;
781        }
782    }
783
784    (0.0, 0.0) // Returns (max_flow, min_cost)
785}
786
787// =====================================================================================
788// region: Matching, Covering, and Partitioning
789// =====================================================================================
790
791/// Checks if a graph is bipartite using BFS-based 2-coloring.
792///
793/// A graph is bipartite if its vertices can be divided into two disjoint and independent sets
794/// `U` and `V` such that every edge connects a vertex in `U` to one in `V`.
795///
796/// # Arguments
797/// * `graph` - The graph to check.
798///
799/// # Returns
800/// `Some(partition)` if bipartite, where `partition[i]` is `0` or `1` indicating the set.
801/// `None` if not bipartite.
802pub fn is_bipartite<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(
803    graph: &Graph<V>,
804) -> Option<Vec<i8>> {
805    let n = graph.nodes.len();
806    let mut colors = vec![-1; n]; // -1: uncolored, 0: color 1, 1: color 2
807
808    for i in 0..n {
809        if colors[i] == -1 {
810            let mut queue = VecDeque::new();
811            queue.push_back(i);
812            colors[i] = 0;
813
814            while let Some(u) = queue.pop_front() {
815                if let Some(neighbors) = graph.adj.get(u) {
816                    for &(v, _) in neighbors {
817                        if colors[v] == -1 {
818                            colors[v] = 1 - colors[u];
819                            queue.push_back(v);
820                        } else if colors[v] == colors[u] {
821                            return None; // Not bipartite
822                        }
823                    }
824                }
825            }
826        }
827    }
828    Some(colors)
829}
830
831/// Finds the maximum cardinality matching in a bipartite graph by reducing it to a max-flow problem.
832///
833/// A matching is a set of edges without common vertices. A maximum matching is one with the largest
834/// possible number of edges. This function constructs a flow network from the bipartite graph
835/// and uses a max-flow algorithm to find the matching.
836///
837/// # Arguments
838/// * `graph` - The bipartite graph.
839/// * `partition` - The partition of vertices into two sets (from `is_bipartite`).
840///
841/// # Returns
842/// A `Vec<(usize, usize)>` representing the edges `(u, v)` in the maximum matching.
843pub fn bipartite_maximum_matching<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(
844    graph: &Graph<V>,
845    partition: &[i8],
846) -> Vec<(usize, usize)> {
847    let n = graph.nodes.len();
848    let s = n; // Source
849    let t = n + 1; // Sink
850    let mut flow_graph = Graph::new(true);
851
852    let mut u_nodes = Vec::new();
853    let mut v_nodes = Vec::new();
854    for i in 0..n {
855        if partition[i] == 0 {
856            u_nodes.push(i);
857        } else {
858            v_nodes.push(i);
859        }
860    }
861
862    // Build flow network
863    for &u in &u_nodes {
864        flow_graph.add_edge(&s, &u, Expr::Constant(1.0));
865        if let Some(neighbors) = graph.adj.get(u) {
866            for &(v, _) in neighbors {
867                flow_graph.add_edge(&u, &v, Expr::Constant(1.0));
868            }
869        }
870    }
871    for &v in &v_nodes {
872        flow_graph.add_edge(&v, &t, Expr::Constant(1.0));
873    }
874
875    // The max flow is equivalent to the max matching size.
876    // To get the actual edges, we need to inspect the flow on edges (u,v).
877    // This requires a version of edmonds_karp that returns the final flow network.
878    // For now, we return an empty vec as a placeholder for the matched edges.
879    let _max_flow = edmonds_karp_max_flow(&flow_graph, s, t);
880    vec![]
881}
882
883// =====================================================================================
884// region: Minimum Spanning Tree & Topological Sort
885// =====================================================================================
886
887/// Finds the Minimum Spanning Tree (MST) of a graph using Prim's algorithm.
888///
889/// Prim's algorithm is a greedy algorithm that finds an MST for a connected,
890/// undirected graph. It grows the MST from an initial vertex by iteratively
891/// adding the cheapest edge that connects a vertex in the tree to one outside the tree.
892///
893/// # Arguments
894/// * `graph` - The graph to analyze.
895/// * `start_node` - The starting node for building the MST.
896///
897/// # Returns
898/// A `Vec<(usize, usize, Expr)>` representing the edges `(u, v, weight)` that form the MST.
899pub fn prim_mst<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(
900    graph: &Graph<V>,
901    start_node: usize,
902) -> Vec<(usize, usize, Expr)> {
903    let n = graph.nodes.len();
904    let mut mst = Vec::new();
905    let mut visited = vec![false; n];
906    let mut pq = std::collections::BinaryHeap::new();
907
908    visited[start_node] = true;
909    if let Some(neighbors) = graph.adj.get(start_node) {
910        for &(v, ref weight) in neighbors {
911            let cost = as_f64(weight).unwrap_or(f64::INFINITY);
912            pq.push((
913                ordered_float::OrderedFloat(-cost),
914                start_node,
915                v,
916                weight.clone(),
917            ));
918        }
919    }
920
921    while let Some((_, u, v, weight)) = pq.pop() {
922        if visited[v] {
923            continue;
924        }
925        visited[v] = true;
926        mst.push((u, v, weight));
927
928        if let Some(neighbors) = graph.adj.get(v) {
929            for &(next_v, ref next_weight) in neighbors {
930                if !visited[next_v] {
931                    let cost = as_f64(next_weight).unwrap_or(f64::INFINITY);
932                    pq.push((
933                        ordered_float::OrderedFloat(-cost),
934                        v,
935                        next_v,
936                        next_weight.clone(),
937                    ));
938                }
939            }
940        }
941    }
942    mst
943}
944
945/// Performs a topological sort on a directed acyclic graph (DAG) using Kahn's algorithm (BFS-based).
946///
947/// A topological sort is a linear ordering of its vertices such that for every directed edge `uv`
948/// from vertex `u` to vertex `v`, `u` comes before `v` in the ordering.
949/// Kahn's algorithm works by iteratively removing vertices with an in-degree of 0.
950///
951/// # Arguments
952/// * `graph` - The DAG to sort.
953///
954/// # Returns
955/// A `Result` containing a `Vec<usize>` of node indices in topological order,
956/// or an error string if the graph has a cycle.
957pub fn topological_sort_kahn<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(
958    graph: &Graph<V>,
959) -> Result<Vec<usize>, String> {
960    if !graph.is_directed {
961        return Err("Topological sort is only defined for directed graphs.".to_string());
962    }
963    let n = graph.nodes.len();
964    let mut in_degree = vec![0; n];
965    for i in 0..n {
966        in_degree[i] = graph.in_degree(i);
967    }
968
969    let mut queue: VecDeque<usize> = (0..n).filter(|&i| in_degree[i] == 0).collect();
970    let mut sorted_order = Vec::new();
971
972    while let Some(u) = queue.pop_front() {
973        sorted_order.push(u);
974        if let Some(neighbors) = graph.adj.get(u) {
975            for &(v, _) in neighbors {
976                in_degree[v] -= 1;
977                if in_degree[v] == 0 {
978                    queue.push_back(v);
979                }
980            }
981        }
982    }
983
984    if sorted_order.len() == n {
985        Ok(sorted_order)
986    } else {
987        Err("Graph has a cycle, topological sort is not possible.".to_string())
988    }
989}
990
991/// Performs a topological sort on a directed acyclic graph (DAG) using a DFS-based algorithm.
992///
993/// This algorithm works by performing a DFS traversal and adding vertices to the sorted list
994/// after all their dependencies (children in the DFS tree) have been visited.
995///
996/// # Arguments
997/// * `graph` - The DAG to sort.
998///
999/// # Returns
1000/// A `Vec<usize>` containing the node IDs in topological order.
1001pub fn topological_sort_dfs<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(
1002    graph: &Graph<V>,
1003) -> Vec<usize> {
1004    let mut visited = HashSet::new();
1005    let mut stack = Vec::new();
1006    for node_id in 0..graph.nodes.len() {
1007        if !visited.contains(&node_id) {
1008            topo_dfs_util(graph, node_id, &mut visited, &mut stack);
1009        }
1010    }
1011    stack.reverse();
1012    stack
1013}
1014
1015pub(crate) fn topo_dfs_util<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(
1016    graph: &Graph<V>,
1017    u: usize,
1018    visited: &mut HashSet<usize>,
1019    stack: &mut Vec<usize>,
1020) {
1021    visited.insert(u);
1022    if let Some(neighbors) = graph.adj.get(u) {
1023        for &(v, _) in neighbors {
1024            if !visited.contains(&v) {
1025                topo_dfs_util(graph, v, visited, stack);
1026            }
1027        }
1028    }
1029    stack.push(u);
1030}
1031
1032/// Finds the minimum vertex cover of a bipartite graph using Kőnig's theorem.
1033///
1034/// Kőnig's theorem states that in any bipartite graph, the number of edges in a maximum matching
1035/// equals the number of vertices in a minimum vertex cover. This function leverages a maximum
1036/// matching to construct the minimum vertex cover.
1037///
1038/// # Arguments
1039/// * `graph` - The bipartite graph.
1040/// * `partition` - The partition of vertices into two sets.
1041/// * `matching` - The set of edges in a maximum matching.
1042///
1043/// # Returns
1044/// A `Vec<usize>` of node indices representing the minimum vertex cover.
1045pub fn bipartite_minimum_vertex_cover<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(
1046    graph: &Graph<V>,
1047    partition: &[i8],
1048    matching: &[(usize, usize)],
1049) -> Vec<usize> {
1050    let mut u_nodes = HashSet::new();
1051    let mut matched_nodes_u = HashSet::new();
1052    for i in 0..partition.len() {
1053        if partition[i] == 0 {
1054            u_nodes.insert(i);
1055        }
1056    }
1057    for &(u, v) in matching {
1058        if u_nodes.contains(&u) {
1059            matched_nodes_u.insert(u);
1060        } else {
1061            matched_nodes_u.insert(v);
1062        }
1063    }
1064
1065    let unmatched_u: Vec<_> = u_nodes.difference(&matched_nodes_u).cloned().collect();
1066
1067    // Find all vertices reachable from unmatched U vertices by alternating paths.
1068    let mut visited = HashSet::new();
1069    let mut queue = VecDeque::from(unmatched_u);
1070    while let Some(u) = queue.pop_front() {
1071        if visited.contains(&u) {
1072            continue;
1073        }
1074        visited.insert(u);
1075        if let Some(neighbors) = graph.adj.get(u) {
1076            for &(v, _) in neighbors {
1077                // If (u,v) is NOT a matching edge, and v is not visited, add v.
1078                if !matching.contains(&(u, v))
1079                    && !matching.contains(&(v, u))
1080                    && !visited.contains(&v)
1081                {
1082                    queue.push_back(v);
1083                }
1084            }
1085        }
1086    }
1087
1088    // The minimum vertex cover is (U \ Z) U (V ∩ Z), where Z is the set of visited vertices.
1089    let mut cover = Vec::new();
1090    for u in u_nodes {
1091        if !visited.contains(&u) {
1092            cover.push(u);
1093        }
1094    }
1095    for i in 0..partition.len() {
1096        if partition[i] == 1 && visited.contains(&i) {
1097            cover.push(i);
1098        }
1099    }
1100    cover
1101}
1102
1103/// Finds the maximum cardinality matching in a bipartite graph using the Hopcroft-Karp algorithm.
1104///
1105/// The Hopcroft-Karp algorithm is an efficient algorithm for finding maximum cardinality matchings
1106/// in bipartite graphs. It works by repeatedly finding a maximal set of shortest augmenting paths.
1107///
1108/// # Arguments
1109/// * `graph` - The bipartite graph.
1110/// * `partition` - The partition of vertices into two sets.
1111///
1112/// # Returns
1113/// A `Vec<(usize, usize)>` representing the edges `(u, v)` in the maximum matching.
1114#[allow(unused_variables)]
1115pub fn hopcroft_karp_bipartite_matching<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(
1116    graph: &Graph<V>,
1117    partition: &[i8],
1118) -> Vec<(usize, usize)> {
1119    let n = graph.nodes.len();
1120    let mut u_nodes = Vec::new();
1121    for i in 0..n {
1122        if partition[i] == 0 {
1123            u_nodes.push(i);
1124        }
1125    }
1126
1127    let mut pair_u = vec![None; n];
1128    let mut pair_v = vec![None; n];
1129    let mut dist = vec![0; n];
1130    let mut matching = 0;
1131
1132    while hopcroft_karp_bfs(graph, &u_nodes, &mut pair_u, &mut pair_v, &mut dist) {
1133        for &u in &u_nodes {
1134            if pair_u[u].is_none() {
1135                if hopcroft_karp_dfs(graph, u, &mut pair_u, &mut pair_v, &mut dist) {
1136                    matching += 1;
1137                }
1138            }
1139        }
1140    }
1141
1142    let mut result = Vec::new();
1143    for u in 0..n {
1144        if let Some(v) = pair_u[u] {
1145            result.push((u, v));
1146        }
1147    }
1148    result
1149}
1150
1151pub(crate) fn hopcroft_karp_bfs<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(
1152    graph: &Graph<V>,
1153    u_nodes: &[usize],
1154    pair_u: &mut [Option<usize>],
1155    pair_v: &mut [Option<usize>],
1156    dist: &mut [usize],
1157) -> bool {
1158    let mut queue = VecDeque::new();
1159    for &u in u_nodes {
1160        if pair_u[u].is_none() {
1161            dist[u] = 0;
1162            queue.push_back(u);
1163        } else {
1164            dist[u] = usize::MAX;
1165        }
1166    }
1167    let mut found_path = false;
1168
1169    while let Some(u) = queue.pop_front() {
1170        if let Some(neighbors) = graph.adj.get(u) {
1171            for &(v, _) in neighbors {
1172                if let Some(next_u_opt) = pair_v.get(v) {
1173                    if let Some(next_u) = next_u_opt {
1174                        if dist[*next_u] == usize::MAX {
1175                            dist[*next_u] = dist[u] + 1;
1176                            queue.push_back(*next_u);
1177                        }
1178                    } else {
1179                        // Unmatched V node found
1180                        found_path = true;
1181                    }
1182                }
1183            }
1184        }
1185    }
1186    found_path
1187}
1188
1189pub(crate) fn hopcroft_karp_dfs<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(
1190    graph: &Graph<V>,
1191    u: usize,
1192    pair_u: &mut [Option<usize>],
1193    pair_v: &mut [Option<usize>],
1194    dist: &mut [usize],
1195) -> bool {
1196    if let Some(neighbors) = graph.adj.get(u) {
1197        for &(v, _) in neighbors {
1198            if let Some(next_u_opt) = pair_v.get(v) {
1199                if let Some(next_u) = next_u_opt {
1200                    if dist[*next_u] == dist[u] + 1 {
1201                        if hopcroft_karp_dfs(graph, *next_u, pair_u, pair_v, dist) {
1202                            pair_v[v] = Some(u);
1203                            pair_u[u] = Some(v);
1204                            return true;
1205                        }
1206                    }
1207                } else {
1208                    // Found augmenting path to an unmatched V node
1209                    pair_v[v] = Some(u);
1210                    pair_u[u] = Some(v);
1211                    return true;
1212                }
1213            }
1214        }
1215    }
1216    dist[u] = usize::MAX;
1217    false
1218}
1219
1220/// Finds the maximum cardinality matching in a general graph using Edmonds's Blossom Algorithm.
1221///
1222/// Edmonds's Blossom Algorithm is a polynomial-time algorithm for finding maximum matchings
1223/// in general (non-bipartite) graphs. It works by iteratively finding augmenting paths
1224/// and handling "blossoms" (odd-length cycles).
1225///
1226/// # Arguments
1227/// * `graph` - The graph to analyze.
1228///
1229/// # Returns
1230/// A `Vec<(usize, usize)>` representing the edges `(u, v)` in the maximum matching.
1231#[allow(unused_variables)]
1232pub fn blossom_algorithm<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(
1233    graph: &Graph<V>,
1234) -> Vec<(usize, usize)> {
1235    let n = graph.nodes.len();
1236    let mut matching = vec![None; n];
1237    let mut matches = 0;
1238
1239    for i in 0..n {
1240        if matching[i].is_none() {
1241            let path = find_augmenting_path_with_blossoms(graph, i, &matching);
1242            if !path.is_empty() {
1243                matches += 1;
1244                let mut u = path[0];
1245                for &v in path.iter().skip(1) {
1246                    matching[u] = Some(v);
1247                    matching[v] = Some(u);
1248                    u = v;
1249                }
1250            }
1251        }
1252    }
1253
1254    let mut result = Vec::new();
1255    for u in 0..n {
1256        if let Some(v) = matching[u] {
1257            if u < v {
1258                result.push((u, v));
1259            }
1260        }
1261    }
1262    result
1263}
1264
1265pub(crate) fn find_augmenting_path_with_blossoms<
1266    V: Eq + std::hash::Hash + Clone + std::fmt::Debug,
1267>(
1268    graph: &Graph<V>,
1269    start_node: usize,
1270    matching: &[Option<usize>],
1271) -> Vec<usize> {
1272    let n = graph.nodes.len();
1273    let mut parent = vec![None; n];
1274    let mut origin = (0..n).collect::<Vec<_>>();
1275    let mut level = vec![-1; n];
1276    let mut queue = VecDeque::new();
1277
1278    level[start_node] = 0;
1279    queue.push_back(start_node);
1280
1281    while let Some(u) = queue.pop_front() {
1282        if let Some(neighbors) = graph.adj.get(u) {
1283            for &(v, _) in neighbors {
1284                if level[v] == -1 {
1285                    // Unvisited node
1286                    if let Some(w) = matching[v] {
1287                        parent[v] = Some(u);
1288                        level[v] = 1;
1289                        level[w] = 0;
1290                        queue.push_back(w);
1291                    } else {
1292                        // Found an augmenting path
1293                        parent[v] = Some(u);
1294                        let mut path = vec![v, u];
1295                        let mut curr = u;
1296                        while let Some(p) = parent[curr] {
1297                            path.push(p);
1298                            curr = p;
1299                        }
1300                        return path;
1301                    }
1302                } else if level[v] == 0 {
1303                    // Found a blossom
1304                    let base = find_common_ancestor(&origin, &parent, u, v);
1305                    contract_blossom::<V>(
1306                        base,
1307                        u,
1308                        v,
1309                        &mut queue,
1310                        &mut level,
1311                        &mut origin,
1312                        &mut parent,
1313                        matching,
1314                    );
1315                    contract_blossom::<V>(
1316                        base,
1317                        v,
1318                        u,
1319                        &mut queue,
1320                        &mut level,
1321                        &mut origin,
1322                        &mut parent,
1323                        matching,
1324                    );
1325                }
1326            }
1327        }
1328    }
1329    vec![] // No augmenting path found
1330}
1331
1332pub(crate) fn find_common_ancestor(
1333    origin: &[usize],
1334    parent: &[Option<usize>],
1335    mut u: usize,
1336    mut v: usize,
1337) -> usize {
1338    let mut visited = vec![false; origin.len()];
1339    loop {
1340        u = origin[u];
1341        visited[u] = true;
1342        if parent[u].is_none() {
1343            break;
1344        }
1345        u = parent[u].unwrap();
1346    }
1347    loop {
1348        v = origin[v];
1349        if visited[v] {
1350            return v;
1351        }
1352        v = parent[v].unwrap();
1353    }
1354}
1355
1356pub(crate) fn contract_blossom<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(
1357    base: usize,
1358    mut u: usize,
1359    v: usize,
1360    queue: &mut VecDeque<usize>,
1361    level: &mut Vec<i32>,
1362    origin: &mut [usize],
1363    parent: &mut Vec<Option<usize>>,
1364    matching: &[Option<usize>],
1365) {
1366    while origin[u] != base {
1367        parent[u] = Some(v);
1368        origin[u] = base;
1369        if let Some(w) = matching[u] {
1370            if level[w] == -1 {
1371                level[w] = 0;
1372                queue.push_back(w);
1373            }
1374        }
1375        u = parent[u].unwrap();
1376    }
1377}
1378
1379// =====================================================================================
1380// region: Path Finding
1381// =====================================================================================
1382
1383/// Finds the shortest path in an unweighted graph from a source node using BFS.
1384///
1385/// Since all edge weights are implicitly 1, BFS naturally finds the shortest path
1386/// in terms of number of edges.
1387///
1388/// # Arguments
1389/// * `graph` - The graph to search.
1390/// * `start_node` - The index of the starting node.
1391///
1392/// # Returns
1393/// A `HashMap<usize, (usize, Option<usize>)>` where keys are node IDs and values are
1394/// `(distance_from_start, predecessor_node_id)`.
1395pub fn shortest_path_unweighted<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(
1396    graph: &Graph<V>,
1397    start_node: usize,
1398) -> HashMap<usize, (usize, Option<usize>)> {
1399    let mut distances = HashMap::new();
1400    let mut predecessors = HashMap::new();
1401    let mut queue = VecDeque::new();
1402
1403    distances.insert(start_node, 0);
1404    predecessors.insert(start_node, None);
1405    queue.push_back(start_node);
1406
1407    while let Some(u) = queue.pop_front() {
1408        let u_dist = *distances.get(&u).unwrap();
1409        if let Some(neighbors) = graph.adj.get(u) {
1410            for &(v, _) in neighbors {
1411                if !distances.contains_key(&v) {
1412                    distances.insert(v, u_dist + 1);
1413                    predecessors.insert(v, Some(u));
1414                    queue.push_back(v);
1415                }
1416            }
1417        }
1418    }
1419
1420    let mut result = HashMap::new();
1421    for (node, dist) in distances {
1422        result.insert(node, (dist, predecessors.get(&node).cloned().flatten()));
1423    }
1424    result
1425}
1426
1427/// Finds the shortest paths from a single source using Dijkstra's algorithm.
1428///
1429/// Dijkstra's algorithm is a greedy algorithm that solves the single-source
1430/// shortest path problem for a graph with non-negative edge weights.
1431///
1432/// # Arguments
1433/// * `graph` - The graph to search.
1434/// * `start_node` - The index of the starting node.
1435///
1436/// # Returns
1437/// A `HashMap<usize, (Expr, Option<usize>)>` where keys are node IDs and values are
1438/// `(shortest_distance, predecessor_node_id)`.
1439pub fn dijkstra<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(
1440    graph: &Graph<V>,
1441    start_node: usize,
1442) -> HashMap<usize, (Expr, Option<usize>)> {
1443    let mut dist = HashMap::new();
1444    let mut prev = HashMap::new();
1445    let mut pq = std::collections::BinaryHeap::new();
1446
1447    for node_id in 0..graph.nodes.len() {
1448        dist.insert(node_id, Expr::Infinity);
1449    }
1450
1451    dist.insert(start_node, Expr::Constant(0.0));
1452    prev.insert(start_node, None);
1453    // BinaryHeap is a max-heap, so we store negative costs.
1454    pq.push((OrderedFloat(0.0), start_node));
1455
1456    while let Some((cost, u)) = pq.pop() {
1457        let cost = -cost.0;
1458        if cost > as_f64(dist.get(&u).unwrap()).unwrap_or(f64::INFINITY) {
1459            continue;
1460        }
1461
1462        if let Some(neighbors) = graph.adj.get(u) {
1463            for &(v, ref weight) in neighbors {
1464                let new_dist = simplify(Expr::Add(
1465                    Box::new(Expr::Constant(cost)),
1466                    Box::new(weight.clone()),
1467                ));
1468                if as_f64(&new_dist).unwrap_or(f64::INFINITY)
1469                    < as_f64(dist.get(&v).unwrap()).unwrap_or(f64::INFINITY)
1470                {
1471                    dist.insert(v, new_dist.clone());
1472                    prev.insert(v, Some(u));
1473                    pq.push((OrderedFloat(-as_f64(&new_dist).unwrap()), v));
1474                }
1475            }
1476        }
1477    }
1478
1479    let mut result = HashMap::new();
1480    for (node, d) in dist {
1481        result.insert(node, (d, prev.get(&node).cloned().flatten()));
1482    }
1483    result
1484}
1485
1486/// Finds all-pairs shortest paths using the Floyd-Warshall algorithm.
1487///
1488/// The Floyd-Warshall algorithm is an all-pairs shortest path algorithm that works
1489/// for both directed and undirected graphs with non-negative or negative edge weights
1490/// (but no negative cycles).
1491///
1492/// # Arguments
1493/// * `graph` - The graph to analyze.
1494///
1495/// # Returns
1496/// An `Expr::Matrix` where `M[i][j]` is the shortest distance from node `i` to node `j`.
1497pub fn floyd_warshall<V: Eq + std::hash::Hash + Clone + std::fmt::Debug>(graph: &Graph<V>) -> Expr {
1498    let n = graph.nodes.len();
1499    let mut dist = vec![vec![Expr::Infinity; n]; n];
1500
1501    for i in 0..n {
1502        dist[i][i] = Expr::Constant(0.0);
1503    }
1504
1505    for u in 0..n {
1506        if let Some(neighbors) = graph.adj.get(u) {
1507            for &(v, ref weight) in neighbors {
1508                dist[u][v] = weight.clone();
1509            }
1510        }
1511    }
1512
1513    for k in 0..n {
1514        for i in 0..n {
1515            for j in 0..n {
1516                let new_dist = simplify(Expr::Add(
1517                    Box::new(dist[i][k].clone()),
1518                    Box::new(dist[k][j].clone()),
1519                ));
1520                // This comparison needs to handle symbolic expressions.
1521                // For now, we assume numerical weights.
1522                if as_f64(&dist[i][j]).unwrap_or(f64::INFINITY)
1523                    > as_f64(&new_dist).unwrap_or(f64::INFINITY)
1524                {
1525                    dist[i][j] = new_dist;
1526                }
1527            }
1528        }
1529    }
1530
1531    Expr::Matrix(dist)
1532}
1533
1534/// Performs spectral analysis on a graph matrix (e.g., Adjacency or Laplacian).
1535///
1536/// This function computes the eigenvalues and eigenvectors of the given matrix,
1537/// which are crucial for understanding various graph properties like connectivity,
1538/// centrality, and clustering.
1539///
1540/// # Arguments
1541/// * `matrix` - The graph matrix as an `Expr::Matrix`.
1542///
1543/// # Returns
1544/// A `Result` containing a tuple `(eigenvalues, eigenvectors_matrix)`.
1545/// `eigenvalues` is a column vector of eigenvalues.
1546/// `eigenvectors_matrix` is a matrix where each column is an eigenvector.
1547pub fn spectral_analysis(matrix: &Expr) -> Result<(Expr, Expr), String> {
1548    crate::symbolic::matrix::eigen_decomposition(matrix)
1549}
1550
1551/// Computes the algebraic connectivity of a graph.
1552///
1553/// The algebraic connectivity is the second-smallest eigenvalue of the Laplacian matrix
1554/// of a graph. It measures how well-connected a graph is and is related to graph robustness.
1555///
1556/// # Arguments
1557/// * `graph` - The graph to analyze.
1558///
1559/// # Returns
1560/// A `Result` containing an `Expr` representing the algebraic connectivity,
1561/// or an error string if computation fails or the graph has fewer than 2 eigenvalues.
1562pub fn algebraic_connectivity<V>(graph: &Graph<V>) -> Result<Expr, String>
1563where
1564    V: Clone,
1565    V: Debug,
1566    V: Eq,
1567    V: Hash,
1568{
1569    let laplacian = graph.to_laplacian_matrix();
1570    let (eigenvalues, _) = spectral_analysis(&laplacian)?;
1571
1572    if let Expr::Matrix(eig_vec) = eigenvalues {
1573        if eig_vec.len() < 2 {
1574            return Err("Graph has fewer than 2 eigenvalues.".to_string());
1575        }
1576        // Eigenvalues from `eigen_decomposition` are not guaranteed to be sorted.
1577        // We need to convert them to f64, sort, and then get the second one.
1578        let mut numerical_eigenvalues = Vec::new();
1579        for val_expr in eig_vec.iter().flatten() {
1580            if let Some(val) = as_f64(val_expr) {
1581                numerical_eigenvalues.push(val);
1582            } else {
1583                return Err("Eigenvalues are not all numerical, cannot sort.".to_string());
1584            }
1585        }
1586        numerical_eigenvalues.sort_by(|a, b| a.partial_cmp(b).unwrap());
1587        Ok(Expr::Constant(numerical_eigenvalues[1]))
1588    } else {
1589        Err("Eigenvalue computation did not return a vector.".to_string())
1590    }
1591}