Skip to main content

rust_igraph/algorithms/operators/
is_same_graph.rs

1//! Structural graph equality (ALGO-CORE-001e, `is_same_graph` slice).
2//!
3//! Counterpart of `igraph_is_same_graph()` from
4//! `references/igraph/src/graph/type_indexededgelist.c:1947`.
5//! Returns `true` iff two graphs have the same vertex count, the
6//! same directedness, and the same edge multiset. Edge ordering
7//! and the orientation of undirected edges are normalised away —
8//! only set-equality of edges matters.
9//!
10//! This is **not** isomorphism: `0-1, 1-2` and `0-2, 1-2` are
11//! isomorphic but not "the same graph" because they use different
12//! edges. The vertex labels (0..vcount) are taken as ground truth.
13
14use crate::core::Graph;
15
16/// Returns `true` iff `g1` and `g2` are identical as labelled
17/// graphs: same vertex count, same directedness, same edge
18/// multiset. Edges differing only in storage order (or, for
19/// undirected, in endpoint orientation) are treated as identical.
20///
21/// Time complexity: `O(E log E)` due to the canonical sort of the
22/// two edge lists — slightly worse than upstream's `O(E)` walk over
23/// pre-sorted `ii` indices, but our `Graph` keeps that sort as a
24/// private detail so we re-sort here. Edge counts up to a few
25/// million sort in well under a second.
26///
27/// Note this is **not** isomorphism: vertex labels matter. Use a
28/// dedicated isomorphism checker (future AWU) if you need to ignore
29/// relabelling.
30///
31/// Counterpart of `igraph_is_same_graph` from
32/// `references/igraph/src/graph/type_indexededgelist.c:1947`.
33///
34/// # Examples
35///
36/// ```
37/// use rust_igraph::{Graph, is_same_graph};
38///
39/// // Same edge set, different insertion order ⇒ same graph.
40/// let mut g1 = Graph::with_vertices(3);
41/// g1.add_edge(0, 1).unwrap();
42/// g1.add_edge(1, 2).unwrap();
43/// let mut g2 = Graph::with_vertices(3);
44/// g2.add_edge(1, 2).unwrap();
45/// g2.add_edge(0, 1).unwrap();
46/// assert!(is_same_graph(&g1, &g2));
47///
48/// // Same vcount + ecount but different edge set ⇒ not the same.
49/// let mut g3 = Graph::with_vertices(3);
50/// g3.add_edge(0, 2).unwrap();
51/// g3.add_edge(1, 2).unwrap();
52/// assert!(!is_same_graph(&g1, &g3));
53/// ```
54#[must_use]
55pub fn is_same_graph(g1: &Graph, g2: &Graph) -> bool {
56    if g1.vcount() != g2.vcount() {
57        return false;
58    }
59    if g1.ecount() != g2.ecount() {
60        return false;
61    }
62    if g1.is_directed() != g2.is_directed() {
63        return false;
64    }
65
66    let m = g1.ecount();
67    if m == 0 {
68        return true;
69    }
70
71    // Canonical form: for undirected, `Graph::add_edge` already
72    // stores `from <= to`. For directed, the (from, to) tuple is
73    // already canonical. So lex-sorting the (from, to) pair lists
74    // and comparing element-wise is enough.
75    // Edge count exceeding u32::MAX is impossible given EdgeId is
76    // u32, but if it ever happened fall back to "not the same".
77    let Ok(m_u32) = u32::try_from(m) else {
78        return false;
79    };
80    let mut e1: Vec<(u32, u32)> = (0..m_u32)
81        .map(|e| g1.edge(e).expect("valid edge id"))
82        .collect();
83    let mut e2: Vec<(u32, u32)> = (0..m_u32)
84        .map(|e| g2.edge(e).expect("valid edge id"))
85        .collect();
86    // `(u32, u32)` derives `Ord` with lex ordering already; no
87    // custom comparator needed.
88    e1.sort_unstable();
89    e2.sort_unstable();
90    e1 == e2
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use crate::core::Graph;
97
98    #[test]
99    fn same_graph_identical_construction() {
100        let mut g1 = Graph::with_vertices(3);
101        g1.add_edges(vec![(0u32, 1u32), (1, 2)]).unwrap();
102        let mut g2 = Graph::with_vertices(3);
103        g2.add_edges(vec![(0u32, 1u32), (1, 2)]).unwrap();
104        assert!(is_same_graph(&g1, &g2));
105    }
106
107    #[test]
108    fn same_graph_edge_order_does_not_matter() {
109        let mut g1 = Graph::with_vertices(3);
110        g1.add_edges(vec![(0u32, 1u32), (1, 2)]).unwrap();
111        let mut g2 = Graph::with_vertices(3);
112        g2.add_edges(vec![(1u32, 2u32), (0, 1)]).unwrap();
113        assert!(is_same_graph(&g1, &g2));
114    }
115
116    #[test]
117    fn same_graph_undirected_endpoint_orientation_does_not_matter() {
118        let mut g1 = Graph::with_vertices(2);
119        g1.add_edge(0, 1).unwrap();
120        let mut g2 = Graph::with_vertices(2);
121        g2.add_edge(1, 0).unwrap();
122        assert!(is_same_graph(&g1, &g2));
123    }
124
125    #[test]
126    fn different_vcount_not_same() {
127        let mut g1 = Graph::with_vertices(3);
128        g1.add_edge(0, 1).unwrap();
129        let mut g2 = Graph::with_vertices(4);
130        g2.add_edge(0, 1).unwrap();
131        assert!(!is_same_graph(&g1, &g2));
132    }
133
134    #[test]
135    fn different_ecount_not_same() {
136        let mut g1 = Graph::with_vertices(3);
137        g1.add_edge(0, 1).unwrap();
138        let mut g2 = Graph::with_vertices(3);
139        g2.add_edges(vec![(0u32, 1u32), (1, 2)]).unwrap();
140        assert!(!is_same_graph(&g1, &g2));
141    }
142
143    #[test]
144    fn directed_vs_undirected_not_same() {
145        let mut g1 = Graph::new(3, true).unwrap();
146        g1.add_edge(0, 1).unwrap();
147        let mut g2 = Graph::with_vertices(3);
148        g2.add_edge(0, 1).unwrap();
149        assert!(!is_same_graph(&g1, &g2));
150    }
151
152    #[test]
153    fn directed_edge_direction_matters() {
154        // 0→1 and 1→0 are distinct directed edges.
155        let mut g1 = Graph::new(2, true).unwrap();
156        g1.add_edge(0, 1).unwrap();
157        let mut g2 = Graph::new(2, true).unwrap();
158        g2.add_edge(1, 0).unwrap();
159        assert!(!is_same_graph(&g1, &g2));
160    }
161
162    #[test]
163    fn parallel_edges_multiplicity_matters() {
164        // {0-1, 0-1} ≠ {0-1} even though vertex/edge sets look similar.
165        let mut g1 = Graph::with_vertices(2);
166        g1.add_edges(vec![(0u32, 1u32), (0, 1)]).unwrap();
167        let mut g2 = Graph::with_vertices(2);
168        g2.add_edges(vec![(0u32, 1u32), (0, 1)]).unwrap();
169        assert!(is_same_graph(&g1, &g2));
170        // Now drop one parallel edge in g2.
171        let mut g3 = Graph::with_vertices(2);
172        g3.add_edge(0, 1).unwrap();
173        assert!(!is_same_graph(&g1, &g3));
174    }
175
176    #[test]
177    fn self_loop_recognised_as_same() {
178        let mut g1 = Graph::with_vertices(2);
179        g1.add_edges(vec![(0u32, 0u32), (0, 1)]).unwrap();
180        let mut g2 = Graph::with_vertices(2);
181        g2.add_edges(vec![(0u32, 1u32), (0, 0)]).unwrap();
182        assert!(is_same_graph(&g1, &g2));
183    }
184
185    #[test]
186    fn empty_graphs_are_same_when_vcount_matches() {
187        let g1 = Graph::with_vertices(0);
188        let g2 = Graph::with_vertices(0);
189        assert!(is_same_graph(&g1, &g2));
190        let g3 = Graph::with_vertices(5);
191        let g4 = Graph::with_vertices(5);
192        assert!(is_same_graph(&g3, &g4));
193    }
194
195    #[test]
196    fn isomorphic_but_different_edge_sets_not_same() {
197        // Both are paths of length 2 but on different vertex labellings:
198        // g1: 0-1-2; g2: 0-2-1. Isomorphic, not "same".
199        let mut g1 = Graph::with_vertices(3);
200        g1.add_edges(vec![(0u32, 1u32), (1, 2)]).unwrap();
201        let mut g2 = Graph::with_vertices(3);
202        g2.add_edges(vec![(0u32, 2u32), (1, 2)]).unwrap();
203        assert!(!is_same_graph(&g1, &g2));
204    }
205
206    #[test]
207    fn reflexivity() {
208        let mut g = Graph::with_vertices(4);
209        g.add_edges(vec![(0u32, 1u32), (0, 2), (1, 3)]).unwrap();
210        let g2 = g.clone();
211        assert!(is_same_graph(&g, &g2));
212    }
213}