use std::collections::{HashSet, VecDeque};
#[derive(Debug, Clone, Default)]
pub struct Layering {
pub layer: Vec<u32>,
pub slot: Vec<usize>,
pub layers: Vec<Vec<usize>>,
pub children: Vec<Vec<usize>>,
}
pub fn compute_layering(node_count: usize, edges: &[(usize, usize)], explicit_roots: &[usize]) -> Layering {
let n = node_count;
if n == 0 {
return Layering::default();
}
let mut children: Vec<Vec<(usize, usize)>> = vec![Vec::new(); n]; let mut in_degree_all = vec![0u32; n];
for (ei, &(a, b)) in edges.iter().enumerate() {
if a >= n || b >= n || a == b {
continue;
}
children[a].push((ei, b));
in_degree_all[b] += 1;
}
let roots: Vec<usize> = {
let explicit: Vec<usize> = explicit_roots.iter().copied().filter(|&r| r < n).collect();
if !explicit.is_empty() {
explicit
} else {
let auto: Vec<usize> = (0..n).filter(|&i| in_degree_all[i] == 0).collect();
if auto.is_empty() {
vec![0]
} else {
auto
}
}
};
let back_edges = find_back_edges(n, &children, &roots);
let mut children_reduced: Vec<Vec<usize>> = vec![Vec::new(); n];
let mut in_degree = vec![0u32; n];
for (ei, &(a, b)) in edges.iter().enumerate() {
if back_edges.contains(&ei) {
continue;
}
if a >= n || b >= n || a == b {
continue;
}
children_reduced[a].push(b);
in_degree[b] += 1;
}
let layer = layer_by_longest_path(n, &children_reduced, &in_degree);
let (slot, layers) = order_by_barycenter(n, &layer, &children_reduced);
Layering { layer, slot, layers, children: children_reduced }
}
fn find_back_edges(n: usize, children: &[Vec<(usize, usize)>], roots: &[usize]) -> HashSet<usize> {
let mut back_edges = HashSet::new();
let mut state = vec![0u8; n];
let mut stack: Vec<(usize, usize)> = Vec::new();
for &r in roots {
dfs_mark(r, children, &mut state, &mut stack, &mut back_edges);
}
for i in 0..n {
dfs_mark(i, children, &mut state, &mut stack, &mut back_edges);
}
back_edges
}
fn dfs_mark(
start: usize,
children: &[Vec<(usize, usize)>],
state: &mut [u8],
stack: &mut Vec<(usize, usize)>,
back_edges: &mut HashSet<usize>,
) {
if state[start] != 0 {
return;
}
state[start] = 1;
stack.clear();
stack.push((start, 0));
while let Some(&(node, child_pos)) = stack.last() {
if child_pos >= children[node].len() {
state[node] = 2;
stack.pop();
continue;
}
let (edge_idx, child) = children[node][child_pos];
let last = stack.len() - 1;
stack[last].1 += 1;
match state[child] {
0 => {
state[child] = 1;
stack.push((child, 0));
}
1 => {
back_edges.insert(edge_idx);
}
_ => {}
}
}
}
fn layer_by_longest_path(n: usize, children: &[Vec<usize>], in_degree: &[u32]) -> Vec<u32> {
let mut layer = vec![0u32; n];
let mut remaining_in = in_degree.to_vec();
let mut visited = vec![false; n];
let mut queue: VecDeque<usize> = VecDeque::new();
for i in 0..n {
if in_degree[i] == 0 {
visited[i] = true;
queue.push_back(i);
}
}
while let Some(node) = queue.pop_front() {
for &child in &children[node] {
let candidate = layer[node] + 1;
if candidate > layer[child] {
layer[child] = candidate;
}
remaining_in[child] = remaining_in[child].saturating_sub(1);
if remaining_in[child] == 0 && !visited[child] {
visited[child] = true;
queue.push_back(child);
}
}
}
for i in 0..n {
if !visited[i] {
layer[i] = 0;
}
}
layer
}
fn sync_slots(layers: &[Vec<usize>], slot_of: &mut [usize]) {
for row in layers {
for (pos, &node) in row.iter().enumerate() {
slot_of[node] = pos;
}
}
}
fn barycenter_key(node: usize, adjacency: &[Vec<usize>], slot_of: &[usize]) -> f64 {
let neighbors = &adjacency[node];
if neighbors.is_empty() {
slot_of[node] as f64
} else {
neighbors.iter().map(|&n| slot_of[n] as f64).sum::<f64>() / neighbors.len() as f64
}
}
fn resort_layer(layers: &mut [Vec<usize>], row_layer: usize, adjacency: &[Vec<usize>], slot_of: &mut [usize]) {
let mut keyed: Vec<(f64, usize)> =
layers[row_layer].iter().map(|&node| (barycenter_key(node, adjacency, slot_of), node)).collect();
keyed.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal).then(a.1.cmp(&b.1)));
layers[row_layer] = keyed.into_iter().map(|(_, node)| node).collect();
sync_slots(layers, slot_of);
}
fn order_by_barycenter(n: usize, layer: &[u32], children: &[Vec<usize>]) -> (Vec<usize>, Vec<Vec<usize>>) {
let max_layer = layer.iter().copied().max().unwrap_or(0) as usize;
let mut layers: Vec<Vec<usize>> = vec![Vec::new(); max_layer + 1];
for i in 0..n {
layers[layer[i] as usize].push(i); }
let mut slot_of = vec![0usize; n];
sync_slots(&layers, &mut slot_of);
let mut parents: Vec<Vec<usize>> = vec![Vec::new(); n];
for (node, kids) in children.iter().enumerate() {
for &c in kids {
parents[c].push(node);
}
}
for l in 1..=max_layer {
resort_layer(&mut layers, l, &parents, &mut slot_of);
}
if max_layer > 0 {
for l in (0..max_layer).rev() {
resort_layer(&mut layers, l, children, &mut slot_of);
}
}
(slot_of, layers)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chain_layers_increase_by_one_each_hop() {
let edges = [(0, 1), (1, 2), (2, 3)];
let layering = compute_layering(4, &edges, &[]);
assert_eq!(layering.layer, vec![0, 1, 2, 3]);
}
#[test]
fn diamond_layer_is_one_plus_max_of_parent_layers() {
let edges = [(0, 1), (0, 2), (1, 3), (2, 3)];
let layering = compute_layering(4, &edges, &[]);
assert_eq!(layering.layer[0], 0);
assert_eq!(layering.layer[1], 1);
assert_eq!(layering.layer[2], 1);
assert_eq!(layering.layer[3], 1 + layering.layer[1].max(layering.layer[2]));
}
#[test]
fn multi_depth_parents_take_the_longest_path() {
let edges = [(0, 1), (2, 3), (3, 4), (1, 5), (4, 5)];
let layering = compute_layering(6, &edges, &[]);
assert_eq!(layering.layer[0], 0); assert_eq!(layering.layer[2], 0); assert_eq!(layering.layer[1], 1); assert_eq!(layering.layer[3], 1); assert_eq!(layering.layer[4], 2); assert_eq!(layering.layer[5], 1 + layering.layer[1].max(layering.layer[4])); assert_eq!(layering.layer[5], 3);
}
#[test]
fn cycle_is_broken_by_a_back_edge_and_layers_cleanly() {
let edges = [(0, 1), (1, 2), (2, 0)];
let layering = compute_layering(3, &edges, &[]);
assert_eq!(layering.layer, vec![0, 1, 2]);
assert_eq!(layering.layers, vec![vec![0], vec![1], vec![2]]);
}
#[test]
fn layering_is_deterministic_across_repeated_calls() {
let edges = [(0, 1), (0, 2), (1, 3), (2, 3), (1, 4), (4, 3)];
let first = compute_layering(5, &edges, &[]);
let second = compute_layering(5, &edges, &[]);
assert_eq!(first.layer, second.layer);
assert_eq!(first.slot, second.slot);
assert_eq!(first.layers, second.layers);
assert_eq!(first.children, second.children);
}
#[test]
fn explicit_roots_override_auto_detection() {
let edges = [(0, 2), (1, 2)];
let layering = compute_layering(3, &edges, &[0]);
assert_eq!(layering.layer[0], 0);
assert_eq!(layering.layer[1], 0);
assert_eq!(layering.layer[2], 1);
}
#[test]
fn empty_topology_produces_empty_layering() {
let layering = compute_layering(0, &[], &[]);
assert!(layering.layer.is_empty());
assert!(layering.layers.is_empty());
}
#[test]
fn self_loops_and_out_of_range_edges_are_dropped_without_panicking() {
let edges = [(0, 0), (0, 99), (99, 1), (0, 1)];
let layering = compute_layering(2, &edges, &[]);
assert_eq!(layering.layer, vec![0, 1]);
}
#[test]
fn children_field_is_the_reduced_back_edge_free_adjacency() {
let edges = [(0, 1), (1, 2), (2, 0), (1, 1), (0, 99)];
let layering = compute_layering(3, &edges, &[]);
assert_eq!(layering.children[0], vec![1]);
assert_eq!(layering.children[1], vec![2]);
assert!(layering.children[2].is_empty(), "the 2->0 back edge must be excluded from the reduced adjacency");
}
fn count_crossings(top_order: &[usize], bottom_order: &[usize], edges: &[(usize, usize)]) -> usize {
let top_slot: std::collections::HashMap<usize, usize> = top_order.iter().enumerate().map(|(i, &n)| (n, i)).collect();
let bottom_slot: std::collections::HashMap<usize, usize> = bottom_order.iter().enumerate().map(|(i, &n)| (n, i)).collect();
let pairs: Vec<(usize, usize)> = edges
.iter()
.filter_map(|&(a, b)| match (top_slot.get(&a), bottom_slot.get(&b)) {
(Some(&ts), Some(&bs)) => Some((ts, bs)),
_ => None,
})
.collect();
let mut crossings = 0usize;
for i in 0..pairs.len() {
for j in (i + 1)..pairs.len() {
let (t1, b1) = pairs[i];
let (t2, b2) = pairs[j];
if (t1 < t2 && b1 > b2) || (t1 > t2 && b1 < b2) {
crossings += 1;
}
}
}
crossings
}
#[test]
fn barycenter_ordering_reduces_crossings_versus_naive_index_order() {
let edges = [(0, 5), (1, 4), (2, 3)];
let layering = compute_layering(6, &edges, &[]);
assert_eq!(layering.layers.len(), 2, "fixture must produce exactly 2 layers");
let naive_top = vec![0usize, 1, 2];
let naive_bottom = vec![3usize, 4, 5];
let naive_crossings = count_crossings(&naive_top, &naive_bottom, &edges);
assert_eq!(naive_crossings, 3, "the fully-reversed mapping must cross on every pair under naive ascending-index order");
let final_top = &layering.layers[0];
let final_bottom = &layering.layers[1];
let final_crossings = count_crossings(final_top, final_bottom, &edges);
assert!(
final_crossings < naive_crossings,
"barycenter ordering must reduce crossings below the naive baseline (naive={naive_crossings}, final={final_crossings})"
);
assert_eq!(final_crossings, 0, "this fixture's reversal is fully resolvable by one barycenter pass — must reach zero crossings");
}
}