Skip to main content

rust_igraph/algorithms/properties/
leap_zagreb.rs

1//! Leap Zagreb indices (ALGO-TR-066).
2//!
3//! These indices use the **second degree** (or **2-distance degree**)
4//! of each vertex: `d₂(v) = |{w : dist(v,w) = 2}|`, the number of
5//! vertices at distance exactly 2 from v.
6//!
7//! - **First leap Zagreb** `LM₁(G) = Σ_v d₂(v)²`
8//! - **Second leap Zagreb** `LM₂(G) = Σ_{(u,v)∈E} d₂(u)·d₂(v)`
9//! - **Third leap Zagreb** `LM₃(G) = Σ_v d(v)·d₂(v)`
10
11#![allow(
12    clippy::cast_possible_truncation,
13    clippy::cast_precision_loss,
14    clippy::many_single_char_names,
15    clippy::needless_range_loop,
16    clippy::too_many_lines,
17    clippy::unnecessary_wraps
18)]
19
20use crate::core::{Graph, IgraphResult};
21
22fn second_degrees(graph: &Graph) -> IgraphResult<Vec<u32>> {
23    let n = graph.vcount() as usize;
24    let mut d2 = vec![0_u32; n];
25
26    for s in 0..n {
27        let mut dist = vec![u32::MAX; n];
28        dist[s] = 0;
29        let mut queue = std::collections::VecDeque::new();
30        queue.push_back(s);
31        while let Some(u) = queue.pop_front() {
32            let d_u = dist[u];
33            if d_u >= 2 {
34                continue;
35            }
36            if let Ok(nbs) = graph.neighbors(u as u32) {
37                for nb in nbs {
38                    let idx = nb as usize;
39                    if dist[idx] == u32::MAX {
40                        dist[idx] = d_u + 1;
41                        queue.push_back(idx);
42                    }
43                }
44            }
45        }
46        let mut count = 0_u32;
47        for &d in &dist {
48            if d == 2 {
49                count += 1;
50            }
51        }
52        d2[s] = count;
53    }
54
55    Ok(d2)
56}
57
58/// Compute the first leap Zagreb index.
59///
60/// `LM₁(G) = Σ_v d₂(v)²` where `d₂(v)` is the number of vertices
61/// at distance exactly 2 from v.
62///
63/// # Examples
64///
65/// ```
66/// use rust_igraph::{Graph, first_leap_zagreb};
67///
68/// // Path 0-1-2: d₂(0)=1, d₂(1)=0, d₂(2)=1
69/// // LM₁ = 1 + 0 + 1 = 2
70/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
71/// assert_eq!(first_leap_zagreb(&g).unwrap(), 2);
72/// ```
73pub fn first_leap_zagreb(graph: &Graph) -> IgraphResult<u64> {
74    let d2 = second_degrees(graph)?;
75    let mut lm1 = 0_u64;
76
77    for &dv in &d2 {
78        let dv64 = u64::from(dv);
79        lm1 = lm1.saturating_add(dv64.saturating_mul(dv64));
80    }
81
82    Ok(lm1)
83}
84
85/// Compute the second leap Zagreb index.
86///
87/// `LM₂(G) = Σ_{(u,v)∈E} d₂(u)·d₂(v)`
88///
89/// Self-loops are skipped.
90///
91/// # Examples
92///
93/// ```
94/// use rust_igraph::{Graph, second_leap_zagreb};
95///
96/// // Path 0-1-2: d₂=[1,0,1]
97/// // edges: (0,1):1×0=0, (1,2):0×1=0 → LM₂=0
98/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
99/// assert_eq!(second_leap_zagreb(&g).unwrap(), 0);
100/// ```
101pub fn second_leap_zagreb(graph: &Graph) -> IgraphResult<u64> {
102    let d2 = second_degrees(graph)?;
103    let mut lm2 = 0_u64;
104
105    for (u, v) in graph.edges() {
106        if u == v {
107            continue;
108        }
109        let du = u64::from(d2[u as usize]);
110        let dv = u64::from(d2[v as usize]);
111        lm2 = lm2.saturating_add(du.saturating_mul(dv));
112    }
113
114    Ok(lm2)
115}
116
117/// Compute the third leap Zagreb index.
118///
119/// `LM₃(G) = Σ_v d(v)·d₂(v)` where `d(v)` is the ordinary degree
120/// and `d₂(v)` is the second degree (count of vertices at distance 2).
121///
122/// # Examples
123///
124/// ```
125/// use rust_igraph::{Graph, third_leap_zagreb};
126///
127/// // Path 0-1-2: d=[1,2,1], d₂=[1,0,1]
128/// // LM₃ = 1×1 + 2×0 + 1×1 = 2
129/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
130/// assert_eq!(third_leap_zagreb(&g).unwrap(), 2);
131/// ```
132pub fn third_leap_zagreb(graph: &Graph) -> IgraphResult<u64> {
133    let d2 = second_degrees(graph)?;
134    let n = graph.vcount() as usize;
135    let mut lm3 = 0_u64;
136
137    for i in 0..n {
138        let deg = graph.degree(i as u32)? as u64;
139        let d2v = u64::from(d2[i]);
140        lm3 = lm3.saturating_add(deg.saturating_mul(d2v));
141    }
142
143    Ok(lm3)
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    fn single_edge() -> Graph {
151        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
152    }
153
154    fn path3() -> Graph {
155        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
156    }
157
158    fn path4() -> Graph {
159        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
160    }
161
162    fn k3() -> Graph {
163        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
164    }
165
166    fn k4() -> Graph {
167        Graph::from_edges(
168            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
169            false,
170            Some(4),
171        )
172        .unwrap()
173    }
174
175    fn cycle4() -> Graph {
176        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
177    }
178
179    fn cycle5() -> Graph {
180        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
181    }
182
183    fn star5() -> Graph {
184        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
185    }
186
187    fn paw() -> Graph {
188        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
189    }
190
191    fn petersen() -> Graph {
192        Graph::from_edges(
193            &[
194                (0, 1),
195                (1, 2),
196                (2, 3),
197                (3, 4),
198                (4, 0),
199                (0, 5),
200                (1, 6),
201                (2, 7),
202                (3, 8),
203                (4, 9),
204                (5, 7),
205                (5, 8),
206                (6, 8),
207                (6, 9),
208                (7, 9),
209            ],
210            false,
211            Some(10),
212        )
213        .unwrap()
214    }
215
216    fn d2(g: &Graph) -> Vec<u32> {
217        second_degrees(g).unwrap()
218    }
219
220    // --- second_degrees ---
221
222    #[test]
223    fn d2_empty() {
224        let g = Graph::with_vertices(0);
225        assert_eq!(d2(&g), Vec::<u32>::new());
226    }
227
228    #[test]
229    fn d2_isolated() {
230        let g = Graph::with_vertices(3);
231        assert_eq!(d2(&g), vec![0, 0, 0]);
232    }
233
234    #[test]
235    fn d2_single_edge() {
236        // 0-1: no vertex at distance 2
237        assert_eq!(d2(&single_edge()), vec![0, 0]);
238    }
239
240    #[test]
241    fn d2_path3() {
242        // 0-1-2: d₂(0)=1 (vertex 2), d₂(1)=0, d₂(2)=1 (vertex 0)
243        assert_eq!(d2(&path3()), vec![1, 0, 1]);
244    }
245
246    #[test]
247    fn d2_path4() {
248        // 0-1-2-3: d₂(0)=1(2), d₂(1)=1(3), d₂(2)=1(0), d₂(3)=1(1)
249        assert_eq!(d2(&path4()), vec![1, 1, 1, 1]);
250    }
251
252    #[test]
253    fn d2_k3() {
254        // All pairs connected directly — no vertex at distance 2
255        assert_eq!(d2(&k3()), vec![0, 0, 0]);
256    }
257
258    #[test]
259    fn d2_k4() {
260        assert_eq!(d2(&k4()), vec![0, 0, 0, 0]);
261    }
262
263    #[test]
264    fn d2_cycle4() {
265        // C4: each vertex has 2 neighbors and 1 vertex at distance 2
266        assert_eq!(d2(&cycle4()), vec![1, 1, 1, 1]);
267    }
268
269    #[test]
270    fn d2_cycle5() {
271        // C5: each vertex has 2 neighbors, 2 vertices at distance 2
272        assert_eq!(d2(&cycle5()), vec![2, 2, 2, 2, 2]);
273    }
274
275    #[test]
276    fn d2_star5() {
277        // Star: center has 0 at dist 2 (all at dist 1),
278        // each leaf has 3 other leaves at dist 2
279        assert_eq!(d2(&star5()), vec![0, 3, 3, 3, 3]);
280    }
281
282    #[test]
283    fn d2_paw() {
284        // Paw: 0-1, 0-2, 1-2, 2-3
285        // d₂(0)=1 (vertex 3), d₂(1)=1 (vertex 3), d₂(2)=0, d₂(3)=2 (0,1)
286        assert_eq!(d2(&paw()), vec![1, 1, 0, 2]);
287    }
288
289    #[test]
290    fn d2_petersen() {
291        // Petersen graph is 3-regular, diameter 2 — each vertex has
292        // 3 neighbors and 6 vertices at distance 2
293        let d2v = d2(&petersen());
294        for &v in &d2v {
295            assert_eq!(v, 6);
296        }
297    }
298
299    // --- first_leap_zagreb ---
300
301    #[test]
302    fn lm1_empty() {
303        let g = Graph::with_vertices(0);
304        assert_eq!(first_leap_zagreb(&g).unwrap(), 0);
305    }
306
307    #[test]
308    fn lm1_isolated() {
309        let g = Graph::with_vertices(5);
310        assert_eq!(first_leap_zagreb(&g).unwrap(), 0);
311    }
312
313    #[test]
314    fn lm1_single_edge() {
315        assert_eq!(first_leap_zagreb(&single_edge()).unwrap(), 0);
316    }
317
318    #[test]
319    fn lm1_path3() {
320        // d₂=[1,0,1], LM₁ = 1+0+1 = 2
321        assert_eq!(first_leap_zagreb(&path3()).unwrap(), 2);
322    }
323
324    #[test]
325    fn lm1_path4() {
326        // d₂=[1,1,1,1], LM₁ = 4
327        assert_eq!(first_leap_zagreb(&path4()).unwrap(), 4);
328    }
329
330    #[test]
331    fn lm1_k3() {
332        assert_eq!(first_leap_zagreb(&k3()).unwrap(), 0);
333    }
334
335    #[test]
336    fn lm1_k4() {
337        assert_eq!(first_leap_zagreb(&k4()).unwrap(), 0);
338    }
339
340    #[test]
341    fn lm1_cycle4() {
342        // d₂=[1,1,1,1], LM₁ = 4
343        assert_eq!(first_leap_zagreb(&cycle4()).unwrap(), 4);
344    }
345
346    #[test]
347    fn lm1_cycle5() {
348        // d₂=[2,2,2,2,2], LM₁ = 5×4 = 20
349        assert_eq!(first_leap_zagreb(&cycle5()).unwrap(), 20);
350    }
351
352    #[test]
353    fn lm1_star5() {
354        // d₂=[0,3,3,3,3], LM₁ = 0+9+9+9+9 = 36
355        assert_eq!(first_leap_zagreb(&star5()).unwrap(), 36);
356    }
357
358    #[test]
359    fn lm1_paw() {
360        // d₂=[1,1,0,2], LM₁ = 1+1+0+4 = 6
361        assert_eq!(first_leap_zagreb(&paw()).unwrap(), 6);
362    }
363
364    #[test]
365    fn lm1_petersen() {
366        // d₂=6 for all, LM₁ = 10×36 = 360
367        assert_eq!(first_leap_zagreb(&petersen()).unwrap(), 360);
368    }
369
370    #[test]
371    fn lm1_complete_is_zero() {
372        // Complete graphs: all pairs at distance 1, d₂=0 for all
373        for g in &[k3(), k4()] {
374            assert_eq!(first_leap_zagreb(g).unwrap(), 0);
375        }
376    }
377
378    // --- second_leap_zagreb ---
379
380    #[test]
381    fn lm2_empty() {
382        let g = Graph::with_vertices(0);
383        assert_eq!(second_leap_zagreb(&g).unwrap(), 0);
384    }
385
386    #[test]
387    fn lm2_single_edge() {
388        assert_eq!(second_leap_zagreb(&single_edge()).unwrap(), 0);
389    }
390
391    #[test]
392    fn lm2_path3() {
393        // d₂=[1,0,1], edges: (0,1):0, (1,2):0 → 0
394        assert_eq!(second_leap_zagreb(&path3()).unwrap(), 0);
395    }
396
397    #[test]
398    fn lm2_path4() {
399        // d₂=[1,1,1,1], edges: (0,1):1, (1,2):1, (2,3):1 → 3
400        assert_eq!(second_leap_zagreb(&path4()).unwrap(), 3);
401    }
402
403    #[test]
404    fn lm2_k3() {
405        assert_eq!(second_leap_zagreb(&k3()).unwrap(), 0);
406    }
407
408    #[test]
409    fn lm2_k4() {
410        assert_eq!(second_leap_zagreb(&k4()).unwrap(), 0);
411    }
412
413    #[test]
414    fn lm2_cycle4() {
415        // d₂=[1,1,1,1], 4 edges × 1 = 4
416        assert_eq!(second_leap_zagreb(&cycle4()).unwrap(), 4);
417    }
418
419    #[test]
420    fn lm2_cycle5() {
421        // d₂=[2,2,2,2,2], 5 edges × 4 = 20
422        assert_eq!(second_leap_zagreb(&cycle5()).unwrap(), 20);
423    }
424
425    #[test]
426    fn lm2_star5() {
427        // d₂=[0,3,3,3,3], edges are (0,leaf): 0×3=0 → 0
428        assert_eq!(second_leap_zagreb(&star5()).unwrap(), 0);
429    }
430
431    #[test]
432    fn lm2_paw() {
433        // d₂=[1,1,0,2], edges: (0,1):1, (0,2):0, (1,2):0, (2,3):0 → 1
434        assert_eq!(second_leap_zagreb(&paw()).unwrap(), 1);
435    }
436
437    #[test]
438    fn lm2_petersen() {
439        // d₂=6 for all, 15 edges × 36 = 540
440        assert_eq!(second_leap_zagreb(&petersen()).unwrap(), 540);
441    }
442
443    // --- third_leap_zagreb ---
444
445    #[test]
446    fn lm3_empty() {
447        let g = Graph::with_vertices(0);
448        assert_eq!(third_leap_zagreb(&g).unwrap(), 0);
449    }
450
451    #[test]
452    fn lm3_isolated() {
453        let g = Graph::with_vertices(5);
454        assert_eq!(third_leap_zagreb(&g).unwrap(), 0);
455    }
456
457    #[test]
458    fn lm3_single_edge() {
459        // d=[1,1], d₂=[0,0] → 0
460        assert_eq!(third_leap_zagreb(&single_edge()).unwrap(), 0);
461    }
462
463    #[test]
464    fn lm3_path3() {
465        // d=[1,2,1], d₂=[1,0,1], LM₃ = 1×1+2×0+1×1 = 2
466        assert_eq!(third_leap_zagreb(&path3()).unwrap(), 2);
467    }
468
469    #[test]
470    fn lm3_path4() {
471        // d=[1,2,2,1], d₂=[1,1,1,1], LM₃ = 1+2+2+1 = 6
472        assert_eq!(third_leap_zagreb(&path4()).unwrap(), 6);
473    }
474
475    #[test]
476    fn lm3_k3() {
477        assert_eq!(third_leap_zagreb(&k3()).unwrap(), 0);
478    }
479
480    #[test]
481    fn lm3_k4() {
482        assert_eq!(third_leap_zagreb(&k4()).unwrap(), 0);
483    }
484
485    #[test]
486    fn lm3_cycle4() {
487        // d=[2,2,2,2], d₂=[1,1,1,1], LM₃ = 4×2 = 8
488        assert_eq!(third_leap_zagreb(&cycle4()).unwrap(), 8);
489    }
490
491    #[test]
492    fn lm3_cycle5() {
493        // d=[2,2,2,2,2], d₂=[2,2,2,2,2], LM₃ = 5×4 = 20
494        assert_eq!(third_leap_zagreb(&cycle5()).unwrap(), 20);
495    }
496
497    #[test]
498    fn lm3_star5() {
499        // d=[4,1,1,1,1], d₂=[0,3,3,3,3], LM₃ = 0+3+3+3+3 = 12
500        assert_eq!(third_leap_zagreb(&star5()).unwrap(), 12);
501    }
502
503    #[test]
504    fn lm3_paw() {
505        // d=[2,2,3,1], d₂=[1,1,0,2], LM₃ = 2+2+0+2 = 6
506        assert_eq!(third_leap_zagreb(&paw()).unwrap(), 6);
507    }
508
509    #[test]
510    fn lm3_petersen() {
511        // d=3, d₂=6 for all, LM₃ = 10×18 = 180
512        assert_eq!(third_leap_zagreb(&petersen()).unwrap(), 180);
513    }
514
515    // --- cross-consistency ---
516
517    #[test]
518    fn lm3_equals_sum_of_d_times_d2() {
519        // LM₃ = Σ d(v)·d₂(v), verify manually
520        for g in &[path3(), path4(), cycle4(), cycle5(), star5(), paw()] {
521            let d2v = d2(g);
522            let n = g.vcount() as usize;
523            let mut expected = 0_u64;
524            for i in 0..n {
525                let deg = g.degree(i as u32).unwrap() as u64;
526                expected += deg * u64::from(d2v[i]);
527            }
528            assert_eq!(third_leap_zagreb(g).unwrap(), expected);
529        }
530    }
531
532    #[test]
533    fn all_complete_graphs_have_zero_leap_zagreb() {
534        for g in &[k3(), k4()] {
535            assert_eq!(first_leap_zagreb(g).unwrap(), 0);
536            assert_eq!(second_leap_zagreb(g).unwrap(), 0);
537            assert_eq!(third_leap_zagreb(g).unwrap(), 0);
538        }
539    }
540
541    #[test]
542    fn all_positive_for_nonclique_connected() {
543        // For connected non-complete graphs with at least 3 vertices,
544        // LM₁ and LM₃ should be positive (some vertex has d₂ > 0)
545        for g in &[path3(), path4(), cycle4(), cycle5(), star5(), paw()] {
546            assert!(first_leap_zagreb(g).unwrap() > 0);
547            assert!(third_leap_zagreb(g).unwrap() > 0);
548        }
549    }
550}