diff --git a/src/graph/CMakeLists.txt b/src/graph/CMakeLists.txt index 46cc78c..71c323e 100644 --- a/src/graph/CMakeLists.txt +++ b/src/graph/CMakeLists.txt @@ -1,7 +1,10 @@ target_sources(clavis_algorithm PRIVATE + bellman_ford.hpp bfs.hpp dfs.hpp dijkstra.hpp floyd_warshall.hpp kruskal.hpp + prim.hpp + strongly_connected_components.hpp ) diff --git a/src/graph/bellman_ford.hpp b/src/graph/bellman_ford.hpp new file mode 100644 index 0000000..089094a --- /dev/null +++ b/src/graph/bellman_ford.hpp @@ -0,0 +1,87 @@ +#ifndef BELLMAN_FORD_HPP +#define BELLMAN_FORD_HPP + +#include +#include +#include +#include +#include + +struct BellmanFordEdge { + std::size_t from; + std::size_t to; + long long weight; +}; + +struct BellmanFordResult { + std::vector distances; + bool hasNegativeCycle; +}; + +/** + * @brief Computes single-source shortest paths in a directed weighted graph. + * + * Edge weights may be negative. A negative cycle is reported only when it is + * reachable from the source vertex. + * + * @param vertexCount Number of vertices in the graph. + * @param edges Directed weighted edges. + * @param source Source vertex. + * @return Shortest distances and whether a reachable negative cycle exists. + * @throws std::out_of_range If the source or an edge endpoint is invalid. + * + * @note Finite path costs must fit in a long long. + * @complexity O(VE) time and O(V) additional space. + */ +[[nodiscard]] inline BellmanFordResult bellmanFord(std::size_t vertexCount, + const std::vector& edges, + std::size_t source) { + if (source >= vertexCount) { + throw std::out_of_range("Source vertex is out of range"); + } + + for (const auto& edge : edges) { + if (edge.from >= vertexCount || edge.to >= vertexCount) { + throw std::out_of_range("Edge endpoint is out of range"); + } + } + + constexpr long long infinity = std::numeric_limits::max(); + std::vector distances(vertexCount, infinity); + distances[source] = 0; + + for (std::size_t pass = 1; pass < vertexCount; ++pass) { + bool updated = false; + for (const auto& edge : edges) { + if (distances[edge.from] == infinity) { + continue; + } + + const long long candidate = distances[edge.from] + edge.weight; + if (candidate < distances[edge.to]) { + distances[edge.to] = candidate; + updated = true; + } + } + + if (!updated) { + break; + } + } + + bool hasNegativeCycle = false; + for (const auto& edge : edges) { + if (distances[edge.from] != infinity && + distances[edge.from] + edge.weight < distances[edge.to]) { + hasNegativeCycle = true; + break; + } + } + + return { + .distances = std::move(distances), + .hasNegativeCycle = hasNegativeCycle, + }; +} + +#endif // BELLMAN_FORD_HPP diff --git a/src/graph/prim.hpp b/src/graph/prim.hpp new file mode 100644 index 0000000..651ab2c --- /dev/null +++ b/src/graph/prim.hpp @@ -0,0 +1,108 @@ +#ifndef PRIM_HPP +#define PRIM_HPP + +#include +#include +#include +#include +#include +#include +#include + +struct PrimEdge { + std::size_t to; + long long weight; +}; + +struct PrimTreeEdge { + std::size_t from; + std::size_t to; + long long weight; +}; + +struct PrimResult { + std::vector edges; + long long totalWeight; + bool isConnected; +}; + +/** + * @brief Computes a minimum spanning tree using Prim's algorithm. + * + * The graph must be undirected, with each edge present in both adjacency + * lists. For a disconnected graph, the result contains the tree for the + * connected component containing the start vertex. + * + * @param graph Undirected weighted graph represented as an adjacency list. + * @param start Vertex from which to grow the tree. + * @return Tree edges, their total weight, and whether every vertex was reached. + * @throws std::out_of_range If the start vertex or an edge endpoint is invalid. + * + * @note The total tree weight must fit in a long long. + * @complexity O(E log E) time and O(V + E) additional space. + */ +[[nodiscard]] inline PrimResult prim(const std::vector>& graph, + std::size_t start = 0) { + if (graph.empty()) { + return { + .edges = {}, + .totalWeight = 0, + .isConnected = true, + }; + } + if (start >= graph.size()) { + throw std::out_of_range("Start vertex is out of range"); + } + + for (const auto& adjacentEdges : graph) { + for (const auto& edge : adjacentEdges) { + if (edge.to >= graph.size()) { + throw std::out_of_range("Edge endpoint is out of range"); + } + } + } + + using QueueEntry = std::tuple; + std::priority_queue, std::greater<>> queue; + std::vector visited(graph.size(), false); + std::vector treeEdges; + long long totalWeight = 0; + std::size_t visitedCount = 1; + + visited[start] = true; + for (const auto& edge : graph[start]) { + queue.emplace(edge.weight, start, edge.to); + } + + while (!queue.empty() && visitedCount < graph.size()) { + const auto [weight, from, to] = queue.top(); + queue.pop(); + + if (visited[to]) { + continue; + } + + visited[to] = true; + ++visitedCount; + treeEdges.push_back({ + .from = from, + .to = to, + .weight = weight, + }); + totalWeight += weight; + + for (const auto& edge : graph[to]) { + if (!visited[edge.to]) { + queue.emplace(edge.weight, to, edge.to); + } + } + } + + return { + .edges = std::move(treeEdges), + .totalWeight = totalWeight, + .isConnected = visitedCount == graph.size(), + }; +} + +#endif // PRIM_HPP diff --git a/src/graph/strongly_connected_components.hpp b/src/graph/strongly_connected_components.hpp new file mode 100644 index 0000000..9383f1c --- /dev/null +++ b/src/graph/strongly_connected_components.hpp @@ -0,0 +1,102 @@ +#ifndef STRONGLY_CONNECTED_COMPONENTS_HPP +#define STRONGLY_CONNECTED_COMPONENTS_HPP + +#include +#include +#include +#include + +struct StronglyConnectedComponentsResult { + std::vector componentOf; + std::vector> components; +}; + +/** + * @brief Partitions a directed graph using the Kosaraju-Sharir algorithm. + * + * @param graph Directed graph represented as an adjacency list. + * @return A component ID for every vertex and the vertices in each component. + * @throws std::out_of_range If an edge endpoint is invalid. + * + * @complexity O(V + E) time and O(V + E) additional space. + */ +[[nodiscard]] inline StronglyConnectedComponentsResult stronglyConnectedComponents( + const std::vector>& graph) { + std::vector> reversedGraph(graph.size()); + for (std::size_t from = 0; from < graph.size(); ++from) { + for (const std::size_t to : graph[from]) { + if (to >= graph.size()) { + throw std::out_of_range("Edge endpoint is out of range"); + } + reversedGraph[to].push_back(from); + } + } + + std::vector visited(graph.size(), false); + std::vector finishOrder; + finishOrder.reserve(graph.size()); + + for (std::size_t start = 0; start < graph.size(); ++start) { + if (visited[start]) { + continue; + } + + std::vector> stack; + stack.emplace_back(start, 0); + visited[start] = true; + + while (!stack.empty()) { + auto& [vertex, nextNeighbor] = stack.back(); + if (nextNeighbor < graph[vertex].size()) { + const std::size_t neighbor = graph[vertex][nextNeighbor]; + ++nextNeighbor; + if (!visited[neighbor]) { + visited[neighbor] = true; + stack.emplace_back(neighbor, 0); + } + } else { + finishOrder.push_back(vertex); + stack.pop_back(); + } + } + } + + std::vector assigned(graph.size(), false); + std::vector componentOf(graph.size()); + std::vector> components; + + for (std::size_t index = finishOrder.size(); index > 0; --index) { + const std::size_t root = finishOrder[index - 1]; + if (assigned[root]) { + continue; + } + + const std::size_t componentId = components.size(); + std::vector component; + std::vector stack = {root}; + assigned[root] = true; + + while (!stack.empty()) { + const std::size_t vertex = stack.back(); + stack.pop_back(); + componentOf[vertex] = componentId; + component.push_back(vertex); + + for (const std::size_t neighbor : reversedGraph[vertex]) { + if (!assigned[neighbor]) { + assigned[neighbor] = true; + stack.push_back(neighbor); + } + } + } + + components.push_back(std::move(component)); + } + + return { + .componentOf = std::move(componentOf), + .components = std::move(components), + }; +} + +#endif // STRONGLY_CONNECTED_COMPONENTS_HPP diff --git a/src/search/CMakeLists.txt b/src/search/CMakeLists.txt index 5ee0cbc..8037e41 100644 --- a/src/search/CMakeLists.txt +++ b/src/search/CMakeLists.txt @@ -1,3 +1,5 @@ target_sources(clavis_algorithm PRIVATE binary_search.hpp + exponential_search.hpp + kmp_search.hpp ) diff --git a/src/search/exponential_search.hpp b/src/search/exponential_search.hpp new file mode 100644 index 0000000..5bc6ae2 --- /dev/null +++ b/src/search/exponential_search.hpp @@ -0,0 +1,62 @@ +#ifndef EXPONENTIAL_SEARCH_HPP +#define EXPONENTIAL_SEARCH_HPP + +#include +#include +#include +#include + +namespace clavis::search { + +/** + * @brief Finds the first occurrence of a value using exponential search. + * + * The search range grows exponentially from the beginning of the sorted + * sequence, then a binary search locates the first matching value. + * + * @tparam T A totally ordered value type. + * @param values Values sorted in ascending order. + * @param target Value to locate. + * @return The first matching position, or std::nullopt if the value is absent. + * + * @complexity O(log(i + 1)) time for a match at position i, O(log N) worst case, + * and O(1) additional space. + */ +template + requires std::totally_ordered +[[nodiscard]] std::optional exponential_search(const std::vector& values, + const T& target) { + if (values.empty()) { + return std::nullopt; + } + + std::size_t bound = 1; + while (bound < values.size() && values[bound] < target) { + if (bound > values.size() / 2) { + bound = values.size(); + break; + } + bound *= 2; + } + + std::size_t left = bound / 2; + std::size_t right = bound < values.size() ? bound + 1 : values.size(); + + while (left < right) { + const std::size_t middle = left + (right - left) / 2; + if (values[middle] < target) { + left = middle + 1; + } else { + right = middle; + } + } + + if (left < values.size() && values[left] == target) { + return left; + } + return std::nullopt; +} + +} // namespace clavis::search + +#endif // EXPONENTIAL_SEARCH_HPP diff --git a/src/search/kmp_search.hpp b/src/search/kmp_search.hpp new file mode 100644 index 0000000..183839c --- /dev/null +++ b/src/search/kmp_search.hpp @@ -0,0 +1,68 @@ +#ifndef KMP_SEARCH_HPP +#define KMP_SEARCH_HPP + +#include +#include +#include +#include + +namespace clavis::search { + +namespace detail { + +[[nodiscard]] inline std::vector buildKmpPrefixTable(std::string_view pattern) { + std::vector prefixTable(pattern.size(), 0); + std::size_t matched = 0; + + for (std::size_t index = 1; index < pattern.size(); ++index) { + while (matched > 0 && pattern[index] != pattern[matched]) { + matched = prefixTable[matched - 1]; + } + if (pattern[index] == pattern[matched]) { + ++matched; + } + prefixTable[index] = matched; + } + + return prefixTable; +} + +} // namespace detail + +/** + * @brief Finds the first occurrence of a pattern using Knuth-Morris-Pratt search. + * + * @param text Text to search. + * @param pattern Pattern to locate. + * @return The starting position of the first match, or std::nullopt if absent. + * + * @note An empty pattern matches at position 0. + * @complexity O(N + M) time and O(M) additional space. + */ +[[nodiscard]] inline std::optional kmp_search(std::string_view text, + std::string_view pattern) { + if (pattern.empty()) { + return 0; + } + + const std::vector prefixTable = detail::buildKmpPrefixTable(pattern); + std::size_t matched = 0; + + for (std::size_t index = 0; index < text.size(); ++index) { + while (matched > 0 && text[index] != pattern[matched]) { + matched = prefixTable[matched - 1]; + } + if (text[index] == pattern[matched]) { + ++matched; + } + if (matched == pattern.size()) { + return index + 1 - pattern.size(); + } + } + + return std::nullopt; +} + +} // namespace clavis::search + +#endif // KMP_SEARCH_HPP diff --git a/tests/graph/CMakeLists.txt b/tests/graph/CMakeLists.txt index e0b8e20..97c3575 100644 --- a/tests/graph/CMakeLists.txt +++ b/tests/graph/CMakeLists.txt @@ -1,7 +1,10 @@ target_sources(clavis_algorithm_test PRIVATE + bellman_ford_test.cpp bfs_test.cpp dfs_test.cpp dijkstra_test.cpp floyd_warshall_test.cpp kruskal_test.cpp + prim_test.cpp + strongly_connected_components_test.cpp ) diff --git a/tests/graph/bellman_ford_test.cpp b/tests/graph/bellman_ford_test.cpp new file mode 100644 index 0000000..703b849 --- /dev/null +++ b/tests/graph/bellman_ford_test.cpp @@ -0,0 +1,65 @@ +#include "../src/graph/bellman_ford.hpp" + +#include + +#include +#include + +TEST(BellmanFordTest, ComputesShortestPathsWithNegativeEdges) { + const std::vector edges = { + {0, 1, 6}, {0, 2, 7}, {1, 2, 8}, {1, 3, 5}, {1, 4, -4}, + {2, 3, -3}, {2, 4, 9}, {3, 1, -2}, {4, 0, 2}, {4, 3, 7}, + }; + + const BellmanFordResult result = bellmanFord(5, edges, 0); + + EXPECT_FALSE(result.hasNegativeCycle); + EXPECT_EQ(result.distances, (std::vector{0, 2, 7, 4, -2})); +} + +TEST(BellmanFordTest, LeavesUnreachableVerticesAtInfinity) { + const std::vector edges = {{0, 1, 3}}; + + const BellmanFordResult result = bellmanFord(3, edges, 0); + + EXPECT_FALSE(result.hasNegativeCycle); + EXPECT_EQ(result.distances[0], 0); + EXPECT_EQ(result.distances[1], 3); + EXPECT_EQ(result.distances[2], std::numeric_limits::max()); +} + +TEST(BellmanFordTest, DetectsReachableNegativeCycle) { + const std::vector edges = { + {0, 1, 1}, + {1, 2, -2}, + {2, 1, -2}, + }; + + const BellmanFordResult result = bellmanFord(3, edges, 0); + + EXPECT_TRUE(result.hasNegativeCycle); +} + +TEST(BellmanFordTest, IgnoresUnreachableNegativeCycle) { + const std::vector edges = { + {0, 1, 2}, + {2, 3, -1}, + {3, 2, -1}, + }; + + const BellmanFordResult result = bellmanFord(4, edges, 0); + + EXPECT_FALSE(result.hasNegativeCycle); +} + +TEST(BellmanFordTest, RejectsInvalidSource) { + const std::vector edges; + + EXPECT_THROW((void)bellmanFord(3, edges, 3), std::out_of_range); +} + +TEST(BellmanFordTest, RejectsInvalidEdgeEndpoint) { + const std::vector edges = {{0, 3, 1}}; + + EXPECT_THROW((void)bellmanFord(3, edges, 0), std::out_of_range); +} diff --git a/tests/graph/prim_test.cpp b/tests/graph/prim_test.cpp new file mode 100644 index 0000000..7b7eceb --- /dev/null +++ b/tests/graph/prim_test.cpp @@ -0,0 +1,89 @@ +#include "../src/graph/prim.hpp" + +#include + +#include + +namespace { + +void addUndirectedEdge(std::vector>& graph, std::size_t first, + std::size_t second, long long weight) { + graph[first].push_back({second, weight}); + graph[second].push_back({first, weight}); +} + +} // namespace + +TEST(PrimTest, ComputesMinimumSpanningTree) { + std::vector> graph(4); + addUndirectedEdge(graph, 0, 1, 1); + addUndirectedEdge(graph, 0, 2, 4); + addUndirectedEdge(graph, 1, 2, 2); + addUndirectedEdge(graph, 1, 3, 5); + addUndirectedEdge(graph, 2, 3, 3); + + const PrimResult result = prim(graph); + + EXPECT_TRUE(result.isConnected); + EXPECT_EQ(result.edges.size(), 3); + EXPECT_EQ(result.totalWeight, 6); +} + +TEST(PrimTest, SupportsNegativeWeights) { + std::vector> graph(3); + addUndirectedEdge(graph, 0, 1, -2); + addUndirectedEdge(graph, 1, 2, 1); + addUndirectedEdge(graph, 0, 2, 4); + + const PrimResult result = prim(graph); + + EXPECT_TRUE(result.isConnected); + EXPECT_EQ(result.edges.size(), 2); + EXPECT_EQ(result.totalWeight, -1); +} + +TEST(PrimTest, ReportsDisconnectedGraph) { + std::vector> graph(4); + addUndirectedEdge(graph, 0, 1, 2); + addUndirectedEdge(graph, 2, 3, 1); + + const PrimResult result = prim(graph); + + EXPECT_FALSE(result.isConnected); + EXPECT_EQ(result.edges.size(), 1); + EXPECT_EQ(result.totalWeight, 2); +} + +TEST(PrimTest, SupportsDifferentStartVertex) { + std::vector> graph(3); + addUndirectedEdge(graph, 0, 1, 3); + addUndirectedEdge(graph, 1, 2, 1); + addUndirectedEdge(graph, 0, 2, 2); + + const PrimResult result = prim(graph, 2); + + EXPECT_TRUE(result.isConnected); + EXPECT_EQ(result.totalWeight, 3); +} + +TEST(PrimTest, AcceptsEmptyGraph) { + const std::vector> graph; + + const PrimResult result = prim(graph); + + EXPECT_TRUE(result.isConnected); + EXPECT_TRUE(result.edges.empty()); + EXPECT_EQ(result.totalWeight, 0); +} + +TEST(PrimTest, RejectsInvalidStartVertex) { + const std::vector> graph(2); + + EXPECT_THROW((void)prim(graph, 2), std::out_of_range); +} + +TEST(PrimTest, RejectsInvalidEdgeEndpoint) { + const std::vector> graph = {{{2, 1}}, {}}; + + EXPECT_THROW((void)prim(graph), std::out_of_range); +} diff --git a/tests/graph/strongly_connected_components_test.cpp b/tests/graph/strongly_connected_components_test.cpp new file mode 100644 index 0000000..64cedcb --- /dev/null +++ b/tests/graph/strongly_connected_components_test.cpp @@ -0,0 +1,78 @@ +#include "../src/graph/strongly_connected_components.hpp" + +#include + +#include +#include + +TEST(StronglyConnectedComponentsTest, PartitionsDirectedGraph) { + const std::vector> graph = { + {1}, {2}, {0, 3}, {4}, {3, 5}, {}, + }; + + const StronglyConnectedComponentsResult result = stronglyConnectedComponents(graph); + + ASSERT_EQ(result.components.size(), 3); + EXPECT_EQ(result.componentOf[0], result.componentOf[1]); + EXPECT_EQ(result.componentOf[1], result.componentOf[2]); + EXPECT_EQ(result.componentOf[3], result.componentOf[4]); + EXPECT_NE(result.componentOf[2], result.componentOf[3]); + EXPECT_NE(result.componentOf[4], result.componentOf[5]); + + std::vector componentSizes; + for (const auto& component : result.components) { + componentSizes.push_back(component.size()); + } + std::ranges::sort(componentSizes); + EXPECT_EQ(componentSizes, (std::vector{1, 2, 3})); +} + +TEST(StronglyConnectedComponentsTest, PlacesDagVerticesInSeparateComponents) { + const std::vector> graph = { + {1}, + {2}, + {}, + }; + + const StronglyConnectedComponentsResult result = stronglyConnectedComponents(graph); + + EXPECT_EQ(result.components.size(), 3); + EXPECT_NE(result.componentOf[0], result.componentOf[1]); + EXPECT_NE(result.componentOf[1], result.componentOf[2]); +} + +TEST(StronglyConnectedComponentsTest, FindsSingleStrongComponent) { + const std::vector> graph = { + {1}, + {2}, + {0}, + }; + + const StronglyConnectedComponentsResult result = stronglyConnectedComponents(graph); + + ASSERT_EQ(result.components.size(), 1); + EXPECT_EQ(result.components.front().size(), 3); +} + +TEST(StronglyConnectedComponentsTest, HandlesIsolatedVertices) { + const std::vector> graph(4); + + const StronglyConnectedComponentsResult result = stronglyConnectedComponents(graph); + + EXPECT_EQ(result.components.size(), 4); +} + +TEST(StronglyConnectedComponentsTest, AcceptsEmptyGraph) { + const std::vector> graph; + + const StronglyConnectedComponentsResult result = stronglyConnectedComponents(graph); + + EXPECT_TRUE(result.componentOf.empty()); + EXPECT_TRUE(result.components.empty()); +} + +TEST(StronglyConnectedComponentsTest, RejectsInvalidEdgeEndpoint) { + const std::vector> graph = {{1}, {2}}; + + EXPECT_THROW((void)stronglyConnectedComponents(graph), std::out_of_range); +} diff --git a/tests/search/CMakeLists.txt b/tests/search/CMakeLists.txt index 5d59c05..3b13b68 100644 --- a/tests/search/CMakeLists.txt +++ b/tests/search/CMakeLists.txt @@ -1,3 +1,5 @@ target_sources(clavis_algorithm_test PRIVATE binary_search_test.cpp + exponential_search_test.cpp + kmp_search_test.cpp ) diff --git a/tests/search/exponential_search_test.cpp b/tests/search/exponential_search_test.cpp new file mode 100644 index 0000000..96c9d07 --- /dev/null +++ b/tests/search/exponential_search_test.cpp @@ -0,0 +1,60 @@ +#include "../src/search/exponential_search.hpp" + +#include + +#include +#include +#include +#include + +TEST(ExponentialSearchTest, FindsValueAtBeginning) { + const std::vector values = {1, 3, 5, 7, 9}; + + EXPECT_EQ(clavis::search::exponential_search(values, 1), std::optional{0}); +} + +TEST(ExponentialSearchTest, FindsValueInMiddle) { + const std::vector values = {1, 3, 5, 7, 9, 11, 13, 15, 17}; + + EXPECT_EQ(clavis::search::exponential_search(values, 9), std::optional{4}); +} + +TEST(ExponentialSearchTest, FindsValueAtEnd) { + const std::vector values = {1, 3, 5, 7, 9}; + + EXPECT_EQ(clavis::search::exponential_search(values, 9), std::optional{4}); +} + +TEST(ExponentialSearchTest, ReturnsFirstDuplicate) { + const std::vector values = {1, 2, 2, 2, 3}; + + EXPECT_EQ(clavis::search::exponential_search(values, 2), std::optional{1}); +} + +TEST(ExponentialSearchTest, ReturnsNulloptWhenValueIsAbsent) { + const std::vector values = {1, 3, 5, 7, 9}; + + EXPECT_EQ(clavis::search::exponential_search(values, 0), std::nullopt); + EXPECT_EQ(clavis::search::exponential_search(values, 6), std::nullopt); + EXPECT_EQ(clavis::search::exponential_search(values, 10), std::nullopt); +} + +TEST(ExponentialSearchTest, HandlesSingleValue) { + const std::vector values = {42}; + + EXPECT_EQ(clavis::search::exponential_search(values, 42), std::optional{0}); + EXPECT_EQ(clavis::search::exponential_search(values, 7), std::nullopt); +} + +TEST(ExponentialSearchTest, HandlesEmptySequence) { + const std::vector values; + + EXPECT_EQ(clavis::search::exponential_search(values, 1), std::nullopt); +} + +TEST(ExponentialSearchTest, SupportsStrings) { + const std::vector values = {"ant", "bee", "cat", "dog"}; + const std::string target = "dog"; + + EXPECT_EQ(clavis::search::exponential_search(values, target), std::optional{3}); +} diff --git a/tests/search/kmp_search_test.cpp b/tests/search/kmp_search_test.cpp new file mode 100644 index 0000000..159f48f --- /dev/null +++ b/tests/search/kmp_search_test.cpp @@ -0,0 +1,57 @@ +#include "../src/search/kmp_search.hpp" + +#include + +#include +#include + +TEST(KmpSearchTest, FindsPatternAtBeginning) { + const std::optional result = clavis::search::kmp_search("algorithm", "algo"); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 0); +} + +TEST(KmpSearchTest, FindsPatternInMiddle) { + const std::optional result = + clavis::search::kmp_search("the quick brown fox", "quick"); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 4); +} + +TEST(KmpSearchTest, FindsPatternAtEnd) { + const std::optional result = clavis::search::kmp_search("searching", "ing"); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 6); +} + +TEST(KmpSearchTest, ReturnsFirstOccurrence) { + const std::optional result = clavis::search::kmp_search("aaaaa", "aaa"); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 0); +} + +TEST(KmpSearchTest, HandlesPrefixFallback) { + const std::optional result = clavis::search::kmp_search("abababac", "ababac"); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 2); +} + +TEST(KmpSearchTest, ReturnsNulloptWhenPatternIsAbsent) { + EXPECT_EQ(clavis::search::kmp_search("algorithm", "rhythm"), std::nullopt); +} + +TEST(KmpSearchTest, EmptyPatternMatchesAtBeginning) { + const std::optional result = clavis::search::kmp_search("algorithm", ""); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 0); +} + +TEST(KmpSearchTest, NonEmptyPatternDoesNotMatchEmptyText) { + EXPECT_EQ(clavis::search::kmp_search("", "pattern"), std::nullopt); +}