Skip to content
This repository was archived by the owner on Jun 18, 2026. It is now read-only.

Commit 8a59f92

Browse files
perf: O(V*Δ²) 4-cycle counting in MotifAnalyzer via 2-path enumeration
Replace O(V² · Δ) all-pairs common-neighbor scan in countSquares() with 2-path enumeration through each vertex's neighbor pairs. For each vertex b, enumerate sorted neighbor pairs (u, w) where u-w are not adjacent — each such pair is a 2-path u-b-w. Accumulate common-neighbor counts per canonical non-adjacent pair, then derive C(k,2) 4-cycles. On sparse graphs (Δ ≪ V), this reduces from ~V²·Δ operations to ~V·Δ² — e.g., 1000 vertices with avg degree 10: old ≈ 10M pair checks, new ≈ 100K 2-path checks. Also simplifies the participation tracking: endpoints get C(k,2) credit each, common neighbors get (k-1) credit, computed directly from the pair counts — eliminates the previous O(k²) inner loop and the post-hoc halving correction.
1 parent 8f52913 commit 8a59f92

1 file changed

Lines changed: 70 additions & 69 deletions

File tree

Gvisual/src/gvisual/MotifAnalyzer.java

Lines changed: 70 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -161,84 +161,85 @@ private void countTrianglesAndWedges(List<String> vertices) {
161161

162162
// ── Square (4-Cycle) Counting ───────────────────────────────────
163163

164+
/**
165+
* Counts 4-cycles (squares) using 2-path enumeration.
166+
*
167+
* <p><b>Algorithm:</b> For each vertex b, enumerate all pairs of b's
168+
* neighbors (u, w) where u &lt; w (lexicographic) and u-w are NOT adjacent.
169+
* Each such pair forms a 2-path u-b-w. A pair (u, w) with k common
170+
* neighbors yields C(k, 2) distinct 4-cycles.</p>
171+
*
172+
* <p><b>Performance:</b> The previous implementation used O(V² · Δ)
173+
* all-pairs enumeration — for every pair of vertices it scanned one
174+
* vertex's neighbor set to count overlap. This version enumerates
175+
* 2-paths per vertex in O(Δ²) time, giving O(V · Δ²) total. On sparse
176+
* graphs where Δ ≪ V, this is orders of magnitude faster (e.g., a
177+
* 1000-vertex graph with average degree 10: old = ~10M pair checks,
178+
* new = ~100K 2-path checks).</p>
179+
*/
164180
private void countSquares(List<String> vertices) {
165181
squareCount = 0;
166182

167-
// For each pair of vertices at distance 2, count common neighbors.
168-
// Each 4-cycle A-B-C-D appears as two paths of length 2 sharing
169-
// endpoints (A-B-C and A-D-C). The number of 4-cycles through
170-
// a pair (A, C) with k common neighbors is C(k, 2).
171-
Map<String, Integer> indexMap = new HashMap<String, Integer>();
172-
for (int i = 0; i < vertices.size(); i++) {
173-
indexMap.put(vertices.get(i), i);
174-
}
183+
// Phase 1: Count common non-adjacent neighbors for each non-adjacent
184+
// pair by enumerating 2-paths through each vertex.
185+
// Key = "min|max" canonical pair string, value = common neighbor count.
186+
Map<String, Integer> pairCommonCount = new HashMap<String, Integer>();
175187

176-
for (int i = 0; i < vertices.size(); i++) {
177-
String u = vertices.get(i);
178-
Set<String> uN = neighborCache.get(u);
179-
if (uN == null) continue;
180-
181-
for (int j = i + 1; j < vertices.size(); j++) {
182-
String w = vertices.get(j);
183-
if (uN.contains(w)) continue; // Skip adjacent pairs
188+
for (String b : vertices) {
189+
Set<String> bNeighbors = neighborCache.get(b);
190+
if (bNeighbors == null || bNeighbors.size() < 2) continue;
184191

185-
Set<String> wN = neighborCache.get(w);
186-
if (wN == null) continue;
187-
188-
// Count common neighbors between u and w
189-
int common = 0;
190-
for (String n : uN) {
191-
if (wN.contains(n)) common++;
192-
}
192+
// Sort b's neighbors for consistent canonical ordering
193+
List<String> nList = new ArrayList<String>(bNeighbors);
194+
Collections.sort(nList);
193195

194-
// C(common, 2) = common * (common - 1) / 2
195-
if (common >= 2) {
196-
int cycles = common * (common - 1) / 2;
197-
squareCount += cycles;
198-
199-
// Track participation for all 4 vertices of each square.
200-
// For each common-neighbor pair (n1, n2) with n1 < n2,
201-
// the square is u - n1 - w - n2. All four get credit.
202-
// (#33: previously only u and w were tracked.)
203-
List<String> commonNeighbors = new ArrayList<>();
204-
for (String n : uN) {
205-
if (wN.contains(n)) commonNeighbors.add(n);
206-
}
207-
for (int a = 0; a < commonNeighbors.size(); a++) {
208-
for (int b = a + 1; b < commonNeighbors.size(); b++) {
209-
addParticipation(u, "square", 1);
210-
addParticipation(w, "square", 1);
211-
addParticipation(commonNeighbors.get(a), "square", 1);
212-
addParticipation(commonNeighbors.get(b), "square", 1);
213-
}
214-
}
196+
for (int i = 0; i < nList.size(); i++) {
197+
String u = nList.get(i);
198+
Set<String> uN = neighborCache.get(u);
199+
for (int j = i + 1; j < nList.size(); j++) {
200+
String w = nList.get(j);
201+
// Only count if u and w are NOT adjacent (otherwise it's
202+
// a triangle edge, not a 4-cycle diagonal)
203+
if (uN != null && uN.contains(w)) continue;
204+
205+
// Canonical key: u < w lexicographically (already sorted)
206+
String key = u + "|" + w;
207+
pairCommonCount.merge(key, 1, Integer::sum);
215208
}
216209
}
217210
}
218-
// Each 4-cycle is counted twice (once from each non-adjacent pair)
219-
// Actually, each square A-B-C-D has two pairs of non-adjacent vertices:
220-
// (A,C) and (B,D). So each square is counted exactly twice.
221-
// However, with our ordered i<j approach, and the fact that both
222-
// non-adjacent pairs are counted, we need to divide by... let me think.
223-
// For square A-B-C-D: non-adjacent pairs are (A,C) and (B,D).
224-
// Both pairs contribute C(2,2)=1 each. So total = 2 per square.
225-
// But we also need to account for common-neighbor participation.
226-
// Actually: each unique 4-cycle is found exactly once per non-adjacent
227-
// pair where i < j. A square has exactly 2 non-adjacent pairs.
228-
// So squareCount is double the actual count.
229-
230-
// Fix participation — we over-counted, divide later
231-
// Actually let's just not fix participation from the non-adjacent
232-
// pair loop — recalculate from the final count
233-
squareCount /= 2;
234-
235-
// Halve square participation counts to match corrected squareCount (#33).
236-
// Each square was found from both non-adjacent pairs, so participation
237-
// values are also 2x the true values.
238-
for (Map<String, Integer> m : vertexParticipation.values()) {
239-
Integer sq = m.get("square");
240-
if (sq != null && sq > 0) {
241-
m.put("square", sq / 2);
211+
212+
// Phase 2: For each non-adjacent pair with k >= 2 common neighbors,
213+
// there are C(k, 2) 4-cycles. Track participation for all vertices.
214+
for (Map.Entry<String, Integer> entry : pairCommonCount.entrySet()) {
215+
int k = entry.getValue();
216+
if (k < 2) continue;
217+
218+
int cycles = k * (k - 1) / 2;
219+
squareCount += cycles;
220+
221+
// Parse the pair
222+
String pairKey = entry.getKey();
223+
int sep = pairKey.indexOf('|');
224+
String u = pairKey.substring(0, sep);
225+
String w = pairKey.substring(sep + 1);
226+
227+
// Each 4-cycle involves u, w, and 2 of the k common neighbors.
228+
// u and w each participate in all C(k,2) cycles.
229+
addParticipation(u, "square", cycles);
230+
addParticipation(w, "square", cycles);
231+
232+
// Each common neighbor b participates in (k-1) of the C(k,2)
233+
// cycles (paired with each of the other k-1 common neighbors).
234+
// Collect common neighbors by re-checking b's neighbors.
235+
Set<String> uN = neighborCache.get(u);
236+
Set<String> wN = neighborCache.get(w);
237+
Set<String> smaller = uN.size() <= wN.size() ? uN : wN;
238+
Set<String> larger = uN.size() <= wN.size() ? wN : uN;
239+
for (String b : smaller) {
240+
if (larger.contains(b)) {
241+
addParticipation(b, "square", k - 1);
242+
}
242243
}
243244
}
244245
}

0 commit comments

Comments
 (0)