Skip to main content

rust_igraph/algorithms/properties/
is_distance_hereditary.rs

1//! Distance-hereditary graph predicate (ALGO-PR-079).
2//!
3//! A connected graph is distance-hereditary if for every pair of
4//! vertices, the distance between them in every connected induced
5//! subgraph containing both equals the distance in the full graph.
6//!
7//! Equivalently, a connected graph is distance-hereditary iff it can
8//! be reduced to a single vertex by repeatedly:
9//! 1. Removing a pendant vertex (degree 1), or
10//! 2. Removing a "false twin" (two non-adjacent vertices with
11//!    identical open neighborhoods), or
12//! 3. Removing a "true twin" (two adjacent vertices with identical
13//!    closed neighborhoods, i.e., same open neighborhood plus each
14//!    other).
15//!
16//! For disconnected graphs, each component must be distance-hereditary.
17//! For directed graphs, the function returns `false`.
18
19use crate::algorithms::connectivity::components::connected_components;
20use crate::core::{Graph, IgraphResult};
21
22/// Check whether a graph is distance-hereditary.
23///
24/// Uses a pruning algorithm: repeatedly remove pendant vertices
25/// (degree 1) and twin vertices until the graph is empty or no more
26/// removals are possible. O(V^2) in the worst case.
27///
28/// Returns `false` for directed graphs.
29///
30/// An empty graph (0 vertices) is distance-hereditary (vacuously).
31/// A single vertex is distance-hereditary.
32/// Any tree is distance-hereditary.
33/// Any block graph is distance-hereditary.
34///
35/// # Examples
36///
37/// ```
38/// use rust_igraph::{Graph, is_distance_hereditary};
39///
40/// // Tree: distance-hereditary
41/// let mut g = Graph::with_vertices(4);
42/// g.add_edge(0, 1).unwrap();
43/// g.add_edge(1, 2).unwrap();
44/// g.add_edge(2, 3).unwrap();
45/// assert!(is_distance_hereditary(&g).unwrap());
46///
47/// // C5 is NOT distance-hereditary
48/// let mut g = Graph::with_vertices(5);
49/// g.add_edge(0, 1).unwrap();
50/// g.add_edge(1, 2).unwrap();
51/// g.add_edge(2, 3).unwrap();
52/// g.add_edge(3, 4).unwrap();
53/// g.add_edge(4, 0).unwrap();
54/// assert!(!is_distance_hereditary(&g).unwrap());
55/// ```
56pub fn is_distance_hereditary(graph: &Graph) -> IgraphResult<bool> {
57    if graph.is_directed() {
58        return Ok(false);
59    }
60
61    let n = graph.vcount() as usize;
62    if n <= 2 {
63        return Ok(true);
64    }
65
66    let cc = connected_components(graph)?;
67    for comp_id in 0..cc.count {
68        let comp_verts: Vec<u32> = (0..graph.vcount())
69            .filter(|&v| cc.membership[v as usize] == comp_id)
70            .collect();
71        if !is_component_distance_hereditary(graph, &comp_verts)? {
72            return Ok(false);
73        }
74    }
75
76    Ok(true)
77}
78
79fn is_component_distance_hereditary(graph: &Graph, vertices: &[u32]) -> IgraphResult<bool> {
80    if vertices.len() <= 2 {
81        return Ok(true);
82    }
83
84    let n = graph.vcount() as usize;
85    let mut removed = vec![false; n];
86    let mut remaining = vertices.len();
87
88    loop {
89        if remaining <= 2 {
90            return Ok(true);
91        }
92
93        let mut progress = false;
94
95        for &v in vertices {
96            if removed[v as usize] {
97                continue;
98            }
99
100            let nbrs = graph.neighbors(v)?;
101            let active_nbrs: Vec<u32> =
102                nbrs.into_iter().filter(|&w| !removed[w as usize]).collect();
103            let deg = active_nbrs.len();
104
105            if deg == 0 {
106                removed[v as usize] = true;
107                remaining = remaining.saturating_sub(1);
108                progress = true;
109                continue;
110            }
111
112            if deg == 1 {
113                removed[v as usize] = true;
114                remaining = remaining.saturating_sub(1);
115                progress = true;
116                continue;
117            }
118
119            if find_and_remove_twin(graph, v, &active_nbrs, &mut removed, n)? {
120                remaining = remaining.saturating_sub(1);
121                progress = true;
122            }
123        }
124
125        if !progress {
126            return Ok(false);
127        }
128    }
129}
130
131fn find_and_remove_twin(
132    graph: &Graph,
133    v: u32,
134    v_nbrs: &[u32],
135    removed: &mut [bool],
136    _n: usize,
137) -> IgraphResult<bool> {
138    let mut v_nbr_set: Vec<u32> = v_nbrs.to_vec();
139    v_nbr_set.sort_unstable();
140
141    for &w in v_nbrs {
142        if removed[w as usize] || w <= v {
143            continue;
144        }
145
146        let w_all_nbrs = graph.neighbors(w)?;
147        let mut w_nbrs: Vec<u32> = w_all_nbrs
148            .into_iter()
149            .filter(|&u| !removed[u as usize])
150            .collect();
151        w_nbrs.sort_unstable();
152
153        // True twins: v and w are adjacent, and N(v)\{w} == N(w)\{v}
154        let v_without_w: Vec<u32> = v_nbr_set.iter().copied().filter(|&x| x != w).collect();
155        let w_without_v: Vec<u32> = w_nbrs.iter().copied().filter(|&x| x != v).collect();
156        if v_without_w == w_without_v {
157            removed[v as usize] = true;
158            return Ok(true);
159        }
160    }
161
162    // Check false twins: v and w are non-adjacent, N(v) == N(w)
163    for &candidate in v_nbrs {
164        if removed[candidate as usize] {
165            continue;
166        }
167        let candidate_nbrs = graph.neighbors(candidate)?;
168        for &w in &candidate_nbrs {
169            if removed[w as usize] || w == v || w <= v {
170                continue;
171            }
172            if v_nbr_set.binary_search(&w).is_ok() {
173                continue;
174            }
175
176            let w_all_nbrs = graph.neighbors(w)?;
177            let mut w_nbrs: Vec<u32> = w_all_nbrs
178                .into_iter()
179                .filter(|&u| !removed[u as usize])
180                .collect();
181            w_nbrs.sort_unstable();
182
183            if v_nbr_set == w_nbrs {
184                removed[v as usize] = true;
185                return Ok(true);
186            }
187        }
188    }
189
190    Ok(false)
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn empty_graph() {
199        let g = Graph::with_vertices(0);
200        assert!(is_distance_hereditary(&g).unwrap());
201    }
202
203    #[test]
204    fn single_vertex() {
205        let g = Graph::with_vertices(1);
206        assert!(is_distance_hereditary(&g).unwrap());
207    }
208
209    #[test]
210    fn single_edge() {
211        let mut g = Graph::with_vertices(2);
212        g.add_edge(0, 1).unwrap();
213        assert!(is_distance_hereditary(&g).unwrap());
214    }
215
216    #[test]
217    fn path_is_dh() {
218        let mut g = Graph::with_vertices(5);
219        g.add_edge(0, 1).unwrap();
220        g.add_edge(1, 2).unwrap();
221        g.add_edge(2, 3).unwrap();
222        g.add_edge(3, 4).unwrap();
223        assert!(is_distance_hereditary(&g).unwrap());
224    }
225
226    #[test]
227    fn tree_is_dh() {
228        let mut g = Graph::with_vertices(7);
229        g.add_edge(0, 1).unwrap();
230        g.add_edge(0, 2).unwrap();
231        g.add_edge(1, 3).unwrap();
232        g.add_edge(1, 4).unwrap();
233        g.add_edge(2, 5).unwrap();
234        g.add_edge(2, 6).unwrap();
235        assert!(is_distance_hereditary(&g).unwrap());
236    }
237
238    #[test]
239    fn triangle_is_dh() {
240        let mut g = Graph::with_vertices(3);
241        g.add_edge(0, 1).unwrap();
242        g.add_edge(1, 2).unwrap();
243        g.add_edge(2, 0).unwrap();
244        assert!(is_distance_hereditary(&g).unwrap());
245    }
246
247    #[test]
248    fn k4_is_dh() {
249        let mut g = Graph::with_vertices(4);
250        for u in 0..4u32 {
251            for v in (u + 1)..4 {
252                g.add_edge(u, v).unwrap();
253            }
254        }
255        assert!(is_distance_hereditary(&g).unwrap());
256    }
257
258    #[test]
259    fn star_is_dh() {
260        let mut g = Graph::with_vertices(5);
261        for i in 1..5u32 {
262            g.add_edge(0, i).unwrap();
263        }
264        assert!(is_distance_hereditary(&g).unwrap());
265    }
266
267    #[test]
268    fn cycle_4_is_dh() {
269        // C4: actually IS distance-hereditary (no induced C5+)
270        let mut g = Graph::with_vertices(4);
271        g.add_edge(0, 1).unwrap();
272        g.add_edge(1, 2).unwrap();
273        g.add_edge(2, 3).unwrap();
274        g.add_edge(3, 0).unwrap();
275        assert!(is_distance_hereditary(&g).unwrap());
276    }
277
278    #[test]
279    fn cycle_5_not_dh() {
280        let mut g = Graph::with_vertices(5);
281        g.add_edge(0, 1).unwrap();
282        g.add_edge(1, 2).unwrap();
283        g.add_edge(2, 3).unwrap();
284        g.add_edge(3, 4).unwrap();
285        g.add_edge(4, 0).unwrap();
286        assert!(!is_distance_hereditary(&g).unwrap());
287    }
288
289    #[test]
290    fn cycle_6_not_dh() {
291        let mut g = Graph::with_vertices(6);
292        g.add_edge(0, 1).unwrap();
293        g.add_edge(1, 2).unwrap();
294        g.add_edge(2, 3).unwrap();
295        g.add_edge(3, 4).unwrap();
296        g.add_edge(4, 5).unwrap();
297        g.add_edge(5, 0).unwrap();
298        assert!(!is_distance_hereditary(&g).unwrap());
299    }
300
301    #[test]
302    fn house_graph_not_dh() {
303        // House: C4 (0-1-2-3) + top edge (0-2)... no.
304        // House: square 0-1-2-3-0 + triangle roof 0-4-3
305        // Wait — house is C5 with chord. Actually house = P5 vertices
306        // with edges: 0-1, 1-2, 2-3, 3-4, 0-4, 0-3
307        // Better: standard house = 5 vertices, square bottom + triangle top
308        // 0-1, 1-2, 2-3, 3-0 (square), 2-4, 3-4 (triangle roof)
309        let mut g = Graph::with_vertices(5);
310        g.add_edge(0, 1).unwrap();
311        g.add_edge(1, 2).unwrap();
312        g.add_edge(2, 3).unwrap();
313        g.add_edge(3, 0).unwrap();
314        g.add_edge(2, 4).unwrap();
315        g.add_edge(3, 4).unwrap();
316        assert!(!is_distance_hereditary(&g).unwrap());
317    }
318
319    #[test]
320    fn edgeless_is_dh() {
321        let g = Graph::with_vertices(5);
322        assert!(is_distance_hereditary(&g).unwrap());
323    }
324
325    #[test]
326    fn disconnected_trees_dh() {
327        let mut g = Graph::with_vertices(6);
328        g.add_edge(0, 1).unwrap();
329        g.add_edge(1, 2).unwrap();
330        g.add_edge(3, 4).unwrap();
331        g.add_edge(4, 5).unwrap();
332        assert!(is_distance_hereditary(&g).unwrap());
333    }
334
335    #[test]
336    fn directed_returns_false() {
337        let mut g = Graph::new(3, true).unwrap();
338        g.add_edge(0, 1).unwrap();
339        g.add_edge(1, 2).unwrap();
340        assert!(!is_distance_hereditary(&g).unwrap());
341    }
342
343    #[test]
344    fn two_triangles_sharing_vertex() {
345        // Block graph → distance-hereditary
346        let mut g = Graph::with_vertices(5);
347        g.add_edge(0, 1).unwrap();
348        g.add_edge(1, 2).unwrap();
349        g.add_edge(2, 0).unwrap();
350        g.add_edge(2, 3).unwrap();
351        g.add_edge(3, 4).unwrap();
352        g.add_edge(4, 2).unwrap();
353        assert!(is_distance_hereditary(&g).unwrap());
354    }
355
356    #[test]
357    fn gem_not_dh() {
358        // Gem = fan graph: P4 (0-1-2-3) + vertex 4 adjacent to all of P4
359        let mut g = Graph::with_vertices(5);
360        g.add_edge(0, 1).unwrap();
361        g.add_edge(1, 2).unwrap();
362        g.add_edge(2, 3).unwrap();
363        g.add_edge(4, 0).unwrap();
364        g.add_edge(4, 1).unwrap();
365        g.add_edge(4, 2).unwrap();
366        g.add_edge(4, 3).unwrap();
367        assert!(!is_distance_hereditary(&g).unwrap());
368    }
369}