use std::collections::VecDeque;
use scirs2_core::ndarray::Array2;
use crate::error::{GraphError, Result};
pub fn topological_sort(adj: &Array2<f64>) -> Result<Vec<usize>> {
let n = adj.nrows();
if n == 0 {
return Ok(vec![]);
}
if adj.ncols() != n {
return Err(GraphError::InvalidGraph("adjacency matrix must be square".into()));
}
let mut in_degree = vec![0usize; n];
for j in 0..n {
for i in 0..n {
if adj[[i, j]] > 0.0 {
in_degree[j] += 1;
}
}
}
let mut queue: VecDeque<usize> = in_degree
.iter()
.enumerate()
.filter(|(_, &d)| d == 0)
.map(|(i, _)| i)
.collect();
let mut order = Vec::with_capacity(n);
while let Some(u) = queue.pop_front() {
order.push(u);
for v in 0..n {
if adj[[u, v]] > 0.0 {
in_degree[v] -= 1;
if in_degree[v] == 0 {
queue.push_back(v);
}
}
}
}
if order.len() != n {
return Err(GraphError::CycleDetected {
start_node: "0".into(),
cycle_length: n - order.len(),
});
}
Ok(order)
}
pub fn all_simple_paths(
adj: &Array2<f64>,
source: usize,
target: usize,
max_length: usize,
) -> Vec<Vec<usize>> {
let n = adj.nrows();
if n == 0 || source >= n || target >= n {
return vec![];
}
if source == target {
return vec![vec![source]];
}
let mut results = Vec::new();
let mut path = vec![source];
let mut visited = vec![false; n];
visited[source] = true;
dfs_paths(adj, source, target, max_length, &mut path, &mut visited, &mut results);
results
}
fn dfs_paths(
adj: &Array2<f64>,
current: usize,
target: usize,
max_length: usize,
path: &mut Vec<usize>,
visited: &mut Vec<bool>,
results: &mut Vec<Vec<usize>>,
) {
if path.len() - 1 >= max_length {
return;
}
let n = adj.nrows();
for next in 0..n {
if adj[[current, next]] <= 0.0 || visited[next] {
continue;
}
path.push(next);
if next == target {
results.push(path.clone());
} else {
visited[next] = true;
dfs_paths(adj, next, target, max_length, path, visited, results);
visited[next] = false;
}
path.pop();
}
}
pub fn eulerian_circuit(adj: &Array2<f64>) -> Result<Vec<usize>> {
let n = adj.nrows();
if n == 0 {
return Err(GraphError::InvalidGraph("empty adjacency matrix".into()));
}
for i in 0..n {
let deg = degree_of(adj, i);
if deg % 2 != 0 {
return Err(GraphError::GraphStructureError {
expected: "all even degrees".into(),
found: format!("node {i} has degree {deg}"),
context: "eulerian_circuit".into(),
});
}
}
let start = (0..n).find(|&i| degree_of(adj, i) > 0).unwrap_or(0);
if !is_eulerian_connected(adj) {
return Err(GraphError::GraphStructureError {
expected: "connected graph".into(),
found: "graph is disconnected".into(),
context: "eulerian_circuit".into(),
});
}
hierholzer(adj, start)
}
pub fn eulerian_path(adj: &Array2<f64>, source: usize) -> Result<Vec<usize>> {
let n = adj.nrows();
if n == 0 {
return Err(GraphError::InvalidGraph("empty adjacency matrix".into()));
}
if source >= n {
return Err(GraphError::InvalidParameter {
param: "source".into(),
value: source.to_string(),
expected: format!("< {n}"),
context: "eulerian_path".into(),
});
}
let odd_nodes: Vec<usize> = (0..n)
.filter(|&i| degree_of(adj, i) % 2 != 0)
.collect();
match odd_nodes.len() {
0 => {
eulerian_circuit(adj)
}
2 => {
if !odd_nodes.contains(&source) {
return Err(GraphError::GraphStructureError {
expected: format!("source {source} must be an odd-degree node"),
found: format!("odd nodes are {:?}", odd_nodes),
context: "eulerian_path".into(),
});
}
if !is_eulerian_connected(adj) {
return Err(GraphError::GraphStructureError {
expected: "connected graph".into(),
found: "graph is disconnected".into(),
context: "eulerian_path".into(),
});
}
hierholzer(adj, source)
}
_ => Err(GraphError::GraphStructureError {
expected: "0 or 2 odd-degree nodes".into(),
found: format!("{} odd-degree nodes", odd_nodes.len()),
context: "eulerian_path".into(),
}),
}
}
fn degree_of(adj: &Array2<f64>, i: usize) -> usize {
(0..adj.ncols()).filter(|&j| adj[[i, j]] > 0.0).count()
}
fn is_eulerian_connected(adj: &Array2<f64>) -> bool {
let n = adj.nrows();
let non_isolated: Vec<usize> = (0..n).filter(|&i| degree_of(adj, i) > 0).collect();
if non_isolated.is_empty() {
return true;
}
let start = non_isolated[0];
let mut visited = vec![false; n];
let mut stack = vec![start];
visited[start] = true;
while let Some(u) = stack.pop() {
for v in 0..n {
if adj[[u, v]] > 0.0 && !visited[v] {
visited[v] = true;
stack.push(v);
}
}
}
non_isolated.iter().all(|&i| visited[i])
}
fn hierholzer(adj: &Array2<f64>, start: usize) -> Result<Vec<usize>> {
let n = adj.nrows();
let mut edge_count = vec![vec![0i64; n]; n];
let mut total_edges = 0i64;
for i in 0..n {
for j in 0..n {
if adj[[i, j]] > 0.0 {
edge_count[i][j] = 1;
total_edges += 1;
}
}
}
total_edges /= 2;
let mut circuit = Vec::new();
let mut stack = vec![start];
while let Some(&u) = stack.last() {
let next = (0..n).find(|&v| edge_count[u][v] > 0);
match next {
Some(v) => {
stack.push(v);
edge_count[u][v] -= 1;
edge_count[v][u] -= 1;
}
None => {
circuit.push(stack.pop().expect("stack not empty"));
}
}
}
if circuit.len() as i64 != total_edges + 1 {
return Err(GraphError::AlgorithmError(format!(
"Hierholzer: expected {} nodes in circuit, got {}",
total_edges + 1,
circuit.len()
)));
}
circuit.reverse();
Ok(circuit)
}
pub fn hamiltonian_path_heuristic(adj: &Array2<f64>, start: usize) -> Result<Vec<usize>> {
let n = adj.nrows();
if n == 0 {
return Err(GraphError::InvalidGraph("empty adjacency matrix".into()));
}
if start >= n {
return Err(GraphError::InvalidParameter {
param: "start".into(),
value: start.to_string(),
expected: format!("< {n}"),
context: "hamiltonian_path_heuristic".into(),
});
}
let mut visited = vec![false; n];
let mut path = vec![start];
visited[start] = true;
for _ in 1..n {
let current = *path.last().expect("path not empty");
let mut best_next = None;
let mut best_w = f64::NEG_INFINITY;
for v in 0..n {
if !visited[v] && adj[[current, v]] > 0.0 && adj[[current, v]] > best_w {
best_w = adj[[current, v]];
best_next = Some(v);
}
}
match best_next {
Some(v) => {
visited[v] = true;
path.push(v);
}
None => break, }
}
Ok(path)
}
pub fn is_bipartite(adj: &Array2<f64>) -> (bool, Option<Vec<usize>>) {
let n = adj.nrows();
if n == 0 {
return (true, Some(vec![]));
}
for i in 0..n {
if adj[[i, i]] > 0.0 {
return (false, None);
}
}
let mut colour = vec![usize::MAX; n];
for start in 0..n {
if colour[start] != usize::MAX {
continue;
}
colour[start] = 0;
let mut queue = VecDeque::new();
queue.push_back(start);
while let Some(u) = queue.pop_front() {
let c = colour[u];
for v in 0..n {
if adj[[u, v]] <= 0.0 {
continue;
}
if colour[v] == usize::MAX {
colour[v] = 1 - c;
queue.push_back(v);
} else if colour[v] == c {
return (false, None);
}
}
}
}
(true, Some(colour))
}
pub fn k_core_decomposition(adj: &Array2<f64>) -> Vec<usize> {
let n = adj.nrows();
if n == 0 {
return vec![];
}
let mut deg: Vec<usize> = (0..n).map(|i| degree_of(adj, i)).collect();
let mut removed = vec![false; n];
let mut core = vec![0usize; n];
let max_deg = *deg.iter().max().unwrap_or(&0);
for k in 0..=max_deg {
let mut changed = true;
while changed {
changed = false;
for u in 0..n {
if removed[u] || deg[u] > k {
continue;
}
removed[u] = true;
core[u] = k;
for v in 0..n {
if !removed[v] && adj[[u, v]] > 0.0 {
deg[v] = deg[v].saturating_sub(1);
changed = true;
}
}
}
}
}
core
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
use std::collections::HashSet;
#[test]
fn test_topo_sort_dag() {
let mut adj = Array2::<f64>::zeros((4, 4));
adj[[0, 1]] = 1.0;
adj[[0, 2]] = 1.0;
adj[[1, 3]] = 1.0;
adj[[2, 3]] = 1.0;
let order = topological_sort(&adj).expect("topo sort");
assert_eq!(order.len(), 4);
let pos: Vec<usize> = {
let mut p = vec![0usize; 4];
for (i, &v) in order.iter().enumerate() {
p[v] = i;
}
p
};
assert!(pos[0] < pos[1], "0 before 1");
assert!(pos[0] < pos[2], "0 before 2");
assert!(pos[1] < pos[3], "1 before 3");
assert!(pos[2] < pos[3], "2 before 3");
}
#[test]
fn test_topo_sort_cycle_error() {
let mut adj = Array2::<f64>::zeros((3, 3));
adj[[0, 1]] = 1.0;
adj[[1, 2]] = 1.0;
adj[[2, 0]] = 1.0;
assert!(topological_sort(&adj).is_err());
}
#[test]
fn test_topo_sort_empty() {
let adj = Array2::<f64>::zeros((0, 0));
assert_eq!(topological_sort(&adj).expect("empty"), vec![] as Vec<usize>);
}
#[test]
fn test_topo_sort_linear_chain() {
let mut adj = Array2::<f64>::zeros((5, 5));
for i in 0..4 {
adj[[i, i + 1]] = 1.0;
}
let order = topological_sort(&adj).expect("chain");
assert_eq!(order, vec![0, 1, 2, 3, 4]);
}
#[test]
fn test_all_simple_paths_triangle() {
let mut adj = Array2::<f64>::zeros((3, 3));
adj[[0, 1]] = 1.0; adj[[1, 0]] = 1.0;
adj[[1, 2]] = 1.0; adj[[2, 1]] = 1.0;
adj[[0, 2]] = 1.0; adj[[2, 0]] = 1.0;
let paths = all_simple_paths(&adj, 0, 2, 3);
assert!(paths.len() >= 2, "should find at least 2 paths, got {}", paths.len());
assert!(paths.iter().any(|p| p == &[0, 2]), "direct path 0→2 not found");
assert!(paths.iter().any(|p| p == &[0, 1, 2]), "path 0→1→2 not found");
}
#[test]
fn test_all_simple_paths_max_length() {
let mut adj = Array2::<f64>::zeros((4, 4));
for i in 0..3 {
adj[[i, i + 1]] = 1.0;
adj[[i + 1, i]] = 1.0;
}
let paths = all_simple_paths(&adj, 0, 3, 1);
assert!(paths.is_empty());
let paths2 = all_simple_paths(&adj, 0, 3, 3);
assert_eq!(paths2.len(), 1);
assert_eq!(paths2[0], vec![0, 1, 2, 3]);
}
#[test]
fn test_all_simple_paths_self_loop() {
let adj = Array2::<f64>::zeros((4, 4));
let paths = all_simple_paths(&adj, 2, 2, 5);
assert_eq!(paths, vec![vec![2]]);
}
#[test]
fn test_eulerian_circuit_cycle4() {
let mut adj = Array2::<f64>::zeros((4, 4));
adj[[0, 1]] = 1.0; adj[[1, 0]] = 1.0;
adj[[1, 2]] = 1.0; adj[[2, 1]] = 1.0;
adj[[2, 3]] = 1.0; adj[[3, 2]] = 1.0;
adj[[3, 0]] = 1.0; adj[[0, 3]] = 1.0;
let circuit = eulerian_circuit(&adj).expect("euler circuit");
assert_eq!(circuit.len(), 5, "circuit length");
assert_eq!(circuit.first(), circuit.last(), "start == end");
let unique: HashSet<usize> = circuit.iter().cloned().collect();
assert_eq!(unique.len(), 4);
}
#[test]
fn test_eulerian_circuit_odd_degree_error() {
let mut adj = Array2::<f64>::zeros((3, 3));
adj[[0, 1]] = 1.0; adj[[1, 0]] = 1.0;
adj[[1, 2]] = 1.0; adj[[2, 1]] = 1.0;
assert!(eulerian_circuit(&adj).is_err());
}
#[test]
fn test_eulerian_path_line() {
let mut adj = Array2::<f64>::zeros((3, 3));
adj[[0, 1]] = 1.0; adj[[1, 0]] = 1.0;
adj[[1, 2]] = 1.0; adj[[2, 1]] = 1.0;
let path = eulerian_path(&adj, 0).expect("euler path");
assert_eq!(path.len(), 3, "path should visit 3 nodes");
assert_eq!(path[0], 0);
assert_eq!(*path.last().expect("path last"), 2);
}
#[test]
fn test_eulerian_path_wrong_start_error() {
let mut adj = Array2::<f64>::zeros((3, 3));
adj[[0, 1]] = 1.0; adj[[1, 0]] = 1.0;
adj[[1, 2]] = 1.0; adj[[2, 1]] = 1.0;
assert!(eulerian_path(&adj, 1).is_err());
}
#[test]
fn test_hamiltonian_heuristic_complete() {
let mut adj = Array2::<f64>::ones((4, 4));
for i in 0..4 { adj[[i, i]] = 0.0; }
let path = hamiltonian_path_heuristic(&adj, 0).expect("ham");
assert_eq!(path.len(), 4);
let unique: HashSet<usize> = path.iter().cloned().collect();
assert_eq!(unique.len(), 4);
}
#[test]
fn test_hamiltonian_heuristic_path_graph() {
let mut adj = Array2::<f64>::zeros((5, 5));
for i in 0..4 {
adj[[i, i + 1]] = 1.0;
adj[[i + 1, i]] = 1.0;
}
let path = hamiltonian_path_heuristic(&adj, 0).expect("ham");
assert_eq!(path, vec![0, 1, 2, 3, 4]);
}
#[test]
fn test_hamiltonian_heuristic_error() {
let adj = Array2::<f64>::zeros((0, 0));
assert!(hamiltonian_path_heuristic(&adj, 0).is_err());
}
#[test]
fn test_bipartite_even_cycle() {
let mut adj = Array2::<f64>::zeros((4, 4));
adj[[0, 1]] = 1.0; adj[[1, 0]] = 1.0;
adj[[1, 2]] = 1.0; adj[[2, 1]] = 1.0;
adj[[2, 3]] = 1.0; adj[[3, 2]] = 1.0;
adj[[3, 0]] = 1.0; adj[[0, 3]] = 1.0;
let (bip, colours) = is_bipartite(&adj);
assert!(bip, "C4 should be bipartite");
let colours = colours.expect("should have colouring");
for i in 0..4 {
for j in 0..4 {
if adj[[i, j]] > 0.0 {
assert_ne!(colours[i], colours[j], "adjacent nodes same colour");
}
}
}
}
#[test]
fn test_bipartite_complete_bipartite() {
let mut adj = Array2::<f64>::zeros((6, 6));
for i in 0..3 {
for j in 3..6 {
adj[[i, j]] = 1.0;
adj[[j, i]] = 1.0;
}
}
let (bip, _) = is_bipartite(&adj);
assert!(bip, "K_{{3,3}} should be bipartite");
}
#[test]
fn test_bipartite_triangle_not() {
let mut adj = Array2::<f64>::zeros((3, 3));
adj[[0, 1]] = 1.0; adj[[1, 0]] = 1.0;
adj[[1, 2]] = 1.0; adj[[2, 1]] = 1.0;
adj[[0, 2]] = 1.0; adj[[2, 0]] = 1.0;
let (bip, _) = is_bipartite(&adj);
assert!(!bip, "K3 (triangle) should NOT be bipartite");
}
#[test]
fn test_bipartite_self_loop_not() {
let mut adj = Array2::<f64>::zeros((3, 3));
adj[[1, 1]] = 1.0; let (bip, _) = is_bipartite(&adj);
assert!(!bip, "self-loop graph should NOT be bipartite");
}
#[test]
fn test_k_core_path_graph() {
let mut adj = Array2::<f64>::zeros((5, 5));
for i in 0..4 {
adj[[i, i + 1]] = 1.0;
adj[[i + 1, i]] = 1.0;
}
let cores = k_core_decomposition(&adj);
assert_eq!(cores.len(), 5);
for (i, &c) in cores.iter().enumerate() {
assert_eq!(c, 1, "path node {i} should have core 1, got {c}");
}
}
#[test]
fn test_k_core_triangle_clique() {
let mut adj = Array2::<f64>::zeros((3, 3));
adj[[0, 1]] = 1.0; adj[[1, 0]] = 1.0;
adj[[1, 2]] = 1.0; adj[[2, 1]] = 1.0;
adj[[0, 2]] = 1.0; adj[[2, 0]] = 1.0;
let cores = k_core_decomposition(&adj);
for &c in &cores {
assert_eq!(c, 2, "triangle node should have core 2");
}
}
#[test]
fn test_k_core_mixed_graph() {
let mut adj = Array2::<f64>::zeros((4, 4));
adj[[0, 1]] = 1.0; adj[[1, 0]] = 1.0;
adj[[1, 2]] = 1.0; adj[[2, 1]] = 1.0;
adj[[0, 2]] = 1.0; adj[[2, 0]] = 1.0;
adj[[0, 3]] = 1.0; adj[[3, 0]] = 1.0;
let cores = k_core_decomposition(&adj);
assert_eq!(cores[0], 2, "node 0 in K3 should be 2-core");
assert_eq!(cores[1], 2, "node 1 in K3 should be 2-core");
assert_eq!(cores[2], 2, "node 2 in K3 should be 2-core");
assert_eq!(cores[3], 1, "pendant node 3 should be 1-core");
}
#[test]
fn test_k_core_empty() {
let adj = Array2::<f64>::zeros((0, 0));
assert_eq!(k_core_decomposition(&adj), vec![] as Vec<usize>);
}
#[test]
fn test_k_core_isolated_nodes() {
let adj = Array2::<f64>::zeros((3, 3));
let cores = k_core_decomposition(&adj);
for &c in &cores {
assert_eq!(c, 0);
}
}
}