Structs & Data Structures · intermediate · ~14 min
Triangles and universal sinks.
Some questions are about a graph's local structure. Counting triangles — triples of mutually adjacent vertices — is a disciplined i<j<k scan so each triangle is counted exactly once. A universal sink is the graph 'celebrity': a vertex everyone points to that points to no one, i.e. an all-zero matrix row with an all-one column off the diagonal.
Triangle counts measure clustering in social and biological networks (the clustering coefficient). The universal-sink / celebrity problem is a classic interview and systems question — finding a single node that everyone depends on but that depends on nothing.
Triangles via i<j<k. Requiring strictly increasing indices counts each triangle once instead of six times.
Universal sink. Vertex v qualifies iff row v is all zeros (out-degree 0) and column v is all ones except the diagonal (everyone else points to v).
Matrix reading. Both properties are pure, careful reads of the adjacency matrix — good practice for row/column reasoning.
Why the sink test needs both halves. An all-zero row alone identifies a vertex that points at nothing, which could simply be isolated; an all-one column alone identifies a popular vertex that may still point elsewhere. Only together do they describe a vertex the whole graph depends on and which depends on nothing.
/* count triangles in an UNDIRECTED graph: i < j < k counts each once */
int count_triangles(const int *adj, int n) {
int c = 0;
for (int i = 0; i < n; i++)
for (int j = i+1; j < n; j++)
if (adj[i*n + j]) /* prune: no edge i-j, skip all k */
for (int k = j+1; k < n; k++)
if (adj[j*n + k] && adj[i*n + k]) c++;
return c;
}
/* a universal sink: out-degree 0, and every OTHER vertex points to it */
int has_universal_sink(const int *adj, int n) {
for (int v = 0; v < n; v++) {
int out = 0;
for (int j = 0; j < n; j++) out += adj[v*n + j];
if (out != 0) continue; /* row must be all zeros */
int ok = 1;
for (int i = 0; i < n && ok; i++)
if (i != v && !adj[i*n + v]) ok = 0; /* column all ones except the diagonal */
if (ok) return 1;
}
return 0;
}
Key points:
i < j < k is what counts each triangle exactly once instead of six times.adj[i][j] before the innermost loop is a real prune, not just style.Many graph questions are structural. Counting triangles (mutually adjacent triples) is a disciplined i<j<k scan so each is counted once. A universal sink — the 'celebrity' with out-degree 0 that everyone points to — is a vertex whose matrix row is all zeros and whose column is all ones off the diagonal. Both are clean exercises in reading an adjacency matrix carefully.
#include <stdio.h> int main(void){ int n=4; /* K4 minus one edge / int adj[16]={ 0,1,1,1, 1,0,1,1, 1,1,0,0, 1,1,0,0 }; int tri=0; for(int i=0;i<n;i++) for(int j=i+1;j<n;j++) if(adj[in+j]) for(int k=j+1;k<n;k++) if(adj[jn+k]&&adj[in+k]) tri++; printf("triangles = %d\n", tri); int sink=-1; for(int v=0;v<n;v++){ int out=0,into=0; for(int j=0;j<n;j++) out+=adj[vn+j]; for(int i=0;i<n;i++) if(i!=v) into+=adj[in+v]; if(out==0&&into==n-1) sink=v; } printf("universal sink = %d\n", sink); return 0; }
| Step | Line | What happens |
|---|---|---|
| 1 | i < j < k |
Each unordered triple is generated in exactly one order, so no division by 6 is needed. |
| 2 | if (adj[i*n + j]) |
If i and j are not adjacent, no triangle can contain both — the whole k loop is skipped. |
| 3 | adj[j][k] && adj[i][k] |
With edge i-j known, these two complete the triangle. |
| 4 | K4 example | Four vertices all mutually connected yield exactly 4 triangles. |
| 5 | sink: row sum | A candidate must have out-degree 0 — it points at nothing. |
| 6 | sink: column check | Every other vertex must point at it. i != v skips the diagonal, which is not required to be set. |
Counting ordered triples and over-counting triangles by 6×. Forgetting a sink must have no outgoing edges as well as all incoming ones. Off-by-one at the diagonal (a vertex need not point to itself).
Compiler errors and warnings:
-Wsign-compare on loop bounds; keep indices int.Runtime symptoms:
i < j < k.i < j but not j < k.adj[v][v] to be set; the diagonal must be excluded with i != v.Technique: test K4 (4 triangles), a square with no diagonals (0), and a 3-vertex graph where vertex 2 is a sink. Those three cover both algorithms and the common miscounts.
adj[x*n + y] with x, y strictly less than n; the loop bounds guarantee that, so do not "optimise" them into forms that could exceed n-1.adj[v][v] = 1, the row-sum test would then reject a valid sink — decide the convention and apply it consistently.n is a denial-of-service risk; bound the input size.Concrete uses: Triangle counts measure clustering in social and biological networks — the local clustering coefficient is built directly from them — and are used in spam and fraud detection, where unusually dense triangles signal collusion. The universal-sink ("celebrity") problem models finding a single node everyone depends on but which depends on nothing: a root package, a central authority, or a terminal state. It is also a classic interview question because a smarter O(n) algorithm exists.
Professional best practices:
Beginner:
i < j < k rather than counting and dividing.Intermediate:
1. (Beginner) Count triangles. Implement int count_triangles(const int *adj, int n) with i < j < k. Example: K4 -> 4; a square without diagonals -> 0. Concepts: counting each triple once.
2. (Beginner) Universal sink. Implement int has_universal_sink(const int *adj, int n) checking both the row and the column. Concepts: the two required conditions.
3. (Intermediate) Show the miscount. Loop all three indices independently and confirm the triangle count comes out 6x too high. Concepts: why ordering the indices matters.
4. (Intermediate) O(n) sink. Implement the linear celebrity algorithm — eliminate one candidate per comparison, then verify the survivor. Concepts: candidate elimination plus a verification pass.
Both properties are careful reads of the adjacency matrix. Counting triangles with strictly increasing indices i < j < k generates each unordered triple exactly once — no dividing by six — and testing edge i-j before entering the innermost loop is a genuine prune. A universal sink is the graph's "celebrity": out-degree 0, so its row is all zeros, while every other vertex points to it, so its column is all ones except the diagonal — and requiring only one of those two conditions is the usual bug. Neither function allocates, so the risks are entirely about index discipline and about the unchecked directed-versus-undirected precondition; note also that the O(n^3) triangle scan needs an input bound on untrusted data.