Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/graph/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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
)
87 changes: 87 additions & 0 deletions src/graph/bellman_ford.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#ifndef BELLMAN_FORD_HPP
#define BELLMAN_FORD_HPP

#include <cstddef>
#include <limits>
#include <stdexcept>
#include <utility>
#include <vector>

struct BellmanFordEdge {
std::size_t from;
std::size_t to;
long long weight;
};

struct BellmanFordResult {
std::vector<long long> 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<BellmanFordEdge>& 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<long long>::max();
std::vector<long long> 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
108 changes: 108 additions & 0 deletions src/graph/prim.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#ifndef PRIM_HPP
#define PRIM_HPP

#include <cstddef>
#include <functional>
#include <queue>
#include <stdexcept>
#include <tuple>
#include <utility>
#include <vector>

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<PrimTreeEdge> 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<std::vector<PrimEdge>>& 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<long long, std::size_t, std::size_t>;
std::priority_queue<QueueEntry, std::vector<QueueEntry>, std::greater<>> queue;
std::vector<bool> visited(graph.size(), false);
std::vector<PrimTreeEdge> 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
102 changes: 102 additions & 0 deletions src/graph/strongly_connected_components.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#ifndef STRONGLY_CONNECTED_COMPONENTS_HPP
#define STRONGLY_CONNECTED_COMPONENTS_HPP

#include <cstddef>
#include <stdexcept>
#include <utility>
#include <vector>

struct StronglyConnectedComponentsResult {
std::vector<std::size_t> componentOf;
std::vector<std::vector<std::size_t>> 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<std::vector<std::size_t>>& graph) {
std::vector<std::vector<std::size_t>> 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<bool> visited(graph.size(), false);
std::vector<std::size_t> finishOrder;
finishOrder.reserve(graph.size());

for (std::size_t start = 0; start < graph.size(); ++start) {
if (visited[start]) {
continue;
}

std::vector<std::pair<std::size_t, std::size_t>> 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<bool> assigned(graph.size(), false);
std::vector<std::size_t> componentOf(graph.size());
std::vector<std::vector<std::size_t>> 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<std::size_t> component;
std::vector<std::size_t> 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
2 changes: 2 additions & 0 deletions src/search/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
target_sources(clavis_algorithm PRIVATE
binary_search.hpp
exponential_search.hpp
kmp_search.hpp
)
62 changes: 62 additions & 0 deletions src/search/exponential_search.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#ifndef EXPONENTIAL_SEARCH_HPP
#define EXPONENTIAL_SEARCH_HPP

#include <concepts>
#include <cstddef>
#include <optional>
#include <vector>

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 <typename T>
requires std::totally_ordered<T>
[[nodiscard]] std::optional<std::size_t> exponential_search(const std::vector<T>& 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
Loading