Skip to main content

scirs2_optimize/combinatorial/
covering.rs

1//! Set cover, weighted set cover, vertex cover, and hitting set algorithms.
2//!
3//! - Greedy log-approximation for (weighted) set cover
4//! - 2-approximation vertex cover via maximal matching
5//! - König's theorem exact minimum vertex cover for bipartite graphs
6//! - Greedy hitting set
7
8use crate::error::OptimizeError;
9
10/// Result type for covering operations.
11pub type CoveringResult<T> = Result<T, OptimizeError>;
12
13// ── Greedy set cover ──────────────────────────────────────────────────────────
14
15/// Greedy set cover (unweighted): at each step, pick the set that covers the
16/// most uncovered elements of `universe`.
17///
18/// Achieves O(ln n) approximation ratio.
19///
20/// Returns the indices of selected sets.
21pub fn greedy_set_cover(universe: usize, sets: &[Vec<usize>]) -> Vec<usize> {
22    if universe == 0 || sets.is_empty() {
23        return vec![];
24    }
25
26    let mut uncovered = vec![true; universe];
27    let mut remaining = universe;
28    let mut selected = Vec::new();
29    let mut used = vec![false; sets.len()];
30
31    while remaining > 0 {
32        // Find the set that covers the most uncovered elements
33        let mut best_idx = None;
34        let mut best_count = 0usize;
35
36        for (i, set) in sets.iter().enumerate() {
37            if used[i] {
38                continue;
39            }
40            let count = set
41                .iter()
42                .filter(|&&e| e < universe && uncovered[e])
43                .count();
44            if count > best_count {
45                best_count = count;
46                best_idx = Some(i);
47            }
48        }
49
50        match best_idx {
51            Some(idx) => {
52                used[idx] = true;
53                selected.push(idx);
54                for &e in &sets[idx] {
55                    if e < universe && uncovered[e] {
56                        uncovered[e] = false;
57                        remaining -= 1;
58                    }
59                }
60            }
61            None => break, // remaining elements cannot be covered
62        }
63    }
64
65    selected
66}
67
68// ── Weighted set cover ────────────────────────────────────────────────────────
69
70/// Greedy weighted set cover: at each step, pick the set with the lowest
71/// cost per newly-covered element.
72///
73/// Achieves O(ln n) approximation ratio.
74///
75/// Returns `(selected_indices, total_cost)`.
76pub fn weighted_set_cover(
77    universe: usize,
78    sets: &[Vec<usize>],
79    costs: &[f64],
80) -> CoveringResult<(Vec<usize>, f64)> {
81    if sets.len() != costs.len() {
82        return Err(OptimizeError::InvalidInput(format!(
83            "sets.len() = {} but costs.len() = {}",
84            sets.len(),
85            costs.len()
86        )));
87    }
88    if universe == 0 || sets.is_empty() {
89        return Ok((vec![], 0.0));
90    }
91
92    let mut uncovered = vec![true; universe];
93    let mut remaining = universe;
94    let mut selected = Vec::new();
95    let mut total_cost = 0.0;
96    let mut used = vec![false; sets.len()];
97
98    while remaining > 0 {
99        let mut best_idx = None;
100        let mut best_ratio = f64::INFINITY;
101
102        for (i, set) in sets.iter().enumerate() {
103            if used[i] {
104                continue;
105            }
106            let new_covers = set
107                .iter()
108                .filter(|&&e| e < universe && uncovered[e])
109                .count();
110            if new_covers == 0 {
111                continue;
112            }
113            let ratio = costs[i] / new_covers as f64;
114            if ratio < best_ratio {
115                best_ratio = ratio;
116                best_idx = Some(i);
117            }
118        }
119
120        match best_idx {
121            Some(idx) => {
122                used[idx] = true;
123                selected.push(idx);
124                total_cost += costs[idx];
125                for &e in &sets[idx] {
126                    if e < universe && uncovered[e] {
127                        uncovered[e] = false;
128                        remaining -= 1;
129                    }
130                }
131            }
132            None => break,
133        }
134    }
135
136    Ok((selected, total_cost))
137}
138
139// ── 2-approximation vertex cover ──────────────────────────────────────────────
140
141/// 2-approximation vertex cover via maximal matching.
142///
143/// Repeatedly picks an arbitrary uncovered edge and adds both endpoints to the
144/// cover.  Guarantees a cover of size ≤ 2 · OPT.
145pub fn vertex_cover_2approx(n: usize, edges: &[(usize, usize)]) -> Vec<usize> {
146    if n == 0 || edges.is_empty() {
147        return vec![];
148    }
149
150    let mut in_cover = vec![false; n];
151    let mut cover = Vec::new();
152
153    for &(u, v) in edges {
154        if u >= n || v >= n {
155            continue;
156        }
157        // If neither endpoint is yet in the cover, add both
158        if !in_cover[u] && !in_cover[v] {
159            in_cover[u] = true;
160            in_cover[v] = true;
161            cover.push(u);
162            cover.push(v);
163        }
164    }
165
166    cover.sort_unstable();
167    cover.dedup();
168    cover
169}
170
171// ── König's theorem: exact min vertex cover for bipartite graphs ──────────────
172
173/// Exact minimum vertex cover for a bipartite graph via König's theorem.
174///
175/// Uses a maximum bipartite matching (augmenting paths) followed by the
176/// alternating-tree construction from König's theorem.
177///
178/// Vertices `0..n_left` are on the left side; vertices `n_left..n_left+n_right`
179/// on the right.  Edges connect left vertex `u` to right vertex `v` (stored as
180/// pairs `(u_left, v_right)` in 0-indexed form on each side).
181///
182/// Returns the vertex cover as a list of global vertex indices
183/// (left side: 0..n_left, right side: n_left..n_left+n_right).
184pub fn min_vertex_cover_bip(n_left: usize, n_right: usize, edges: &[(usize, usize)]) -> Vec<usize> {
185    if n_left == 0 || n_right == 0 {
186        return vec![];
187    }
188
189    // Build adjacency for left→right
190    let mut adj_left: Vec<Vec<usize>> = vec![vec![]; n_left];
191    for &(u, v) in edges {
192        if u < n_left && v < n_right {
193            adj_left[u].push(v);
194        }
195    }
196
197    // Maximum matching via Hopcroft-Karp (simplified augmenting paths)
198    let mut match_left = vec![usize::MAX; n_left];
199    let mut match_right = vec![usize::MAX; n_right];
200
201    for u in 0..n_left {
202        let mut visited = vec![false; n_right];
203        augment_bip(
204            u,
205            &adj_left,
206            &mut match_left,
207            &mut match_right,
208            &mut visited,
209        );
210    }
211
212    // König's construction:
213    // Let U = unmatched left vertices.
214    // Alternating BFS/DFS from U:
215    //   - from left vertex: go to all right neighbours
216    //   - from right vertex: go to its matched left vertex
217    // T_L = left vertices reachable, T_R = right vertices reachable
218    // Min vertex cover = (L \ T_L) ∪ T_R
219
220    let mut reachable_left = vec![false; n_left];
221    let mut reachable_right = vec![false; n_right];
222
223    // Start from unmatched left vertices
224    let mut stack: Vec<(bool, usize)> = Vec::new(); // (is_left, index)
225    for u in 0..n_left {
226        if match_left[u] == usize::MAX {
227            stack.push((true, u));
228            reachable_left[u] = true;
229        }
230    }
231
232    while let Some((is_left, v)) = stack.pop() {
233        if is_left {
234            // Explore all right neighbours
235            for &r in &adj_left[v] {
236                if !reachable_right[r] {
237                    reachable_right[r] = true;
238                    stack.push((false, r));
239                }
240            }
241        } else {
242            // From right vertex v, follow matching edge to left
243            let ml = match_right[v];
244            if ml != usize::MAX && !reachable_left[ml] {
245                reachable_left[ml] = true;
246                stack.push((true, ml));
247            }
248        }
249    }
250
251    // Cover = (L \ T_L) ∪ T_R  (as global indices)
252    let mut cover = Vec::new();
253    for u in 0..n_left {
254        if !reachable_left[u] {
255            cover.push(u); // left side global index = u
256        }
257    }
258    for v in 0..n_right {
259        if reachable_right[v] {
260            cover.push(n_left + v); // right side global index = n_left + v
261        }
262    }
263
264    cover.sort_unstable();
265    cover
266}
267
268/// Augmenting path DFS for bipartite matching.
269fn augment_bip(
270    u: usize,
271    adj: &[Vec<usize>],
272    match_left: &mut Vec<usize>,
273    match_right: &mut Vec<usize>,
274    visited: &mut Vec<bool>,
275) -> bool {
276    for &v in &adj[u] {
277        if visited[v] {
278            continue;
279        }
280        visited[v] = true;
281        let prev = match_right[v];
282        if prev == usize::MAX || augment_bip(prev, adj, match_left, match_right, visited) {
283            match_left[u] = v;
284            match_right[v] = u;
285            return true;
286        }
287    }
288    false
289}
290
291// ── Greedy hitting set ────────────────────────────────────────────────────────
292
293/// Greedy hitting set: find a small set H ⊆ {0..universe-1} such that every
294/// input set contains at least one element of H.
295///
296/// Equivalent to set cover on the dual hypergraph.  Greedily picks the element
297/// that "hits" the most unhit sets.
298///
299/// Returns the hitting set as a sorted list of element indices.
300pub fn hitting_set(universe: usize, sets: &[Vec<usize>]) -> Vec<usize> {
301    if universe == 0 || sets.is_empty() {
302        return vec![];
303    }
304
305    let m = sets.len();
306    let mut hit = vec![false; m]; // whether set i has been hit
307    let mut remaining = m;
308    let mut chosen = Vec::new();
309    let mut in_hitting_set = vec![false; universe];
310
311    while remaining > 0 {
312        // For each element, count how many unhit sets it appears in
313        let mut best_elem = None;
314        let mut best_count = 0usize;
315
316        for e in 0..universe {
317            if in_hitting_set[e] {
318                continue;
319            }
320            let count = sets
321                .iter()
322                .enumerate()
323                .filter(|(i, s)| !hit[*i] && s.contains(&e))
324                .count();
325            if count > best_count {
326                best_count = count;
327                best_elem = Some(e);
328            }
329        }
330
331        match best_elem {
332            Some(e) => {
333                in_hitting_set[e] = true;
334                chosen.push(e);
335                for (i, s) in sets.iter().enumerate() {
336                    if !hit[i] && s.contains(&e) {
337                        hit[i] = true;
338                        remaining -= 1;
339                    }
340                }
341            }
342            None => break, // some sets cannot be hit (empty sets)
343        }
344    }
345
346    chosen.sort_unstable();
347    chosen
348}
349
350// ─────────────────────────────────────────────────────────────────────────────
351// Tests
352// ─────────────────────────────────────────────────────────────────────────────
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    fn is_set_cover(universe: usize, sets: &[Vec<usize>], selected: &[usize]) -> bool {
358        let mut covered = vec![false; universe];
359        for &idx in selected {
360            for &e in &sets[idx] {
361                if e < universe {
362                    covered[e] = true;
363                }
364            }
365        }
366        covered.iter().all(|&c| c)
367    }
368
369    #[test]
370    fn test_greedy_set_cover() {
371        let sets = vec![vec![0, 1, 2], vec![1, 3], vec![2, 4], vec![3, 4]];
372        let sel = greedy_set_cover(5, &sets);
373        assert!(is_set_cover(5, &sets, &sel));
374        assert!(!sel.is_empty());
375    }
376
377    #[test]
378    fn test_weighted_set_cover() {
379        let sets = vec![vec![0, 1, 2], vec![1, 3], vec![2, 4], vec![3, 4]];
380        let costs = vec![3.0, 1.0, 1.0, 1.0];
381        let (sel, cost) = weighted_set_cover(5, &sets, &costs).expect("unexpected None or Err");
382        assert!(is_set_cover(5, &sets, &sel));
383        assert!(cost > 0.0);
384    }
385
386    #[test]
387    fn test_weighted_set_cover_mismatch() {
388        let sets = vec![vec![0, 1]];
389        let costs = vec![1.0, 2.0]; // length mismatch
390        assert!(weighted_set_cover(2, &sets, &costs).is_err());
391    }
392
393    #[test]
394    fn test_vertex_cover_2approx() {
395        // Triangle: needs 2 vertices to cover
396        let edges = vec![(0, 1), (1, 2), (2, 0)];
397        let cover = vertex_cover_2approx(3, &edges);
398        // Verify it's a valid cover
399        for &(u, v) in &edges {
400            assert!(cover.contains(&u) || cover.contains(&v));
401        }
402    }
403
404    #[test]
405    fn test_vertex_cover_path() {
406        // Path 0-1-2-3: minimum cover = {1, 2} or {1, 3}
407        let edges = vec![(0, 1), (1, 2), (2, 3)];
408        let cover = vertex_cover_2approx(4, &edges);
409        for &(u, v) in &edges {
410            assert!(cover.contains(&u) || cover.contains(&v));
411        }
412        assert!(cover.len() <= 4); // at most 2 * OPT = 4
413    }
414
415    #[test]
416    fn test_kings_bipartite() {
417        // K_{2,2}: min vertex cover = 2 (by König = max matching = 2)
418        let edges = vec![(0, 0), (0, 1), (1, 0), (1, 1)];
419        let cover = min_vertex_cover_bip(2, 2, &edges);
420        // Verify it covers all edges
421        for &(u, v) in &edges {
422            assert!(cover.contains(&u) || cover.contains(&(2 + v)));
423        }
424        assert_eq!(cover.len(), 2);
425    }
426
427    #[test]
428    fn test_kings_path_bip() {
429        // Path: left={0}, right={0}, edge (0,0) → min cover = 1
430        let edges = vec![(0, 0)];
431        let cover = min_vertex_cover_bip(1, 1, &edges);
432        assert_eq!(cover.len(), 1);
433        assert!(cover.contains(&0) || cover.contains(&1));
434    }
435
436    #[test]
437    fn test_hitting_set() {
438        // Universe 5, sets: {0,1},{1,2},{2,3},{3,4}
439        let sets = vec![vec![0, 1], vec![1, 2], vec![2, 3], vec![3, 4]];
440        let hs = hitting_set(5, &sets);
441        // Verify every set is hit
442        for s in &sets {
443            assert!(s.iter().any(|e| hs.contains(e)));
444        }
445    }
446
447    #[test]
448    fn test_empty_inputs() {
449        assert!(greedy_set_cover(0, &[]).is_empty());
450        assert!(vertex_cover_2approx(0, &[]).is_empty());
451        assert!(hitting_set(0, &[]).is_empty());
452    }
453}