1#![allow(
29 clippy::cast_possible_truncation,
30 clippy::cast_sign_loss,
31 clippy::cast_possible_wrap
32)]
33
34use crate::algorithms::flow::dominator_tree::{DominatorMode, DominatorTree, dominator_tree};
35use crate::algorithms::flow::provan_shier::{EStack, MarkedQueue, Pivot, provan_shier_list};
36use crate::algorithms::operators::induced_subgraph::{InducedSubgraphResult, induced_subgraph};
37use crate::core::graph::EdgeId;
38use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
39
40#[derive(Clone, Debug, PartialEq, Eq)]
48pub struct StCuts {
49 pub cuts: Vec<Vec<EdgeId>>,
51 pub partition1s: Vec<Vec<VertexId>>,
53}
54
55pub fn all_st_cuts(graph: &Graph, source: VertexId, target: VertexId) -> IgraphResult<StCuts> {
89 let n = graph.vcount();
90
91 if !graph.is_directed() {
92 return Err(IgraphError::InvalidArgument(
93 "all_st_cuts: listing all s-t cuts is only implemented for directed graphs".to_string(),
94 ));
95 }
96 if source >= n {
97 return Err(IgraphError::VertexOutOfRange { id: source, n });
98 }
99 if target >= n {
100 return Err(IgraphError::VertexOutOfRange { id: target, n });
101 }
102 if source == target {
103 return Err(IgraphError::InvalidArgument(
104 "all_st_cuts: source and target must differ".to_string(),
105 ));
106 }
107
108 let n_us = n as usize;
109 let mut pivot_fn = AllCutsPivot { graph };
110 let mut partitions = provan_shier_list(n_us, source, target, &mut pivot_fn)?;
111
112 let mut cuts: Vec<Vec<EdgeId>> = Vec::with_capacity(partitions.len());
115 let mut in_s = vec![false; n_us];
116 for part in &mut partitions {
117 part.sort_unstable();
118 for &v in part.iter() {
119 in_s[v as usize] = true;
120 }
121 let mut cut: Vec<EdgeId> = Vec::new();
122 for e in 0..graph.ecount() {
123 let (from, to) = graph.edge(e as EdgeId)?;
124 if in_s[from as usize] && !in_s[to as usize] {
125 cut.push(e as EdgeId);
126 }
127 }
128 cut.sort_unstable();
129 cuts.push(cut);
130 for &v in part.iter() {
131 in_s[v as usize] = false;
132 }
133 }
134
135 Ok(StCuts {
136 cuts,
137 partition1s: partitions,
138 })
139}
140
141struct AllCutsPivot<'a> {
144 graph: &'a Graph,
145}
146
147impl Pivot for AllCutsPivot<'_> {
148 fn pivot(
149 &mut self,
150 s: &MarkedQueue,
151 t: &EStack,
152 source: VertexId,
153 target: VertexId,
154 ) -> IgraphResult<(VertexId, Vec<VertexId>)> {
155 pivot(self.graph, s, t, source, target)
156 }
157}
158
159#[allow(clippy::many_single_char_names)]
166fn pivot(
167 graph: &Graph,
168 s: &MarkedQueue,
169 t: &EStack,
170 source: VertexId,
171 target: VertexId,
172) -> IgraphResult<(VertexId, Vec<VertexId>)> {
173 let n = graph.vcount() as usize;
174
175 let mut keep: Vec<VertexId> = Vec::new();
177 for i in 0..n {
178 if !s.iselement(i as VertexId) {
179 keep.push(i as VertexId);
180 }
181 }
182 let sbar: InducedSubgraphResult = induced_subgraph(graph, &keep)?;
183 let root = sbar.map[target as usize];
184
185 let dt = dominator_tree(&sbar.graph, root, DominatorMode::In)?;
187
188 let mut gamma_s = vec![false; n];
191 if s.size() == 0 {
192 gamma_s[source as usize] = true;
193 } else {
194 for i in 0..n {
195 if s.iselement(i as VertexId) {
196 for nei in graph.out_neighbors_vec(i as VertexId)? {
197 if !s.iselement(nei) {
198 gamma_s[nei as usize] = true;
199 }
200 }
201 }
202 }
203 }
204
205 let mut leftout_old: Vec<VertexId> = Vec::with_capacity(dt.leftout.len());
208 for &lo in &dt.leftout {
209 let old = sbar.invmap[lo as usize];
210 leftout_old.push(old);
211 gamma_s[old as usize] = false;
212 }
213
214 let m = if dt.tree.ecount() > 0 {
216 minimal_gamma(&dt, &sbar, &gamma_s, n)
217 } else {
218 Vec::new()
219 };
220
221 if m.is_empty() {
222 return Ok((0, Vec::new()));
223 }
224
225 let sbar_n = sbar.graph.vcount() as usize;
227 let mut children: Vec<Vec<u32>> = vec![Vec::new(); sbar_n];
228 for v in 0..sbar_n {
229 let p = dt.idom[v];
230 if p >= 0 {
231 children[p as usize].push(v as u32);
232 }
233 }
234
235 let gamma_s_vec: Vec<VertexId> = (0..n)
236 .filter(|&i| gamma_s[i])
237 .map(|i| i as VertexId)
238 .collect();
239
240 for &m_old in &m {
241 let min_new = sbar.map[m_old as usize];
242 let mut nuv: Vec<VertexId> = dom_subtree(&children, min_new)
244 .into_iter()
245 .map(|x| sbar.invmap[x as usize])
246 .collect();
247
248 let isv_min = restricted_bfs(graph, &gamma_s_vec, &nuv, n)?;
250
251 let blocked = isv_min.iter().any(|&u| t.iselement(u) || u == target);
254 if !blocked {
255 nuv.extend_from_slice(&leftout_old);
257 let isv = restricted_bfs(graph, &[m_old], &nuv, n)?;
258 return Ok((m_old, isv));
259 }
260 }
261
262 Ok((0, Vec::new()))
263}
264
265fn minimal_gamma(
269 dt: &DominatorTree,
270 sbar: &InducedSubgraphResult,
271 gamma_s: &[bool],
272 n: usize,
273) -> Vec<VertexId> {
274 let mut nomark: Vec<bool> = (0..n).map(|i| !gamma_s[i]).collect();
278
279 for g_old in 0..n {
280 if !gamma_s[g_old] {
281 continue;
282 }
283 let gn = sbar.map[g_old];
284 if gn == u32::MAX {
285 continue;
286 }
287 let mut cur = dt.idom[gn as usize];
288 while cur >= 0 {
289 let anc_old = sbar.invmap[cur as usize] as usize;
290 if gamma_s[anc_old] {
291 nomark[anc_old] = true;
292 }
293 cur = dt.idom[cur as usize];
294 }
295 }
296
297 (0..n)
298 .filter(|&i| !nomark[i])
299 .map(|i| i as VertexId)
300 .collect()
301}
302
303fn dom_subtree(children: &[Vec<u32>], root: u32) -> Vec<u32> {
306 let mut out = Vec::new();
307 let mut stack = vec![root];
308 while let Some(v) = stack.pop() {
309 out.push(v);
310 for &c in &children[v as usize] {
311 stack.push(c);
312 }
313 }
314 out
315}
316
317fn restricted_bfs(
321 graph: &Graph,
322 roots: &[VertexId],
323 restricted: &[VertexId],
324 n: usize,
325) -> IgraphResult<Vec<VertexId>> {
326 let mut allowed = vec![false; n];
327 for &r in restricted {
328 allowed[r as usize] = true;
329 }
330 let mut visited = vec![false; n];
331 let mut order: Vec<VertexId> = Vec::new();
332 let mut queue: std::collections::VecDeque<VertexId> = std::collections::VecDeque::new();
333
334 for &root in roots {
335 if !allowed[root as usize] || visited[root as usize] {
336 continue;
337 }
338 visited[root as usize] = true;
339 order.push(root);
340 queue.push_back(root);
341 while let Some(u) = queue.pop_front() {
342 for nei in graph.out_neighbors_vec(u)? {
343 if allowed[nei as usize] && !visited[nei as usize] {
344 visited[nei as usize] = true;
345 order.push(nei);
346 queue.push_back(nei);
347 }
348 }
349 }
350 }
351
352 Ok(order)
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 fn build(n: u32, edges: &[(u32, u32)]) -> Graph {
362 let mut g = Graph::new(n, true).expect("directed graph");
363 for &(u, v) in edges {
364 g.add_edge(u, v).expect("add edge");
365 }
366 g
367 }
368
369 fn target_reachable_minus_cut(
373 g: &Graph,
374 source: VertexId,
375 target: VertexId,
376 cut: &[EdgeId],
377 ) -> bool {
378 let n = g.vcount() as usize;
379 let removed: std::collections::HashSet<EdgeId> = cut.iter().copied().collect();
380 let mut adj: Vec<Vec<VertexId>> = vec![Vec::new(); n];
381 for e in 0..g.ecount() as EdgeId {
382 if removed.contains(&e) {
383 continue;
384 }
385 let (from, to) = g.edge(e).expect("edge");
386 adj[from as usize].push(to);
387 }
388 let mut visited = vec![false; n];
389 let mut stack = vec![source];
390 visited[source as usize] = true;
391 while let Some(u) = stack.pop() {
392 if u == target {
393 return true;
394 }
395 for &w in &adj[u as usize] {
396 if !visited[w as usize] {
397 visited[w as usize] = true;
398 stack.push(w);
399 }
400 }
401 }
402 false
403 }
404
405 fn assert_cuts_consistent(g: &Graph, source: VertexId, target: VertexId, res: &StCuts) {
408 assert_eq!(
409 res.cuts.len(),
410 res.partition1s.len(),
411 "cuts and partition1s must have equal length"
412 );
413 let n = g.vcount() as usize;
414 let mut seen: std::collections::HashSet<Vec<VertexId>> = std::collections::HashSet::new();
415 for (part, cut) in res.partition1s.iter().zip(res.cuts.iter()) {
416 for w in part.windows(2) {
418 assert!(w[0] < w[1], "partition not strictly ascending: {part:?}");
419 }
420 for w in cut.windows(2) {
421 assert!(w[0] < w[1], "cut not strictly ascending: {cut:?}");
422 }
423 assert!(
425 part.contains(&source),
426 "source {source} missing from {part:?}"
427 );
428 assert!(
429 !part.contains(&target),
430 "target {target} present in {part:?}"
431 );
432 assert!(seen.insert(part.clone()), "duplicate partition {part:?}");
434
435 let mut in_s = vec![false; n];
436 for &v in part {
437 in_s[v as usize] = true;
438 }
439 let mut crossing: Vec<EdgeId> = Vec::new();
441 for e in 0..g.ecount() as EdgeId {
442 let (from, to) = g.edge(e).expect("edge");
443 if in_s[from as usize] && !in_s[to as usize] {
444 crossing.push(e);
445 }
446 }
447 crossing.sort_unstable();
448 assert_eq!(cut, &crossing, "cut != crossing-out edges of {part:?}");
449 assert!(
451 !target_reachable_minus_cut(g, source, target, cut),
452 "target still reachable after removing cut {cut:?} (partition {part:?})"
453 );
454 }
455 }
456
457 #[test]
459 fn golden_six_node_nine_cuts() {
460 let g = build(6, &[(0, 1), (1, 2), (1, 3), (2, 4), (3, 4), (1, 5), (5, 4)]);
461 let res = all_st_cuts(&g, 0, 4).expect("compute");
462
463 let expected = StCuts {
464 partition1s: vec![
465 vec![0],
466 vec![0, 1],
467 vec![0, 1, 5],
468 vec![0, 1, 3],
469 vec![0, 1, 3, 5],
470 vec![0, 1, 2],
471 vec![0, 1, 2, 5],
472 vec![0, 1, 2, 3],
473 vec![0, 1, 2, 3, 5],
474 ],
475 cuts: vec![
476 vec![0],
477 vec![1, 2, 5],
478 vec![1, 2, 6],
479 vec![1, 4, 5],
480 vec![1, 4, 6],
481 vec![2, 3, 5],
482 vec![2, 3, 6],
483 vec![3, 4, 5],
484 vec![3, 4, 6],
485 ],
486 };
487 assert_eq!(res, expected, "golden fixture mismatch");
488 assert_cuts_consistent(&g, 0, 4, &res);
489 }
490
491 #[test]
493 fn simple_path_two_cuts() {
494 let g = build(3, &[(0, 1), (1, 2)]);
495 let res = all_st_cuts(&g, 0, 2).expect("compute");
496 assert_eq!(res.partition1s, vec![vec![0], vec![0, 1]]);
497 assert_eq!(res.cuts, vec![vec![0], vec![1]]);
498 assert_cuts_consistent(&g, 0, 2, &res);
499 }
500
501 #[test]
504 fn complete_digraph_k5_has_eight_cuts() {
505 let mut edges = Vec::new();
506 for u in 0..5 {
507 for v in 0..5 {
508 if u != v {
509 edges.push((u, v));
510 }
511 }
512 }
513 let g = build(5, &edges);
514 let res = all_st_cuts(&g, 0, 4).expect("compute");
515 assert_eq!(res.cuts.len(), 8, "K5 should yield 2^(5-2) = 8 cuts");
516 assert_cuts_consistent(&g, 0, 4, &res);
517 }
518
519 #[test]
521 fn empty_graph_source_out_of_range() {
522 let g = Graph::new(0, true).expect("empty directed graph");
523 let err = all_st_cuts(&g, 0, 1).expect_err("must reject empty graph");
524 match err {
525 IgraphError::VertexOutOfRange { id, n } => {
526 assert_eq!(id, 0);
527 assert_eq!(n, 0);
528 }
529 other => panic!("unexpected error: {other:?}"),
530 }
531 }
532
533 #[test]
536 fn single_vertex_rejects_equal_endpoints() {
537 let g = Graph::new(1, true).expect("single vertex directed graph");
538 let err = all_st_cuts(&g, 0, 0).expect_err("must reject source == target");
539 assert!(matches!(err, IgraphError::InvalidArgument(_)));
540 let err2 = all_st_cuts(&g, 0, 1).expect_err("must reject out-of-range target");
542 assert!(matches!(
543 err2,
544 IgraphError::VertexOutOfRange { id: 1, n: 1 }
545 ));
546 }
547
548 #[test]
550 fn rejects_undirected_graph() {
551 let g = Graph::with_vertices(4);
552 let err = all_st_cuts(&g, 0, 3).expect_err("must reject undirected");
553 assert!(matches!(err, IgraphError::InvalidArgument(_)));
554 }
555
556 #[test]
558 fn rejects_equal_source_and_target() {
559 let g = build(3, &[(0, 1), (1, 2)]);
560 let err = all_st_cuts(&g, 1, 1).expect_err("must reject source == target");
561 assert!(matches!(err, IgraphError::InvalidArgument(_)));
562 }
563
564 #[test]
566 fn rejects_out_of_range_endpoints() {
567 let g = build(3, &[(0, 1), (1, 2)]);
568 match all_st_cuts(&g, 5, 1).expect_err("bad source") {
569 IgraphError::VertexOutOfRange { id, n } => {
570 assert_eq!((id, n), (5, 3));
571 }
572 other => panic!("unexpected error: {other:?}"),
573 }
574 match all_st_cuts(&g, 0, 9).expect_err("bad target") {
575 IgraphError::VertexOutOfRange { id, n } => {
576 assert_eq!((id, n), (9, 3));
577 }
578 other => panic!("unexpected error: {other:?}"),
579 }
580 }
581
582 #[test]
587 fn unreachable_target_is_consistent() {
588 let g = build(3, &[(1, 2)]);
590 let res = all_st_cuts(&g, 0, 2).expect("compute");
591 assert!(res.cuts.is_empty(), "expected empty cut list, got {res:?}");
594 assert_cuts_consistent(&g, 0, 2, &res);
596 }
597}