@@ -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 < 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