@@ -45,6 +45,25 @@ public enum VertexOrdering {
4545
4646 private final Graph <String , Edge > graph ;
4747
48+ /**
49+ * Pre-built adjacency sets for O(1) neighbor lookups.
50+ * Replaces repeated {@code graph.isNeighbor()} calls (which are O(degree)
51+ * in JUNG's sparse graph implementations) with HashSet.contains() — O(1).
52+ * Built once at construction; reused by chromaticLowerBound(),
53+ * analyzeColorClasses(), and anywhere else that tests adjacency.
54+ */
55+ private final Map <String , Set <String >> adjacency ;
56+
57+ /**
58+ * Pre-cached degree per vertex. Eliminates repeated {@code graph.degree()}
59+ * calls in sort comparators (O(V log V) calls in LARGEST_FIRST),
60+ * edgeChromaticBounds(), maxDegree(), and DSatur initialization.
61+ */
62+ private final Map <String , Integer > degreeCache ;
63+
64+ /** Cached max degree — computed once from degreeCache. */
65+ private final int cachedMaxDegree ;
66+
4867 /**
4968 * Creates a new GraphColoringAnalyzer for the given graph.
5069 *
@@ -56,6 +75,18 @@ public GraphColoringAnalyzer(Graph<String, Edge> graph) {
5675 throw new IllegalArgumentException ("Graph must not be null" );
5776 }
5877 this .graph = graph ;
78+
79+ // Pre-build adjacency sets and degree cache — O(V + E) once,
80+ // then all subsequent neighbor/degree queries are O(1).
81+ this .adjacency = GraphUtils .buildAdjacencyMap (graph );
82+ this .degreeCache = new HashMap <>(graph .getVertexCount () * 2 );
83+ int maxDeg = 0 ;
84+ for (Map .Entry <String , Set <String >> entry : adjacency .entrySet ()) {
85+ int deg = entry .getValue ().size ();
86+ degreeCache .put (entry .getKey (), deg );
87+ if (deg > maxDeg ) maxDeg = deg ;
88+ }
89+ this .cachedMaxDegree = maxDeg ;
5990 }
6091
6192 // ── Greedy Coloring ─────────────────────────────────────────────
@@ -156,23 +187,24 @@ public ColoringResult computeDSatur() {
156187 Map <String , Integer > colorAssignment = new HashMap <>();
157188 Map <String , Set <Integer >> saturation = new HashMap <>();
158189
159- // Cache degree per vertex to avoid repeated graph.degree() calls
160- Map < String , Integer > degreeCache = new HashMap <>();
190+ // Use the pre-computed degree cache from construction instead of
191+ // calling graph.degree() V times here.
161192 for (String v : vertices ) {
162193 saturation .put (v , new HashSet <>());
163- degreeCache .put (v , graph .degree (v ));
164194 }
165195
166196 // Priority queue ordered by: saturation desc, degree desc, name asc.
167197 // Using a TreeSet with a Comparator gives O(log V) removal of the
168198 // max-priority element and O(log V) re-insertion on saturation updates,
169199 // replacing the previous O(V) linear scan per iteration.
200+ // Captures the instance-level degreeCache for O(1) degree lookups.
201+ final Map <String , Integer > localDegreeCache = this .degreeCache ;
170202 TreeSet <String > pq = new TreeSet <>((a , b ) -> {
171203 int satA = saturation .get (a ).size ();
172204 int satB = saturation .get (b ).size ();
173205 if (satA != satB ) return Integer .compare (satB , satA ); // desc
174- int degA = degreeCache . get ( a );
175- int degB = degreeCache . get ( b );
206+ int degA = localDegreeCache . getOrDefault ( a , 0 );
207+ int degB = localDegreeCache . getOrDefault ( b , 0 );
176208 if (degA != degB ) return Integer .compare (degB , degA ); // desc
177209 return a .compareTo (b ); // asc (tiebreaker for TreeSet uniqueness)
178210 });
@@ -237,6 +269,20 @@ public ColoringResult computeDSatur() {
237269 *
238270 * @return lower bound on chromatic number (clique number estimate)
239271 */
272+ /**
273+ * Computes a lower bound on the chromatic number using greedy clique
274+ * detection. The chromatic number is at least the size of the largest
275+ * clique found.
276+ *
277+ * <p><b>Performance:</b> Uses pre-built adjacency sets for O(1)
278+ * neighbor checks (via {@code HashSet.contains()}) instead of the
279+ * previous {@code graph.isNeighbor()} which is O(degree) per call
280+ * in JUNG's sparse graph backing store. Also uses the pre-cached
281+ * degree map for candidate sorting, eliminating O(V log V)
282+ * graph API calls per vertex.</p>
283+ *
284+ * @return lower bound on chromatic number (clique number estimate)
285+ */
240286 public int chromaticLowerBound () {
241287 Collection <String > vertices = graph .getVertices ();
242288 if (vertices .isEmpty ()) {
@@ -250,16 +296,21 @@ public int chromaticLowerBound() {
250296 List <String > clique = new ArrayList <>();
251297 clique .add (start );
252298
299+ Set <String > startNeighbors = adjacency .get (start );
300+ if (startNeighbors == null || startNeighbors .isEmpty ()) continue ;
301+
253302 // Sort candidates by degree descending for better heuristic
254- List <String > candidates = new ArrayList <>();
255- Collection < String > neighbors = GraphUtils . neighborsOf ( graph , start );
256- candidates . addAll ( neighbors );
257- candidates . sort (( a , b ) -> Integer . compare ( graph . degree ( b ), graph . degree ( a )));
303+ List <String > candidates = new ArrayList <>(startNeighbors );
304+ candidates . sort (( a , b ) -> Integer . compare (
305+ degreeCache . getOrDefault ( b , 0 ),
306+ degreeCache . getOrDefault ( a , 0 )));
258307
259308 for (String candidate : candidates ) {
309+ // O(1) adjacency check per clique member via HashSet
310+ Set <String > candidateNeighbors = adjacency .get (candidate );
260311 boolean adjacent = true ;
261312 for (String member : clique ) {
262- if (!graph . isNeighbor ( candidate , member )) {
313+ if (!candidateNeighbors . contains ( member )) {
263314 adjacent = false ;
264315 break ;
265316 }
@@ -456,12 +507,15 @@ public Map<String, Object> analyzeColorClasses(ColoringResult result) {
456507 analysis .put ("balanceRatio" , largest > 0
457508 ? (double ) smallest / largest : 1.0 );
458509
459- // Verify each color class is an independent set
510+ // Verify each color class is an independent set.
511+ // Uses pre-built adjacency sets for O(1) checks instead of
512+ // graph.isNeighbor() which is O(degree) per call.
460513 boolean allIndependent = true ;
461514 for (List <String > cls : classes .values ()) {
462515 for (int i = 0 ; i < cls .size (); i ++) {
516+ Set <String > iNeighbors = adjacency .get (cls .get (i ));
463517 for (int j = i + 1 ; j < cls .size (); j ++) {
464- if (graph . isNeighbor ( cls . get ( i ), cls .get (j ))) {
518+ if (iNeighbors != null && iNeighbors . contains ( cls .get (j ))) {
465519 allIndependent = false ;
466520 break ;
467521 }
@@ -492,21 +546,17 @@ public Map<String, Object> analyzeColorClasses(ColoringResult result) {
492546 *
493547 * @return array with [lower bound, upper bound] for Edge chromatic number
494548 */
549+ /**
550+ * Estimates the Edge chromatic number bounds using Vizing's theorem.
551+ * Uses the pre-cached max degree — O(1) instead of O(V) graph API calls.
552+ *
553+ * @return array with [lower bound, upper bound] for Edge chromatic number
554+ */
495555 public int [] edgeChromaticBounds () {
496- Collection <String > vertices = graph .getVertices ();
497- if (vertices .isEmpty () || graph .getEdgeCount () == 0 ) {
556+ if (graph .getVertexCount () == 0 || graph .getEdgeCount () == 0 ) {
498557 return new int []{0 , 0 };
499558 }
500-
501- int maxDegree = 0 ;
502- for (String v : vertices ) {
503- int deg = graph .degree (v );
504- if (deg > maxDegree ) {
505- maxDegree = deg ;
506- }
507- }
508-
509- return new int []{maxDegree , maxDegree + 1 };
559+ return new int []{cachedMaxDegree , cachedMaxDegree + 1 };
510560 }
511561
512562 /**
@@ -515,15 +565,14 @@ public int[] edgeChromaticBounds() {
515565 *
516566 * @return maximum degree
517567 */
568+ /**
569+ * Returns the maximum vertex degree (Δ), using the pre-cached value.
570+ * O(1) instead of O(V) graph API calls.
571+ *
572+ * @return maximum degree
573+ */
518574 public int maxDegree () {
519- int max = 0 ;
520- for (String v : graph .getVertices ()) {
521- int deg = graph .degree (v );
522- if (deg > max ) {
523- max = deg ;
524- }
525- }
526- return max ;
575+ return cachedMaxDegree ;
527576 }
528577
529578 // ── Report Generation ───────────────────────────────────────────
@@ -589,8 +638,12 @@ private List<String> getOrderedVertices(VertexOrdering ordering) {
589638 Collections .sort (vertices );
590639 break ;
591640 case LARGEST_FIRST :
641+ // Use cached degree map for O(1) lookups in the comparator,
642+ // eliminating O(V log V) graph.degree() API calls.
592643 vertices .sort ((a , b ) -> {
593- int cmp = Integer .compare (graph .degree (b ), graph .degree (a ));
644+ int cmp = Integer .compare (
645+ degreeCache .getOrDefault (b , 0 ),
646+ degreeCache .getOrDefault (a , 0 ));
594647 return cmp != 0 ? cmp : a .compareTo (b );
595648 });
596649 break ;
@@ -691,8 +744,7 @@ private ColoringResult greedyColor(List<String> vertexOrder) {
691744 // Upper bound on colors needed is max-degree + 1; pre-allocate a
692745 // boolean array instead of a HashSet<Integer> per vertex to avoid
693746 // boxing and hashing overhead in the inner loop.
694- int maxDeg = maxDegree ();
695- boolean [] usedColors = new boolean [maxDeg + 2 ];
747+ boolean [] usedColors = new boolean [cachedMaxDegree + 2 ];
696748
697749 for (String vertex : vertexOrder ) {
698750 // Reset only the slots we dirtied (cheaper than Arrays.fill
0 commit comments