Skip to content

Commit

Permalink
feat: Prims MST (#103)
Browse files Browse the repository at this point in the history
closes #51
  • Loading branch information
bobluppes authored Sep 28, 2023
1 parent 3491969 commit 3c7839a
Show file tree
Hide file tree
Showing 15 changed files with 580 additions and 176 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,9 @@ take a look at the [docs](https://bobluppes.github.io/graaf/docs/algorithms/intr
- Bellman-Ford
3. [**Cycle Detection Algorithms**](https://bobluppes.github.io/graaf/docs/category/cycle-detection-algorithms):
- DFS-Based Cycle Detection
4. **Minimum Spanning Tree (MST) Algorithms**
4. [**Minimum Spanning Tree (MST) Algorithms**](https://bobluppes.github.io/graaf/docs/category/minimum-spanning-tree)
- Kruskal's Algorithm
- Prim's Algorithm
5. [**Strongly Connected Components Algorithms
**](https://bobluppes.github.io/graaf/docs/category/strongly-connected-components):
- Tarjan's Strongly Connected Components
Expand Down
6 changes: 6 additions & 0 deletions docs/docs/algorithms/minimum-spanning-tree/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"label": "Minimum Spanning Tree",
"link": {
"type": "generated-index"
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
---
sidebar_position: 4
---

# Kruskal's Algorithm
Kruskal's algorithm finds the minimum spanning forest of an undirected edge-weighted graph. If the graph is connected, it finds a minimum spanning tree.

Kruskal's algorithm finds the minimum spanning forest of an undirected edge-weighted graph. If the graph is connected,
it finds a minimum spanning tree.
The algorithm is implemented with disjoint set union and finding minimum weighted edges.
Worst-case performance is `O(|E|log|V|)`, where `|E|` is the number of edges and `|V|` is the number of vertices in the
graph. Memory usage is `O(V+E)` for maintaining vertices (DSU) and edges.
Expand All @@ -21,9 +19,11 @@ template <typename V, typename E>
```
- **graph** The graph to extract MST or MSF.
- **return** Returns a vector of edges that form MST if the graph is connected, otherwise it returns the minimum spanning forest.
- **return** Returns a vector of edges that form MST if the graph is connected, otherwise it returns the minimum
spanning forest.
### Special case
In case of multiply edges with same weight leading to a vertex, prioritizing vertices with lesser vertex number.
```cpp
Expand Down
26 changes: 26 additions & 0 deletions docs/docs/algorithms/minimum-spanning-tree/prim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Prim's Algorithm

Prim's algorithm computes the minimum spanning tree (MST) of a connected, undirected graph with weighted edges. Starting
with an arbitrary vertex, the algorithm iteratively selects the edge with the smallest weight that connects a
vertex in the tree to a vertex outside the tree, adding it to the MST.

The algorithm's worst-case time complexity is O(∣E∣log∣V∣).

Unlike Kruskal's algorithm, Prim's algorithm works efficiently on dense graphs. A limitation is that it requires the
graph to be connected and does not handle disconnected graphs or graphs with negative-weight cycles.

Prim's MST is often used in network design, such as electrical wiring and telecommunications.

[wikipedia](https://en.wikipedia.org/wiki/Prim%27s_algorithm)

## Syntax

```cpp
template <typename V, typename E>
[[nodiscard]] std::optional<std::vector<edge_id_t> > prim_minimum_spanning_tree(
const graph<V, E, graph_type::UNDIRECTED>& graph, vertex_id_t start_vertex);
```
- **graph** The undirected graph for which we want to compute the MST.
- **start_vertex** The vertex ID which should be the root of the MST.
- **return** Returns a vector of edges that form MST if the graph is connected, otherwise returns an empty optional.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ std::unordered_map<vertex_id_t, int> greedy_graph_coloring(const GRAPH& graph) {

// Iterate through each vertex
for (const auto& [current_vertex_id, _] : vertices) {

// Iterate through neighboring vertices
// Find the smallest available color for the current vertex
int available_color{0};
Expand Down
22 changes: 21 additions & 1 deletion include/graaflib/algorithm/minimum_spanning_tree.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
#pragma once

#include <graaflib/graph.h>
#include <graaflib/types.h>

#include <optional>
#include <vector>

namespace graaf::algorithm {
/**
Expand All @@ -16,5 +20,21 @@ template <typename V, typename E>
[[nodiscard]] std::vector<edge_id_t> kruskal_minimum_spanning_tree(
const graph<V, E, graph_type::UNDIRECTED>& graph);

}; // namespace graaf::algorithm
/**
* Computes the minimum spanning tree (MST) of a graph using Prim's algorithm.
*
* @tparam V The vertex type of the graph.
* @tparam E The edge type of the graph.
* @param graph The input graph. Should be undirected.
* @param start_vertex The starting vertex for the MST construction.
* @return An optional containing a vector of edges forming the MST if it
* exists, or an empty optional if the MST doesn't exist (e.g., graph is not
* connected).
*/
template <typename V, typename E>
[[nodiscard]] std::optional<std::vector<edge_id_t> > prim_minimum_spanning_tree(
const graph<V, E, graph_type::UNDIRECTED>& graph, vertex_id_t start_vertex);

} // namespace graaf::algorithm

#include "minimum_spanning_tree.tpp"
189 changes: 119 additions & 70 deletions include/graaflib/algorithm/minimum_spanning_tree.tpp
Original file line number Diff line number Diff line change
@@ -1,101 +1,150 @@
#pragma once

#include <graaflib/types.h>

#include <algorithm>
#include <unordered_map>
#include <graaflib/types.h>
#include <unordered_set>

namespace graaf::algorithm {

// Disjoint Set Union to maintain sets of vertices
namespace detail {
void do_make_set(vertex_id_t v,
std::unordered_map<vertex_id_t, vertex_id_t>& parent,
std::unordered_map<vertex_id_t, vertex_id_t>& rank) {
parent[v] = v;
rank[v] = 0;
std::unordered_map<vertex_id_t, vertex_id_t>& parent,
std::unordered_map<vertex_id_t, vertex_id_t>& rank) {
parent[v] = v;
rank[v] = 0;
}

vertex_id_t do_find_set(vertex_id_t vertex,
std::unordered_map<vertex_id_t, vertex_id_t>& parent) {
if (vertex == parent[vertex]) {
return vertex;
}
return parent[vertex] = do_find_set(parent[vertex], parent);
std::unordered_map<vertex_id_t, vertex_id_t>& parent) {
if (vertex == parent[vertex]) {
return vertex;
}
return parent[vertex] = do_find_set(parent[vertex], parent);
}

void do_merge_sets(vertex_id_t vertex_a, vertex_id_t vertex_b,
std::unordered_map<vertex_id_t, vertex_id_t>& parent,
std::unordered_map<vertex_id_t, vertex_id_t>& rank) {
vertex_a = do_find_set(vertex_a, parent);
vertex_b = do_find_set(vertex_b, parent);

if (vertex_a != vertex_b) {
if (rank[vertex_a] < rank[vertex_b]) {
std::swap(vertex_a, vertex_b);
}
parent[vertex_b] = vertex_a;

if (rank[vertex_a] == rank[vertex_b]) {
++rank[vertex_a];
}
}
std::unordered_map<vertex_id_t, vertex_id_t>& parent,
std::unordered_map<vertex_id_t, vertex_id_t>& rank) {
vertex_a = do_find_set(vertex_a, parent);
vertex_b = do_find_set(vertex_b, parent);

if (vertex_a != vertex_b) {
if (rank[vertex_a] < rank[vertex_b]) {
std::swap(vertex_a, vertex_b);
}
parent[vertex_b] = vertex_a;

if (rank[vertex_a] == rank[vertex_b]) {
++rank[vertex_a];
}
}
}

template <typename T>
struct edge_to_process : public weighted_edge<T> {
public:
vertex_id_t vertex_a, vertex_b;
T weight_;

[[nodiscard]] T get_weight() const noexcept override { return weight_; }

edge_to_process(vertex_id_t vertex_u, vertex_id_t vertex_w,
T weight)
: vertex_a{vertex_u}, vertex_b{vertex_w}, weight_{weight} {};
edge_to_process(){};
~edge_to_process(){};

bool operator!=(const edge_to_process& e) const noexcept {
return this->weight_ != e.weight_;
}
struct edge_to_process : public weighted_edge<T> {
public:
vertex_id_t vertex_a, vertex_b;
T weight_;

[[nodiscard]] T get_weight() const noexcept override { return weight_; }

edge_to_process(vertex_id_t vertex_u, vertex_id_t vertex_w, T weight)
: vertex_a{vertex_u}, vertex_b{vertex_w}, weight_{weight} {};
edge_to_process(){};
~edge_to_process(){};

bool operator!=(const edge_to_process& e) const noexcept {
return this->weight_ != e.weight_;
}
};
}; // namespace detail

template <class GRAPH_T>
[[nodiscard]] std::vector<edge_id_t> find_candidate_edges(
const GRAPH_T& graph,
const std::unordered_set<vertex_id_t>& fringe_vertices) {
std::vector<edge_id_t> candidates{};

for (const auto fringe_vertex : fringe_vertices) {
for (const auto neighbor : graph.get_neighbors(fringe_vertex)) {
if (!fringe_vertices.contains(neighbor)) {
candidates.emplace_back(fringe_vertex, neighbor);
}
}
}

return candidates;
}

}; // namespace detail

template <typename V, typename E>
std::vector<edge_id_t> kruskal_minimum_spanning_tree(
const graph<V, E, graph_type::UNDIRECTED>& graph) {
// unordered_map in case of deletion of vertices
std::unordered_map<vertex_id_t, vertex_id_t> rank, parent;
std::vector<detail::edge_to_process<E>> edges_to_process{};
std::vector<edge_id_t> mst_edges{};

// unordered_map in case of deletion of vertices
std::unordered_map<vertex_id_t, vertex_id_t> rank, parent;
std::vector<detail::edge_to_process<E>> edges_to_process{};
std::vector<edge_id_t> mst_edges{};
for (const auto& vertex : graph.get_vertices()) {
detail::do_make_set(vertex.first, parent, rank);
}
for (const auto& edge : graph.get_edges()) {
edges_to_process.push_back(
{edge.first.first, edge.first.second, edge.second});
}

for (const auto& vertex : graph.get_vertices()) {
detail::do_make_set(vertex.first, parent, rank);
std::sort(edges_to_process.begin(), edges_to_process.end(),
[](detail::edge_to_process<E>& e1, detail::edge_to_process<E>& e2) {
if (e1 != e2) return e1.get_weight() < e2.get_weight();
return e1.vertex_a < e2.vertex_a || e1.vertex_b < e2.vertex_b;
});

for (const auto& edge : edges_to_process) {
if (detail::do_find_set(edge.vertex_a, parent) !=
detail::do_find_set(edge.vertex_b, parent)) {
mst_edges.push_back({edge.vertex_a, edge.vertex_b});
detail::do_merge_sets(edge.vertex_a, edge.vertex_b, parent, rank);
}
for (const auto& edge : graph.get_edges()) {
edges_to_process.push_back({edge.first.first, edge.first.second, edge.second});
// Found MST E == V - 1
if (mst_edges.size() == graph.vertex_count() - 1) return mst_edges;
}
// Returns minimum spanning forest
return mst_edges;
}

template <typename V, typename E>
std::optional<std::vector<edge_id_t>> prim_minimum_spanning_tree(
const graph<V, E, graph_type::UNDIRECTED>& graph,
vertex_id_t start_vertex) {
std::vector<edge_id_t> edges_in_mst{};
edges_in_mst.reserve(
graph.edge_count()); // Reserve the upper bound of edges in the mst

std::unordered_set<vertex_id_t> fringe_vertices{start_vertex};

while (fringe_vertices.size() < graph.vertex_count()) {
const auto candidates{detail::find_candidate_edges(graph, fringe_vertices)};

if (candidates.empty()) {
// The graph is not connected
return std::nullopt;
}

std::sort(edges_to_process.begin(), edges_to_process.end(),
[](detail::edge_to_process<E>& e1,
detail::edge_to_process<E>& e2) {
if (e1 != e2)
return e1.get_weight() < e2.get_weight();
return e1.vertex_a < e2.vertex_a || e1.vertex_b < e2.vertex_b;
});

for (const auto& edge : edges_to_process) {
if (detail::do_find_set(edge.vertex_a, parent) !=
detail::do_find_set(edge.vertex_b, parent)) {
mst_edges.push_back({edge.vertex_a, edge.vertex_b});
detail::do_merge_sets(edge.vertex_a, edge.vertex_b, parent, rank);
}
// Found MST E == V - 1
if (mst_edges.size() == graph.vertex_count() - 1)
return mst_edges;
}
// Returns minimum spanning forest
return mst_edges;
const edge_id_t mst_edge{*std::ranges::min_element(
candidates,
[graph](const edge_id_t& lhs, const edge_id_t& rhs) -> bool {
return get_weight(graph.get_edge(lhs)) <
get_weight(graph.get_edge(rhs));
})};

edges_in_mst.emplace_back(mst_edge);
fringe_vertices.insert(mst_edge.second);
}

return edges_in_mst;
}

}; // namespace graaf::algorithm
Loading

0 comments on commit 3c7839a

Please sign in to comment.