Skip to main content

rust_igraph/algorithms/properties/
hosoya_index.rs

1//! Hosoya index (ALGO-TR-036).
2//!
3//! The **Hosoya index** (Z-index) of a graph is the total number of
4//! matchings, including the empty matching. A *k*-matching is a set
5//! of *k* pairwise non-adjacent edges. The Hosoya index equals
6//! `Σ_{k=0..⌊n/2⌋} m(G, k)` where `m(G, k)` is the number of
7//! *k*-matchings.
8//!
9//! Also provides `matching_count_sequence` returning the full vector
10//! `[m(G,0), m(G,1), ..., m(G,⌊n/2⌋)]`.
11
12#![allow(
13    clippy::cast_possible_truncation,
14    clippy::cast_precision_loss,
15    clippy::many_single_char_names,
16    clippy::needless_range_loop,
17    clippy::too_many_lines
18)]
19
20use crate::core::{Graph, IgraphResult};
21
22/// Compute the Hosoya index (Z-index) of a graph.
23///
24/// The Hosoya index equals the total number of matchings (including
25/// the empty matching). For a graph with no edges, `Z(G) = 1`.
26///
27/// Only feasible for small/sparse graphs — the value grows
28/// exponentially.
29///
30/// # Examples
31///
32/// ```
33/// use rust_igraph::{Graph, hosoya_index};
34///
35/// // Path 0-1-2: matchings are {}, {01}, {12} → Z = 3
36/// let g = Graph::from_edges(&[(0,1),(1,2)], false, Some(3)).unwrap();
37/// assert_eq!(hosoya_index(&g).unwrap(), 3);
38/// ```
39pub fn hosoya_index(graph: &Graph) -> IgraphResult<u64> {
40    let seq = matching_count_sequence(graph)?;
41    let total: u64 = seq.iter().sum();
42    Ok(total)
43}
44
45/// Compute the matching count sequence `[m(G,0), m(G,1), …]`.
46///
47/// `m(G, k)` is the number of *k*-matchings (sets of *k* pairwise
48/// non-adjacent edges). `m(G, 0) = 1` always.
49///
50/// # Examples
51///
52/// ```
53/// use rust_igraph::{Graph, matching_count_sequence};
54///
55/// // K_3 (triangle): m(0)=1, m(1)=3 → [1, 3]
56/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
57/// assert_eq!(matching_count_sequence(&g).unwrap(), vec![1, 3]);
58/// ```
59pub fn matching_count_sequence(graph: &Graph) -> IgraphResult<Vec<u64>> {
60    let n = graph.vcount() as usize;
61    let edges: Vec<(usize, usize)> = graph
62        .edges()
63        .map(|(u, v)| (u as usize, v as usize))
64        .collect();
65
66    let max_k = n / 2;
67    let mut counts = vec![0_u64; max_k.saturating_add(1)];
68    counts[0] = 1;
69
70    if edges.is_empty() {
71        return Ok(counts);
72    }
73
74    let unique_edges = deduplicate_edges(&edges, graph.is_directed());
75    let m = unique_edges.len();
76
77    let mut used = vec![false; n];
78    enumerate_matchings(&unique_edges, m, 0, 0, &mut used, &mut counts);
79
80    Ok(counts)
81}
82
83fn deduplicate_edges(edges: &[(usize, usize)], directed: bool) -> Vec<(usize, usize)> {
84    let mut seen = std::collections::HashSet::new();
85    let mut result = Vec::new();
86    for &(u, v) in edges {
87        if u == v {
88            continue;
89        }
90        let key = if directed {
91            (u, v)
92        } else {
93            (u.min(v), u.max(v))
94        };
95        if seen.insert(key) {
96            result.push(key);
97        }
98    }
99    result
100}
101
102fn enumerate_matchings(
103    edges: &[(usize, usize)],
104    m: usize,
105    start: usize,
106    k: usize,
107    used: &mut [bool],
108    counts: &mut [u64],
109) {
110    for i in start..m {
111        let (u, v) = edges[i];
112        if used[u] || used[v] {
113            continue;
114        }
115        used[u] = true;
116        used[v] = true;
117        let new_k = k.saturating_add(1);
118        if new_k < counts.len() {
119            counts[new_k] = counts[new_k].saturating_add(1);
120            enumerate_matchings(edges, m, i.saturating_add(1), new_k, used, counts);
121        }
122        used[u] = false;
123        used[v] = false;
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    fn empty() -> Graph {
132        Graph::with_vertices(0)
133    }
134
135    fn single() -> Graph {
136        Graph::with_vertices(1)
137    }
138
139    fn no_edges() -> Graph {
140        Graph::with_vertices(4)
141    }
142
143    fn single_edge() -> Graph {
144        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
145    }
146
147    fn path3() -> Graph {
148        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
149    }
150
151    fn path4() -> Graph {
152        Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false, Some(4)).unwrap()
153    }
154
155    fn path5() -> Graph {
156        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)], false, Some(5)).unwrap()
157    }
158
159    fn k3() -> Graph {
160        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
161    }
162
163    fn k4() -> Graph {
164        Graph::from_edges(
165            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
166            false,
167            Some(4),
168        )
169        .unwrap()
170    }
171
172    fn cycle4() -> Graph {
173        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
174    }
175
176    fn cycle5() -> Graph {
177        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], false, Some(5)).unwrap()
178    }
179
180    fn star4() -> Graph {
181        Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false, Some(4)).unwrap()
182    }
183
184    // --- hosoya_index ---
185
186    #[test]
187    fn hi_empty() {
188        assert_eq!(hosoya_index(&empty()).unwrap(), 1);
189    }
190
191    #[test]
192    fn hi_single() {
193        assert_eq!(hosoya_index(&single()).unwrap(), 1);
194    }
195
196    #[test]
197    fn hi_no_edges() {
198        assert_eq!(hosoya_index(&no_edges()).unwrap(), 1);
199    }
200
201    #[test]
202    fn hi_single_edge() {
203        // {}, {01} → 2
204        assert_eq!(hosoya_index(&single_edge()).unwrap(), 2);
205    }
206
207    #[test]
208    fn hi_path3() {
209        // {}, {01}, {12} → 3
210        assert_eq!(hosoya_index(&path3()).unwrap(), 3);
211    }
212
213    #[test]
214    fn hi_path4() {
215        // P_4: Z = 5 (Fibonacci pattern: Z(P_n) = F(n+1))
216        // {}, {01}, {12}, {23}, {01,23} → 5
217        assert_eq!(hosoya_index(&path4()).unwrap(), 5);
218    }
219
220    #[test]
221    fn hi_path5() {
222        // P_5: Z = 8 (F(6) = 8)
223        assert_eq!(hosoya_index(&path5()).unwrap(), 8);
224    }
225
226    #[test]
227    fn hi_k3() {
228        // K_3: {}, {01}, {02}, {12} → 4
229        assert_eq!(hosoya_index(&k3()).unwrap(), 4);
230    }
231
232    #[test]
233    fn hi_k4() {
234        // K_4: 6 edges, perfect matchings = 3
235        // m(0)=1, m(1)=6, m(2)=3 → Z = 10
236        assert_eq!(hosoya_index(&k4()).unwrap(), 10);
237    }
238
239    #[test]
240    fn hi_cycle4() {
241        // C_4: {}, {01},{12},{23},{30}, {01,23},{12,30} → 7
242        assert_eq!(hosoya_index(&cycle4()).unwrap(), 7);
243    }
244
245    #[test]
246    fn hi_cycle5() {
247        // C_5: m(0)=1, m(1)=5, m(2)=5 → Z = 11
248        assert_eq!(hosoya_index(&cycle5()).unwrap(), 11);
249    }
250
251    #[test]
252    fn hi_star4() {
253        // Star K_{1,3}: 3 edges, no 2-matching possible
254        // {}, {01}, {02}, {03} → 4
255        assert_eq!(hosoya_index(&star4()).unwrap(), 4);
256    }
257
258    // --- matching_count_sequence ---
259
260    #[test]
261    fn mcs_empty() {
262        assert_eq!(matching_count_sequence(&empty()).unwrap(), vec![1]);
263    }
264
265    #[test]
266    fn mcs_single_edge() {
267        assert_eq!(matching_count_sequence(&single_edge()).unwrap(), vec![1, 1]);
268    }
269
270    #[test]
271    fn mcs_path3() {
272        assert_eq!(matching_count_sequence(&path3()).unwrap(), vec![1, 2]);
273    }
274
275    #[test]
276    fn mcs_path4() {
277        // m(0)=1, m(1)=3, m(2)=1
278        assert_eq!(matching_count_sequence(&path4()).unwrap(), vec![1, 3, 1]);
279    }
280
281    #[test]
282    fn mcs_k4() {
283        // m(0)=1, m(1)=6, m(2)=3
284        assert_eq!(matching_count_sequence(&k4()).unwrap(), vec![1, 6, 3]);
285    }
286
287    #[test]
288    fn mcs_cycle4() {
289        // m(0)=1, m(1)=4, m(2)=2
290        assert_eq!(matching_count_sequence(&cycle4()).unwrap(), vec![1, 4, 2]);
291    }
292
293    #[test]
294    fn mcs_cycle5() {
295        // m(0)=1, m(1)=5, m(2)=5
296        assert_eq!(matching_count_sequence(&cycle5()).unwrap(), vec![1, 5, 5]);
297    }
298
299    // --- cross-consistency ---
300
301    #[test]
302    fn hosoya_equals_sum_of_sequence() {
303        for g in &[path3(), path4(), path5(), k3(), k4(), cycle4(), cycle5()] {
304            let z = hosoya_index(g).unwrap();
305            let seq = matching_count_sequence(g).unwrap();
306            let sum: u64 = seq.iter().sum();
307            assert_eq!(z, sum);
308        }
309    }
310
311    #[test]
312    fn m0_always_one() {
313        for g in &[empty(), single(), no_edges(), path4(), k4()] {
314            let seq = matching_count_sequence(g).unwrap();
315            assert_eq!(seq[0], 1);
316        }
317    }
318
319    #[test]
320    fn m1_equals_edge_count() {
321        for g in &[single_edge(), path3(), path4(), k3(), k4(), cycle4()] {
322            let seq = matching_count_sequence(g).unwrap();
323            if seq.len() > 1 {
324                assert_eq!(seq[1], g.ecount() as u64);
325            }
326        }
327    }
328
329    #[test]
330    fn path_hosoya_fibonacci() {
331        // Z(P_n) = F(n+1) where F is Fibonacci (F(1)=1, F(2)=1, F(3)=2, ...)
332        let fib = [1, 1, 2, 3, 5, 8, 13];
333        for n in 1_u32..=6 {
334            let edges: Vec<(u32, u32)> = (0..n.saturating_sub(1)).map(|i| (i, i + 1)).collect();
335            let g = if n == 1 {
336                Graph::with_vertices(1)
337            } else {
338                Graph::from_edges(&edges, false, Some(n)).unwrap()
339            };
340            assert_eq!(
341                hosoya_index(&g).unwrap(),
342                fib[n as usize],
343                "Z(P_{n}) should be F({})",
344                n + 1
345            );
346        }
347    }
348}