From e3162668b3f35849b8987e66d0e9bc2fd8ad5601 Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Thu, 31 Aug 2023 22:07:20 +0300 Subject: [PATCH] feat: Kruskal's Minimum Spanning Tree (#82) --- README.md | 4 +- .../shortest-path/minimum_spanning_tree.md | 60 ++++++ .../algorithm/minimum_spanning_tree.h | 20 ++ .../algorithm/minimum_spanning_tree.tpp | 101 +++++++++ .../algorithm/minimum_spanning_tree_test.cpp | 203 ++++++++++++++++++ 5 files changed, 387 insertions(+), 1 deletion(-) create mode 100644 docs/docs/algorithms/shortest-path/minimum_spanning_tree.md create mode 100644 include/graaflib/algorithm/minimum_spanning_tree.h create mode 100644 include/graaflib/algorithm/minimum_spanning_tree.tpp create mode 100644 test/graaflib/algorithm/minimum_spanning_tree_test.cpp diff --git a/README.md b/README.md index 0e0d5b05..aae2e938 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,9 @@ Algorithms implemented in the Graaf library include the following. For more info - Bellman-Ford 3. [**Cycle Detection Algorithms**](https://bobluppes.github.io/graaf/docs/category/cycle-detection-algorithms): - DFS-Based Cycle Detection -4. [**Strongly Connected Components Algorithms**](https://bobluppes.github.io/graaf/docs/category/strongly-connected-components): +4. **Minimum Spanning Tree (MST) Algorithms** + - Kruskal's Algorithm +5. [**Strongly Connected Components Algorithms**](https://bobluppes.github.io/graaf/docs/category/strongly-connected-components): - Tarjan's Strongly Connected Components # Contributing diff --git a/docs/docs/algorithms/shortest-path/minimum_spanning_tree.md b/docs/docs/algorithms/shortest-path/minimum_spanning_tree.md new file mode 100644 index 00000000..e3d5bfb8 --- /dev/null +++ b/docs/docs/algorithms/shortest-path/minimum_spanning_tree.md @@ -0,0 +1,60 @@ +--- +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. +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. + +[wikipedia](https://en.wikipedia.org/wiki/Kruskal%27s_algorithm) + +## Syntax + +Calculates the shortest path with the minimum edge sum. + +```cpp +template +[[nodiscard]] std::vector kruskal_minimum_spanning_tree( + const graph& graph); +``` + +- **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. + +### Special case +In case of multiply edges with same weight leading to a vertex, prioritizing vertices with lesser vertex number. + +```cpp +std::sort(edges_to_process.begin(), edges_to_process.end(), + [](detail::edge_to_process& e1, + detail::edge_to_process& 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 custom type edge, we should provide < and != operators + +```cpp +struct custom_edge : public graaf::weighted_edge { + public: + int weight_{}; + + [[nodiscard]] int get_weight() const noexcept override { return weight_; } + + custom_edge(int weight): weight_{weight} {}; + custom_edge(){}; + ~custom_edge(){}; + + // Providing '<' and '!=' operators for sorting edges + bool operator<(const custom_edge& e) const noexcept { + return this->weight_ < e.weight_; + } + bool operator!=(const custom_edge& e) const noexcept { + return this->weight_ != e.weight_; + } +}; +``` diff --git a/include/graaflib/algorithm/minimum_spanning_tree.h b/include/graaflib/algorithm/minimum_spanning_tree.h new file mode 100644 index 00000000..e3acfd6a --- /dev/null +++ b/include/graaflib/algorithm/minimum_spanning_tree.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +namespace graaf::algorithm { +/** + * Computes the minimum spanning tree (MST) or minimum spanning forest of a + * graph using Kruskal's algorithm. + * + * @tparam V The vertex type of the graph. + * @tparam E The edge type of the graph. + * @param graph The input graph. + * @return A vector of edges forming the MST or minimum spanning forest. + */ +template +[[nodiscard]] std::vector kruskal_minimum_spanning_tree( + const graph& graph); + +}; // namespace graaf::algorithm +#include "minimum_spanning_tree.tpp" \ No newline at end of file diff --git a/include/graaflib/algorithm/minimum_spanning_tree.tpp b/include/graaflib/algorithm/minimum_spanning_tree.tpp new file mode 100644 index 00000000..f0998918 --- /dev/null +++ b/include/graaflib/algorithm/minimum_spanning_tree.tpp @@ -0,0 +1,101 @@ +#pragma once + +#include +#include +#include + +namespace graaf::algorithm { + +// Disjoint Set Union to maintain sets of vertices +namespace detail { +void do_make_set(vertex_id_t v, + std::unordered_map& parent, + std::unordered_map& rank) { + parent[v] = v; + rank[v] = 0; +} + +vertex_id_t do_find_set(vertex_id_t vertex, + std::unordered_map& 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& parent, + std::unordered_map& 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 +struct edge_to_process : public weighted_edge { + 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 +std::vector kruskal_minimum_spanning_tree( + const graph& graph) { + + // unordered_map in case of deletion of vertices + std::unordered_map rank, parent; + std::vector> edges_to_process{}; + std::vector 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}); + } + + std::sort(edges_to_process.begin(), edges_to_process.end(), + [](detail::edge_to_process& e1, + detail::edge_to_process& 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; +} +}; // namespace graaf::algorithm diff --git a/test/graaflib/algorithm/minimum_spanning_tree_test.cpp b/test/graaflib/algorithm/minimum_spanning_tree_test.cpp new file mode 100644 index 00000000..8c0ee650 --- /dev/null +++ b/test/graaflib/algorithm/minimum_spanning_tree_test.cpp @@ -0,0 +1,203 @@ +#include +#include +#include +#include +#include + +#include +#include + +namespace graaf::algorithm { + +template +class my_weighted_edge : public weighted_edge { + public: + explicit my_weighted_edge(T weight) : weight_{weight} {} + + [[nodiscard]] T get_weight() const noexcept override { return weight_; } + + private: + T weight_{}; +}; + +template +struct MSTTest : public testing::Test { + using edge_t = typename T::second_type; +}; + +using weighted_graph_types = testing::Types< + /** + * Primitive edge type undirected graph + */ + std::pair, int>, + std::pair, unsigned long>, + std::pair, float>, + std::pair, long double>, + + /** + * Non primitive weighted edge type undirected graph + */ + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>, + std::pair>, + my_weighted_edge>>; + +TYPED_TEST_SUITE(MSTTest, weighted_graph_types); + +TYPED_TEST(MSTTest, SparseGraph) { + undirected_graph graph{}; + const auto vertex_1 = graph.add_vertex(1); + const auto vertex_2 = graph.add_vertex(2); + const auto vertex_3 = graph.add_vertex(3); + const auto vertex_4 = graph.add_vertex(4); + + graph.add_edge(vertex_3, vertex_1, 4); + graph.add_edge(vertex_2, vertex_4, 6); + graph.add_edge(vertex_3, vertex_4, 5); + graph.add_edge(vertex_1, vertex_2, 15); + + auto mst = graaf::algorithm::kruskal_minimum_spanning_tree(graph); + std::vector expected_mst{ + {vertex_1, vertex_3}, {vertex_3, vertex_4}, {vertex_2, vertex_4}}; + + ASSERT_EQ(expected_mst, mst); +} + +TYPED_TEST(MSTTest, TwoComponents) { + undirected_graph graph{}; + + const auto vertex_1 = graph.add_vertex(1); + const auto vertex_2 = graph.add_vertex(2); + const auto vertex_3 = graph.add_vertex(3); + const auto vertex_4 = graph.add_vertex(4); + + const auto vertex_5 = graph.add_vertex(5); + const auto vertex_6 = graph.add_vertex(6); + const auto vertex_7 = graph.add_vertex(7); + const auto vertex_8 = graph.add_vertex(8); + const auto vertex_9 = graph.add_vertex(9); + + graph.add_edge(vertex_3, vertex_1, 0.3f); + graph.add_edge(vertex_2, vertex_4, 0.14f); + graph.add_edge(vertex_3, vertex_4, -5.3f); + graph.add_edge(vertex_1, vertex_2, 223.0f); + + graph.add_edge(vertex_5, vertex_6, 2.0f); + graph.add_edge(vertex_7, vertex_8, 4.0f); + graph.add_edge(vertex_9, vertex_8, -23.0f); + graph.add_edge(vertex_6, vertex_9, 3.0f); + graph.add_edge(vertex_8, vertex_6, 0.15f); + + auto mst = graaf::algorithm::kruskal_minimum_spanning_tree(graph); + std::vector expected_mst{ + {vertex_8, vertex_9}, {vertex_3, vertex_4}, {vertex_2, vertex_4}, + {vertex_6, vertex_8}, {vertex_1, vertex_3}, {vertex_5, vertex_6}, + {vertex_7, vertex_8}}; + + ASSERT_EQ(expected_mst, mst); +} + +TYPED_TEST(MSTTest, EqualEdgeWeight) { + undirected_graph graph{}; + const auto vertex_1 = graph.add_vertex(1); + const auto vertex_2 = graph.add_vertex(2); + const auto vertex_3 = graph.add_vertex(3); + const auto vertex_4 = graph.add_vertex(4); + + graph.add_edge(vertex_3, vertex_1, 5); + graph.add_edge(vertex_2, vertex_4, 5); + graph.add_edge(vertex_3, vertex_4, 5); + graph.add_edge(vertex_1, vertex_2, 5); + graph.add_edge(vertex_4, vertex_1, 5); + + auto mst = graaf::algorithm::kruskal_minimum_spanning_tree(graph); + std::vector expected_mst{ + {vertex_1, vertex_2}, {vertex_1, vertex_3}, {vertex_1, vertex_4}}; + + ASSERT_EQ(expected_mst, mst); +} + +TYPED_TEST(MSTTest, CompleteGraph) { + undirected_graph graph{}; + const auto vertex_1 = graph.add_vertex(1); + const auto vertex_2 = graph.add_vertex(2); + const auto vertex_3 = graph.add_vertex(3); + const auto vertex_4 = graph.add_vertex(4); + + graph.add_edge(vertex_1, vertex_2, 35); + graph.add_edge(vertex_1, vertex_3, 4); + graph.add_edge(vertex_1, vertex_4, 45); + graph.add_edge(vertex_2, vertex_4, 6); + graph.add_edge(vertex_2, vertex_3, 55); + graph.add_edge(vertex_3, vertex_4, 5); + + auto mst = graaf::algorithm::kruskal_minimum_spanning_tree(graph); + std::vector expected_mst{ + {vertex_1, vertex_3}, {vertex_3, vertex_4}, {vertex_2, vertex_4}}; + + ASSERT_EQ(expected_mst, mst); +} + +TYPED_TEST(MSTTest, NullGraph) { + undirected_graph graph{}; + + const auto vertex_1 = graph.add_vertex(1); + const auto vertex_2 = graph.add_vertex(2); + const auto vertex_3 = graph.add_vertex(3); + const auto vertex_4 = graph.add_vertex(4); + + auto mst = graaf::algorithm::kruskal_minimum_spanning_tree(graph); + std::vector example_mst{ + {vertex_3, vertex_4}, {vertex_2, vertex_4}, {vertex_1, vertex_3}}; + + ASSERT_NE(example_mst, mst); +} + +struct custom_vertex { + int vertex_{}; + std::string name{}; +}; + +struct custom_edge : public graaf::weighted_edge { + public: + int weight_{}; + + [[nodiscard]] int get_weight() const noexcept override { return weight_; } + + custom_edge(int weight) : weight_{weight} {}; + custom_edge(){}; + ~custom_edge(){}; + + // Providing '<' and '!=' operators for sorting edges + bool operator<(const custom_edge& e) const noexcept { + return this->weight_ < e.weight_; + } + bool operator!=(const custom_edge& e) const noexcept { + return this->weight_ != e.weight_; + } +}; + +TYPED_TEST(MSTTest, CustomEdgeVertexOneComponentGraph) { + undirected_graph graph{}; + + const auto vertex_1 = graph.add_vertex(custom_vertex{1, "Custom vertex 1"}); + const auto vertex_2 = graph.add_vertex(custom_vertex{2, "Custom vertex 2"}); + const auto vertex_3 = graph.add_vertex(custom_vertex{3, "Custom vertex 3"}); + const auto vertex_4 = graph.add_vertex(custom_vertex{4, "Custom vertex 4"}); + + graph.add_edge(vertex_3, vertex_1, 4); + graph.add_edge(vertex_2, vertex_4, 6); + graph.add_edge(vertex_3, vertex_4, 5); + graph.add_edge(vertex_1, vertex_2, 16); + + auto mst = graaf::algorithm::kruskal_minimum_spanning_tree(graph); + std::vector expected_mst{ + {vertex_1, vertex_3}, {vertex_3, vertex_4}, {vertex_2, vertex_4}}; + + ASSERT_EQ(expected_mst, mst); +} +} // namespace graaf::algorithm