use crate::core::{Graph, IgraphResult};
pub fn list_triangles(graph: &Graph) -> IgraphResult<Vec<(u32, u32, u32)>> {
let n = graph.vcount() as usize;
if n < 3 {
return Ok(Vec::new());
}
let adj = build_simple_adj(graph)?;
let mut degree_order: Vec<usize> = (0..n).collect();
degree_order.sort_by(|&a, &b| adj[a].len().cmp(&adj[b].len()));
let mut rank: Vec<usize> = vec![0; n];
for (i, &node) in degree_order.iter().enumerate() {
rank[node] = i;
}
let filtered: Vec<Vec<usize>> = (0..n)
.map(|v| {
adj[v]
.iter()
.copied()
.filter(|&w| rank[w] > rank[v])
.collect()
})
.collect();
let mut mark: Vec<usize> = vec![usize::MAX; n];
let mut triangles: Vec<(u32, u32, u32)> = Vec::new();
for node_idx in (0..n).rev() {
let node = degree_order[node_idx];
for &nei in &filtered[node] {
mark[nei] = node;
}
for &nei in &filtered[node] {
for &nei2 in &filtered[nei] {
if mark[nei2] == node {
#[allow(clippy::cast_possible_truncation)]
triangles.push((node as u32, nei as u32, nei2 as u32));
}
}
}
}
Ok(triangles)
}
fn build_simple_adj(graph: &Graph) -> IgraphResult<Vec<Vec<usize>>> {
let n = graph.vcount() as usize;
let m = graph.ecount();
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
for eid_usize in 0..m {
#[allow(clippy::cast_possible_truncation)]
let eid = eid_usize as u32;
let (from, to) = graph.edge(eid)?;
let f = from as usize;
let t = to as usize;
if f != t {
adj[f].push(t);
adj[t].push(f);
}
}
for neighbors in &mut adj {
neighbors.sort_unstable();
neighbors.dedup();
}
Ok(adj)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
fn normalize_triangle(t: (u32, u32, u32)) -> Vec<u32> {
let mut v = vec![t.0, t.1, t.2];
v.sort_unstable();
v
}
fn triangle_set(tris: &[(u32, u32, u32)]) -> HashSet<Vec<u32>> {
tris.iter().map(|&t| normalize_triangle(t)).collect()
}
#[test]
fn empty_graph() {
let g = Graph::with_vertices(0);
let tris = list_triangles(&g).unwrap();
assert!(tris.is_empty());
}
#[test]
fn no_triangles() {
let mut g = Graph::with_vertices(4);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 3).unwrap();
g.add_edge(3, 0).unwrap();
let tris = list_triangles(&g).unwrap();
assert!(tris.is_empty());
}
#[test]
fn single_triangle() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
let tris = list_triangles(&g).unwrap();
assert_eq!(tris.len(), 1);
let t = normalize_triangle(tris[0]);
assert_eq!(t, vec![0, 1, 2]);
}
#[test]
fn complete_4() {
let mut g = Graph::with_vertices(4);
for i in 0..4u32 {
for j in (i + 1)..4 {
g.add_edge(i, j).unwrap();
}
}
let tris = list_triangles(&g).unwrap();
assert_eq!(tris.len(), 4);
let set = triangle_set(&tris);
assert!(set.contains(&vec![0, 1, 2]));
assert!(set.contains(&vec![0, 1, 3]));
assert!(set.contains(&vec![0, 2, 3]));
assert!(set.contains(&vec![1, 2, 3]));
}
#[test]
fn two_triangles_sharing_edge() {
let mut g = Graph::with_vertices(4);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
g.add_edge(2, 3).unwrap();
g.add_edge(3, 0).unwrap();
let tris = list_triangles(&g).unwrap();
assert_eq!(tris.len(), 2);
let set = triangle_set(&tris);
assert!(set.contains(&vec![0, 1, 2]));
assert!(set.contains(&vec![0, 2, 3]));
}
#[test]
fn disconnected_triangles() {
let mut g = Graph::with_vertices(6);
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
g.add_edge(3, 4).unwrap();
g.add_edge(4, 5).unwrap();
g.add_edge(5, 3).unwrap();
let tris = list_triangles(&g).unwrap();
assert_eq!(tris.len(), 2);
let set = triangle_set(&tris);
assert!(set.contains(&vec![0, 1, 2]));
assert!(set.contains(&vec![3, 4, 5]));
}
#[test]
fn directed_ignores_direction() {
let mut g = Graph::new(3, true).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
let tris = list_triangles(&g).unwrap();
assert_eq!(tris.len(), 1);
}
#[test]
fn self_loops_ignored() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 0).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
let tris = list_triangles(&g).unwrap();
assert!(tris.is_empty());
}
#[test]
fn multi_edges_ignored() {
let mut g = Graph::with_vertices(3);
g.add_edge(0, 1).unwrap();
g.add_edge(0, 1).unwrap();
g.add_edge(1, 2).unwrap();
g.add_edge(2, 0).unwrap();
let tris = list_triangles(&g).unwrap();
assert_eq!(tris.len(), 1);
}
#[test]
fn uniqueness() {
let mut g = Graph::with_vertices(5);
for i in 0..5u32 {
for j in (i + 1)..5 {
g.add_edge(i, j).unwrap();
}
}
let tris = list_triangles(&g).unwrap();
assert_eq!(tris.len(), 10);
let set = triangle_set(&tris);
assert_eq!(set.len(), 10);
}
#[test]
fn star_no_triangles() {
let mut g = Graph::with_vertices(5);
g.add_edge(0, 1).unwrap();
g.add_edge(0, 2).unwrap();
g.add_edge(0, 3).unwrap();
g.add_edge(0, 4).unwrap();
let tris = list_triangles(&g).unwrap();
assert!(tris.is_empty());
}
#[test]
fn two_vertices() {
let mut g = Graph::with_vertices(2);
g.add_edge(0, 1).unwrap();
let tris = list_triangles(&g).unwrap();
assert!(tris.is_empty());
}
}