use rustc_hash::{FxHashMap, FxHashSet};
use std::cmp::Ordering;
pub trait GraphView<N: Copy + Eq + std::hash::Hash> {
fn successors(&self, n: N, out: &mut Vec<N>);
fn predecessors(&self, n: N, out: &mut Vec<N>);
fn exists(&self, n: N) -> bool;
}
#[derive(Debug, Clone)]
pub struct Cycle<N> {
pub path: Vec<N>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct PkStats {
pub relabeled: usize,
pub dfs_visited: usize,
}
#[derive(Debug, Clone, Copy)]
pub struct PkConfig {
pub visit_budget: usize,
pub compaction_interval_ops: u64,
}
impl Default for PkConfig {
fn default() -> Self {
Self {
visit_budget: 50_000,
compaction_interval_ops: 100_000,
}
}
}
#[derive(Debug)]
pub struct DynamicTopo<N: Copy + Eq + std::hash::Hash + Ord> {
pos: FxHashMap<N, u32>,
order: Vec<N>,
op_count: u64,
cfg: PkConfig,
succ_buf: Vec<N>,
pred_buf: Vec<N>,
}
impl<N> DynamicTopo<N>
where
N: Copy + Eq + std::hash::Hash + Ord,
{
pub fn new(nodes: impl IntoIterator<Item = N>, cfg: PkConfig) -> Self {
let mut order: Vec<N> = nodes.into_iter().collect();
order.sort(); let mut pos = FxHashMap::default();
for (i, n) in order.iter().enumerate() {
pos.insert(*n, i as u32);
}
Self {
pos,
order,
op_count: 0,
cfg,
succ_buf: Vec::new(),
pred_buf: Vec::new(),
}
}
pub fn rebuild_full<G: GraphView<N>>(&mut self, graph: &G) {
let mut nodes: Vec<N> = self
.order
.iter()
.copied()
.filter(|n| graph.exists(*n))
.collect();
nodes.sort();
let set: FxHashSet<N> = nodes.iter().copied().collect();
let mut indeg: FxHashMap<N, usize> = nodes.iter().map(|&n| (n, 0usize)).collect();
for &n in &nodes {
self.pred_buf.clear();
graph.predecessors(n, &mut self.pred_buf);
for &p in &self.pred_buf {
if set.contains(&p) {
*indeg.get_mut(&n).unwrap() += 1;
}
}
}
let mut zero: Vec<N> = indeg
.iter()
.filter_map(|(&n, &d)| if d == 0 { Some(n) } else { None })
.collect();
zero.sort_by(|a, b| b.cmp(a));
let mut out: Vec<N> = Vec::with_capacity(nodes.len());
while let Some(n) = zero.pop() {
out.push(n);
self.succ_buf.clear();
graph.successors(n, &mut self.succ_buf);
for &s in &self.succ_buf {
if let Some(d) = indeg.get_mut(&s) {
*d -= 1;
if *d == 0 {
zero.push(s);
}
}
}
zero.sort_by(|a, b| b.cmp(a)); }
if out.len() != nodes.len() {
out = nodes; }
self.order = out;
self.pos.clear();
for (i, n) in self.order.iter().enumerate() {
self.pos.insert(*n, i as u32);
}
}
pub fn try_add_edge<G: GraphView<N>>(
&mut self,
graph: &G,
x: N,
y: N,
) -> Result<PkStats, Cycle<N>> {
if x == y {
return Err(Cycle { path: vec![x, y] });
}
let px = match self.pos.get(&x).copied() {
Some(v) => v,
None => self.add_missing(x),
};
let py = match self.pos.get(&y).copied() {
Some(v) => v,
None => self.add_missing(y),
};
if px < py {
return Ok(PkStats::default());
}
let mut stack: Vec<N> = vec![y];
let mut parent: FxHashMap<N, N> = FxHashMap::default();
let mut visited: FxHashSet<N> = FxHashSet::default();
let mut affected: Vec<N> = Vec::new();
let mut visited_cnt = 0usize;
while let Some(u) = stack.pop() {
if !visited.insert(u) {
continue;
}
visited_cnt += 1;
if visited_cnt > self.cfg.visit_budget {
self.rebuild_full(graph);
self.op_count += 1;
return self.try_add_edge(graph, x, y);
}
if u == x {
let mut path = vec![x];
let mut cur = x;
while cur != y {
cur = *parent.get(&cur).unwrap();
path.push(cur);
}
path.reverse();
return Err(Cycle { path });
}
affected.push(u);
self.succ_buf.clear();
graph.successors(u, &mut self.succ_buf);
for &s in &self.succ_buf {
if let Some(&ps) = self.pos.get(&s)
&& ps <= px
&& !visited.contains(&s)
{
parent.insert(s, u);
stack.push(s);
}
}
}
let relabeled = self.splice_after(px as usize, &affected);
self.op_count += 1;
if self
.op_count
.is_multiple_of(self.cfg.compaction_interval_ops)
{
self.compact_ranks();
}
Ok(PkStats {
relabeled,
dfs_visited: visited_cnt,
})
}
pub fn remove_edge(&mut self, _x: N, _y: N) {
self.op_count += 1;
if self
.op_count
.is_multiple_of(self.cfg.compaction_interval_ops)
{
self.compact_ranks();
}
}
pub fn apply_bulk<G: GraphView<N>>(
&mut self,
graph: &G,
removes: &[(N, N)],
adds: &[(N, N)],
) -> Result<PkStats, Cycle<N>> {
for &(x, y) in removes {
let _ = (x, y);
self.remove_edge(x, y);
}
let mut stats = PkStats::default();
for &(x, y) in adds {
match self.try_add_edge(graph, x, y) {
Ok(s) => {
stats.relabeled += s.relabeled;
stats.dfs_visited += s.dfs_visited;
}
Err(c) => {
return Err(c);
}
}
}
Ok(stats)
}
#[inline]
pub fn topo_order(&self) -> &[N] {
&self.order
}
pub fn compact_ranks(&mut self) {
self.order
.sort_by(|a, b| match self.pos[a].cmp(&self.pos[b]) {
Ordering::Equal => a.cmp(b),
o => o,
});
self.pos.clear();
for (i, n) in self.order.iter().enumerate() {
self.pos.insert(*n, i as u32);
}
}
pub fn layers_for<G: GraphView<N>>(
&self,
graph: &G,
subset: &[N],
max_layer_width: Option<usize>,
) -> Vec<Vec<N>> {
if subset.is_empty() {
return Vec::new();
}
let subset_set: FxHashSet<N> = subset.iter().copied().collect();
let mut indeg: FxHashMap<N, usize> = subset.iter().map(|&n| (n, 0usize)).collect();
let mut pred_buf = Vec::new();
for &n in subset {
pred_buf.clear();
graph.predecessors(n, &mut pred_buf);
for &p in &pred_buf {
if subset_set.contains(&p) {
*indeg.get_mut(&n).unwrap() += 1;
}
}
}
let mut zero: Vec<N> = indeg
.iter()
.filter_map(|(&n, &d)| if d == 0 { Some(n) } else { None })
.collect();
zero.sort_by(|a, b| self.pos[a].cmp(&self.pos[b]).then_with(|| a.cmp(b)));
let mut layers: Vec<Vec<N>> = Vec::new();
let mut succ_buf = Vec::new();
while !zero.is_empty() {
let mut layer = Vec::new();
let cap = max_layer_width.unwrap_or(usize::MAX);
for _ in 0..zero.len().min(cap) {
layer.push(zero.remove(0));
}
layer.sort_by(|a, b| self.pos[a].cmp(&self.pos[b]).then_with(|| a.cmp(b)));
for &u in &layer {
succ_buf.clear();
graph.successors(u, &mut succ_buf);
for &v in &succ_buf {
if let Some(d) = indeg.get_mut(&v) {
*d -= 1;
if *d == 0 {
zero.push(v);
}
}
}
}
zero.sort_by(|a, b| self.pos[a].cmp(&self.pos[b]).then_with(|| a.cmp(b)));
layers.push(layer);
}
layers
}
#[inline]
fn add_missing(&mut self, n: N) -> u32 {
let idx = self.order.len() as u32;
self.order.push(n);
self.pos.insert(n, idx);
idx
}
pub fn ensure_nodes(&mut self, nodes: impl IntoIterator<Item = N>) {
for n in nodes {
if !self.pos.contains_key(&n) {
self.add_missing(n);
}
}
}
fn splice_after(&mut self, after_pos: usize, affected: &[N]) -> usize {
if affected.is_empty() {
return 0;
}
let mark: FxHashSet<N> = affected.iter().copied().collect();
let mut left: Vec<N> = Vec::with_capacity(self.order.len() - affected.len());
let mut block: Vec<N> = Vec::with_capacity(affected.len());
for &n in &self.order {
if mark.contains(&n) {
block.push(n);
} else {
left.push(n);
}
}
let mut new_order: Vec<N> = Vec::with_capacity(self.order.len());
let mut i = 0usize;
while i < left.len() {
new_order.push(left[i]);
i += 1;
if i - 1 == after_pos {
new_order.extend(block.iter().copied());
}
}
if after_pos >= left.len() {
new_order.extend(block.iter().copied());
}
self.order = new_order;
for (i, &n) in self.order.iter().enumerate() {
self.pos.insert(n, i as u32);
}
affected.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rustc_hash::FxHashMap;
#[derive(Default)]
struct SimpleGraph {
succ: FxHashMap<u32, Vec<u32>>, pred: FxHashMap<u32, Vec<u32>>, }
impl SimpleGraph {
fn add_edge(&mut self, x: u32, y: u32) {
self.succ.entry(x).or_default().push(y);
self.pred.entry(y).or_default().push(x);
}
fn remove_edge(&mut self, x: u32, y: u32) {
if let Some(v) = self.succ.get_mut(&x) {
v.retain(|&t| t != y);
}
if let Some(v) = self.pred.get_mut(&y) {
v.retain(|&s| s != x);
}
}
}
impl GraphView<u32> for SimpleGraph {
fn successors(&self, n: u32, out: &mut Vec<u32>) {
out.clear();
if let Some(v) = self.succ.get(&n) {
out.extend(v.iter().copied());
}
}
fn predecessors(&self, n: u32, out: &mut Vec<u32>) {
out.clear();
if let Some(v) = self.pred.get(&n) {
out.extend(v.iter().copied());
}
}
fn exists(&self, _n: u32) -> bool {
true
}
}
fn idx(order: &[u32], n: u32) -> usize {
order.iter().position(|&x| x == n).unwrap()
}
#[test]
fn rebuild_full_basic_chain() {
let mut g = SimpleGraph::default();
g.add_edge(1, 2);
g.add_edge(2, 3);
let nodes = [1, 2, 3, 4];
let mut pk = DynamicTopo::new(nodes, PkConfig::default());
pk.rebuild_full(&g);
let layers = pk.layers_for(&g, &nodes, None);
assert!(!layers.is_empty());
assert!(layers[0].contains(&1));
let order = pk.topo_order().to_vec();
assert!(idx(&order, 1) < idx(&order, 2));
assert!(idx(&order, 2) < idx(&order, 3));
}
#[test]
fn add_edge_forward_no_relabel() {
let g = SimpleGraph::default();
let nodes = [1, 2, 3, 4];
let mut pk = DynamicTopo::new(nodes, PkConfig::default());
pk.rebuild_full(&g);
let stats = pk.try_add_edge(&g, 1, 3).unwrap();
assert_eq!(stats.relabeled, 0);
let order = pk.topo_order();
assert!(idx(order, 1) < idx(order, 3));
}
#[test]
fn add_edge_backedge_splices_without_cycle() {
let g = SimpleGraph::default();
let nodes = [1, 2, 3, 4];
let mut pk = DynamicTopo::new(nodes, PkConfig::default());
pk.rebuild_full(&g);
let _ = pk.try_add_edge(&g, 3, 2).unwrap();
let order = pk.topo_order();
assert!(idx(order, 3) < idx(order, 2));
}
#[test]
fn detect_cycle_on_add() {
let mut g = SimpleGraph::default();
g.add_edge(2, 3);
g.add_edge(3, 4);
let nodes = [1, 2, 3, 4];
let mut pk = DynamicTopo::new(nodes, PkConfig::default());
pk.rebuild_full(&g);
let res = pk.try_add_edge(&g, 4, 2);
assert!(res.is_err());
}
#[test]
fn layers_with_width_cap() {
let mut g = SimpleGraph::default();
g.add_edge(1, 3);
g.add_edge(2, 3);
let nodes = [1, 2, 3, 4];
let mut pk = DynamicTopo::new(nodes, PkConfig::default());
pk.rebuild_full(&g);
let layers = pk.layers_for(&g, &nodes, Some(1));
assert!(layers.iter().all(|layer| layer.len() <= 1));
let mut all: Vec<u32> = layers.into_iter().flatten().collect();
all.sort();
assert_eq!(all, vec![1, 2, 3, 4]);
}
#[test]
fn layers_unbounded_expected_first_layer() {
let mut g = SimpleGraph::default();
g.add_edge(1, 3);
g.add_edge(2, 3); let nodes = [1, 2, 3, 4];
let mut pk = DynamicTopo::new(nodes, PkConfig::default());
pk.rebuild_full(&g);
let layers = pk.layers_for(&g, &nodes, None);
assert!(!layers.is_empty());
let mut got = layers[0].clone();
got.sort();
assert_eq!(got, vec![1, 2, 4]);
assert_eq!(layers[1], vec![3]);
}
#[test]
fn apply_bulk_adds_then_layers() {
let mut g = SimpleGraph::default();
let nodes = [1, 2, 3];
let mut pk = DynamicTopo::new(nodes, PkConfig::default());
pk.rebuild_full(&g);
g.add_edge(1, 2);
g.add_edge(2, 3);
let _stats = pk.apply_bulk(&g, &[], &[(1, 2), (2, 3)]).unwrap();
let layers = pk.layers_for(&g, &nodes, None);
assert_eq!(layers.len(), 3);
}
#[test]
fn budget_config_is_respected_in_api_surface() {
let g = SimpleGraph::default();
let mut pk = DynamicTopo::new(
[1u32, 2, 3, 4, 5],
PkConfig {
visit_budget: 1,
compaction_interval_ops: 10,
},
);
pk.rebuild_full(&g);
assert_eq!(pk.cfg.visit_budget, 1);
}
#[test]
fn ensure_nodes_appends_missing() {
let g = SimpleGraph::default();
let mut pk = DynamicTopo::new([1u32], PkConfig::default());
pk.ensure_nodes([2u32, 3u32]);
let order = pk.topo_order();
assert_eq!(order.len(), 3);
assert!(idx(order, 1) < idx(order, 2));
assert!(idx(order, 2) < idx(order, 3));
}
#[test]
fn compact_ranks_keeps_nodes() {
let g = SimpleGraph::default();
let nodes = [3, 1, 4, 2];
let mut pk = DynamicTopo::new(nodes, PkConfig::default());
pk.rebuild_full(&g);
pk.compact_ranks();
let mut order = pk.topo_order().to_vec();
order.sort();
assert_eq!(order, vec![1, 2, 3, 4]);
}
#[test]
fn remove_edge_does_not_change_order() {
let mut g = SimpleGraph::default();
g.add_edge(1, 3);
let nodes = [1, 2, 3];
let mut pk = DynamicTopo::new(nodes, PkConfig::default());
pk.rebuild_full(&g);
let before = pk.topo_order().to_vec();
pk.remove_edge(2, 1); let after = pk.topo_order().to_vec();
assert_eq!(before, after);
}
#[test]
fn apply_bulk_mixed_removes_and_adds() {
let mut g = SimpleGraph::default();
g.add_edge(1, 2);
g.add_edge(2, 3);
let nodes = [1, 2, 3, 4];
let mut pk = DynamicTopo::new(nodes, PkConfig::default());
pk.rebuild_full(&g);
let _ = pk.apply_bulk(&g, &[], &[(1, 2), (2, 3)]).unwrap();
g.remove_edge(1, 2);
g.add_edge(1, 3);
let stats = pk.apply_bulk(&g, &[(1, 2)], &[(1, 3)]).unwrap();
let order = pk.topo_order().to_vec();
assert!(idx(&order, 1) < idx(&order, 3));
}
#[test]
fn layers_for_subset_only() {
let mut g = SimpleGraph::default();
g.add_edge(1, 2);
g.add_edge(2, 3);
g.add_edge(4, 5);
let nodes = [1, 2, 3, 4, 5];
let mut pk = DynamicTopo::new(nodes, PkConfig::default());
pk.rebuild_full(&g);
let subset = vec![2, 3, 5];
let layers = pk.layers_for(&g, &subset, None);
let mut flat: Vec<u32> = layers.iter().flatten().copied().collect();
flat.sort();
assert_eq!(flat, vec![2, 3, 5]);
let pos2 = layers.iter().position(|lay| lay.contains(&2)).unwrap();
let pos3 = layers.iter().position(|lay| lay.contains(&3)).unwrap();
assert!(pos2 < pos3);
assert!(layers[0].contains(&5));
}
#[test]
fn compact_ranks_repeated_stability() {
let mut g = SimpleGraph::default();
g.add_edge(1, 3);
g.add_edge(2, 3);
let nodes = [1, 2, 3, 4];
let mut pk = DynamicTopo::new(nodes, PkConfig::default());
pk.rebuild_full(&g);
let _ = pk.try_add_edge(&g, 4, 2); let baseline = pk.topo_order().to_vec();
for _ in 0..10 {
pk.compact_ranks();
assert_eq!(baseline, pk.topo_order());
}
}
#[test]
fn compact_ranks_is_stable_repeated() {
let g = SimpleGraph::default();
let nodes = [1, 2, 3, 4];
let mut pk = DynamicTopo::new(nodes, PkConfig::default());
pk.rebuild_full(&g);
let _ = pk.try_add_edge(&g, 3, 2).unwrap();
let before = pk.topo_order().to_vec();
for _ in 0..5 {
pk.compact_ranks();
assert_eq!(pk.topo_order(), &before);
}
}
}