Problem
Given an undirected graph G = (V, E) with n = |V| vertices and m = |E| edges, count the number of triangles, i.e. unordered triples {u, v, w} such that all three pairs are edges.
Assumptions worth stating explicitly:
- The graph is simple (no self-loops, no parallel edges). If the input may contain them, drop self-loops and deduplicate edges first — otherwise a parallel edge is counted as a triangle.
- The graph is undirected: an edge stored once must be readable from both endpoints.
- Triangles are counted once each, not three times (once per vertex).
Approaches and their costs
| Approach | Time | Space | When to use |
|---|---|---|---|
| Brute force over triples | O(n^3) | O(n²) or O(m) | n ≤ ~400 |
| For each edge, intersect the two adjacency lists | O(m · d_max) | O(n + m) | Small maximum degree |
| Forward / node-iterator (oriented) | O(m·√m) | O(n + m) | General sparse–medium graphs |
| Adjacency bitsets | O(n³/64) | O(n²/64) | Dense graphs, n up to ~50·10³ |
| Matrix multiplication (A³ diagonal) | O(n^ω) | O(n²) | Dense, theoretical |
The crossover matters: O(m·√m) beats O(n³) whenever m ≪ n². For a dense graph (m ≈ n²) the bitset method is far better than the edge-based one.
Main algorithm: orientation (forward) method
Idea
Orient every edge from the "smaller" endpoint to the "larger" one under the ordering
u ≺ v ⟺ (deg(u), u) < (deg(v), v)
This turns G into a DAG. A triangle {a, b, c} with a ≺ b ≺ c then has exactly the arcs a → b, a → c, b → c. So it is found exactly once, when we are at vertex a and test whether its two out-neighbours b and c are adjacent.
Why it is fast: out-degree bound
If u → v then deg(u) ≤ deg(v). Since the two degrees multiply to at least deg(u)·deg(v) ≥ deg(u)² and also deg(u)·deg(v) ≤ (Σ deg)² / 4 = m²… more directly: among any two adjacent vertices the smaller degree d satisfies d² ≤ Σ deg(v) = 2m, hence d ≤ √(2m).
Therefore every vertex has out-degree at most √(2m).
Total work = Σ_u outdeg(u)² ≤ √(2m) · Σ_u outdeg(u) = √(2m) · m = O(m^{3/2}).
The bound is tight in the worst case (e.g. a star of cliques), but the ordering by (degree, id) keeps real-world graphs close to O(m·√m) and often much better.
Correctness
Every triangle has a unique minimum vertex a under ≺; both other edges leave a as arcs, and the third edge is an arc between the two out-neighbours. Counting adjacency tests that succeed at each vertex therefore yields each triangle once and only once.
Code
Python
```python import sys from collections import defaultdict
def count_triangles(n, edges): adj = [set() for _ in range(n)] for u, v in edges: if u == v: # ignore self-loops continue adj[u].add(v) adj[v].add(u)
# orient u -> v iff (deg(u), u) < (deg(v), v)
out = [[] for _ in range(n)]
for u in range(n):
du = len(adj[u])
for v in adj[u]:
if (du, u) < (len(adj[v]), v):
out[u].append(v)
total = 0
for u in range(n):
neigh = out[u]
k = len(neigh)
for i in range(k):
v = neigh[i]
# v's neighbours are a superset-safe set for the lookup
av = adj[v]
for j in range(i + 1, k):
if neigh[j] in av:
total += 1
return total
if name == "main": data = sys.stdin.read().split() it = iter(data) n = int(next(it)); m = int(next(it)) edges = [(int(next(it)), int(next(it))) for _ in range(m)] print(count_triangles(n, edges)) ```
Input format assumed: n m, then m lines of u v (0-based vertices).
C++ (fast, sorted adjacency + binary search)
```cpp
include
using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m;
if (!(cin >> n >> m)) return 0;
vector<vector<int>> adj(n);
vector<pair<int,int>> e;
e.reserve(m);
for (int i = 0; i < m; i++) {
int u, v; cin >> u >> v;
if (u == v) continue; // drop self-loops
adj[u].push_back(v);
adj[v].push_back(u);
e.push_back({u, v});
}
for (int i = 0; i < n; i++) {
sort(adj[i].begin(), adj[i].end());
adj[i].erase(unique(adj[i].begin(), adj[i].end()), adj[i].end()); // dedup
}
// orient low -> high
vector<vector<int>> out(n);
for (int u = 0; u < n; u++)
for (int v : adj[u])
if (make_pair(adj[u].size(), u) < make_pair(adj[v].size(), v))
out[u].push_back(v);
long long total = 0;
for (int u = 0; u < n; u++) {
const auto& nb = out[u];
for (size_t i = 0; i < nb.size(); i++)
for (size_t j = i + 1; j < nb.size(); j++) {
int a = nb[i], b = nb[j];
// binary-search the pair in the smaller adjacency list
if (binary_search(adj[a].begin(), adj[a].end(), b)) total++;
}
}
cout << total << "\n";
return 0;
} ```
Complexity: O(m log d + m·√m·log d) — the extra log d comes from membership tests; replace sorted vectors with hash sets if constant factors dominate, at the cost of worse cache behaviour.
C++ with bitsets (dense graphs)
cpp
// adjacency rows as dynamic bitsets; count = sum over edges (u,v) of popcount(B[u] & B[v]) / 3
long long total = 0;
for (int u = 0; u < n; u++)
for (int v : adj[u])
if (u < v)
total += (B[u] & B[v]).count();
total /= 3; // each triangle counted 3 times, once per edge
Time O(m·n/64), which is O(n³/64) on dense input.
Edge cases and pitfalls
- Self-loops — never part of a triangle; remove them or they corrupt degree counts and the ordering.
- Parallel edges — deduplicate, or
adj-list intersections produce spurious counts. - Counting 3× — the edge-intersection formula
Σ_{(u,v)∈E} |N(u) ∩ N(v)|equals3·T. Divide by 3 (integer division is safe only if you are sure no partial matches leaked in; with a symmetric simple graph it is exact). - Directed input — if the graph is directed and you want directed triangles (3-cycles), the orientation trick does not apply directly; enumerate
u → v → wand testu → w. nup to 10⁵ butmsmall — the forward method scales withm, so it is fine; never materialise ann × nmatrix.- Overflow — the answer is
Θ(n³)in the worst case; use 64-bit (long long) even whennis modest. - Triangle enumeration vs. counting — if you must list the triangles, the same algorithm outputs them at
O(m^{3/2} + T); counting only avoids materialisingTresults. - All-triangles-per-vertex (clustering coefficient) — count each triangle three times instead; adjust the divisor.
Complexity summary of the recommended solution
- Time:
O(m·√m)worst case,O(Σ_u outdeg(u)²)in practice. - Space:
O(n + m). - Output-sensitive variant:
O(m^{3/2} + T)when enumerating.
A candidatura é feita diretamente no site da empresa — sem intermediários, sem black boxes.
Candidatar no site da empresa →