Skip to content

Commit 46f54ca

Browse files
committed
feat: implement Kruskal's algorithm and associated tests for Minimum Spanning Tree
feat: add binary search algorithms and comprehensive tests for various data types
1 parent 5aa863d commit 46f54ca

File tree

4 files changed

+555
-0
lines changed

4 files changed

+555
-0
lines changed

src/graph/kruskal.hpp

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
#ifndef KRUSKAL_HPP
2+
#define KRUSKAL_HPP
3+
4+
#include <algorithm>
5+
#include <concepts>
6+
#include <iostream>
7+
#include <vector>
8+
9+
/**
10+
* @brief Edge structure for Kruskal's algorithm
11+
*/
12+
struct KruskalEdge {
13+
int from;
14+
int to;
15+
long long weight;
16+
17+
// Operator for sorting edges by weight
18+
bool operator<(const KruskalEdge& other) const {
19+
return weight < other.weight;
20+
}
21+
};
22+
23+
/**
24+
* @brief Disjoint Set Union (DSU) data structure for Kruskal's algorithm
25+
*/
26+
class DisjointSet {
27+
private:
28+
std::vector<int> parent;
29+
std::vector<int> rank;
30+
31+
public:
32+
/**
33+
* @brief Initialize a DSU with n elements
34+
* @param n Number of elements
35+
*/
36+
DisjointSet(int n) : parent(n), rank(n, 0) {
37+
for (int i = 0; i < n; i++) {
38+
parent[i] = i; // Each element is its own parent initially
39+
}
40+
}
41+
42+
/**
43+
* @brief Find the representative of the set containing x
44+
* @param x Element to find
45+
* @return Representative of the set
46+
*/
47+
int find(int x) {
48+
if (parent[x] != x) {
49+
parent[x] = find(parent[x]); // Path compression
50+
}
51+
return parent[x];
52+
}
53+
54+
/**
55+
* @brief Union of two sets
56+
* @param x First element
57+
* @param y Second element
58+
* @return True if x and y were in different sets, false otherwise
59+
*/
60+
bool unite(int x, int y) {
61+
int rootX = find(x);
62+
int rootY = find(y);
63+
64+
if (rootX == rootY) {
65+
return false; // Already in the same set
66+
}
67+
68+
// Union by rank
69+
if (rank[rootX] < rank[rootY]) {
70+
parent[rootX] = rootY;
71+
} else if (rank[rootX] > rank[rootY]) {
72+
parent[rootY] = rootX;
73+
} else {
74+
parent[rootY] = rootX;
75+
rank[rootX]++;
76+
}
77+
78+
return true;
79+
}
80+
};
81+
82+
/**
83+
* @brief Kruskal's algorithm for finding the Minimum Spanning Tree (MST) of a graph
84+
*
85+
* @param n Number of vertices in the graph
86+
* @param edges Vector of edges in the graph
87+
* @return Vector of edges that form the MST and the total weight of the MST
88+
*/
89+
std::pair<std::vector<KruskalEdge>, long long> kruskal(int n, std::vector<KruskalEdge>& edges) {
90+
// Sort edges by weight
91+
std::sort(edges.begin(), edges.end());
92+
93+
DisjointSet dsu(n);
94+
std::vector<KruskalEdge> mst;
95+
long long totalWeight = 0;
96+
97+
for (const auto& edge : edges) {
98+
if (dsu.unite(edge.from, edge.to)) {
99+
// This edge is part of the MST
100+
mst.push_back(edge);
101+
totalWeight += edge.weight;
102+
103+
// If we have n-1 edges, we have a complete MST
104+
if (mst.size() == n - 1) {
105+
break;
106+
}
107+
}
108+
}
109+
110+
return {mst, totalWeight};
111+
}
112+
113+
/**
114+
* @brief Check if the graph is connected
115+
*
116+
* @param n Number of vertices
117+
* @param mst The MST edges
118+
* @return True if the graph is connected, false otherwise
119+
*/
120+
bool isConnected(int n, const std::vector<KruskalEdge>& mst) {
121+
return mst.size() == n - 1;
122+
}
123+
124+
#endif // KRUSKAL_HPP

src/search/binary_search.hpp

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#ifndef BINARY_SEARCH_HPP
2+
#define BINARY_SEARCH_HPP
3+
4+
#include <concepts>
5+
#include <functional>
6+
#include <vector>
7+
8+
namespace clavis {
9+
namespace search {
10+
11+
/**
12+
* @brief Binary search algorithm for finding an element in a sorted array
13+
*
14+
* @tparam T Type of elements in the array (must be comparable)
15+
* @param arr Sorted array to search in
16+
* @param target Element to search for
17+
* @return Index of the target element if found, -1 otherwise
18+
*/
19+
template <typename T>
20+
requires std::totally_ordered<T>
21+
int binary_search(const std::vector<T>& arr, const T& target) {
22+
int left = 0;
23+
int right = arr.size() - 1;
24+
25+
while (left <= right) {
26+
int mid = left + (right - left) / 2; // Avoid potential overflow
27+
28+
if (arr[mid] == target) {
29+
return mid; // Found the target
30+
} else if (arr[mid] < target) {
31+
left = mid + 1; // Search in the right half
32+
} else {
33+
right = mid - 1; // Search in the left half
34+
}
35+
}
36+
37+
return -1; // Target not found
38+
}
39+
40+
/**
41+
* @brief Lower bound binary search - finds the first element not less than the target
42+
*
43+
* @tparam T Type of elements in the array (must be comparable)
44+
* @param arr Sorted array to search in
45+
* @param target Element to search for
46+
* @return Index of the first element not less than target, or arr.size() if all elements are less than target
47+
*/
48+
template <typename T>
49+
requires std::totally_ordered<T>
50+
int lower_bound(const std::vector<T>& arr, const T& target) {
51+
int left = 0;
52+
int right = arr.size(); // Note: right is arr.size(), not arr.size() - 1
53+
54+
while (left < right) {
55+
int mid = left + (right - left) / 2;
56+
57+
if (arr[mid] < target) {
58+
left = mid + 1;
59+
} else {
60+
right = mid;
61+
}
62+
}
63+
64+
return left; // Returns arr.size() if all elements are less than target
65+
}
66+
67+
/**
68+
* @brief Upper bound binary search - finds the first element greater than the target
69+
*
70+
* @tparam T Type of elements in the array (must be comparable)
71+
* @param arr Sorted array to search in
72+
* @param target Element to search for
73+
* @return Index of the first element greater than target, or arr.size() if no such element exists
74+
*/
75+
template <typename T>
76+
requires std::totally_ordered<T>
77+
int upper_bound(const std::vector<T>& arr, const T& target) {
78+
int left = 0;
79+
int right = arr.size(); // Note: right is arr.size(), not arr.size() - 1
80+
81+
while (left < right) {
82+
int mid = left + (right - left) / 2;
83+
84+
if (arr[mid] <= target) {
85+
left = mid + 1;
86+
} else {
87+
right = mid;
88+
}
89+
}
90+
91+
return left; // Returns arr.size() if all elements are less than or equal to target
92+
}
93+
94+
/**
95+
* @brief Binary search on a predicate function
96+
*
97+
* @tparam T Type of elements in the search space
98+
* @param left Left boundary of the search space (inclusive)
99+
* @param right Right boundary of the search space (exclusive)
100+
* @param predicate Function that returns true for elements that satisfy the condition
101+
* @return The first element in the range [left, right) for which predicate returns true, or right if none exists
102+
*/
103+
template <typename T>
104+
requires std::integral<T>
105+
T binary_search_predicate(T left, T right, const std::function<bool(T)>& predicate) {
106+
while (left < right) {
107+
T mid = left + (right - left) / 2;
108+
109+
if (predicate(mid)) {
110+
right = mid;
111+
} else {
112+
left = mid + 1;
113+
}
114+
}
115+
116+
return left; // Returns right if no element satisfies the predicate
117+
}
118+
119+
} // namespace search
120+
} // namespace clavis
121+
122+
#endif // BINARY_SEARCH_HPP

tests/graph/kruskal_test.cpp

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
#include "../src/graph/kruskal.hpp"
2+
3+
#include <gtest/gtest.h>
4+
5+
/**
6+
* @brief Test fixture for Kruskal's Algorithm
7+
*/
8+
class KruskalTest : public ::testing::Test {
9+
protected:
10+
};
11+
12+
/**
13+
* @test Basic test for small graph
14+
*/
15+
TEST_F(KruskalTest, SmallGraph) {
16+
// Create a small graph with 4 vertices
17+
// 0 -- (1, weight=1) -- 1
18+
// | |
19+
// (2, weight=2) (4, weight=4)
20+
// | |
21+
// 2 -- (3, weight=3) -- 3
22+
std::vector<KruskalEdge> edges = {
23+
{0, 1, 1}, // Edge 0-1 with weight 1
24+
{0, 2, 2}, // Edge 0-2 with weight 2
25+
{2, 3, 3}, // Edge 2-3 with weight 3
26+
{1, 3, 4}, // Edge 1-3 with weight 4
27+
};
28+
29+
auto [mst, totalWeight] = kruskal(4, edges);
30+
31+
// The MST should include edges 0-1, 0-2, and 2-3
32+
// Total weight should be 1 + 2 + 3 = 6
33+
EXPECT_EQ(mst.size(), 3);
34+
EXPECT_EQ(totalWeight, 6);
35+
36+
// Check if the graph is connected
37+
EXPECT_TRUE(isConnected(4, mst));
38+
39+
// Verify the edges in the MST
40+
// The first edge should be 0-1 with weight 1
41+
EXPECT_EQ(mst[0].from, 0);
42+
EXPECT_EQ(mst[0].to, 1);
43+
EXPECT_EQ(mst[0].weight, 1);
44+
45+
// The second edge should be 0-2 with weight 2
46+
EXPECT_EQ(mst[1].from, 0);
47+
EXPECT_EQ(mst[1].to, 2);
48+
EXPECT_EQ(mst[1].weight, 2);
49+
50+
// The third edge should be 2-3 with weight 3
51+
EXPECT_EQ(mst[2].from, 2);
52+
EXPECT_EQ(mst[2].to, 3);
53+
EXPECT_EQ(mst[2].weight, 3);
54+
}
55+
56+
/**
57+
* @test Test with a disconnected graph
58+
*/
59+
TEST_F(KruskalTest, DisconnectedGraph) {
60+
// Create a disconnected graph with 5 vertices
61+
// Component 1: 0 -- (1, weight=1) -- 1
62+
// Component 2: 2 -- (3, weight=3) -- 3 -- (4, weight=4) -- 4
63+
std::vector<KruskalEdge> edges = {
64+
{0, 1, 1}, // Edge 0-1 with weight 1
65+
{2, 3, 3}, // Edge 2-3 with weight 3
66+
{3, 4, 4}, // Edge 3-4 with weight 4
67+
};
68+
69+
auto [mst, totalWeight] = kruskal(5, edges);
70+
71+
// The MST should include all 3 edges
72+
// Total weight should be 1 + 3 + 4 = 8
73+
EXPECT_EQ(mst.size(), 3);
74+
EXPECT_EQ(totalWeight, 8);
75+
76+
// Check if the graph is disconnected
77+
EXPECT_FALSE(isConnected(5, mst));
78+
}
79+
80+
/**
81+
* @test Test with a cycle in the graph
82+
*/
83+
TEST_F(KruskalTest, CycleGraph) {
84+
// Create a graph with a cycle
85+
// 0 -- (1, weight=1) -- 1
86+
// | |
87+
// (4, weight=4) (2, weight=2)
88+
// | |
89+
// 3 -- (3, weight=3) -- 2
90+
std::vector<KruskalEdge> edges = {
91+
{0, 1, 1}, // Edge 0-1 with weight 1
92+
{1, 2, 2}, // Edge 1-2 with weight 2
93+
{2, 3, 3}, // Edge 2-3 with weight 3
94+
{0, 3, 4}, // Edge 0-3 with weight 4
95+
};
96+
97+
auto [mst, totalWeight] = kruskal(4, edges);
98+
99+
// The MST should include edges 0-1, 1-2, and 2-3
100+
// Total weight should be 1 + 2 + 3 = 6
101+
EXPECT_EQ(mst.size(), 3);
102+
EXPECT_EQ(totalWeight, 6);
103+
104+
// Check if the graph is connected
105+
EXPECT_TRUE(isConnected(4, mst));
106+
107+
// The edge 0-3 should not be in the MST because it creates a cycle
108+
bool hasEdge03 = false;
109+
for (const auto& edge : mst) {
110+
if ((edge.from == 0 && edge.to == 3) || (edge.from == 3 && edge.to == 0)) {
111+
hasEdge03 = true;
112+
break;
113+
}
114+
}
115+
EXPECT_FALSE(hasEdge03);
116+
}
117+
118+
/**
119+
* @test Test with multiple possible MSTs
120+
*/
121+
TEST_F(KruskalTest, MultipleMSTs) {
122+
// Create a graph where multiple MSTs are possible
123+
// 0 -- (1, weight=1) -- 1
124+
// | |
125+
// (1, weight=1) (1, weight=1)
126+
// | |
127+
// 2 -- (1, weight=1) -- 3
128+
std::vector<KruskalEdge> edges = {
129+
{0, 1, 1}, // Edge 0-1 with weight 1
130+
{0, 2, 1}, // Edge 0-2 with weight 1
131+
{1, 3, 1}, // Edge 1-3 with weight 1
132+
{2, 3, 1}, // Edge 2-3 with weight 1
133+
};
134+
135+
auto [mst, totalWeight] = kruskal(4, edges);
136+
137+
// The MST should include 3 edges, all with weight 1
138+
// Total weight should be 3
139+
EXPECT_EQ(mst.size(), 3);
140+
EXPECT_EQ(totalWeight, 3);
141+
142+
// Check if the graph is connected
143+
EXPECT_TRUE(isConnected(4, mst));
144+
}

0 commit comments

Comments
 (0)