Skip to main content

rust_igraph/algorithms/properties/
is_geodetic.rs

1//! Geodetic graph predicate (ALGO-PR-078).
2//!
3//! A geodetic graph has a unique shortest path between every pair of
4//! reachable vertices. Trees are geodetic. Complete graphs are
5//! geodetic. Block graphs are geodetic.
6//!
7//! Recognition: for each vertex, run BFS and track the number of
8//! shortest-path predecessors. If any vertex is reached via more than
9//! one shortest path, the graph is not geodetic. O(V*(V+E)).
10//!
11//! For directed graphs, shortest paths follow edge direction.
12
13use std::collections::VecDeque;
14
15use crate::core::{Graph, IgraphResult};
16
17/// Check whether a graph is geodetic.
18///
19/// A geodetic graph has exactly one shortest path between every pair
20/// of connected vertices.
21///
22/// For directed graphs, paths follow edge direction; unreachable
23/// pairs are ignored.
24///
25/// An empty graph (0 vertices) is geodetic (vacuously).
26/// A single vertex is geodetic.
27/// Any tree is geodetic.
28///
29/// # Examples
30///
31/// ```
32/// use rust_igraph::{Graph, is_geodetic};
33///
34/// // Tree (path): geodetic
35/// let mut g = Graph::with_vertices(4);
36/// g.add_edge(0, 1).unwrap();
37/// g.add_edge(1, 2).unwrap();
38/// g.add_edge(2, 3).unwrap();
39/// assert!(is_geodetic(&g).unwrap());
40///
41/// // C4 is NOT geodetic (two shortest paths between opposite vertices)
42/// let mut g = Graph::with_vertices(4);
43/// g.add_edge(0, 1).unwrap();
44/// g.add_edge(1, 2).unwrap();
45/// g.add_edge(2, 3).unwrap();
46/// g.add_edge(3, 0).unwrap();
47/// assert!(!is_geodetic(&g).unwrap());
48/// ```
49pub fn is_geodetic(graph: &Graph) -> IgraphResult<bool> {
50    let n = graph.vcount();
51    if n <= 2 {
52        return Ok(true);
53    }
54
55    let n_usize = n as usize;
56    let mut dist = vec![u32::MAX; n_usize];
57    let mut nrgeo = vec![0u32; n_usize];
58    let mut queue: VecDeque<u32> = VecDeque::new();
59
60    for source in 0..n {
61        dist.fill(u32::MAX);
62        nrgeo.fill(0);
63
64        dist[source as usize] = 0;
65        nrgeo[source as usize] = 1;
66        queue.clear();
67        queue.push_back(source);
68
69        while let Some(v) = queue.pop_front() {
70            let next_dist = dist[v as usize].saturating_add(1);
71            let neighbors = graph.neighbors(v)?;
72
73            for w in neighbors {
74                let w_idx = w as usize;
75                if dist[w_idx] == u32::MAX {
76                    dist[w_idx] = next_dist;
77                    nrgeo[w_idx] = 1;
78                    queue.push_back(w);
79                } else if dist[w_idx] == next_dist {
80                    nrgeo[w_idx] = nrgeo[w_idx].saturating_add(1);
81                    if nrgeo[w_idx] > 1 {
82                        return Ok(false);
83                    }
84                }
85            }
86        }
87    }
88
89    Ok(true)
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn empty_graph() {
98        let g = Graph::with_vertices(0);
99        assert!(is_geodetic(&g).unwrap());
100    }
101
102    #[test]
103    fn single_vertex() {
104        let g = Graph::with_vertices(1);
105        assert!(is_geodetic(&g).unwrap());
106    }
107
108    #[test]
109    fn single_edge() {
110        let mut g = Graph::with_vertices(2);
111        g.add_edge(0, 1).unwrap();
112        assert!(is_geodetic(&g).unwrap());
113    }
114
115    #[test]
116    fn path_is_geodetic() {
117        let mut g = Graph::with_vertices(5);
118        g.add_edge(0, 1).unwrap();
119        g.add_edge(1, 2).unwrap();
120        g.add_edge(2, 3).unwrap();
121        g.add_edge(3, 4).unwrap();
122        assert!(is_geodetic(&g).unwrap());
123    }
124
125    #[test]
126    fn tree_is_geodetic() {
127        let mut g = Graph::with_vertices(7);
128        g.add_edge(0, 1).unwrap();
129        g.add_edge(0, 2).unwrap();
130        g.add_edge(1, 3).unwrap();
131        g.add_edge(1, 4).unwrap();
132        g.add_edge(2, 5).unwrap();
133        g.add_edge(2, 6).unwrap();
134        assert!(is_geodetic(&g).unwrap());
135    }
136
137    #[test]
138    fn triangle_is_geodetic() {
139        // K3: unique shortest path between every pair (direct edge)
140        let mut g = Graph::with_vertices(3);
141        g.add_edge(0, 1).unwrap();
142        g.add_edge(1, 2).unwrap();
143        g.add_edge(2, 0).unwrap();
144        assert!(is_geodetic(&g).unwrap());
145    }
146
147    #[test]
148    fn k4_is_geodetic() {
149        // Complete graph: every pair connected by direct edge
150        let mut g = Graph::with_vertices(4);
151        for u in 0..4u32 {
152            for v in (u + 1)..4 {
153                g.add_edge(u, v).unwrap();
154            }
155        }
156        assert!(is_geodetic(&g).unwrap());
157    }
158
159    #[test]
160    fn cycle_4_not_geodetic() {
161        // C4: vertices 0 and 2 have two shortest paths (0-1-2 and 0-3-2)
162        let mut g = Graph::with_vertices(4);
163        g.add_edge(0, 1).unwrap();
164        g.add_edge(1, 2).unwrap();
165        g.add_edge(2, 3).unwrap();
166        g.add_edge(3, 0).unwrap();
167        assert!(!is_geodetic(&g).unwrap());
168    }
169
170    #[test]
171    fn cycle_5_is_geodetic() {
172        // C5: all pairs at distance 2 have unique shortest paths
173        // (odd cycles are geodetic)
174        let mut g = Graph::with_vertices(5);
175        g.add_edge(0, 1).unwrap();
176        g.add_edge(1, 2).unwrap();
177        g.add_edge(2, 3).unwrap();
178        g.add_edge(3, 4).unwrap();
179        g.add_edge(4, 0).unwrap();
180        assert!(is_geodetic(&g).unwrap());
181    }
182
183    #[test]
184    fn cycle_6_not_geodetic() {
185        // C6: opposite vertices (e.g., 0 and 3) have two shortest paths
186        let mut g = Graph::with_vertices(6);
187        g.add_edge(0, 1).unwrap();
188        g.add_edge(1, 2).unwrap();
189        g.add_edge(2, 3).unwrap();
190        g.add_edge(3, 4).unwrap();
191        g.add_edge(4, 5).unwrap();
192        g.add_edge(5, 0).unwrap();
193        assert!(!is_geodetic(&g).unwrap());
194    }
195
196    #[test]
197    fn cycle_3_is_geodetic() {
198        // C3 = K3 → geodetic
199        let mut g = Graph::with_vertices(3);
200        g.add_edge(0, 1).unwrap();
201        g.add_edge(1, 2).unwrap();
202        g.add_edge(2, 0).unwrap();
203        assert!(is_geodetic(&g).unwrap());
204    }
205
206    #[test]
207    fn star_is_geodetic() {
208        let mut g = Graph::with_vertices(5);
209        for i in 1..5u32 {
210            g.add_edge(0, i).unwrap();
211        }
212        assert!(is_geodetic(&g).unwrap());
213    }
214
215    #[test]
216    fn two_triangles_sharing_vertex_is_geodetic() {
217        // Block graph: two K3 sharing vertex 2
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, 0).unwrap();
222        g.add_edge(2, 3).unwrap();
223        g.add_edge(3, 4).unwrap();
224        g.add_edge(4, 2).unwrap();
225        assert!(is_geodetic(&g).unwrap());
226    }
227
228    #[test]
229    fn disconnected_trees_geodetic() {
230        let mut g = Graph::with_vertices(6);
231        g.add_edge(0, 1).unwrap();
232        g.add_edge(1, 2).unwrap();
233        g.add_edge(3, 4).unwrap();
234        g.add_edge(4, 5).unwrap();
235        assert!(is_geodetic(&g).unwrap());
236    }
237
238    #[test]
239    fn edgeless_is_geodetic() {
240        let g = Graph::with_vertices(5);
241        assert!(is_geodetic(&g).unwrap());
242    }
243
244    #[test]
245    fn directed_path_geodetic() {
246        let mut g = Graph::new(4, true).unwrap();
247        g.add_edge(0, 1).unwrap();
248        g.add_edge(1, 2).unwrap();
249        g.add_edge(2, 3).unwrap();
250        assert!(is_geodetic(&g).unwrap());
251    }
252
253    #[test]
254    fn directed_diamond_not_geodetic() {
255        // 0→1, 0→2, 1→3, 2→3: two shortest paths 0→3
256        let mut g = Graph::new(4, true).unwrap();
257        g.add_edge(0, 1).unwrap();
258        g.add_edge(0, 2).unwrap();
259        g.add_edge(1, 3).unwrap();
260        g.add_edge(2, 3).unwrap();
261        assert!(!is_geodetic(&g).unwrap());
262    }
263
264    #[test]
265    fn k4_minus_edge_not_geodetic() {
266        // K4 minus edge (2,3): 0 and 1 each connect to 2 and 3
267        // Path 2→3: can go 2-0-3 and 2-1-3 → two shortest paths
268        let mut g = Graph::with_vertices(4);
269        g.add_edge(0, 1).unwrap();
270        g.add_edge(0, 2).unwrap();
271        g.add_edge(0, 3).unwrap();
272        g.add_edge(1, 2).unwrap();
273        g.add_edge(1, 3).unwrap();
274        assert!(!is_geodetic(&g).unwrap());
275    }
276}