Skip to main content

rust_igraph/algorithms/
edge_cover.rs

1//! Edge cover algorithms (ALGO-MA-002).
2//!
3//! An edge cover is a set of edges such that every vertex in the
4//! graph is incident to at least one edge in the set. An edge cover
5//! exists if and only if the graph has no isolated vertices.
6//!
7//! We compute a minimum edge cover via a greedy maximal matching
8//! followed by covering unmatched vertices with any incident edge.
9//! For graphs without isolated vertices, the result size equals
10//! n - |matching|, which is optimal by König's theorem.
11
12use crate::core::{Graph, IgraphError, IgraphResult};
13
14/// Edge ID type alias for clarity.
15type EdgeId = u32;
16
17/// Check whether a set of edge IDs forms a valid edge cover.
18///
19/// An edge cover is valid if every vertex in the graph is incident
20/// to at least one edge in the set.
21///
22/// # Examples
23///
24/// ```
25/// use rust_igraph::{Graph, is_edge_cover};
26///
27/// let mut g = Graph::with_vertices(4);
28/// g.add_edge(0, 1).unwrap(); // edge 0
29/// g.add_edge(1, 2).unwrap(); // edge 1
30/// g.add_edge(2, 3).unwrap(); // edge 2
31///
32/// assert!(is_edge_cover(&g, &[0, 2]));   // covers all 4 vertices
33/// assert!(!is_edge_cover(&g, &[1]));      // vertices 0, 3 uncovered
34/// ```
35#[allow(clippy::cast_possible_truncation)]
36pub fn is_edge_cover(graph: &Graph, cover: &[EdgeId]) -> bool {
37    let n = graph.vcount() as usize;
38    if n == 0 {
39        return true;
40    }
41
42    let mut covered = vec![false; n];
43
44    for &eid in cover {
45        if let Ok((u, v)) = graph.edge(eid) {
46            covered[u as usize] = true;
47            covered[v as usize] = true;
48        }
49    }
50
51    covered.iter().all(|&c| c)
52}
53
54/// Compute a minimum edge cover.
55///
56/// Returns a set of edge IDs such that every vertex is incident to
57/// at least one edge in the set, and the number of edges is
58/// minimized.
59///
60/// The algorithm first computes a greedy maximal matching, then
61/// covers each unmatched vertex by adding any one of its incident
62/// edges. By König's theorem, the result has size n - |matching|,
63/// which is optimal.
64///
65/// # Errors
66///
67/// Returns an error if the graph has isolated vertices (vertices
68/// with degree 0), since no edge cover exists in that case.
69///
70/// # Examples
71///
72/// ```
73/// use rust_igraph::{Graph, minimum_edge_cover, is_edge_cover};
74///
75/// // Path 0-1-2: minimum edge cover is {0-1, 1-2} (2 edges)
76/// let mut g = Graph::with_vertices(3);
77/// g.add_edge(0, 1).unwrap();
78/// g.add_edge(1, 2).unwrap();
79/// let cover = minimum_edge_cover(&g).unwrap();
80/// assert!(is_edge_cover(&g, &cover));
81/// assert_eq!(cover.len(), 2);
82///
83/// // K3: minimum edge cover is 2 edges
84/// let mut g = Graph::with_vertices(3);
85/// g.add_edge(0, 1).unwrap();
86/// g.add_edge(1, 2).unwrap();
87/// g.add_edge(2, 0).unwrap();
88/// let cover = minimum_edge_cover(&g).unwrap();
89/// assert!(is_edge_cover(&g, &cover));
90/// assert_eq!(cover.len(), 2);
91/// ```
92#[allow(clippy::cast_possible_truncation)]
93pub fn minimum_edge_cover(graph: &Graph) -> IgraphResult<Vec<EdgeId>> {
94    let n = graph.vcount();
95
96    if n == 0 {
97        return Ok(Vec::new());
98    }
99
100    let directed = graph.is_directed();
101
102    // Check for isolated vertices
103    for v in 0..n {
104        let out_edges = graph.incident(v)?;
105        let has_edges = if directed {
106            let in_edges = graph.incident_in(v)?;
107            !out_edges.is_empty() || !in_edges.is_empty()
108        } else {
109            !out_edges.is_empty()
110        };
111        if !has_edges {
112            return Err(IgraphError::InvalidArgument(format!(
113                "vertex {v} has degree 0; no edge cover exists"
114            )));
115        }
116    }
117
118    let ecount = graph.ecount();
119    if ecount == 0 {
120        return Err(IgraphError::InvalidArgument(
121            "graph has vertices but no edges; no edge cover exists".into(),
122        ));
123    }
124
125    // Step 1: Greedy maximal matching
126    let mut matched = vec![false; n as usize];
127    let mut cover = Vec::new();
128
129    for eid in 0..ecount as u32 {
130        let (u, v) = graph.edge(eid)?;
131        if !matched[u as usize] && !matched[v as usize] && u != v {
132            matched[u as usize] = true;
133            matched[v as usize] = true;
134            cover.push(eid);
135        }
136    }
137
138    // Step 2: Cover unmatched vertices
139    for v in 0..n {
140        if !matched[v as usize] {
141            let mut edges = graph.incident(v)?;
142            if directed && edges.is_empty() {
143                edges = graph.incident_in(v)?;
144            }
145            if let Some(&eid) = edges.first() {
146                if !cover.contains(&eid) {
147                    cover.push(eid);
148                }
149                matched[v as usize] = true;
150            }
151        }
152    }
153
154    cover.sort_unstable();
155    cover.dedup();
156    Ok(cover)
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn empty_graph() {
165        let g = Graph::with_vertices(0);
166        let cover = minimum_edge_cover(&g).unwrap();
167        assert!(cover.is_empty());
168    }
169
170    #[test]
171    fn isolated_vertex_error() {
172        let g = Graph::with_vertices(3);
173        assert!(minimum_edge_cover(&g).is_err());
174    }
175
176    #[test]
177    fn single_edge() {
178        let mut g = Graph::with_vertices(2);
179        g.add_edge(0, 1).unwrap();
180        let cover = minimum_edge_cover(&g).unwrap();
181        assert_eq!(cover.len(), 1);
182        assert!(is_edge_cover(&g, &cover));
183    }
184
185    #[test]
186    fn path_3() {
187        let mut g = Graph::with_vertices(3);
188        g.add_edge(0, 1).unwrap();
189        g.add_edge(1, 2).unwrap();
190        let cover = minimum_edge_cover(&g).unwrap();
191        assert!(is_edge_cover(&g, &cover));
192        assert_eq!(cover.len(), 2);
193    }
194
195    #[test]
196    fn triangle() {
197        let mut g = Graph::with_vertices(3);
198        g.add_edge(0, 1).unwrap();
199        g.add_edge(1, 2).unwrap();
200        g.add_edge(2, 0).unwrap();
201        let cover = minimum_edge_cover(&g).unwrap();
202        assert!(is_edge_cover(&g, &cover));
203        assert_eq!(cover.len(), 2);
204    }
205
206    #[test]
207    fn star_graph() {
208        let mut g = Graph::with_vertices(5);
209        for i in 1..5u32 {
210            g.add_edge(0, i).unwrap();
211        }
212        let cover = minimum_edge_cover(&g).unwrap();
213        assert!(is_edge_cover(&g, &cover));
214        // Star: matching has 1 edge, cover = 5 - 1 = 4? No.
215        // Actually star matching: one edge (0,1). Unmatched: 2,3,4.
216        // Cover adds edges (0,2), (0,3), (0,4). Total = 4.
217        // Optimal is also 4 (each leaf needs its own edge).
218        // Wait: n=5, max matching=2 (e.g., (0,1) and... no, star max matching=1
219        // since center is used). So min edge cover = 5-1 = 4.
220        assert_eq!(cover.len(), 4);
221    }
222
223    #[test]
224    fn complete_k4() {
225        let mut g = Graph::with_vertices(4);
226        for u in 0..4u32 {
227            for v in (u + 1)..4 {
228                g.add_edge(u, v).unwrap();
229            }
230        }
231        let cover = minimum_edge_cover(&g).unwrap();
232        assert!(is_edge_cover(&g, &cover));
233        // K4: max matching = 2 (perfect matching), cover = 4 - 2 = 2
234        assert_eq!(cover.len(), 2);
235    }
236
237    #[test]
238    fn bipartite_k22() {
239        let mut g = Graph::with_vertices(4);
240        g.add_edge(0, 2).unwrap();
241        g.add_edge(0, 3).unwrap();
242        g.add_edge(1, 2).unwrap();
243        g.add_edge(1, 3).unwrap();
244        let cover = minimum_edge_cover(&g).unwrap();
245        assert!(is_edge_cover(&g, &cover));
246        // K_{2,2}: perfect matching of size 2, cover = 4 - 2 = 2
247        assert_eq!(cover.len(), 2);
248    }
249
250    #[test]
251    fn path_5() {
252        let mut g = Graph::with_vertices(5);
253        g.add_edge(0, 1).unwrap();
254        g.add_edge(1, 2).unwrap();
255        g.add_edge(2, 3).unwrap();
256        g.add_edge(3, 4).unwrap();
257        let cover = minimum_edge_cover(&g).unwrap();
258        assert!(is_edge_cover(&g, &cover));
259        // Path of 5: max matching = 2, cover = 5 - 2 = 3
260        assert_eq!(cover.len(), 3);
261    }
262
263    #[test]
264    fn directed_graph() {
265        let mut g = Graph::new(3, true).unwrap();
266        g.add_edge(0, 1).unwrap();
267        g.add_edge(1, 2).unwrap();
268        let cover = minimum_edge_cover(&g).unwrap();
269        assert!(is_edge_cover(&g, &cover));
270    }
271
272    #[test]
273    fn self_loop_only() {
274        let mut g = Graph::with_vertices(1);
275        g.add_edge(0, 0).unwrap();
276        let cover = minimum_edge_cover(&g).unwrap();
277        assert!(is_edge_cover(&g, &cover));
278        assert_eq!(cover.len(), 1);
279    }
280
281    #[test]
282    fn mixed_isolated_error() {
283        let mut g = Graph::with_vertices(3);
284        g.add_edge(0, 1).unwrap();
285        // vertex 2 is isolated
286        assert!(minimum_edge_cover(&g).is_err());
287    }
288
289    #[test]
290    fn parallel_edges() {
291        let mut g = Graph::with_vertices(2);
292        g.add_edge(0, 1).unwrap();
293        g.add_edge(0, 1).unwrap();
294        let cover = minimum_edge_cover(&g).unwrap();
295        assert!(is_edge_cover(&g, &cover));
296        assert_eq!(cover.len(), 1);
297    }
298
299    #[test]
300    fn sorted_output() {
301        let mut g = Graph::with_vertices(4);
302        g.add_edge(2, 3).unwrap();
303        g.add_edge(0, 1).unwrap();
304        let cover = minimum_edge_cover(&g).unwrap();
305        for i in 1..cover.len() {
306            assert!(cover[i] > cover[i - 1]);
307        }
308    }
309
310    #[test]
311    fn validator_positive() {
312        let mut g = Graph::with_vertices(4);
313        g.add_edge(0, 1).unwrap(); // 0
314        g.add_edge(1, 2).unwrap(); // 1
315        g.add_edge(2, 3).unwrap(); // 2
316        assert!(is_edge_cover(&g, &[0, 2]));
317        assert!(is_edge_cover(&g, &[0, 1, 2]));
318    }
319
320    #[test]
321    fn validator_negative() {
322        let mut g = Graph::with_vertices(4);
323        g.add_edge(0, 1).unwrap();
324        g.add_edge(1, 2).unwrap();
325        g.add_edge(2, 3).unwrap();
326        assert!(!is_edge_cover(&g, &[]));
327        assert!(!is_edge_cover(&g, &[1])); // 0, 3 uncovered
328    }
329}