-
Notifications
You must be signed in to change notification settings - Fork 18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Branch mst adjmatrix #80
Changes from 19 commits
5b67340
98003fa
dcf0343
a9c021b
b323502
15c79cf
c982fcb
32f6b34
9e00f17
4c0cf98
25301d4
b4ddf1e
4905709
90b5406
956747d
70771ce
5932261
0667d91
003140e
dec4bd2
a93535f
3e2bdfe
dac6a61
776dc24
f67cf0f
2fdafe0
f9b2620
7f84208
8357bc1
2b2602c
35550ed
1fe6a23
05cacf2
f5bc79f
f30a77a
f09df8f
250e9b2
3e34e9f
62529c5
8365586
f5ced43
08c84a4
4852a7e
84d35f7
79e65ed
6351ab2
eb870c4
aa63c3c
be51a19
50f4836
484b676
f552c24
54d6536
13925b3
a4a4097
47625d8
c3bbf20
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# Minimum Spanning Tree Algorithms | ||
|
||
## Background | ||
|
||
Minimum Spanning Tree (MST) algorithms are used to find the minimum spanning tree of a weighted, connected graph. A | ||
spanning tree of a graph is a connected, acyclic subgraph that includes all the vertices of the original graph. An MST | ||
is a spanning tree with the minimum possible total edge weight. | ||
|
||
## Prim's Algorithm | ||
|
||
We will discuss more implementation-specific details and complexity analysis in the respective folders. In short, | ||
1. [Prim's Algorithm](prim) is a greedy algorithm that finds the minimum spanning tree of a graph by starting from an | ||
arbitrary node (vertex) and adding the edge with the minimum weight that connects the current tree to a new node, adding | ||
the node to the current tree, until all nodes are included in the tree. | ||
|
||
## Notes | ||
|
||
### Difference between Minimum Spanning Tree and Shortest Path | ||
It is important to note that a Minimum Spanning Tree of a graph does not represent the shortest path between all the | ||
nodes. See below for an example: | ||
|
||
The below graph is a weighted, connected graph with 5 nodes and 6 edges: | ||
![original graph img](../../../../../docs/assets/images/originalGraph.jpg) | ||
|
||
The following is the Minimum Spanning Tree of the above graph: | ||
![MST img](../../../../../docs/assets/images/MST.jpg) | ||
|
||
Taking node A and D into consideration, the shortest path between them is A -> D, with a total weight of 4. | ||
![SPOriginal img](../../../../../docs/assets/images/SPOriginal.jpg) | ||
|
||
However, the shortest path between A and D in the Minimum Spanning Tree is A -> C -> D, with a total weight of 5, which | ||
is not the shortest path in the original graph. | ||
![SPMST img](../../../../../docs/assets/images/SPMST.jpg) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
package algorithms.minimumSpanningTree.kruskal; | ||
|
||
import java.util.HashMap; | ||
import java.util.Map; | ||
|
||
/** | ||
* Implementation of quick-find structure; Turns a list of objects into a data structure that supports union operations | ||
*/ | ||
public class DisjointSet { | ||
private final Map<Node, String> identifier; | ||
|
||
/** | ||
* Constructor to initialize Disjoint Set with a known list of listOfNodes. | ||
* @param listOfNodes | ||
*/ | ||
public DisjointSet(Node[] listOfNodes) { | ||
identifier = new HashMap<>(); | ||
for (Node currNode : listOfNodes) { | ||
// Component identifier is the same as the node's identifier | ||
identifier.put(currNode, currNode.getIdentifier()); | ||
} | ||
} | ||
|
||
/** | ||
* Checks if object a and object b are in the same component. | ||
* @param a | ||
* @param b | ||
* @return a boolean value | ||
*/ | ||
public boolean find(Node a, Node b) { | ||
if (!identifier.containsKey(a) || !identifier.containsKey(b)) { // key(s) does not even exist | ||
return false; | ||
} | ||
return identifier.get(a).equals(identifier.get(b)); | ||
} | ||
|
||
/** | ||
* Merge the components of object a and object b. | ||
* @param a | ||
* @param b | ||
*/ | ||
public void union(Node a, Node b) { | ||
if (!identifier.containsKey(a) || !identifier.containsKey(b)) { // key(s) does not even exist; do nothing | ||
return; | ||
} | ||
|
||
if (identifier.get(a).equals(identifier.get(b))) { // already same; do nothing | ||
return; | ||
} | ||
|
||
String compOfA = identifier.get(a); | ||
String compOfB = identifier.get(b); | ||
for (Node obj : identifier.keySet()) { | ||
if (identifier.get(obj).equals(compOfA)) { | ||
identifier.put(obj, compOfB); | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package algorithms.minimumSpanningTree.kruskal; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. our other files typically place helper class in the same file. For consistency sake, we could shift this to Kruskal.java too. But if there are any concerns do bring it up! |
||
|
||
/** | ||
* Edge class to represent an edge in the graph | ||
*/ | ||
public class Edge implements Comparable<Edge> { | ||
private final Node source; | ||
private final Node destination; | ||
private final int weight; | ||
|
||
/** | ||
* Constructor for Edge | ||
*/ | ||
public Edge(Node source, Node destination, int weight) { | ||
this.source = source; | ||
this.destination = destination; | ||
this.weight = weight; | ||
} | ||
|
||
public int getWeight() { | ||
return weight; | ||
} | ||
|
||
public Node getSource() { | ||
return source; | ||
} | ||
|
||
public Node getDestination() { | ||
return destination; | ||
} | ||
|
||
@Override | ||
public int compareTo(Edge other) { | ||
return Integer.compare(this.weight, other.weight); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
package algorithms.minimumSpanningTree.kruskal; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
/** | ||
* Implementation of Prim's Algorithm to find MSTs | ||
* Idea: | ||
* Starting from any source (this will be the first node to be in the MST), pick the lightest outgoing edge, and | ||
* include the node at the other end as part of a set of nodes S. Now repeatedly do the above by picking the lightest | ||
* outgoing edge adjacent to any node in the MST (ensure the other end of the node is not already in the MST). | ||
* Repeat until S contains all nodes in the graph. S is the MST. | ||
* Actual implementation: | ||
* No Edge class was implemented. Instead, the weights of the edges are stored in a 2D array adjacency matrix. An | ||
* adjacency list may be used instead | ||
* A Node class is implemented to encapsulate the current minimum weight to reach the node. | ||
*/ | ||
public class Kruskal { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. overall logic is neat and clear! thanks! |
||
public static int[][] getKruskalMST(Node[] nodes, int[][] adjacencyMatrix) { | ||
int numOfNodes = nodes.length; | ||
List<Edge> edges = new ArrayList<>(); | ||
|
||
// Convert adjacency matrix to list of edges | ||
for (int i = 0; i < numOfNodes; i++) { | ||
for (int j = i + 1; j < numOfNodes; j++) { | ||
if (adjacencyMatrix[i][j] != Integer.MAX_VALUE) { | ||
edges.add(new Edge(nodes[i], nodes[j], adjacencyMatrix[i][j])); | ||
} | ||
} | ||
} | ||
|
||
// Sort edges by weight | ||
edges.sort(Edge::compareTo); | ||
|
||
// Initialize Disjoint Set for vertex tracking | ||
DisjointSet ds = new DisjointSet(nodes); | ||
|
||
int[][] mstMatrix = new int[numOfNodes][numOfNodes]; | ||
|
||
// Initialize the MST matrix to represent no edges with Integer.MAX_VALUE and 0 for self loops | ||
for (int i = 0; i < nodes.length; i++) { | ||
for (int j = 0; j < nodes.length; j++) { | ||
mstMatrix[i][j] = (i == j) ? 0 : Integer.MAX_VALUE; | ||
} | ||
} | ||
|
||
// Process edges to build MST | ||
for (Edge edge : edges) { | ||
Node source = edge.getSource(); | ||
Node destination = edge.getDestination(); | ||
if (!ds.find(source, destination)) { | ||
mstMatrix[source.getIndex()][destination.getIndex()] = edge.getWeight(); | ||
mstMatrix[destination.getIndex()][source.getIndex()] = edge.getWeight(); | ||
ds.union(source, destination); | ||
} | ||
} | ||
|
||
return mstMatrix; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cant rmb if this was what we discussed. But we do this just as a representation to capture the edges right? Do you know of any online helper functions to visualise the mst lol |
||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package algorithms.minimumSpanningTree.kruskal; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yup same for this class also. Maybe can put at the bottom as helper classes |
||
|
||
/** | ||
* Node class to represent a node in the graph | ||
* Note: In our Node class, we do not allow the currMinWeight to be updated after initialization to prevent any | ||
* reference issues in the PriorityQueue. | ||
*/ | ||
public class Node { | ||
private final int index; // Index of this node in the adjacency matrix | ||
private final String identifier; | ||
|
||
/** | ||
* Constructor for Node | ||
* @param identifier | ||
* @param index | ||
*/ | ||
public Node(String identifier, int index) { | ||
this.identifier = identifier; | ||
this.index = index; | ||
} | ||
|
||
/** | ||
* Getter for identifier | ||
* @return identifier | ||
*/ | ||
public String getIdentifier() { | ||
return identifier; | ||
} | ||
|
||
public int getIndex() { | ||
return index; | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return "Node{" + "identifier='" + identifier + '\'' + ", index=" + index + '}'; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
package algorithms.minimumSpanningTree.prim; | ||
|
||
/** | ||
* Node class to represent a node in the graph | ||
* Note: In our Node class, we do not allow the currMinWeight to be updated after initialization to prevent any | ||
* reference issues in the PriorityQueue. | ||
*/ | ||
public class Node { | ||
private final int currMinWeight; // Current minimum weight to get to this node | ||
private int index; // Index of this node in the adjacency matrix | ||
private final String identifier; | ||
|
||
/** | ||
* Constructor for Node | ||
* @param identifier | ||
* @param index | ||
* @param currMinWeight | ||
*/ | ||
public Node(String identifier, int index, int currMinWeight) { | ||
this.identifier = identifier; | ||
this.index = index; | ||
this.currMinWeight = currMinWeight; | ||
} | ||
|
||
/** | ||
* Constructor for Node with default currMinWeight | ||
* @param identifier | ||
* @param index | ||
*/ | ||
public Node(String identifier, int index) { | ||
this.identifier = identifier; | ||
this.index = index; | ||
this.currMinWeight = Integer.MAX_VALUE; | ||
} | ||
|
||
/** | ||
* Getter and setter for currMinWeight | ||
*/ | ||
public int getCurrMinWeight() { | ||
return currMinWeight; | ||
} | ||
|
||
/** | ||
* Getter for identifier | ||
* @return identifier | ||
*/ | ||
public String getIdentifier() { | ||
return identifier; | ||
} | ||
|
||
public int getIndex() { | ||
return index; | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return "Node{" + "identifier='" + identifier + '\'' + ", index=" + index + '}'; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
package algorithms.minimumSpanningTree.prim; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i believe the underlying assumption is that the graph is connected right? i think both ur implementation should work fine for unconnected graphs too (not a thorough analysis.. jsut a quick run-through in my head) - that is, your return matrix should capture separate MSTs, if exist. Do double check and lmk if im wrong! |
||
|
||
import java.util.Arrays; | ||
import java.util.PriorityQueue; | ||
|
||
/** | ||
* Implementation of Prim's Algorithm to find MSTs | ||
* Idea: | ||
* Starting from any source (this will be the first node to be in the MST), pick the lightest outgoing edge, and | ||
* include the node at the other end as part of a set of nodes S. Now repeatedly do the above by picking the lightest | ||
* outgoing edge adjacent to any node in the MST (ensure the other end of the node is not already in the MST). | ||
* Repeat until S contains all nodes in the graph. S is the MST. | ||
* Actual implementation: | ||
* No Edge class was implemented. Instead, the weights of the edges are stored in a 2D array adjacency matrix. An | ||
* adjacency list may be used instead | ||
* A Node class is implemented to encapsulate the current minimum weight to reach the node. | ||
*/ | ||
public class Prim { | ||
public static int[][] getPrimsMST(Node[] nodes, int[][] adjacencyMatrix) { | ||
PriorityQueue<Node> pq = new PriorityQueue<>((a, b) -> a.getCurrMinWeight() - b.getCurrMinWeight()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe a small helper comment to remind its a min-heap |
||
int[][] mstMatrix = new int[nodes.length][nodes.length]; // MST adjacency matrix | ||
|
||
int[] parent = new int[nodes.length]; // To track the parent node of each node in the MST | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice. Map<> works too but i think array is even simpler. |
||
Arrays.fill(parent, -1); // Initialize parent array with -1, indicating no parent | ||
|
||
boolean[] visited = new boolean[nodes.length]; // To track visited nodes | ||
Arrays.fill(visited, false); // Initialize visited array with false, indicating not visited | ||
|
||
// Initialize the MST matrix to represent no edges with Integer.MAX_VALUE and 0 for self loops | ||
for (int i = 0; i < nodes.length; i++) { | ||
for (int j = 0; j < nodes.length; j++) { | ||
mstMatrix[i][j] = (i == j) ? 0 : Integer.MAX_VALUE; | ||
} | ||
} | ||
|
||
// Add all nodes to the priority queue, with each node's curr min weight already set to Integer.MAX_VALUE | ||
pq.addAll(Arrays.asList(nodes)); | ||
|
||
while (!pq.isEmpty()) { | ||
Node current = pq.poll(); | ||
|
||
int currentIndex = current.getIndex(); | ||
|
||
if (visited[currentIndex]) { // Skip if node is already visited | ||
continue; | ||
} | ||
|
||
visited[currentIndex] = true; | ||
|
||
for (int i = 0; i < nodes.length; i++) { | ||
if (adjacencyMatrix[currentIndex][i] != Integer.MAX_VALUE && !visited[nodes[i].getIndex()]) { | ||
int weight = adjacencyMatrix[currentIndex][i]; | ||
|
||
if (weight < nodes[i].getCurrMinWeight()) { | ||
Node newNode = new Node(nodes[i].getIdentifier(), nodes[i].getIndex(), weight); | ||
parent[i] = currentIndex; // Set current node as parent of adjacent node | ||
pq.add(newNode); | ||
} | ||
} | ||
} | ||
} | ||
|
||
// Build MST matrix based on parent array | ||
for (int i = 1; i < nodes.length; i++) { | ||
int p = parent[i]; | ||
if (p != -1) { | ||
int weight = adjacencyMatrix[p][i]; | ||
mstMatrix[p][i] = weight; | ||
mstMatrix[i][p] = weight; // For undirected graphs | ||
} | ||
} | ||
|
||
return mstMatrix; | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice, but we should align our implementation of disjoint set. There already is an existing one which u can either import or copy-paste (maybe this is preferable? to be explicit).
Ok but i did notice a concern as i was looking through. Both urs and my implementation does not handle duplicates well. I will tweak the official DisjointSet implementation of our repo to identify by memory address instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok on 2nd thought. this is quite a non-concern. DisjointSet isn't at all suited for duplicates