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