use std::collections::VecDeque;
use crate::error::{SolverError, SolverResult};
#[derive(Debug, Clone)]
pub struct Permutation {
pub perm: Vec<usize>,
pub iperm: Vec<usize>,
}
impl Permutation {
pub fn new(perm: Vec<usize>) -> SolverResult<Self> {
let n = perm.len();
let mut iperm = vec![0usize; n];
let mut seen = vec![false; n];
for (i, &p) in perm.iter().enumerate() {
if p >= n {
return Err(SolverError::InternalError(format!(
"permutation index {p} out of range for size {n}"
)));
}
if seen[p] {
return Err(SolverError::InternalError(format!(
"duplicate permutation index {p}"
)));
}
seen[p] = true;
iperm[p] = i;
}
Ok(Self { perm, iperm })
}
pub fn identity(n: usize) -> Self {
Self {
perm: (0..n).collect(),
iperm: (0..n).collect(),
}
}
pub fn apply<T: Copy>(&self, data: &[T]) -> Vec<T> {
self.perm.iter().map(|&p| data[p]).collect()
}
pub fn compose(&self, other: &Permutation) -> SolverResult<Permutation> {
let composed: Vec<usize> = other.perm.iter().map(|&i| self.perm[i]).collect();
Permutation::new(composed)
}
pub fn inverse(&self) -> &[usize] {
&self.iperm
}
}
#[derive(Debug, Clone)]
pub struct AdjacencyGraph {
pub num_vertices: usize,
pub adj_ptr: Vec<usize>,
pub adj_list: Vec<usize>,
}
impl AdjacencyGraph {
pub fn from_symmetric_csr(row_ptr: &[i32], col_indices: &[i32], n: usize) -> Self {
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
for row in 0..n {
let start = row_ptr[row] as usize;
let end = row_ptr[row + 1] as usize;
for &col_i32 in &col_indices[start..end] {
let col = col_i32 as usize;
if col != row && col < n {
adj[row].push(col);
}
}
}
for list in &mut adj {
list.sort_unstable();
list.dedup();
}
let mut adj_ptr = Vec::with_capacity(n + 1);
let mut adj_list = Vec::new();
adj_ptr.push(0);
for list in &adj {
adj_list.extend_from_slice(list);
adj_ptr.push(adj_list.len());
}
Self {
num_vertices: n,
adj_ptr,
adj_list,
}
}
pub fn degree(&self, v: usize) -> usize {
self.adj_ptr[v + 1] - self.adj_ptr[v]
}
pub fn neighbors(&self, v: usize) -> &[usize] {
&self.adj_list[self.adj_ptr[v]..self.adj_ptr[v + 1]]
}
}
#[derive(Debug, Clone)]
pub struct SeparatorResult {
pub separator: Vec<usize>,
pub part_a: Vec<usize>,
pub part_b: Vec<usize>,
pub balance_ratio: f64,
}
#[derive(Debug, Clone)]
pub struct OrderingQuality {
pub estimated_fill: usize,
pub separator_size: usize,
pub balance_ratio: f64,
}
fn bfs_levels(graph: &AdjacencyGraph, source: usize, in_subgraph: &[bool]) -> Vec<Vec<usize>> {
let n = graph.num_vertices;
let mut visited = vec![false; n];
visited[source] = true;
let mut levels: Vec<Vec<usize>> = vec![vec![source]];
let mut queue = VecDeque::new();
queue.push_back(source);
while !queue.is_empty() {
let mut next_level = Vec::new();
let count = queue.len();
for _ in 0..count {
if let Some(v) = queue.pop_front() {
for &nb in graph.neighbors(v) {
if in_subgraph[nb] && !visited[nb] {
visited[nb] = true;
next_level.push(nb);
queue.push_back(nb);
}
}
}
}
if !next_level.is_empty() {
levels.push(next_level);
}
}
levels
}
fn pseudo_peripheral(graph: &AdjacencyGraph, subgraph: &[usize]) -> usize {
if subgraph.is_empty() {
return 0;
}
let n = graph.num_vertices;
let mut in_sub = vec![false; n];
for &v in subgraph {
in_sub[v] = true;
}
let mut start = subgraph[0];
let mut min_deg = usize::MAX;
for &v in subgraph {
let d = graph.neighbors(v).iter().filter(|&&nb| in_sub[nb]).count();
if d < min_deg {
min_deg = d;
start = v;
}
}
for _ in 0..2 {
let levels = bfs_levels(graph, start, &in_sub);
if let Some(last) = levels.last() {
let mut best = last[0];
let mut best_deg = usize::MAX;
for &v in last {
let d = graph.neighbors(v).iter().filter(|&&nb| in_sub[nb]).count();
if d < best_deg {
best_deg = d;
best = v;
}
}
start = best;
}
}
start
}
fn coarsen(graph: &AdjacencyGraph, subgraph: &[usize]) -> (Vec<i32>, usize) {
let n = graph.num_vertices;
let mut coarse_map = vec![-1i32; n];
let mut matched = vec![false; n];
let mut coarse_id = 0usize;
let mut sorted = subgraph.to_vec();
sorted.sort_unstable_by_key(|&v| graph.degree(v));
let in_sub: Vec<bool> = {
let mut s = vec![false; n];
for &v in subgraph {
s[v] = true;
}
s
};
for &v in &sorted {
if matched[v] {
continue;
}
let mut best_nb = None;
let mut best_weight = 0usize;
for &nb in graph.neighbors(v) {
if in_sub[nb] && !matched[nb] && nb != v {
let w = graph.degree(v) + graph.degree(nb);
if w > best_weight {
best_weight = w;
best_nb = Some(nb);
}
}
}
let cid = coarse_id as i32;
coarse_map[v] = cid;
matched[v] = true;
if let Some(nb) = best_nb {
coarse_map[nb] = cid;
matched[nb] = true;
}
coarse_id += 1;
}
(coarse_map, coarse_id)
}
fn bfs_bisect(graph: &AdjacencyGraph, subgraph: &[usize]) -> SeparatorResult {
if subgraph.len() <= 1 {
return SeparatorResult {
separator: subgraph.to_vec(),
part_a: Vec::new(),
part_b: Vec::new(),
balance_ratio: 1.0,
};
}
let n = graph.num_vertices;
let mut in_sub = vec![false; n];
for &v in subgraph {
in_sub[v] = true;
}
let start = pseudo_peripheral(graph, subgraph);
let levels = bfs_levels(graph, start, &in_sub);
if levels.len() <= 2 {
let half = subgraph.len() / 2;
let part_a = subgraph[..half].to_vec();
let separator = vec![subgraph[half]];
let part_b = if half + 1 < subgraph.len() {
subgraph[half + 1..].to_vec()
} else {
Vec::new()
};
let balance = compute_balance(part_a.len(), part_b.len());
return SeparatorResult {
separator,
part_a,
part_b,
balance_ratio: balance,
};
}
let mid = levels.len() / 2;
let separator = levels[mid].clone();
let mut part_a = Vec::new();
let mut part_b = Vec::new();
for (i, level) in levels.iter().enumerate() {
if i < mid {
part_a.extend_from_slice(level);
} else if i > mid {
part_b.extend_from_slice(level);
}
}
let balance = compute_balance(part_a.len(), part_b.len());
SeparatorResult {
separator,
part_a,
part_b,
balance_ratio: balance,
}
}
fn compute_balance(a: usize, b: usize) -> f64 {
if a == 0 && b == 0 {
return 1.0;
}
let min_v = a.min(b) as f64;
let max_v = a.max(b) as f64;
if max_v == 0.0 { 1.0 } else { min_v / max_v }
}
fn fm_refine(graph: &AdjacencyGraph, result: &mut SeparatorResult) {
let n = graph.num_vertices;
let mut partition = vec![0u8; n];
for &v in &result.part_a {
partition[v] = 0;
}
for &v in &result.part_b {
partition[v] = 1;
}
for &v in &result.separator {
partition[v] = 2;
}
let total = result.part_a.len() + result.part_b.len() + result.separator.len();
if total <= 3 {
return;
}
let mut improved = true;
let mut iterations = 0;
while improved && iterations < 10 {
improved = false;
iterations += 1;
let sep_copy = result.separator.clone();
for &sv in &sep_copy {
let mut na = 0usize;
let mut nb = 0usize;
for &nb_v in graph.neighbors(sv) {
match partition[nb_v] {
0 => na += 1,
1 => nb += 1,
_ => {}
}
}
if na > 0 && nb == 0 {
partition[sv] = 0;
result.part_a.push(sv);
result.separator.retain(|&x| x != sv);
improved = true;
} else if nb > 0 && na == 0 {
partition[sv] = 1;
result.part_b.push(sv);
result.separator.retain(|&x| x != sv);
improved = true;
}
}
}
let a_len = result.part_a.len();
let b_len = result.part_b.len();
if a_len > 2 * b_len + 2 {
let mut candidates: Vec<usize> = result
.part_a
.iter()
.copied()
.filter(|&v| {
graph
.neighbors(v)
.iter()
.any(|&nb| partition[nb] == 1 || partition[nb] == 2)
})
.collect();
candidates.sort_unstable_by_key(|&v| graph.degree(v));
let to_move = (a_len - b_len) / 4;
for &v in candidates.iter().take(to_move) {
partition[v] = 2;
result.separator.push(v);
result.part_a.retain(|&x| x != v);
}
} else if b_len > 2 * a_len + 2 {
let mut candidates: Vec<usize> = result
.part_b
.iter()
.copied()
.filter(|&v| {
graph
.neighbors(v)
.iter()
.any(|&nb| partition[nb] == 0 || partition[nb] == 2)
})
.collect();
candidates.sort_unstable_by_key(|&v| graph.degree(v));
let to_move = (b_len - a_len) / 4;
for &v in candidates.iter().take(to_move) {
partition[v] = 2;
result.separator.push(v);
result.part_b.retain(|&x| x != v);
}
}
result.balance_ratio = compute_balance(result.part_a.len(), result.part_b.len());
}
pub struct VertexSeparator;
impl VertexSeparator {
pub fn find_separator(graph: &AdjacencyGraph, subgraph: &[usize]) -> SeparatorResult {
if subgraph.len() <= 3 {
return Self::trivial_separator(subgraph);
}
let (coarse_map, coarse_count) = coarsen(graph, subgraph);
if coarse_count >= subgraph.len() || coarse_count <= 1 {
let mut result = bfs_bisect(graph, subgraph);
fm_refine(graph, &mut result);
return result;
}
let mut coarse_repr: Vec<Option<usize>> = vec![None; coarse_count];
for &v in subgraph {
let cid = coarse_map[v];
if cid >= 0 {
let cid = cid as usize;
if coarse_repr[cid].is_none() {
coarse_repr[cid] = Some(v);
}
}
}
let coarse_verts: Vec<usize> = coarse_repr.iter().filter_map(|&x| x).collect();
let coarse_result = bfs_bisect(graph, &coarse_verts);
let n = graph.num_vertices;
let mut fine_part = vec![0u8; n]; for &v in &coarse_result.part_a {
let cid = coarse_map[v];
for &fv in subgraph {
if coarse_map[fv] == cid {
fine_part[fv] = 0;
}
}
}
for &v in &coarse_result.part_b {
let cid = coarse_map[v];
for &fv in subgraph {
if coarse_map[fv] == cid {
fine_part[fv] = 1;
}
}
}
for &v in &coarse_result.separator {
let cid = coarse_map[v];
for &fv in subgraph {
if coarse_map[fv] == cid {
fine_part[fv] = 2;
}
}
}
let mut separator = Vec::new();
let mut part_a = Vec::new();
let mut part_b = Vec::new();
for &v in subgraph {
if fine_part[v] == 2 {
separator.push(v);
} else {
let is_boundary = graph
.neighbors(v)
.iter()
.any(|&nb| fine_part[nb] != fine_part[v] && fine_part[nb] != 2);
if is_boundary {
separator.push(v);
} else if fine_part[v] == 0 {
part_a.push(v);
} else {
part_b.push(v);
}
}
}
let balance = compute_balance(part_a.len(), part_b.len());
let mut result = SeparatorResult {
separator,
part_a,
part_b,
balance_ratio: balance,
};
fm_refine(graph, &mut result);
result
}
fn trivial_separator(subgraph: &[usize]) -> SeparatorResult {
match subgraph.len() {
0 => SeparatorResult {
separator: Vec::new(),
part_a: Vec::new(),
part_b: Vec::new(),
balance_ratio: 1.0,
},
1 => SeparatorResult {
separator: subgraph.to_vec(),
part_a: Vec::new(),
part_b: Vec::new(),
balance_ratio: 1.0,
},
2 => SeparatorResult {
separator: vec![subgraph[0]],
part_a: vec![subgraph[1]],
part_b: Vec::new(),
balance_ratio: 0.0,
},
_ => SeparatorResult {
separator: vec![subgraph[1]],
part_a: vec![subgraph[0]],
part_b: vec![subgraph[2]],
balance_ratio: 1.0,
},
}
}
}
pub struct MinimumDegreeOrdering;
impl MinimumDegreeOrdering {
pub fn compute(graph: &AdjacencyGraph, vertices: &[usize]) -> Vec<usize> {
if vertices.is_empty() {
return Vec::new();
}
let n = graph.num_vertices;
let mut in_sub = vec![false; n];
for &v in vertices {
in_sub[v] = true;
}
let mut eliminated = vec![false; n];
let mut order = Vec::with_capacity(vertices.len());
for _ in 0..vertices.len() {
let mut best = None;
let mut best_deg = usize::MAX;
for &v in vertices {
if eliminated[v] {
continue;
}
let deg = graph
.neighbors(v)
.iter()
.filter(|&&nb| in_sub[nb] && !eliminated[nb])
.count();
if deg < best_deg {
best_deg = deg;
best = Some(v);
}
}
if let Some(v) = best {
order.push(v);
eliminated[v] = true;
}
}
order
}
}
const ND_THRESHOLD: usize = 32;
const MAX_DEPTH: usize = 64;
pub struct NestedDissectionOrdering;
impl NestedDissectionOrdering {
pub fn compute(graph: &AdjacencyGraph) -> SolverResult<Permutation> {
let n = graph.num_vertices;
if n == 0 {
return Permutation::new(Vec::new());
}
let vertices: Vec<usize> = (0..n).collect();
let mut order = Vec::with_capacity(n);
Self::recurse(graph, &vertices, &mut order, 0);
Permutation::new(order)
}
fn recurse(graph: &AdjacencyGraph, subgraph: &[usize], order: &mut Vec<usize>, depth: usize) {
if subgraph.len() <= ND_THRESHOLD || depth >= MAX_DEPTH {
let md_order = MinimumDegreeOrdering::compute(graph, subgraph);
order.extend(md_order);
return;
}
let sep = VertexSeparator::find_separator(graph, subgraph);
if !sep.part_a.is_empty() {
Self::recurse(graph, &sep.part_a, order, depth + 1);
}
if !sep.part_b.is_empty() {
Self::recurse(graph, &sep.part_b, order, depth + 1);
}
order.extend(&sep.separator);
}
}
pub fn analyze_ordering(graph: &AdjacencyGraph, perm: &Permutation) -> OrderingQuality {
let n = graph.num_vertices;
if n == 0 {
return OrderingQuality {
estimated_fill: 0,
separator_size: 0,
balance_ratio: 1.0,
};
}
let iperm = perm.inverse();
let mut fill = 0usize;
let mut parent = vec![n; n];
for i in 0..n {
let orig = perm.perm[i]; let mut visited = vec![false; n];
visited[i] = true;
for &nb in graph.neighbors(orig) {
let mut j = iperm[nb]; while j < n && !visited[j] && j > i {
visited[j] = true;
fill += 1;
if parent[j] == n || parent[j] > i {
if parent[j] == n {
parent[j] = i;
}
}
j = parent[j];
}
}
}
let vertices: Vec<usize> = (0..n).collect();
let sep = if n > 3 {
VertexSeparator::find_separator(graph, &vertices)
} else {
SeparatorResult {
separator: Vec::new(),
part_a: Vec::new(),
part_b: Vec::new(),
balance_ratio: 1.0,
}
};
OrderingQuality {
estimated_fill: fill,
separator_size: sep.separator.len(),
balance_ratio: sep.balance_ratio,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn path_graph(n: usize) -> AdjacencyGraph {
let mut row_ptr = vec![0i32];
let mut col_indices = Vec::new();
for i in 0..n {
if i > 0 {
col_indices.push((i - 1) as i32);
}
if i + 1 < n {
col_indices.push((i + 1) as i32);
}
row_ptr.push(col_indices.len() as i32);
}
AdjacencyGraph::from_symmetric_csr(&row_ptr, &col_indices, n)
}
fn grid_graph(rows: usize, cols: usize) -> AdjacencyGraph {
let n = rows * cols;
let mut row_ptr = vec![0i32];
let mut col_indices = Vec::new();
for r in 0..rows {
for c in 0..cols {
let v = r * cols + c;
if r > 0 {
col_indices.push((v - cols) as i32);
}
if c > 0 {
col_indices.push((v - 1) as i32);
}
if c + 1 < cols {
col_indices.push((v + 1) as i32);
}
if r + 1 < rows {
col_indices.push((v + cols) as i32);
}
row_ptr.push(col_indices.len() as i32);
}
}
AdjacencyGraph::from_symmetric_csr(&row_ptr, &col_indices, n)
}
#[test]
fn permutation_identity() {
let p = Permutation::identity(5);
assert_eq!(p.perm, vec![0, 1, 2, 3, 4]);
assert_eq!(p.inverse(), &[0, 1, 2, 3, 4]);
}
#[test]
fn permutation_valid() {
let p = Permutation::new(vec![2, 0, 1]).expect("valid perm");
assert_eq!(p.perm, vec![2, 0, 1]);
assert_eq!(p.iperm, vec![1, 2, 0]);
}
#[test]
fn permutation_invalid_duplicate() {
let result = Permutation::new(vec![0, 0, 1]);
assert!(result.is_err());
}
#[test]
fn permutation_invalid_out_of_range() {
let result = Permutation::new(vec![0, 5, 2]);
assert!(result.is_err());
}
#[test]
fn permutation_apply() {
let p = Permutation::new(vec![2, 0, 1]).expect("valid perm");
let data = vec![10, 20, 30];
let result = p.apply(&data);
assert_eq!(result, vec![30, 10, 20]);
}
#[test]
fn permutation_compose() {
let p1 = Permutation::new(vec![1, 2, 0]).expect("valid");
let p2 = Permutation::new(vec![2, 0, 1]).expect("valid");
let composed = p1.compose(&p2).expect("compose");
assert_eq!(composed.perm, vec![0, 1, 2]);
}
#[test]
fn adjacency_graph_path() {
let g = path_graph(5);
assert_eq!(g.num_vertices, 5);
assert_eq!(g.degree(0), 1);
assert_eq!(g.degree(2), 2);
assert_eq!(g.degree(4), 1);
assert_eq!(g.neighbors(2), &[1, 3]);
}
#[test]
fn adjacency_graph_grid() {
let g = grid_graph(3, 3);
assert_eq!(g.num_vertices, 9);
assert_eq!(g.degree(0), 2);
assert_eq!(g.degree(4), 4);
}
#[test]
fn separator_splits_graph() {
let g = grid_graph(4, 4);
let verts: Vec<usize> = (0..16).collect();
let sep = VertexSeparator::find_separator(&g, &verts);
let total = sep.separator.len() + sep.part_a.len() + sep.part_b.len();
assert_eq!(total, 16);
assert!(!sep.separator.is_empty());
let n = g.num_vertices;
let mut part = vec![0u8; n];
for &v in &sep.part_a {
part[v] = 1;
}
for &v in &sep.part_b {
part[v] = 2;
}
for &v in &sep.part_a {
for &nb in g.neighbors(v) {
assert_ne!(part[nb], 2, "edge {v}-{nb} crosses separator");
}
}
}
#[test]
fn nd_ordering_valid_permutation() {
let g = grid_graph(4, 4);
let perm = NestedDissectionOrdering::compute(&g).expect("nd ordering");
assert_eq!(perm.perm.len(), 16);
let mut sorted = perm.perm.clone();
sorted.sort_unstable();
assert_eq!(sorted, (0..16).collect::<Vec<_>>());
}
#[test]
fn nd_ordering_empty_graph() {
let g = AdjacencyGraph::from_symmetric_csr(&[0], &[], 0);
let perm = NestedDissectionOrdering::compute(&g).expect("empty");
assert!(perm.perm.is_empty());
}
#[test]
fn nd_ordering_single_vertex() {
let g = AdjacencyGraph::from_symmetric_csr(&[0, 0], &[], 1);
let perm = NestedDissectionOrdering::compute(&g).expect("single");
assert_eq!(perm.perm, vec![0]);
}
#[test]
fn nd_reduces_fill_vs_natural() {
let g = grid_graph(8, 8);
let nd_perm = NestedDissectionOrdering::compute(&g).expect("nd");
let natural_perm = Permutation::identity(64);
let nd_quality = analyze_ordering(&g, &nd_perm);
let nat_quality = analyze_ordering(&g, &natural_perm);
assert!(
nd_quality.estimated_fill <= nat_quality.estimated_fill,
"ND fill {} should be <= natural fill {}",
nd_quality.estimated_fill,
nat_quality.estimated_fill
);
}
#[test]
fn minimum_degree_ordering_valid() {
let g = path_graph(10);
let verts: Vec<usize> = (0..10).collect();
let order = MinimumDegreeOrdering::compute(&g, &verts);
assert_eq!(order.len(), 10);
let mut sorted = order.clone();
sorted.sort_unstable();
assert_eq!(sorted, (0..10).collect::<Vec<_>>());
}
#[test]
fn ordering_quality_metrics() {
let g = grid_graph(4, 4);
let perm = NestedDissectionOrdering::compute(&g).expect("nd");
let quality = analyze_ordering(&g, &perm);
assert!(quality.estimated_fill < 200, "fill should be reasonable");
assert!(quality.balance_ratio >= 0.0 && quality.balance_ratio <= 1.0);
}
#[test]
fn larger_grid_nd() {
let g = grid_graph(16, 16);
let perm = NestedDissectionOrdering::compute(&g).expect("nd 16x16");
assert_eq!(perm.perm.len(), 256);
let mut sorted = perm.perm.clone();
sorted.sort_unstable();
assert_eq!(sorted, (0..256).collect::<Vec<_>>());
}
}