diff --git a/src/testing/graph_topology_testing.rs b/src/testing/graph_topology_testing.rs index 2c4c394..ef8410c 100644 --- a/src/testing/graph_topology_testing.rs +++ b/src/testing/graph_topology_testing.rs @@ -49,6 +49,12 @@ macro_rules! graph_topology_tests { incidence_cursor_copy, incident_vertices_incidences_consistency, incident_edges_incidences_consistency, + common_neighbors, + common_neighbors_same_vertex, + common_neighbors_single, + common_neighbors_disconnected, + common_neighbors_multiple_edges, + common_neighbors_loop_edge, ); }; @@ -771,3 +777,118 @@ where ); } } + +pub fn common_neighbors() +where + G::Vertex: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + // Pairs of vertex indices with the indices of their expected common neighbors. + let cases: [(usize, usize, &[usize]); _] = [ + (0, 3, &[1]), + (1, 6, &[2, 3]), + (5, 8, &[]), + (2, 9, &[5, 6]), + (7, 3, &[]), + (0, 1, &[]), + (6, 7, &[9]), + // Loop only on one side, adjacent. + (2, 6, &[2]), + // Loop only on one side, not adjacent. + (2, 7, &[4]), + // Adjacent through a double edge, and both have a loop. + (2, 4, &[1, 2, 4]), + ]; + for (i, j, expected) in cases { + let expected: Vec<_> = expected.iter().map(|&k| vertices[k]).collect(); + assert_common_neighbors(&graph, vertices[i], vertices[j], &expected); + } +} + +pub fn common_neighbors_same_vertex() +where + G::Vertex: Debug, +{ + let (graph, vertices, _, _) = G::standard(); + // Vertex 1 has a double edge to vertex 0, but no loop. + assert_common_neighbors( + &graph, + vertices[1], + vertices[1], + &[vertices[0], vertices[2], vertices[3], vertices[4]], + ); + // Vertex 4 has a loop, and is therefore its own common neighbor with itself. + assert_common_neighbors( + &graph, + vertices[4], + vertices[4], + &[ + vertices[1], + vertices[2], + vertices[4], + vertices[7], + vertices[8], + ], + ); +} + +pub fn common_neighbors_single() +where + G::Vertex: Debug, +{ + let (graph, v) = G::single_vertex(); + assert_common_neighbors(&graph, v, v, &[]); +} + +pub fn common_neighbors_disconnected() +where + G::Vertex: Debug, +{ + let (graph, v) = G::disconnected(); + assert_common_neighbors(&graph, v[0], v[0], &[]); + assert_common_neighbors(&graph, v[0], v[1], &[]); +} + +pub fn common_neighbors_multiple_edges() +where + G::Vertex: Debug, +{ + let (graph, [v0, v1], _) = G::multiple_edges::<3>(); + assert_common_neighbors(&graph, v0, v1, &[]); + assert_common_neighbors(&graph, v1, v1, &[v0]); +} + +pub fn common_neighbors_loop_edge() +where + G::Vertex: Debug, +{ + let (graph, v, _) = G::loop_edge(); + assert_common_neighbors(&graph, v, v, &[v]); +} + +/// Asserts that `u` and `v` have exactly the `expected` common neighbors, each yielded once, for +/// both argument orders. +fn assert_common_neighbors( + graph: &G, + u: G::Vertex, + v: G::Vertex, + expected: &[G::Vertex], +) where + G::Vertex: Debug, +{ + for (a, b) in [(u, v), (v, u)] { + let mut remaining = expected.to_vec(); + for w in graph.common_neighbors(a, b) { + let pos = remaining.iter().position(|&x| x == w).unwrap_or_else(|| { + panic!( + "unexpected or repeated common neighbor {w:?} of vertices {a:?} and {b:?} from iterator" + ) + }); + remaining.swap_remove(pos); + } + assert!( + remaining.is_empty(), + "expected common neighbors {remaining:?} of vertices {a:?} and {b:?} were not matched by iterator" + ); + } +} diff --git a/src/traits.rs b/src/traits.rs index 7f27523..005eea3 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -175,6 +175,58 @@ pub trait GraphTopology { /// /// Panics if `v` is not a valid vertex of this graph. fn incidence_cursor(&self, v: Self::Vertex) -> Self::IncidenceCursor; + + /// Returns an iterator over all vertices that are adjacent to both `u` and `v`. + /// + /// Every common neighbor is yielded exactly once, in unspecified order, regardless of how + /// many parallel edges connect it to `u` or `v`. This method is symmetric up to permutations, + /// i.e. swapping the arguments will yield the same vertices, but not necessarily in the same + /// order. + /// + /// If `u` has a loop and is adjacent to `v`, then `u` is a common neighbor, and will be yielded + /// exactly once, and analogously for a loop at `v`. + /// + /// If `u == v`, then the common neighbors are all neighbors of `u`, including `u` itself if and + /// only if it has a loop. + /// + /// All neighbors of `u` are collected when this method is called, while those of `v` are + /// traversed lazily. + /// + /// # Panics + /// + /// Panics if `u` or `v` is not a valid vertex of this graph. + /// + /// # Examples + /// + /// ``` + /// use grapherity::models::AppendGraph; + /// use grapherity::prelude::*; + /// + /// let mut graph = AppendGraph::new(); + /// let [u, w, v] = core::array::from_fn(|_| graph.add_vertex()); + /// // Parallel edges do not yield `w` more than once. + /// graph.add_edge(u, w); + /// graph.add_edge(u, w); + /// graph.add_edge(w, v); + /// assert!(graph.common_neighbors(u, v).eq([w])); + /// ``` + // TODO: FrozenGraph could provide a sorted-merge override, if its vertices were sorted. + fn common_neighbors( + &self, + u: Self::Vertex, + v: Self::Vertex, + ) -> impl Iterator { + // TODO: Instead of Vec, this could use a vertex ElementMap with marked neighbors if the product of the degrees of u and v is large compared to |V|. + // TODO: Instead of Vec, this could use a reusable, user-provided(?) "scratch-pad" vertex ElementMap, to not pay its allocation. + let mut u_neighbors: Vec<_> = self.adjacent_vertices(u).collect(); + let mut v_neighbors = self.adjacent_vertices(v); + std::iter::from_fn(move || { + let w = v_neighbors.find(|w| u_neighbors.contains(w))?; + // Removes all parallel entries, so that `w` is not yielded again. + u_neighbors.retain(|x| *x != w); + Some(w) + }) + } } /// A trait that adds construction operations to an undirected graph topology.