Skip to main content

uni_algo/algo/algorithms/
triangle_count.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2026 Dragonscale Team
3
4//! Triangle Count Algorithm.
5
6use crate::algo::GraphProjection;
7use crate::algo::algorithms::Algorithm;
8use rayon::prelude::*;
9use uni_common::core::id::Vid;
10
11pub struct TriangleCount;
12
13#[derive(Debug, Clone, Default)]
14pub struct TriangleCountConfig;
15
16pub struct TriangleCountResult {
17    pub global_count: u64,
18    pub node_counts: Vec<(Vid, u64)>,
19}
20
21impl Algorithm for TriangleCount {
22    type Config = TriangleCountConfig;
23    type Result = TriangleCountResult;
24
25    fn name() -> &'static str {
26        "triangleCount"
27    }
28
29    fn needs_reverse() -> bool {
30        // Triangle count typically treats graph as undirected to find {u, v, w} set.
31        // If directed, we look for cycles.
32        // Standard algo.triangleCount is undirected.
33        true
34    }
35
36    fn run(graph: &GraphProjection, _config: Self::Config) -> Self::Result {
37        let n = graph.vertex_count();
38        if n == 0 {
39            return TriangleCountResult {
40                global_count: 0,
41                node_counts: Vec::new(),
42            };
43        }
44
45        let mut node_counts = vec![0u64; n];
46
47        // Parallel iteration
48        // To avoid double counting, we iterate u, then v in N(u) where v > u.
49        // Then intersection of N(u) and N(v) gives w.
50        // If w > v, we found a unique triangle {u, v, w}.
51        // But we want per-node counts.
52        // Each triangle {u, v, w} contributes +1 to count(u), count(v), count(w).
53        // Since we iterate u < v < w, we can atomically add.
54        // Or we can just compute per-node: for u, iterate all pairs v, w in N(u) and check edge? O(d^2).
55        // Better: iterate u, neighbors v. intersection = N(u) & N(v).
56        // For each w in intersection, we have triangle.
57
58        // Let's use sorted adjacency lists for fast intersection.
59        // GraphProjection stores neighbors but not necessarily sorted.
60        // Sorting is needed.
61
62        // Pre-processing: Sort neighbor lists (or create sorted view).
63        // Since we can't modify GraphProjection easily in-place (it's immutable ref),
64        // we might just collect and sort, or use HashSet.
65        // HashSet is slower but easier.
66        // Given typically small degrees, simple scan might be okay?
67        // Let's create sorted adjacency for efficient intersection.
68
69        // Actually, we can just parallelize over `u` and accumulate local counts.
70        // `count(u)` is number of triangles u participates in.
71        // `count(u) = sum_{v in N(u)} |N(u) intersect N(v)| / 2`.
72
73        // This works for undirected.
74
75        let adj: Vec<Vec<u32>> = (0..n)
76            .into_par_iter()
77            .map(|i| {
78                let mut neighbors = Vec::new();
79                neighbors.extend_from_slice(graph.out_neighbors(i as u32));
80                if graph.has_reverse() {
81                    // Merge in_neighbors, dedup
82                    let in_n = graph.in_neighbors(i as u32);
83                    neighbors.extend_from_slice(in_n);
84                    neighbors.sort_unstable();
85                    neighbors.dedup();
86                } else {
87                    neighbors.sort_unstable();
88                }
89                neighbors
90            })
91            .collect();
92
93        // Now compute counts
94        // To update node_counts in parallel without atomics per node, we can just return counts?
95        // Actually, `count(u)` depends on `u`'s neighbors.
96        // We can compute `count(u)` independently.
97
98        node_counts
99            .par_iter_mut()
100            .enumerate()
101            .for_each(|(u, count)| {
102                let neighbors = &adj[u];
103                let mut local_triangles = 0;
104
105                // For each pair of neighbors, check if they are connected
106                // Using intersection is faster if degree is large: sum_{v in N(u)} |N(u) intersect N(v)|
107                // Then divide by 2 because each edge (v, w) is counted twice (once for v, once for w).
108
109                for &v in neighbors {
110                    let neighbors_v = &adj[v as usize];
111                    // Intersect sorted lists
112                    local_triangles += intersect_sorted_len(neighbors, neighbors_v);
113                }
114
115                *count = local_triangles as u64 / 2;
116            });
117
118        let total: u64 = node_counts.iter().sum::<u64>() / 3; // Each triangle counted 3 times (once per node)
119
120        let results = node_counts
121            .into_iter()
122            .enumerate()
123            .map(|(i, c)| (graph.to_vid(i as u32), c))
124            .collect();
125
126        TriangleCountResult {
127            global_count: total,
128            node_counts: results,
129        }
130    }
131}
132
133fn intersect_sorted_len(a: &[u32], b: &[u32]) -> usize {
134    let mut i = 0;
135    let mut j = 0;
136    let mut count = 0;
137
138    // Branchless intersection
139    // Compiler auto-vectorization friendly
140    while i < a.len() && j < b.len() {
141        let va = a[i];
142        let vb = b[j];
143
144        let le = va <= vb;
145        let ge = va >= vb;
146
147        count += (le && ge) as usize;
148        i += le as usize;
149        j += ge as usize;
150    }
151    count
152}