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

Commit c297a0f

Browse files
feat: add Minimum Spanning Tree (Kruskal's algorithm + Union-Find)
Implements MST computation for weighted undirected graphs using Kruskal's algorithm with Union-Find (disjoint set with path compression + union by rank) for near-linear performance. Features: - Kruskal's algorithm computes MST or minimum spanning forest (one tree per connected component for disconnected graphs) - Rich result API: total weight, edge count, vertex count, component count, connectivity check, edge type distribution - Per-component breakdown: vertices, edges, weight, dominant edge type - Stats: heaviest/lightest edge (bottleneck analysis), average weight - Human-readable summary string - Unmodifiable result collections (defensive copies) UI Integration: - New 'Minimum Spanning Tree' panel in right sidebar - Compute/Clear buttons with real-time stats display - MST edges highlighted in bright green (non-MST edges dimmed) - Thicker stroke for MST edges (2.5px vs normal) - Component breakdown shown for disconnected graphs (forests) - Edge type distribution labeled with human-readable names - Coexists with existing overlays (shortest path, community) Tests: - 42 comprehensive JUnit tests covering: - Constructor validation (null graph) - Empty, single vertex, single edge, disconnected pairs - Triangle, square, K4, K5, star, chain graphs - Disconnected graphs (forest mode with component breakdown) - Equal weights, zero weights, large weights - Edge type distribution (single type, mixed, empty) - Stats methods (heaviest, lightest, average, summary) - Component analysis (weights, dominant types, IDs) - Union-Find internals (basic, path compression, self-union) - Determinism (multiple compute calls) - Immutability (unmodifiable result collections) +1346 lines (380 MinimumSpanningTree.java, 776 test, 190 Main.java)
1 parent b1472f0 commit c297a0f

3 files changed

Lines changed: 1346 additions & 4 deletions

File tree

Gvisual/src/gvisual/Main.java

Lines changed: 190 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,16 @@ public class Main extends JFrame {
181181
private JRadioButton pathByHops;
182182
private JRadioButton pathByWeight;
183183

184+
// --- MST fields ---
185+
private JPanel mstPanel;
186+
private JButton mstComputeButton;
187+
private JButton mstClearButton;
188+
private JLabel mstSummaryLabel;
189+
private JLabel mstStatsLabel;
190+
private JLabel mstComponentsLabel;
191+
private boolean mstOverlayActive;
192+
private Set<edge> mstEdges;
193+
184194
// --- Centrality analysis fields ---
185195
private JPanel centralityPanel;
186196
private JButton centralityComputeButton;
@@ -228,6 +238,7 @@ public Main() throws FileNotFoundException, Exception {
228238
initializeStatsPanel();
229239
initializePathPanel();
230240
initializeCommunityPanel();
241+
initializeMSTPanel();
231242
initializeCentralityPanel();
232243
initializeTimeLine();
233244
initializeToolBar();
@@ -595,6 +606,10 @@ public Paint transform(edge edge) {
595606
if (pathEdges != null && pathEdges.contains(edge)) {
596607
return Color.YELLOW;
597608
}
609+
// MST overlay — highlight MST edges in bright green
610+
if (mstOverlayActive && mstEdges != null && mstEdges.contains(edge)) {
611+
return new Color(0, 255, 100);
612+
}
598613
// Community overlay mode — color edges by community
599614
if (communityOverlayActive && nodeCommunityMap != null) {
600615
Integer c1 = nodeCommunityMap.get(edge.getVertex1());
@@ -606,6 +621,10 @@ public Paint transform(edge edge) {
606621
}
607622
return new Color(100, 100, 100, 80); // cross-community edges are dim
608623
}
624+
// MST overlay — dim non-MST edges
625+
if (mstOverlayActive && mstEdges != null && !mstEdges.contains(edge)) {
626+
return new Color(80, 80, 80, 60);
627+
}
609628
if (edge.getType().equalsIgnoreCase("f")) {
610629
return FRIEND_COLOR;
611630
} else if (edge.getType().equalsIgnoreCase("fs")) {
@@ -706,6 +725,10 @@ public Stroke transform(edge i) {
706725
if (pathEdges != null && pathEdges.contains(i)) {
707726
return new BasicStroke(3.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
708727
}
728+
// MST edges get thicker solid stroke
729+
if (mstOverlayActive && mstEdges != null && mstEdges.contains(i)) {
730+
return new BasicStroke(2.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
731+
}
709732
float dash[] = {1.0f};
710733
float width = i.getWeight() / 40 + 1.0f;
711734
return new BasicStroke(width, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 10.0f, dash, 0.0f);
@@ -1200,6 +1223,164 @@ private void clearCommunityOverlay() {
12001223
refreshGraph();
12011224
}
12021225

1226+
/**
1227+
* Initializes the Minimum Spanning Tree panel with compute/clear buttons
1228+
* and result display.
1229+
*/
1230+
public final void initializeMSTPanel() {
1231+
mstOverlayActive = false;
1232+
mstEdges = new HashSet<edge>();
1233+
1234+
mstPanel = new JPanel();
1235+
mstPanel.setLayout(new BoxLayout(mstPanel, BoxLayout.Y_AXIS));
1236+
mstPanel.setBorder(BorderFactory.createTitledBorder(
1237+
BorderFactory.createEtchedBorder(EtchedBorder.LOWERED),
1238+
"Minimum Spanning Tree",
1239+
TitledBorder.CENTER,
1240+
TitledBorder.TOP));
1241+
1242+
Font labelFont = new Font("SansSerif", Font.PLAIN, 12);
1243+
1244+
mstSummaryLabel = new JLabel("<html>Click 'Compute' to find the MST.</html>");
1245+
mstSummaryLabel.setFont(labelFont);
1246+
mstSummaryLabel.setAlignmentX(JLabel.LEFT_ALIGNMENT);
1247+
1248+
mstStatsLabel = new JLabel("");
1249+
mstStatsLabel.setFont(labelFont);
1250+
mstStatsLabel.setAlignmentX(JLabel.LEFT_ALIGNMENT);
1251+
1252+
mstComponentsLabel = new JLabel("");
1253+
mstComponentsLabel.setFont(labelFont);
1254+
mstComponentsLabel.setAlignmentX(JLabel.LEFT_ALIGNMENT);
1255+
1256+
// Buttons
1257+
mstComputeButton = new JButton("Compute");
1258+
mstComputeButton.setAlignmentX(JButton.LEFT_ALIGNMENT);
1259+
mstComputeButton.addActionListener(new ActionListener() {
1260+
public void actionPerformed(ActionEvent e) {
1261+
runMSTComputation();
1262+
}
1263+
});
1264+
1265+
mstClearButton = new JButton("Clear");
1266+
mstClearButton.setAlignmentX(JButton.LEFT_ALIGNMENT);
1267+
mstClearButton.addActionListener(new ActionListener() {
1268+
public void actionPerformed(ActionEvent e) {
1269+
clearMSTOverlay();
1270+
}
1271+
});
1272+
1273+
JPanel buttonPanel = new JPanel();
1274+
buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));
1275+
buttonPanel.setAlignmentX(JPanel.LEFT_ALIGNMENT);
1276+
buttonPanel.add(mstComputeButton);
1277+
buttonPanel.add(Box.createHorizontalStrut(4));
1278+
buttonPanel.add(mstClearButton);
1279+
1280+
mstPanel.add(mstSummaryLabel);
1281+
mstPanel.add(Box.createVerticalStrut(4));
1282+
mstPanel.add(buttonPanel);
1283+
mstPanel.add(Box.createVerticalStrut(4));
1284+
mstPanel.add(mstStatsLabel);
1285+
mstPanel.add(Box.createVerticalStrut(4));
1286+
mstPanel.add(mstComponentsLabel);
1287+
}
1288+
1289+
/**
1290+
* Computes the MST and activates the edge highlight overlay.
1291+
*/
1292+
private void runMSTComputation() {
1293+
if (g == null || g.getVertexCount() == 0) {
1294+
mstSummaryLabel.setText("<html>No graph loaded.</html>");
1295+
return;
1296+
}
1297+
1298+
MinimumSpanningTree mstComputer = new MinimumSpanningTree(g);
1299+
MinimumSpanningTree.MSTResult result = mstComputer.compute();
1300+
1301+
mstOverlayActive = true;
1302+
mstEdges.clear();
1303+
mstEdges.addAll(result.getEdges());
1304+
1305+
// Summary
1306+
mstSummaryLabel.setText("<html><b style='color:#00FF00'>"
1307+
+ result.getSummary() + "</b></html>");
1308+
1309+
// Stats
1310+
StringBuilder stats = new StringBuilder("<html>");
1311+
stats.append(String.format("<b>Vertices:</b> %d<br/>", result.getVertexCount()));
1312+
stats.append(String.format("<b>MST Edges:</b> %d<br/>", result.getEdgeCount()));
1313+
stats.append(String.format("<b>Total Weight:</b> %.1f<br/>", result.getTotalWeight()));
1314+
stats.append(String.format("<b>Avg Weight:</b> %.1f<br/>", result.getAverageWeight()));
1315+
1316+
if (result.getHeaviestEdge() != null) {
1317+
edge heavy = result.getHeaviestEdge();
1318+
stats.append(String.format("<b>Bottleneck:</b> %s↔%s (%.1f)<br/>",
1319+
heavy.getVertex1(), heavy.getVertex2(), heavy.getWeight()));
1320+
}
1321+
if (result.getLightestEdge() != null) {
1322+
edge light = result.getLightestEdge();
1323+
stats.append(String.format("<b>Lightest:</b> %s↔%s (%.1f)<br/>",
1324+
light.getVertex1(), light.getVertex2(), light.getWeight()));
1325+
}
1326+
1327+
// Edge type distribution
1328+
Map<String, Integer> dist = result.getEdgeTypeDistribution();
1329+
if (!dist.isEmpty()) {
1330+
stats.append("<b>Types:</b> ");
1331+
boolean first = true;
1332+
for (Map.Entry<String, Integer> entry : dist.entrySet()) {
1333+
if (!first) stats.append(", ");
1334+
stats.append(getDominantLabel(entry.getKey())).append("=").append(entry.getValue());
1335+
first = false;
1336+
}
1337+
stats.append("<br/>");
1338+
}
1339+
stats.append("</html>");
1340+
mstStatsLabel.setText(stats.toString());
1341+
1342+
// Component breakdown (for forests)
1343+
if (result.getComponentCount() > 1) {
1344+
StringBuilder comps = new StringBuilder("<html><b>Components:</b><br/>");
1345+
int shown = Math.min(result.getComponents().size(), 6);
1346+
for (int i = 0; i < shown; i++) {
1347+
MinimumSpanningTree.MSTComponent comp = result.getComponents().get(i);
1348+
comps.append(String.format("&nbsp;C%d: %d nodes, %d edges, wt=%.1f",
1349+
comp.getId(), comp.getSize(), comp.getEdges().size(), comp.getTotalWeight()));
1350+
String dominant = comp.getDominantType();
1351+
if (dominant != null) {
1352+
comps.append(" (").append(getDominantLabel(dominant)).append(")");
1353+
}
1354+
comps.append("<br/>");
1355+
}
1356+
if (result.getComponents().size() > shown) {
1357+
comps.append("...and ").append(result.getComponents().size() - shown).append(" more<br/>");
1358+
}
1359+
comps.append("</html>");
1360+
mstComponentsLabel.setText(comps.toString());
1361+
} else {
1362+
mstComponentsLabel.setText("");
1363+
}
1364+
1365+
mstPanel.revalidate();
1366+
mstPanel.repaint();
1367+
refreshGraph();
1368+
}
1369+
1370+
/**
1371+
* Clears the MST overlay and resets the panel.
1372+
*/
1373+
private void clearMSTOverlay() {
1374+
mstOverlayActive = false;
1375+
mstEdges.clear();
1376+
mstSummaryLabel.setText("<html>Click 'Compute' to find the MST.</html>");
1377+
mstStatsLabel.setText("");
1378+
mstComponentsLabel.setText("");
1379+
mstPanel.revalidate();
1380+
mstPanel.repaint();
1381+
refreshGraph();
1382+
}
1383+
12031384
/**
12041385
* Initializes the centrality analysis panel with compute/clear buttons,
12051386
* metric selector, and ranked results display.
@@ -1518,9 +1699,13 @@ public final void showRightPane() {
15181699
JSplitPane splitPaneCommunity = new JSplitPane(JSplitPane.VERTICAL_SPLIT,
15191700
splitPanePath, communityPanel);
15201701

1521-
// Add centrality panel below community panel
1702+
// Add MST panel below community panel
1703+
JSplitPane splitPaneMST = new JSplitPane(JSplitPane.VERTICAL_SPLIT,
1704+
splitPaneCommunity, mstPanel);
1705+
1706+
// Add centrality panel below MST panel
15221707
JSplitPane splitPaneCentrality = new JSplitPane(JSplitPane.VERTICAL_SPLIT,
1523-
splitPaneCommunity, centralityPanel);
1708+
splitPaneMST, centralityPanel);
15241709

15251710
// Add stats panel below centrality panel
15261711
JSplitPane splitPane2 = new JSplitPane(JSplitPane.VERTICAL_SPLIT,
@@ -1529,8 +1714,9 @@ public final void showRightPane() {
15291714
splitPane1.setDividerLocation(400);
15301715
splitPanePath.setDividerLocation(510);
15311716
splitPaneCommunity.setDividerLocation(640);
1532-
splitPaneCentrality.setDividerLocation(820);
1533-
splitPane2.setDividerLocation(1050);
1717+
splitPaneMST.setDividerLocation(760);
1718+
splitPaneCentrality.setDividerLocation(920);
1719+
splitPane2.setDividerLocation(1150);
15341720
add(splitPane2, BorderLayout.EAST);
15351721

15361722
}

0 commit comments

Comments
 (0)