use super::elimination_tree::EliminationTree;
#[cfg(test)]
thread_local! {
static SORT_WORK: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
pub fn postorder(etree: &EliminationTree) -> (Vec<usize>, Vec<usize>) {
let n = etree.n;
if n == 0 {
return (Vec::new(), Vec::new());
}
let children = etree.children();
let sizes = etree.subtree_sizes();
let roots = etree.roots();
let mut order = Vec::with_capacity(n);
let mut stack: Vec<(usize, Vec<usize>, usize)> = Vec::new();
let mut sorted_roots = roots;
sorted_roots.sort_unstable_by_key(|&r| sizes[r]);
for &root in &sorted_roots {
stack.push((root, sorted_children_by_size(&children[root], &sizes), 0));
while let Some((node, sorted_children, child_idx)) = stack.last_mut() {
let node_id = *node;
if *child_idx < sorted_children.len() {
let child = sorted_children[*child_idx];
*child_idx += 1;
let next = sorted_children_by_size(&children[child], &sizes);
stack.push((child, next, 0));
} else {
order.push(node_id);
stack.pop();
}
}
}
let mut inv = vec![0usize; n];
for (k, &node) in order.iter().enumerate() {
inv[node] = k;
}
(order, inv)
}
pub fn biased_postorder(etree: &EliminationTree, bias: &[bool]) -> (Vec<usize>, Vec<usize>) {
let n = etree.n;
debug_assert_eq!(
bias.len(),
n,
"biased_postorder bias length must equal etree.n"
);
if n == 0 {
return (Vec::new(), Vec::new());
}
let children = etree.children();
let sizes = etree.subtree_sizes();
let roots = etree.roots();
let mut order = Vec::with_capacity(n);
let mut stack: Vec<(usize, Vec<usize>, usize)> = Vec::new();
let mut sorted_roots = roots;
sorted_roots.sort_unstable_by_key(|&r| sizes[r]);
for &root in &sorted_roots {
let merged = merge_bias_partition(&children[root], &sizes, bias);
stack.push((root, merged, 0));
while let Some((node, sorted_children, child_idx)) = stack.last_mut() {
let node_id = *node;
if *child_idx < sorted_children.len() {
let child = sorted_children[*child_idx];
*child_idx += 1;
let next_children = merge_bias_partition(&children[child], &sizes, bias);
stack.push((child, next_children, 0));
} else {
order.push(node_id);
stack.pop();
}
}
}
let mut inv = vec![0usize; n];
for (k, &node) in order.iter().enumerate() {
inv[node] = k;
}
(order, inv)
}
pub fn schur_constrained_postorder(
etree: &EliminationTree,
is_schur: &[bool],
) -> (Vec<usize>, Vec<usize>) {
let n = etree.n;
debug_assert_eq!(
is_schur.len(),
n,
"schur_constrained_postorder is_schur length must equal etree.n"
);
if n == 0 {
return (Vec::new(), Vec::new());
}
let children = etree.children();
let sizes = etree.subtree_sizes();
let roots = etree.roots();
let mut order = Vec::with_capacity(n);
let sorted_roots = schur_partition_children(&roots, &sizes, is_schur);
let mut stack: Vec<(usize, Vec<usize>, usize)> = Vec::new();
for &root in sorted_roots.iter() {
let merged = schur_partition_children(&children[root], &sizes, is_schur);
stack.push((root, merged, 0));
while let Some((node, sorted_children, child_idx)) = stack.last_mut() {
let node_id = *node;
if *child_idx < sorted_children.len() {
let child = sorted_children[*child_idx];
*child_idx += 1;
let next_children = schur_partition_children(&children[child], &sizes, is_schur);
stack.push((child, next_children, 0));
} else {
if !is_schur[node_id] {
order.push(node_id);
}
stack.pop();
}
}
}
for (k, &flag) in is_schur.iter().enumerate() {
if flag {
order.push(k);
}
}
let mut inv = vec![0usize; n];
for (k, &node) in order.iter().enumerate() {
inv[node] = k;
}
(order, inv)
}
fn schur_partition_children(children: &[usize], sizes: &[usize], is_schur: &[bool]) -> Vec<usize> {
let mut nonschur: Vec<usize> = children.iter().copied().filter(|&c| !is_schur[c]).collect();
let mut schur: Vec<usize> = children.iter().copied().filter(|&c| is_schur[c]).collect();
nonschur.sort_unstable_by_key(|&c| sizes[c]);
schur.sort_unstable();
nonschur.extend(schur);
nonschur
}
fn sorted_children_by_size(children: &[usize], sizes: &[usize]) -> Vec<usize> {
#[cfg(test)]
SORT_WORK.with(|w| w.set(w.get() + children.len()));
let mut v = children.to_vec();
v.sort_unstable_by_key(|&c| sizes[c]);
v
}
fn merge_bias_partition(children: &[usize], sizes: &[usize], bias: &[bool]) -> Vec<usize> {
let mut early: Vec<usize> = children.iter().copied().filter(|&c| !bias[c]).collect();
let mut late: Vec<usize> = children.iter().copied().filter(|&c| bias[c]).collect();
early.sort_unstable_by_key(|&c| sizes[c]);
late.sort_unstable_by_key(|&c| sizes[c]);
early.extend(late);
early
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sparse::csc::CscMatrix;
#[test]
fn test_postorder_tridiagonal() {
let m =
CscMatrix::from_triplets(4, &[0, 1, 1, 2, 2, 3, 3], &[0, 0, 1, 1, 2, 2, 3], &[1.0; 7])
.unwrap();
let pat = m.symmetric_pattern();
let etree = EliminationTree::from_pattern(&pat);
let (order, inv) = postorder(&etree);
assert_eq!(order.len(), 4);
assert_eq!(order, vec![0, 1, 2, 3]);
for (k, &node) in order.iter().enumerate() {
assert_eq!(inv[node], k);
}
}
#[test]
fn test_postorder_valid_topological_order() {
let m = CscMatrix::from_triplets(
5,
&[0, 1, 2, 3, 4, 1, 2, 3, 4],
&[0, 0, 0, 0, 0, 1, 2, 3, 4],
&[1.0; 9],
)
.unwrap();
let pat = m.symmetric_pattern();
let etree = EliminationTree::from_pattern(&pat);
let (order, inv) = postorder(&etree);
assert_eq!(order.len(), 5);
for j in 0..5 {
if let Some(p) = etree.parent[j] {
assert!(
inv[j] < inv[p],
"child {} (pos {}) should appear before parent {} (pos {})",
j,
inv[j],
p,
inv[p]
);
}
}
}
#[test]
fn test_postorder_diagonal() {
let m = CscMatrix::from_triplets(3, &[0, 1, 2], &[0, 1, 2], &[1.0; 3]).unwrap();
let pat = m.symmetric_pattern();
let etree = EliminationTree::from_pattern(&pat);
let (order, _) = postorder(&etree);
assert_eq!(order.len(), 3);
let mut sorted = order.clone();
sorted.sort();
assert_eq!(sorted, vec![0, 1, 2]);
}
#[test]
fn test_postorder_inverse_roundtrip() {
let m =
CscMatrix::from_triplets(4, &[0, 1, 1, 2, 2, 3, 3], &[0, 0, 1, 1, 2, 2, 3], &[1.0; 7])
.unwrap();
let pat = m.symmetric_pattern();
let etree = EliminationTree::from_pattern(&pat);
let (order, inv) = postorder(&etree);
for j in 0..4 {
assert_eq!(order[inv[j]], j);
}
for k in 0..4 {
assert_eq!(inv[order[k]], k);
}
}
#[test]
fn test_schur_postorder_no_schur_matches_postorder() {
let m = CscMatrix::from_triplets(
5,
&[0, 1, 2, 3, 4, 1, 2, 3, 4],
&[0, 0, 0, 0, 0, 1, 2, 3, 4],
&[1.0; 9],
)
.unwrap();
let pat = m.symmetric_pattern();
let etree = EliminationTree::from_pattern(&pat);
let (a, _) = postorder(&etree);
let (b, _) = schur_constrained_postorder(&etree, &[false; 5]);
assert_eq!(a, b);
}
#[test]
fn test_schur_postorder_chain_tail_pinned() {
let m = CscMatrix::from_triplets(
5,
&[0, 1, 1, 2, 2, 3, 3, 4, 4],
&[0, 0, 1, 1, 2, 2, 3, 3, 4],
&[1.0; 9],
)
.unwrap();
let pat = m.symmetric_pattern();
let etree = EliminationTree::from_pattern(&pat);
let mut is_schur = vec![false; 5];
is_schur[3] = true;
is_schur[4] = true;
let (post, inv) = schur_constrained_postorder(&etree, &is_schur);
assert_eq!(post[3], 3);
assert_eq!(post[4], 4);
assert_eq!(inv[3], 3);
assert_eq!(inv[4], 4);
}
#[test]
fn test_schur_postorder_topological_property() {
let m = CscMatrix::from_triplets(
6,
&[0, 1, 2, 3, 4, 5, 1, 4, 2, 4, 3, 4, 5],
&[0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4],
&[1.0; 13],
)
.unwrap();
let pat = m.symmetric_pattern();
let etree = EliminationTree::from_pattern(&pat);
let mut is_schur = vec![false; 6];
is_schur[4] = true;
is_schur[5] = true;
let (_post, inv) = schur_constrained_postorder(&etree, &is_schur);
for j in 0..6 {
if let Some(p) = etree.parent[j] {
assert!(
inv[j] < inv[p],
"child {} (pos {}) must precede parent {} (pos {})",
j,
inv[j],
p,
inv[p]
);
}
}
assert!(inv[4] >= 4);
assert!(inv[5] >= 4);
assert!(inv[4] < inv[5] || inv[5] < inv[4]); }
#[test]
fn test_schur_postorder_forest_tail_identity() {
let etree = EliminationTree {
parent: vec![
Some(1),
Some(2),
Some(3),
Some(5),
Some(7),
None,
None,
None,
],
n: 8,
};
let is_schur = vec![false, false, false, false, true, true, true, true];
let (post, inv) = schur_constrained_postorder(&etree, &is_schur);
for k in 4..8 {
assert_eq!(
post[k], k,
"tail identity violated: post[{}] = {} (expected {})",
k, post[k], k
);
assert_eq!(inv[k], k);
}
for j in 0..8 {
if let Some(p) = etree.parent[j] {
assert!(
inv[j] < inv[p],
"child {} (pos {}) must precede parent {} (pos {})",
j,
inv[j],
p,
inv[p]
);
}
}
let mut prefix: Vec<usize> = post[0..4].to_vec();
prefix.sort();
assert_eq!(prefix, vec![0, 1, 2, 3]);
}
#[test]
fn test_schur_postorder_empty_etree() {
let etree = EliminationTree {
parent: Vec::new(),
n: 0,
};
let (post, inv) = schur_constrained_postorder(&etree, &[]);
assert!(post.is_empty());
assert!(inv.is_empty());
}
#[test]
fn test_postorder_empty() {
let etree = EliminationTree {
parent: Vec::new(),
n: 0,
};
let (order, inv) = postorder(&etree);
assert!(order.is_empty());
assert!(inv.is_empty());
}
fn star_etree(n: usize) -> EliminationTree {
let mut rows = Vec::new();
let mut cols = Vec::new();
for i in 0..n {
rows.push(i);
cols.push(i); if i < n - 1 {
rows.push(n - 1);
cols.push(i); }
}
let vals = vec![1.0; rows.len()];
let m = CscMatrix::from_triplets(n, &rows, &cols, &vals).unwrap();
let pat = m.symmetric_pattern();
EliminationTree::from_pattern(&pat)
}
#[test]
fn test_postorder_star_sort_work_is_linear() {
let n = 2000;
let etree = star_etree(n);
assert_eq!(etree.children()[n - 1].len(), n - 1);
assert_eq!(etree.roots(), vec![n - 1]);
SORT_WORK.with(|w| w.set(0));
let (order, inv) = postorder(&etree);
let work = SORT_WORK.with(|w| w.get());
assert_eq!(order.len(), n);
for j in 0..n {
if let Some(p) = etree.parent[j] {
assert!(inv[j] < inv[p], "child {j} must precede parent {p}");
}
}
assert!(
work <= 4 * n,
"postorder sort work {work} exceeds the linear bound {} (n={n}); \
the O(n²·log n) sort-on-every-stack-visit regression is back",
4 * n
);
}
}