1use crate::components::SquareComponent;
22use crate::incidence::EqualityIncidence;
23use crate::matching::BipartiteMatching;
24
25#[derive(Debug, Clone, Default)]
28pub struct BlockTriangularBlock {
29 pub eq_rows: Vec<usize>,
30 pub cols: Vec<usize>,
31}
32
33#[derive(Debug, Clone, Default)]
35pub struct BlockTriangularForm {
36 pub blocks: Vec<BlockTriangularBlock>,
38}
39
40#[allow(clippy::expect_used)]
43impl BlockTriangularForm {
44 pub fn of_component(
82 inc: &EqualityIncidence,
83 m: &BipartiteMatching,
84 component: &SquareComponent,
85 ) -> Self {
86 let n = component.eq_rows.len();
87 debug_assert_eq!(
88 n,
89 component.cols.len(),
90 "square component shapes must match"
91 );
92 debug_assert!(
97 component.eq_rows.iter().all(|&r| m.row_to_var[r].is_some()),
98 "SquareComponent invariant violated: unmatched row in eq_rows"
99 );
100 if n == 0 {
101 return Self::default();
102 }
103
104 let mut col_to_node: std::collections::HashMap<usize, usize> =
107 std::collections::HashMap::with_capacity(n);
108 for (i, &r) in component.eq_rows.iter().enumerate() {
109 let c = m.row_to_var[r].expect("square row must be matched");
110 col_to_node.insert(c, i);
111 }
112
113 let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
116 for (i, &r) in component.eq_rows.iter().enumerate() {
117 let own_col = m.row_to_var[r].expect("matched");
118 for &c in inc.neighbors(r) {
119 if c == own_col {
120 continue;
121 }
122 if let Some(&j) = col_to_node.get(&c) {
123 if j != i {
124 adj[i].push(j);
125 }
126 }
127 }
128 adj[i].sort_unstable();
129 adj[i].dedup();
130 }
131
132 let sccs = tarjan_scc(&adj);
138
139 let blocks: Vec<BlockTriangularBlock> = sccs
140 .into_iter()
141 .map(|scc| {
142 let mut eq_rows: Vec<usize> = scc.iter().map(|&i| component.eq_rows[i]).collect();
143 let mut cols: Vec<usize> = scc
144 .iter()
145 .map(|&i| {
146 let r = component.eq_rows[i];
147 m.row_to_var[r].expect("matched")
148 })
149 .collect();
150 eq_rows.sort_unstable();
151 cols.sort_unstable();
152 BlockTriangularBlock { eq_rows, cols }
153 })
154 .collect();
155 debug_assert!(blocks.len() <= n);
156 Self { blocks }
157 }
158}
159
160#[allow(clippy::expect_used)]
165fn tarjan_scc(adj: &[Vec<usize>]) -> Vec<Vec<usize>> {
166 let n = adj.len();
167 const UNVISITED: usize = usize::MAX;
168 let mut index = vec![UNVISITED; n];
169 let mut lowlink = vec![0usize; n];
170 let mut on_stack = vec![false; n];
171 let mut stack: Vec<usize> = Vec::new();
172 let mut next_index: usize = 0;
173 let mut sccs: Vec<Vec<usize>> = Vec::new();
174
175 for v0 in 0..n {
177 if index[v0] != UNVISITED {
178 continue;
179 }
180 let mut call_stack: Vec<(usize, usize)> = Vec::new();
181 index[v0] = next_index;
182 lowlink[v0] = next_index;
183 next_index += 1;
184 stack.push(v0);
185 on_stack[v0] = true;
186 call_stack.push((v0, 0));
187
188 while let Some(&(v, ref_pos)) = call_stack.last() {
189 let pos = ref_pos;
190 if pos < adj[v].len() {
191 let w = adj[v][pos];
192 call_stack.last_mut().expect("non-empty").1 = pos + 1;
195 if index[w] == UNVISITED {
196 index[w] = next_index;
197 lowlink[w] = next_index;
198 next_index += 1;
199 stack.push(w);
200 on_stack[w] = true;
201 call_stack.push((w, 0));
202 } else if on_stack[w] {
203 lowlink[v] = lowlink[v].min(index[w]);
204 }
205 } else {
206 if lowlink[v] == index[v] {
208 let mut scc = Vec::new();
210 while let Some(w) = stack.pop() {
211 on_stack[w] = false;
212 scc.push(w);
213 if w == v {
214 break;
215 }
216 }
217 sccs.push(scc);
218 }
219 call_stack.pop();
220 if let Some(&mut (parent, _)) = call_stack.last_mut() {
221 lowlink[parent] = lowlink[parent].min(lowlink[v]);
222 }
223 }
224 }
225 }
226
227 sccs
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use crate::components::SquareComponents;
234 use crate::dulmage_mendelsohn::DulmageMendelsohnPartition;
235 use crate::matching::hopcroft_karp;
236
237 fn eq_inc(n_vars: usize, n_rows: usize, edges: &[(usize, usize)]) -> EqualityIncidence {
238 let mut per_row: Vec<Vec<usize>> = vec![Vec::new(); n_rows];
239 for &(r, v) in edges {
240 per_row[r].push(v);
241 }
242 let mut adj_ptr = Vec::with_capacity(n_rows + 1);
243 let mut vars = Vec::new();
244 adj_ptr.push(0);
245 for row in per_row.iter_mut() {
246 row.sort_unstable();
247 row.dedup();
248 vars.extend_from_slice(row);
249 adj_ptr.push(vars.len());
250 }
251 EqualityIncidence {
252 n_vars,
253 eq_row_inner_idx: (0..n_rows).collect(),
254 adj_ptr,
255 vars,
256 }
257 }
258
259 fn btf_of(n_vars: usize, n_rows: usize, edges: &[(usize, usize)]) -> Vec<BlockTriangularForm> {
260 let inc = eq_inc(n_vars, n_rows, edges);
261 let m = hopcroft_karp(&inc);
262 let dm = DulmageMendelsohnPartition::from_matching(&inc, &m);
263 let comps = SquareComponents::of_square_part(&inc, &m, &dm);
264 comps
265 .components
266 .iter()
267 .map(|c| BlockTriangularForm::of_component(&inc, &m, c))
268 .collect()
269 }
270
271 #[test]
272 fn btf_singleton_block() {
273 let btfs = btf_of(1, 1, &[(0, 0)]);
275 assert_eq!(btfs.len(), 1);
276 assert_eq!(btfs[0].blocks.len(), 1);
277 assert_eq!(btfs[0].blocks[0].eq_rows, vec![0]);
278 assert_eq!(btfs[0].blocks[0].cols, vec![0]);
279 }
280
281 #[test]
282 fn btf_chain_lower_triangular() {
283 let edges = [(0, 0), (1, 0), (1, 1), (2, 0), (2, 1), (2, 2)];
289 let btfs = btf_of(3, 3, &edges);
290 assert_eq!(btfs.len(), 1);
291 let btf = &btfs[0];
292 assert_eq!(btf.blocks.len(), 3);
293 assert_eq!(btf.blocks[0].eq_rows, vec![0]);
294 assert_eq!(btf.blocks[1].eq_rows, vec![1]);
295 assert_eq!(btf.blocks[2].eq_rows, vec![2]);
296 }
297
298 #[test]
299 fn btf_full_cycle_single_block() {
300 let edges = [(0, 0), (0, 1), (1, 1), (1, 2), (2, 2), (2, 0)];
305 let btfs = btf_of(3, 3, &edges);
306 assert_eq!(btfs.len(), 1);
307 let btf = &btfs[0];
308 assert_eq!(btf.blocks.len(), 1);
309 assert_eq!(btf.blocks[0].eq_rows, vec![0, 1, 2]);
310 assert_eq!(btf.blocks[0].cols, vec![0, 1, 2]);
311 }
312
313 #[test]
314 fn btf_two_subcycles_chained() {
315 let edges = [
321 (0, 0),
322 (0, 1),
323 (1, 1),
324 (1, 0),
325 (2, 2),
326 (2, 3),
327 (3, 3),
328 (3, 2),
329 (2, 0), ];
331 let btfs = btf_of(4, 4, &edges);
332 assert_eq!(btfs.len(), 1);
333 let btf = &btfs[0];
334 assert_eq!(btf.blocks.len(), 2, "two size-2 SCCs");
335 assert_eq!(btf.blocks[0].eq_rows, vec![0, 1]);
336 assert_eq!(btf.blocks[1].eq_rows, vec![2, 3]);
337 }
338
339 #[test]
340 fn btf_empty_component() {
341 let inc = eq_inc(0, 0, &[]);
342 let m = hopcroft_karp(&inc);
343 let comp = SquareComponent {
344 eq_rows: vec![],
345 cols: vec![],
346 };
347 let btf = BlockTriangularForm::of_component(&inc, &m, &comp);
348 assert!(btf.blocks.is_empty());
349 }
350
351 #[test]
352 fn btf_elimination_order_respects_dependencies() {
353 let edges = [
360 (0, 0),
361 (1, 1),
362 (1, 0),
363 (2, 2),
364 (2, 1),
365 (3, 3),
366 (3, 4),
367 (3, 2), (4, 4),
369 (4, 3),
370 ];
371 let btfs = btf_of(5, 5, &edges);
372 assert_eq!(btfs.len(), 1);
373 let btf = &btfs[0];
374 let mut col_block = std::collections::HashMap::new();
376 for (b_idx, block) in btf.blocks.iter().enumerate() {
377 for &c in &block.cols {
378 col_block.insert(c, b_idx);
379 }
380 }
381 let inc = eq_inc(5, 5, &edges);
385 for (k, block) in btf.blocks.iter().enumerate() {
386 for &r in &block.eq_rows {
387 for &c in inc.neighbors(r) {
388 if block.cols.contains(&c) {
389 continue;
390 }
391 let owner = *col_block.get(&c).expect("col owned by some block");
392 assert!(owner < k, "block {k} uses col {c} from later block {owner}");
393 }
394 }
395 }
396 assert_eq!(btf.blocks.len(), 4);
400 assert_eq!(btf.blocks[0].eq_rows, vec![0]);
401 assert_eq!(btf.blocks[1].eq_rows, vec![1]);
402 assert_eq!(btf.blocks[2].eq_rows, vec![2]);
403 assert_eq!(btf.blocks[3].eq_rows, vec![3, 4]);
404 }
405
406 #[test]
407 fn btf_self_loop_singleton() {
408 let btfs = btf_of(1, 1, &[(0, 0)]);
411 assert_eq!(btfs.len(), 1);
412 assert_eq!(btfs[0].blocks.len(), 1);
413 }
414
415 #[test]
416 fn btf_three_disjoint_singletons() {
417 let edges = [(0, 0), (1, 1), (2, 2)];
418 let btfs = btf_of(3, 3, &edges);
419 assert_eq!(btfs.len(), 3);
421 for btf in &btfs {
422 assert_eq!(btf.blocks.len(), 1);
423 }
424 }
425
426 fn reference_sccs_in_elim_order(adj: &[Vec<usize>]) -> Vec<Vec<usize>> {
434 let n = adj.len();
435 let mut reach = vec![vec![false; n]; n];
437 for (i, row) in reach.iter_mut().enumerate().take(n) {
438 for &j in &adj[i] {
439 row[j] = true;
440 }
441 }
442 for k in 0..n {
444 for i in 0..n {
445 if reach[i][k] {
446 for j in 0..n {
447 if reach[k][j] {
448 reach[i][j] = true;
449 }
450 }
451 }
452 }
453 }
454 let mut comp_of = vec![usize::MAX; n];
456 let mut comps: Vec<Vec<usize>> = Vec::new();
457 for i in 0..n {
458 if comp_of[i] != usize::MAX {
459 continue;
460 }
461 let id = comps.len();
462 let mut bucket = vec![i];
463 comp_of[i] = id;
464 for j in (i + 1)..n {
465 let mutual = (reach[i][j] || i == j) && (reach[j][i] || i == j);
466 if mutual {
467 bucket.push(j);
468 comp_of[j] = id;
469 }
470 }
471 bucket.sort_unstable();
472 comps.push(bucket);
473 }
474 let n_c = comps.len();
476 let mut c_adj: Vec<std::collections::BTreeSet<usize>> =
477 vec![std::collections::BTreeSet::new(); n_c];
478 let mut indeg = vec![0usize; n_c];
479 for (i, row) in adj.iter().enumerate().take(n) {
480 for &j in row {
481 let ci = comp_of[i];
482 let cj = comp_of[j];
483 if ci != cj && c_adj[ci].insert(cj) {
484 indeg[cj] += 1;
485 }
486 }
487 }
488 let mut rev_indeg = vec![0usize; n_c];
493 for adj_set in &c_adj {
494 for &j in adj_set {
495 rev_indeg[j] += 1;
496 }
497 }
498 let mut out_deg = vec![0usize; n_c];
501 for (i, adj_set) in c_adj.iter().enumerate().take(n_c) {
502 out_deg[i] = adj_set.len();
503 let _ = i; }
505 let mut queue: std::collections::BTreeSet<usize> =
506 (0..n_c).filter(|&i| out_deg[i] == 0).collect();
507 let mut order: Vec<Vec<usize>> = Vec::with_capacity(n_c);
508 let mut rev_adj: Vec<Vec<usize>> = vec![Vec::new(); n_c];
510 for (i, adj_set) in c_adj.iter().enumerate().take(n_c) {
511 for &j in adj_set {
512 rev_adj[j].push(i);
513 }
514 }
515 let _ = rev_indeg;
516 while let Some(&c) = queue.iter().next() {
517 queue.remove(&c);
518 order.push(comps[c].clone());
519 for &pred in &rev_adj[c] {
520 out_deg[pred] -= 1;
521 if out_deg[pred] == 0 {
522 queue.insert(pred);
523 }
524 }
525 }
526 assert_eq!(order.len(), n_c, "topological sort lost an SCC");
527 order
528 }
529
530 #[test]
538 fn btf_fuzz_invariants() {
539 let mut state: u64 = 0x1234_5678_9abc_def0;
540 let mut next = || -> u64 {
541 state = state
542 .wrapping_mul(6364136223846793005)
543 .wrapping_add(1442695040888963407);
544 state >> 32
545 };
546
547 for trial in 0..30 {
548 let n_rows = 1 + (next() % 4) as usize;
549 let n_vars = 1 + (next() % 4) as usize;
550 let max_edges = (n_rows * n_vars).min(8);
551 let n_edges = (next() % (max_edges as u64 + 1)) as usize;
552
553 let mut edge_set = std::collections::BTreeSet::<(usize, usize)>::new();
554 let mut draws = 0usize;
555 while edge_set.len() < n_edges {
556 let r = (next() % n_rows as u64) as usize;
557 let v = (next() % n_vars as u64) as usize;
558 edge_set.insert((r, v));
559 draws += 1;
560 assert!(draws < 10_000);
561 }
562 let edges: Vec<(usize, usize)> = edge_set.into_iter().collect();
563
564 let inc = eq_inc(n_vars, n_rows, &edges);
565 let m = hopcroft_karp(&inc);
566 let dm = DulmageMendelsohnPartition::from_matching(&inc, &m);
567 let comps = SquareComponents::of_square_part(&inc, &m, &dm);
568
569 for (ci, comp) in comps.components.iter().enumerate() {
570 let our_btf = BlockTriangularForm::of_component(&inc, &m, comp);
571 let n = comp.eq_rows.len();
572
573 let mut col_to_node: std::collections::HashMap<usize, usize> =
576 std::collections::HashMap::with_capacity(n);
577 for (i, &r) in comp.eq_rows.iter().enumerate() {
578 let c = m.row_to_var[r].expect("matched");
579 col_to_node.insert(c, i);
580 }
581 let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
582 for (i, &r) in comp.eq_rows.iter().enumerate() {
583 let own_col = m.row_to_var[r].expect("matched");
584 for &c in inc.neighbors(r) {
585 if c == own_col {
586 continue;
587 }
588 if let Some(&j) = col_to_node.get(&c) {
589 if j != i {
590 adj[i].push(j);
591 }
592 }
593 }
594 adj[i].sort_unstable();
595 adj[i].dedup();
596 }
597
598 let ref_sccs = reference_sccs_in_elim_order(&adj);
599
600 assert_eq!(
602 our_btf.blocks.len(),
603 ref_sccs.len(),
604 "trial {trial} comp {ci}: block count differs (ours={}, ref={})",
605 our_btf.blocks.len(),
606 ref_sccs.len(),
607 );
608
609 for (bi, (ours, theirs)) in our_btf.blocks.iter().zip(ref_sccs.iter()).enumerate() {
614 let ours_nodes: std::collections::BTreeSet<usize> = ours
615 .eq_rows
616 .iter()
617 .map(|r| {
618 comp.eq_rows
619 .iter()
620 .position(|x| x == r)
621 .expect("row in component")
622 })
623 .collect();
624 let theirs_nodes: std::collections::BTreeSet<usize> =
625 theirs.iter().copied().collect();
626 assert_eq!(
627 ours_nodes, theirs_nodes,
628 "trial {trial} comp {ci} block {bi}: \
629 node sets differ (ours={:?}, ref={:?})",
630 ours_nodes, theirs_nodes
631 );
632 }
633
634 let sum: usize = our_btf.blocks.iter().map(|b| b.eq_rows.len()).sum();
636 assert_eq!(sum, n, "trial {trial} comp {ci}: block sizes don't add up");
637
638 let mut col_block = std::collections::HashMap::new();
640 for (b_idx, block) in our_btf.blocks.iter().enumerate() {
641 for &c in &block.cols {
642 col_block.insert(c, b_idx);
643 }
644 }
645 for (k, block) in our_btf.blocks.iter().enumerate() {
646 for &r in &block.eq_rows {
647 for &c in inc.neighbors(r) {
648 if !col_block.contains_key(&c) {
649 continue; }
651 if block.cols.contains(&c) {
652 continue;
653 }
654 let owner = col_block[&c];
655 assert!(
656 owner < k,
657 "trial {trial} comp {ci}: block {k} uses col {c} \
658 from later block {owner}"
659 );
660 }
661 }
662 }
663 }
664 }
665 }
666}