@@ -54,84 +54,101 @@ public MSTResult compute() {
5454 0 , 0.0f , 0 , vertexCount );
5555 }
5656
57- // Collect all edges and sort by weight (Kruskal's)
58- List <Edge > sortedEdges = new ArrayList <Edge >();
59- Set <Edge > seen = new HashSet <Edge >();
60- for (Edge e : graph .getEdges ()) {
61- if (!seen .contains (e )) {
62- sortedEdges .add (e );
63- seen .add (e );
64- }
57+ // Intern vertices to dense integer ids once. This lets Union-Find
58+ // operate on int[] arrays instead of paying the per-call cost of
59+ // HashMap<String,String> lookups + String.equals chains in the
60+ // path-compression loop. For graphs with V vertices and E edges,
61+ // this drops the Kruskal merge phase from ~6 hashmap ops + 2
62+ // String.equals per edge to a handful of array reads.
63+ String [] idToVertex = new String [vertexCount ];
64+ HashMap <String , Integer > vertexToId = new HashMap <>(vertexCount * 2 );
65+ int idx = 0 ;
66+ for (String vertex : vertices ) {
67+ vertexToId .put (vertex , idx );
68+ idToVertex [idx ] = vertex ;
69+ idx ++;
6570 }
66- Collections .sort (sortedEdges , (Edge a , Edge b ) -> {
67- return Float .compare (a .getWeight (), b .getWeight ());
68- });
6971
70- // Union-Find
71- UnionFind uf = new UnionFind (vertices );
72+ // Collect distinct edges. The previous implementation used a
73+ // HashSet<Edge> probe per edge; JUNG's getEdges() is already a
74+ // Collection view of distinct edges, but we keep the dedupe via
75+ // the LinkedHashSet ctor — it's cheaper than a separate contains+add.
76+ Collection <Edge > edgeView = graph .getEdges ();
77+ ArrayList <Edge > sortedEdges = new ArrayList <>(
78+ edgeView instanceof Set ? edgeView : new LinkedHashSet <>(edgeView ));
79+ sortedEdges .sort ((Edge a , Edge b ) -> Float .compare (a .getWeight (), b .getWeight ()));
7280
73- List <Edge > mstEdges = new ArrayList <Edge >();
81+ // Union-Find on int ids.
82+ UnionFind uf = new UnionFind (vertexCount );
83+
84+ ArrayList <Edge > mstEdges = new ArrayList <>(Math .min (sortedEdges .size (), vertexCount ));
7485 float totalWeight = 0.0f ;
7586
7687 for (Edge e : sortedEdges ) {
77- String u = e .getVertex1 ();
78- String v = e .getVertex2 ();
79- if (!uf .find (u ).equals (uf .find (v ))) {
80- uf .union (u , v );
88+ Integer uIdBoxed = vertexToId .get (e .getVertex1 ());
89+ Integer vIdBoxed = vertexToId .get (e .getVertex2 ());
90+ // Defensive: skip edges that reference vertices the graph no
91+ // longer reports. This preserves the previous behavior of
92+ // "sortedEdges only contains JUNG-managed edges".
93+ if (uIdBoxed == null || vIdBoxed == null ) continue ;
94+ int uId = uIdBoxed ;
95+ int vId = vIdBoxed ;
96+ if (uf .union (uId , vId )) {
8197 mstEdges .add (e );
8298 totalWeight += e .getWeight ();
99+ if (mstEdges .size () == vertexCount - 1 ) {
100+ break ; // MST is complete; no need to inspect heavier edges
101+ }
83102 }
84103 }
85104
86- // Build per-component breakdown
87- Map <String , List <String >> rootToVertices = new LinkedHashMap <String , List <String >>();
88- for (String vertex : vertices ) {
89- String root = uf .find (vertex );
90- List <String > members = rootToVertices .get (root );
91- if (members == null ) {
92- members = new ArrayList <String >();
93- rootToVertices .put (root , members );
105+ // Build per-component breakdown indexed by canonical root id.
106+ List <List <String >> rootToVertices = new ArrayList <>();
107+ List <List <Edge >> rootToEdges = new ArrayList <>();
108+ int [] rootToCompId = new int [vertexCount ];
109+ Arrays .fill (rootToCompId , -1 );
110+
111+ for (int i = 0 ; i < vertexCount ; i ++) {
112+ int root = uf .find (i );
113+ int compId = rootToCompId [root ];
114+ if (compId == -1 ) {
115+ compId = rootToVertices .size ();
116+ rootToCompId [root ] = compId ;
117+ rootToVertices .add (new ArrayList <>());
118+ rootToEdges .add (new ArrayList <>());
94119 }
95- members . add (vertex );
120+ rootToVertices . get ( compId ). add (idToVertex [ i ] );
96121 }
97122
98- // Map edges to their component
99- Map <String , List <Edge >> rootToEdges = new LinkedHashMap <String , List <Edge >>();
100123 for (Edge e : mstEdges ) {
101- String root = uf .find (e .getVertex1 ());
102- List <Edge > compEdges = rootToEdges .get (root );
103- if (compEdges == null ) {
104- compEdges = new ArrayList <Edge >();
105- rootToEdges .put (root , compEdges );
106- }
107- compEdges .add (e );
124+ Integer endpointId = vertexToId .get (e .getVertex1 ());
125+ if (endpointId == null ) continue ;
126+ int compId = rootToCompId [uf .find (endpointId )];
127+ rootToEdges .get (compId ).add (e );
108128 }
109129
110- List < MSTComponent > components = new ArrayList < MSTComponent >();
111- int compId = 0 ;
112- // Sort components by size descending for consistent output
113- List < Map . Entry < String , List < String >>> sortedComps =
114- new ArrayList < Map . Entry < String , List < String >>>( rootToVertices . entrySet ()) ;
115- Collections . sort ( sortedComps , ( Map . Entry < String , List < String >> a , Map . Entry < String , List < String >> b ) -> {
116- return Integer . compare ( b . getValue (). size (), a . getValue (). size ()) ;
117- });
118-
119- for ( Map . Entry < String , List < String >> entry : sortedComps ) {
120- String root = entry . getKey ( );
121- List < String > members = entry . getValue ();
122- List < Edge > compEdges = rootToEdges . get ( root ) ;
123- if ( compEdges == null ) compEdges = Collections .< Edge > emptyList ( );
124-
130+ // Sort components by size descending for consistent output. We
131+ // package each component's vertices+edges together so we don't
132+ // have to look them up again after sorting.
133+ int componentCount = rootToVertices . size ();
134+ Integer [] order = new Integer [ componentCount ] ;
135+ for ( int i = 0 ; i < componentCount ; i ++) order [ i ] = i ;
136+ final List < List < String >> rtv = rootToVertices ;
137+ Arrays . sort ( order , ( Integer a , Integer b ) ->
138+ Integer . compare ( rtv . get ( b ). size (), rtv . get ( a ). size ()));
139+
140+ List < MSTComponent > components = new ArrayList <>( componentCount );
141+ for ( int outId = 0 ; outId < componentCount ; outId ++) {
142+ int srcId = order [ outId ] ;
143+ List < String > members = rootToVertices . get ( srcId );
144+ List < Edge > compEdges = rootToEdges . get ( srcId );
125145 float compWeight = 0.0f ;
126146 for (Edge e : compEdges ) {
127147 compWeight += e .getWeight ();
128148 }
129-
130- components .add (new MSTComponent (compId ++, members , compEdges , compWeight ));
149+ components .add (new MSTComponent (outId , members , compEdges , compWeight ));
131150 }
132151
133- int componentCount = rootToVertices .size ();
134-
135152 return new MSTResult (mstEdges , components , componentCount , totalWeight , mstEdges .size (), vertexCount );
136153 }
137154
@@ -142,57 +159,69 @@ public MSTResult compute() {
142159
143160 /**
144161 * Disjoint set data structure for Kruskal's algorithm.
162+ *
163+ * <p>Indexed by dense integer vertex ids. The previous implementation
164+ * used {@code Map<String,String>} parent/rank tables, which paid the
165+ * cost of a HashMap lookup and a {@link String#equals(Object)} chain
166+ * on every step of the path-compression loop. The current
167+ * implementation uses two {@code int[]} arrays, giving constant-time
168+ * array reads in the hot loop and substantially less allocation
169+ * (no boxed {@link Integer} for the rank table).</p>
145170 */
146171 static class UnionFind {
147- private final Map <String , String > parent ;
148- private final Map <String , Integer > rank ;
149-
150- UnionFind (Collection <String > elements ) {
151- parent = new HashMap <String , String >();
152- rank = new HashMap <String , Integer >();
153- for (String e : elements ) {
154- parent .put (e , e );
155- rank .put (e , 0 );
172+ private final int [] parent ;
173+ private final byte [] rank ; // tree height is bounded by log2(V); byte is plenty
174+
175+ UnionFind (int n ) {
176+ parent = new int [n ];
177+ rank = new byte [n ];
178+ for (int i = 0 ; i < n ; i ++) {
179+ parent [i ] = i ;
156180 }
157181 }
158182
159183 /**
160- * Find with path compression.
184+ * Find with iterative path compression (two-pass, no recursion) .
161185 */
162- String find (String x ) {
163- String root = x ;
164- while (! root . equals ( parent . get ( root )) ) {
165- root = parent . get ( root ) ;
186+ int find (int x ) {
187+ int root = x ;
188+ while (parent [ root ] != root ) {
189+ root = parent [ root ] ;
166190 }
167- // Path compression
168- String current = x ;
169- while (! current . equals ( root ) ) {
170- String next = parent . get ( current ) ;
171- parent . put ( current , root ) ;
191+ // Path compression: point every node on the path directly at the root.
192+ int current = x ;
193+ while (parent [ current ] != root ) {
194+ int next = parent [ current ] ;
195+ parent [ current ] = root ;
172196 current = next ;
173197 }
174198 return root ;
175199 }
176200
177201 /**
178202 * Union by rank.
203+ *
204+ * @return true if the two elements were in distinct components
205+ * and have been merged; false if they were already in the
206+ * same component.
179207 */
180- void union (String a , String b ) {
181- String rootA = find (a );
182- String rootB = find (b );
183- if (rootA . equals ( rootB )) return ;
208+ boolean union (int a , int b ) {
209+ int rootA = find (a );
210+ int rootB = find (b );
211+ if (rootA == rootB ) return false ;
184212
185- int rankA = rank . get ( rootA ) ;
186- int rankB = rank . get ( rootB ) ;
213+ int rankA = rank [ rootA ] ;
214+ int rankB = rank [ rootB ] ;
187215
188216 if (rankA < rankB ) {
189- parent . put ( rootA , rootB ) ;
217+ parent [ rootA ] = rootB ;
190218 } else if (rankA > rankB ) {
191- parent . put ( rootB , rootA ) ;
219+ parent [ rootB ] = rootA ;
192220 } else {
193- parent . put ( rootB , rootA ) ;
194- rank . put ( rootA , rankA + 1 );
221+ parent [ rootB ] = rootA ;
222+ rank [ rootA ] = ( byte ) ( rankA + 1 );
195223 }
224+ return true ;
196225 }
197226 }
198227
0 commit comments