Skip to main content

oxicuda_graphalg/separation/
planar_separator.rs

1//! Lipton-Tarjan style planar separator (1979).
2//!
3//! Given a (planar) graph on `n` vertices, computes a vertex separator `S`
4//! together with a partition of the remaining vertices into `A` and `B` such
5//! that
6//!
7//! * there is **no edge** between `A` and `B` (removing `S` disconnects them),
8//! * each side is balanced: `|A| <= 2n/3` and `|B| <= 2n/3`.
9//!
10//! # Construction (scope)
11//!
12//! This is a *compact* realisation of the Lipton-Tarjan program, built from the
13//! two ingredients the theorem relies on, chosen per input:
14//!
15//! * **BFS levelling** (the heart of Lipton-Tarjan). Run a breadth-first search
16//!   from a root. Because every edge of a BFS-layered graph joins vertices in
17//!   the same or adjacent levels, deleting *one entire level* `l` separates the
18//!   strictly-lower levels from the strictly-higher levels. The *median* level
19//!   always balances both sides below `n/2 <= 2n/3`; among all levels that keep
20//!   both sides within `2n/3` we pick the **smallest** one as the separator. On
21//!   a `sqrt(n) x sqrt(n)` grid the BFS levels are anti-diagonals of size
22//!   `O(sqrt(n))`, giving the textbook `O(sqrt(n))` separator.
23//!
24//! * **Tree centroid** specialisation. For a tree (the degenerate planar case)
25//!   the BFS-level separator can be as large as `n/2`, yet a single *centroid*
26//!   vertex already splits every branch below `n/2`. When the input component is
27//!   a tree we therefore return the centroid (a size-`1` separator) and balance
28//!   its branches into `A`/`B` with an exact subset-sum partition.
29//!
30//! Disconnected inputs are handled component-wise. The balance and
31//! "no `A`-`B` edge" guarantees hold for every input; the `O(sqrt(n))` *size*
32//! guarantee is for the planar classes exercised in the tests (grids, trees,
33//! paths), matching the latitude of the assignment.
34
35use std::collections::VecDeque;
36
37use crate::error::{GraphalgError, GraphalgResult};
38use crate::repr::adjacency_list::AdjacencyList;
39
40/// The three parts of a separator decomposition. Together they partition
41/// `0..n` with no edge crossing between [`SeparatorResult::part_a`] and
42/// [`SeparatorResult::part_b`].
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct SeparatorResult {
45    /// First balanced part.
46    pub part_a: Vec<usize>,
47    /// The separator (whose removal disconnects `A` from `B`).
48    pub separator: Vec<usize>,
49    /// Second balanced part.
50    pub part_b: Vec<usize>,
51}
52
53/// Planar separator solver over an undirected graph.
54#[derive(Debug, Clone)]
55pub struct PlanarSeparator {
56    n: usize,
57    adj: Vec<Vec<usize>>,
58}
59
60impl PlanarSeparator {
61    /// Create an edgeless solver on `n` vertices.
62    pub fn new(n: usize) -> Self {
63        Self {
64            n,
65            adj: vec![Vec::new(); n],
66        }
67    }
68
69    /// Add an undirected edge `(u, v)`. Self-loops are ignored; parallel edges
70    /// are de-duplicated.
71    pub fn add_edge(&mut self, u: usize, v: usize) -> GraphalgResult<()> {
72        if u >= self.n || v >= self.n {
73            return Err(GraphalgError::IndexOutOfBounds {
74                index: u.max(v),
75                len: self.n,
76            });
77        }
78        if u == v {
79            return Ok(());
80        }
81        if !self.adj[u].contains(&v) {
82            self.adj[u].push(v);
83        }
84        if !self.adj[v].contains(&u) {
85            self.adj[v].push(u);
86        }
87        Ok(())
88    }
89
90    /// Build a planar separator solver from an [`AdjacencyList`], symmetrising
91    /// the edges (the graph is treated as undirected).
92    pub fn from_adjacency_list(g: &AdjacencyList) -> GraphalgResult<Self> {
93        let mut sep = Self::new(g.n);
94        for u in 0..g.n {
95            for &v in &g.edges[u] {
96                sep.add_edge(u, v)?;
97            }
98        }
99        Ok(sep)
100    }
101
102    /// Number of vertices.
103    pub fn num_nodes(&self) -> usize {
104        self.n
105    }
106
107    /// Compute a balanced vertex separator.
108    pub fn separate(&self) -> GraphalgResult<SeparatorResult> {
109        if self.n == 0 {
110            return Ok(SeparatorResult {
111                part_a: Vec::new(),
112                separator: Vec::new(),
113                part_b: Vec::new(),
114            });
115        }
116        if self.n == 1 {
117            return Ok(SeparatorResult {
118                part_a: vec![0],
119                separator: Vec::new(),
120                part_b: Vec::new(),
121            });
122        }
123        let comps = self.connected_components();
124        let limit = (2 * self.n) / 3;
125        if comps.len() == 1 {
126            let (a, s, b) = self.separate_component(&comps[0])?;
127            return Ok(finalize(a, s, b));
128        }
129        // Multiple components.
130        let max_comp = comps.iter().map(|c| c.len()).max().unwrap_or(0);
131        if max_comp <= limit {
132            // No separator needed: bin-pack components into A and B.
133            let sizes: Vec<usize> = comps.iter().map(|c| c.len()).collect();
134            let (a_idx, b_idx) = best_partition(&sizes);
135            let mut part_a = Vec::new();
136            let mut part_b = Vec::new();
137            for &i in &a_idx {
138                part_a.extend_from_slice(&comps[i]);
139            }
140            for &i in &b_idx {
141                part_b.extend_from_slice(&comps[i]);
142            }
143            return Ok(finalize(part_a, Vec::new(), part_b));
144        }
145        // One giant component dominates: separate inside it and distribute the
146        // remaining (small) components onto the lighter side.
147        let giant_idx = comps
148            .iter()
149            .enumerate()
150            .max_by_key(|(_, c)| c.len())
151            .map(|(i, _)| i)
152            .unwrap_or(0);
153        let (mut ga, gs, mut gb) = self.separate_component(&comps[giant_idx])?;
154        for (i, c) in comps.iter().enumerate() {
155            if i == giant_idx {
156                continue;
157            }
158            if ga.len() <= gb.len() {
159                ga.extend_from_slice(c);
160            } else {
161                gb.extend_from_slice(c);
162            }
163        }
164        Ok(finalize(ga, gs, gb))
165    }
166
167    /// Connected components as vertex lists.
168    fn connected_components(&self) -> Vec<Vec<usize>> {
169        let mut comp_id = vec![usize::MAX; self.n];
170        let mut comps: Vec<Vec<usize>> = Vec::new();
171        for start in 0..self.n {
172            if comp_id[start] != usize::MAX {
173                continue;
174            }
175            let id = comps.len();
176            let mut members = Vec::new();
177            let mut queue = VecDeque::new();
178            queue.push_back(start);
179            comp_id[start] = id;
180            while let Some(u) = queue.pop_front() {
181                members.push(u);
182                for &w in &self.adj[u] {
183                    if comp_id[w] == usize::MAX {
184                        comp_id[w] = id;
185                        queue.push_back(w);
186                    }
187                }
188            }
189            comps.push(members);
190        }
191        comps
192    }
193
194    /// Number of undirected edges with both endpoints inside `verts`.
195    fn component_edge_count(&self, in_set: &[bool], verts: &[usize]) -> usize {
196        let mut deg_sum = 0usize;
197        for &u in verts {
198            for &w in &self.adj[u] {
199                if in_set[w] {
200                    deg_sum += 1;
201                }
202            }
203        }
204        deg_sum / 2
205    }
206
207    /// Separate a single connected component (`verts`) into `(A, S, B)`.
208    fn separate_component(
209        &self,
210        verts: &[usize],
211    ) -> GraphalgResult<(Vec<usize>, Vec<usize>, Vec<usize>)> {
212        let k = verts.len();
213        if k == 0 {
214            return Ok((Vec::new(), Vec::new(), Vec::new()));
215        }
216        if k == 1 {
217            return Ok((vec![verts[0]], Vec::new(), Vec::new()));
218        }
219        let mut in_set = vec![false; self.n];
220        for &v in verts {
221            in_set[v] = true;
222        }
223        let edge_count = self.component_edge_count(&in_set, verts);
224        if edge_count == k - 1 {
225            self.tree_centroid_separator(verts, &in_set)
226        } else {
227            self.bfs_level_separator(verts, &in_set)
228        }
229    }
230
231    /// Tree case: remove a centroid, split its branches into `A`/`B`.
232    fn tree_centroid_separator(
233        &self,
234        verts: &[usize],
235        in_set: &[bool],
236    ) -> GraphalgResult<(Vec<usize>, Vec<usize>, Vec<usize>)> {
237        let k = verts.len();
238        let root = verts[0];
239        // Rooted BFS order + parents.
240        let mut parent = vec![usize::MAX; self.n];
241        let mut visited = vec![false; self.n];
242        let mut order = Vec::with_capacity(k);
243        let mut queue = VecDeque::new();
244        queue.push_back(root);
245        visited[root] = true;
246        while let Some(u) = queue.pop_front() {
247            order.push(u);
248            for &w in &self.adj[u] {
249                if in_set[w] && !visited[w] {
250                    visited[w] = true;
251                    parent[w] = u;
252                    queue.push_back(w);
253                }
254            }
255        }
256        // Subtree sizes (reverse BFS order).
257        let mut size = vec![0usize; self.n];
258        for &v in verts {
259            size[v] = 1;
260        }
261        for &u in order.iter().rev() {
262            if parent[u] != usize::MAX {
263                size[parent[u]] += size[u];
264            }
265        }
266        // Centroid: minimise the largest component after removal.
267        let mut best_c = root;
268        let mut best_max = usize::MAX;
269        for &u in verts {
270            let mut mx = k - size[u]; // parent side
271            for &w in &self.adj[u] {
272                if in_set[w] && parent[w] == u {
273                    mx = mx.max(size[w]);
274                }
275            }
276            if mx < best_max {
277                best_max = mx;
278                best_c = u;
279            }
280        }
281        // Branch components after removing best_c.
282        let mut removed = vec![false; self.n];
283        removed[best_c] = true;
284        let mut branch_visited = vec![false; self.n];
285        branch_visited[best_c] = true;
286        let mut branches: Vec<Vec<usize>> = Vec::new();
287        for &w in &self.adj[best_c] {
288            if in_set[w] && !branch_visited[w] {
289                let mut comp = Vec::new();
290                let mut q = VecDeque::new();
291                q.push_back(w);
292                branch_visited[w] = true;
293                while let Some(u) = q.pop_front() {
294                    comp.push(u);
295                    for &x in &self.adj[u] {
296                        if in_set[x] && !branch_visited[x] && x != best_c {
297                            branch_visited[x] = true;
298                            q.push_back(x);
299                        }
300                    }
301                }
302                branches.push(comp);
303            }
304        }
305        let sizes: Vec<usize> = branches.iter().map(|c| c.len()).collect();
306        let (a_idx, b_idx) = best_partition(&sizes);
307        let mut part_a = Vec::new();
308        let mut part_b = Vec::new();
309        for &i in &a_idx {
310            part_a.extend_from_slice(&branches[i]);
311        }
312        for &i in &b_idx {
313            part_b.extend_from_slice(&branches[i]);
314        }
315        Ok((part_a, vec![best_c], part_b))
316    }
317
318    /// General case: separate by removing the smallest balancing BFS level.
319    fn bfs_level_separator(
320        &self,
321        verts: &[usize],
322        in_set: &[bool],
323    ) -> GraphalgResult<(Vec<usize>, Vec<usize>, Vec<usize>)> {
324        let k = verts.len();
325        let root = verts[0];
326        let mut level = vec![usize::MAX; self.n];
327        let mut queue = VecDeque::new();
328        level[root] = 0;
329        queue.push_back(root);
330        let mut max_level = 0usize;
331        while let Some(u) = queue.pop_front() {
332            let lu = level[u];
333            if lu > max_level {
334                max_level = lu;
335            }
336            for &w in &self.adj[u] {
337                if in_set[w] && level[w] == usize::MAX {
338                    level[w] = lu + 1;
339                    queue.push_back(w);
340                }
341            }
342        }
343        // Level sizes and cumulative counts.
344        let mut level_size = vec![0usize; max_level + 1];
345        for &v in verts {
346            level_size[level[v]] += 1;
347        }
348        let mut prefix = vec![0usize; max_level + 2];
349        for l in 0..=max_level {
350            prefix[l + 1] = prefix[l] + level_size[l];
351        }
352        let limit = (2 * k) / 3;
353        // Among levels keeping both sides within 2k/3, pick the smallest level.
354        let mut chosen = usize::MAX;
355        let mut chosen_size = usize::MAX;
356        for l in 0..=max_level {
357            let below = prefix[l];
358            let above = k - prefix[l + 1];
359            if below <= limit && above <= limit && level_size[l] < chosen_size {
360                chosen_size = level_size[l];
361                chosen = l;
362            }
363        }
364        // The median level always qualifies, so `chosen` is set; guard anyway.
365        if chosen == usize::MAX {
366            chosen = 0;
367        }
368        let mut part_a = Vec::new();
369        let mut separator = Vec::new();
370        let mut part_b = Vec::new();
371        for &v in verts {
372            let lv = level[v];
373            if lv < chosen {
374                part_a.push(v);
375            } else if lv == chosen {
376                separator.push(v);
377            } else {
378                part_b.push(v);
379            }
380        }
381        Ok((part_a, separator, part_b))
382    }
383}
384
385/// Sort each part and assemble a [`SeparatorResult`].
386fn finalize(mut a: Vec<usize>, mut s: Vec<usize>, mut b: Vec<usize>) -> SeparatorResult {
387    a.sort_unstable();
388    s.sort_unstable();
389    b.sort_unstable();
390    SeparatorResult {
391        part_a: a,
392        separator: s,
393        part_b: b,
394    }
395}
396
397/// Split item `sizes` into two index groups whose total sizes are as balanced
398/// as possible, via exact subset-sum dynamic programming.
399fn best_partition(sizes: &[usize]) -> (Vec<usize>, Vec<usize>) {
400    let m = sizes.len();
401    if m == 0 {
402        return (Vec::new(), Vec::new());
403    }
404    let total: usize = sizes.iter().sum();
405    if total == 0 {
406        return ((0..m).collect(), Vec::new());
407    }
408    let mut reach = vec![vec![false; total + 1]; m + 1];
409    reach[0][0] = true;
410    for i in 1..=m {
411        let s = sizes[i - 1];
412        for x in 0..=total {
413            if reach[i - 1][x] {
414                reach[i][x] = true;
415                if x + s <= total {
416                    reach[i][x + s] = true;
417                }
418            }
419        }
420    }
421    let target = total / 2;
422    let mut best_x = 0usize;
423    let mut best_diff = usize::MAX;
424    for x in 0..=total {
425        if reach[m][x] {
426            let diff = x.abs_diff(target);
427            if diff < best_diff {
428                best_diff = diff;
429                best_x = x;
430            }
431        }
432    }
433    let mut a = Vec::new();
434    let mut b = Vec::new();
435    let mut x = best_x;
436    for i in (1..=m).rev() {
437        let s = sizes[i - 1];
438        if x >= s && reach[i - 1][x - s] {
439            a.push(i - 1);
440            x -= s;
441        } else {
442            b.push(i - 1);
443        }
444    }
445    (a, b)
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451    use std::collections::BTreeSet;
452
453    /// Verify the structural separator invariants against an explicit edge list.
454    fn check_separator(
455        n: usize,
456        edges: &[(usize, usize)],
457        res: &SeparatorResult,
458        balance_limit: usize,
459    ) {
460        // Partition of 0..n with no overlaps.
461        let mut seen = vec![0u8; n];
462        for &v in res.part_a.iter().chain(&res.separator).chain(&res.part_b) {
463            seen[v] += 1;
464        }
465        for (v, &c) in seen.iter().enumerate() {
466            assert_eq!(c, 1, "vertex {v} appears {c} times (must be exactly once)");
467        }
468        // No edge between A and B.
469        let a: BTreeSet<usize> = res.part_a.iter().copied().collect();
470        let b: BTreeSet<usize> = res.part_b.iter().copied().collect();
471        for &(u, v) in edges {
472            let ab = a.contains(&u) && b.contains(&v);
473            let ba = b.contains(&u) && a.contains(&v);
474            assert!(!ab && !ba, "edge ({u},{v}) crosses A-B separator");
475        }
476        // Balance.
477        assert!(
478            res.part_a.len() <= balance_limit,
479            "|A|={} exceeds {balance_limit}",
480            res.part_a.len()
481        );
482        assert!(
483            res.part_b.len() <= balance_limit,
484            "|B|={} exceeds {balance_limit}",
485            res.part_b.len()
486        );
487    }
488
489    fn build_grid(a: usize) -> (PlanarSeparator, Vec<(usize, usize)>) {
490        let n = a * a;
491        let mut sep = PlanarSeparator::new(n);
492        let mut edges = Vec::new();
493        for i in 0..a {
494            for j in 0..a {
495                let id = i * a + j;
496                if j + 1 < a {
497                    sep.add_edge(id, id + 1).expect("edge");
498                    edges.push((id, id + 1));
499                }
500                if i + 1 < a {
501                    sep.add_edge(id, id + a).expect("edge");
502                    edges.push((id, id + a));
503                }
504            }
505        }
506        (sep, edges)
507    }
508
509    #[test]
510    fn grid_separator_is_small_and_balanced() {
511        let a = 7;
512        let n = a * a;
513        let (sep, edges) = build_grid(a);
514        let res = sep.separate().expect("separate");
515        let limit = (2 * n) / 3;
516        check_separator(n, &edges, &res, limit);
517        // O(sqrt(n)): a single anti-diagonal/row is ~ sqrt(n).
518        let bound = (3.0 * (n as f64).sqrt()).ceil() as usize;
519        assert!(
520            res.separator.len() <= bound,
521            "separator size {} exceeds O(sqrt(n)) bound {bound}",
522            res.separator.len()
523        );
524        // Removing S must leave both sides non-empty for a grid (real split).
525        assert!(!res.part_a.is_empty() && !res.part_b.is_empty());
526    }
527
528    #[test]
529    fn grid_partition_property_holds() {
530        let a = 5;
531        let n = a * a;
532        let (sep, edges) = build_grid(a);
533        let res = sep.separate().expect("separate");
534        check_separator(n, &edges, &res, (2 * n) / 3);
535    }
536
537    #[test]
538    fn path_has_size_one_separator() {
539        let n = 11;
540        let mut sep = PlanarSeparator::new(n);
541        let mut edges = Vec::new();
542        for i in 0..n - 1 {
543            sep.add_edge(i, i + 1).expect("edge");
544            edges.push((i, i + 1));
545        }
546        let res = sep.separate().expect("separate");
547        check_separator(n, &edges, &res, (2 * n) / 3);
548        assert_eq!(
549            res.separator.len(),
550            1,
551            "path centroid separator is one vertex"
552        );
553    }
554
555    #[test]
556    fn balanced_binary_tree_uses_centroid() {
557        // Complete binary tree with 15 nodes: BFS-level would remove ~8 leaves,
558        // but the centroid is a single vertex.
559        let n = 15;
560        let mut sep = PlanarSeparator::new(n);
561        let mut edges = Vec::new();
562        for i in 1..n {
563            let p = (i - 1) / 2;
564            sep.add_edge(p, i).expect("edge");
565            edges.push((p, i));
566        }
567        let res = sep.separate().expect("separate");
568        check_separator(n, &edges, &res, (2 * n) / 3);
569        assert_eq!(res.separator.len(), 1, "tree separator is the centroid");
570    }
571
572    #[test]
573    fn star_separator_is_center() {
574        let n = 9;
575        let mut sep = PlanarSeparator::new(n);
576        let mut edges = Vec::new();
577        for i in 1..n {
578            sep.add_edge(0, i).expect("edge");
579            edges.push((0, i));
580        }
581        let res = sep.separate().expect("separate");
582        check_separator(n, &edges, &res, (2 * n) / 3);
583        // Removing the hub disconnects everything; centroid = hub.
584        assert_eq!(res.separator, vec![0]);
585    }
586
587    #[test]
588    fn disconnected_components_partition_with_empty_separator() {
589        // Two triangles (disjoint), each 3 vertices.
590        let n = 6;
591        let mut sep = PlanarSeparator::new(n);
592        let edges = [(0, 1), (1, 2), (0, 2), (3, 4), (4, 5), (3, 5)];
593        for &(u, v) in &edges {
594            sep.add_edge(u, v).expect("edge");
595        }
596        let res = sep.separate().expect("separate");
597        check_separator(n, &edges, &res, (2 * n) / 3);
598        assert!(
599            res.separator.is_empty(),
600            "two equal triangles split with no separator"
601        );
602        assert_eq!(res.part_a.len(), 3);
603        assert_eq!(res.part_b.len(), 3);
604    }
605
606    #[test]
607    fn trivial_graphs_handled() {
608        let empty = PlanarSeparator::new(0);
609        let r0 = empty.separate().expect("separate");
610        assert!(r0.part_a.is_empty() && r0.separator.is_empty() && r0.part_b.is_empty());
611
612        let single = PlanarSeparator::new(1);
613        let r1 = single.separate().expect("separate");
614        assert_eq!(r1.part_a, vec![0]);
615        assert!(r1.separator.is_empty() && r1.part_b.is_empty());
616    }
617
618    #[test]
619    fn rejects_out_of_range_edge() {
620        let mut sep = PlanarSeparator::new(3);
621        assert!(sep.add_edge(0, 5).is_err());
622    }
623
624    #[test]
625    fn from_adjacency_list_round_trip() {
626        let mut g = AdjacencyList::new(4);
627        g.add_undirected_edge(0, 1).expect("edge");
628        g.add_undirected_edge(1, 2).expect("edge");
629        g.add_undirected_edge(2, 3).expect("edge");
630        let sep = PlanarSeparator::from_adjacency_list(&g).expect("build");
631        let res = sep.separate().expect("separate");
632        let edges = [(0, 1), (1, 2), (2, 3)];
633        check_separator(4, &edges, &res, (2 * 4) / 3);
634    }
635}