diff --git a/BUILD.bazel b/BUILD.bazel index ebb68f63..e7c4876c 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -346,6 +346,21 @@ cc_library( ) +cc_library( + name = "fixed_graph", + hdrs = ["include/fixed_containers/fixed_graph.hpp"], + includes = includes_config(), + strip_include_prefix = strip_include_prefix_config(), + deps = [ + ":concepts", + ":fixed_map", + ":fixed_vector", + ":preconditions", + ":source_location", + ], + copts = ["-std=c++20"], +) + cc_library( name = "fixed_doubly_linked_list", hdrs = ["include/fixed_containers/fixed_doubly_linked_list.hpp"], @@ -1364,6 +1379,17 @@ cc_test( ) +cc_test( + name = "fixed_graph_test", + srcs = ["test/fixed_graph_test.cpp"], + deps = [ + ":fixed_graph", + "@com_google_googletest//:gtest", + "@com_google_googletest//:gtest_main", + ], + copts = ["-std=c++20"], +) + cc_test( name = "fixed_doubly_linked_list_test", srcs = ["test/fixed_doubly_linked_list_test.cpp"], diff --git a/README.md b/README.md index d25f1764..ee1fc2ad 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ The fixed-container types have identical APIs to their std:: equivalents, so you | `FixedCircularQueue` | `std::queue` API with Circular Buffer semantics | | `FixedBitset` | `std::bitset` | | `FixedString` | `std::string` | + | `FixedGraph` | (no direct `std::` equivalent) | | `FixedMap` | `std::map` | | `FixedSet` | `std::set` | | `FixedUnorderedMap` | `std::unordered_map` | @@ -273,6 +274,63 @@ More examples can be found [here](test/enums_test_common.hpp). static_assert(s1.max_size() == 11); ``` +- FixedGraph + ```C++ + #include "fixed_containers/fixed_graph.hpp" + using namespace fixed_containers; + + // Directed, unweighted graph with capacity for 8 nodes, up to 6 outgoing edges each + using Graph = FixedGraph; // + + constexpr auto g = []() { + Graph gr{}; + auto a = gr.add_node(0); + auto b = gr.add_node(1); + auto c = gr.add_node(2); + gr.add_edge(a, b); // 0 -> 1 + gr.add_edge(b, c); // 1 -> 2 + gr.add_edge(a, c); // 0 -> 2 + return gr; + }(); + + static_assert(g.node_count() == 3); + static_assert(g.has_edge(0, 1)); + static_assert(g.has_edge(1, 2)); + static_assert(g.shortest_path(0, 2).size() == 2); // 0 -> 2 direct + + // Runtime example (BFS traversal) + void traverse() { + Graph gr{}; + auto n0 = gr.add_node(0); + auto n1 = gr.add_node(1); + auto n2 = gr.add_node(2); + auto n3 = gr.add_node(3); + gr.add_edge(n0, n1); + gr.add_edge(n1, n2); + gr.add_edge(n0, n3); + gr.add_edge(n3, n2); + + FixedVector order{}; + gr.bfs(n0, [&](std::size_t idx){ order.push_back(static_cast(gr.node_at(idx))); }); + // 'order' now holds a BFS visitation order starting from node 0 + } + + // Weighted undirected example using double edge weights + using WUGraph = FixedGraph; // undirected weighted + constexpr auto wug = [](){ + WUGraph gr{}; + auto a = gr.add_node(0); + auto b = gr.add_node(1); + auto c = gr.add_node(2); + gr.add_edge(a, b, 1.5); + gr.add_edge(b, c, 2.25); + gr.add_edge(a, c, 5.0); + // Dijkstra shortest path 0 -> 2 should pick 0-1-2 (1.5 + 2.25 = 3.75 < 5.0) + auto path = gr.dijkstra_shortest_path(a, c); + return gr; + }(); + ``` + - EnumMap ```C++ enum class Color { RED, YELLOW, BLUE}; diff --git a/include/fixed_containers/fixed_graph.hpp b/include/fixed_containers/fixed_graph.hpp new file mode 100644 index 00000000..6ea23682 --- /dev/null +++ b/include/fixed_containers/fixed_graph.hpp @@ -0,0 +1,1490 @@ +#pragma once + +#include "fixed_containers/concepts.hpp" +#include "fixed_containers/fixed_map.hpp" +#include "fixed_containers/fixed_vector.hpp" +#include "fixed_containers/preconditions.hpp" +#include "fixed_containers/source_location.hpp" + +#include +#include +#include +#include +#include +#include +#include // std::memcpy +#include + + +namespace fixed_containers +{ + +using NodeIndex = std::size_t; + +// Storage classes for FixedGraph + +template +class AdjacencyListStorage +{ +private: + using EdgeStorage = typename std::conditional::value, + NodeIndex, + std::pair>::type; + using AdjacencyList = FixedVector; + FixedVector storage_; + +public: + constexpr AdjacencyListStorage() + { + storage_.resize(MAX_NODES, AdjacencyList{}); + } + + template + constexpr void add_edge(NodeIndex from, NodeIndex to, Args&&... args) + { + if (storage_[from].size() >= MAX_EDGES_PER_NODE) return; + if constexpr (std::is_void_v) + { + storage_[from].push_back(to); + } + else + { + storage_[from].push_back({to, std::forward(args)...}); + } + if constexpr (!DIRECTED) + { + if (storage_[to].size() >= MAX_EDGES_PER_NODE) return; + if constexpr (std::is_void_v) + { + storage_[to].push_back(from); + } + else + { + storage_[to].push_back({from, std::forward(args)...}); + } + } + } + + constexpr bool has_edge(NodeIndex from, NodeIndex to) const + { + const auto& list = storage_[from]; + if constexpr (std::is_void_v) + { + return std::find(list.begin(), list.end(), to) != list.end(); + } + else + { + return std::find_if(list.begin(), list.end(), [to](const auto& p){ return p.first == to; }) != list.end(); + } + } + + constexpr FixedVector neighbors(NodeIndex node) const + { + FixedVector result{}; + const auto& list = storage_[node]; + for (const auto& e : list) + { + result.push_back(e); + } + return result; + } + + constexpr std::size_t edge_count(std::size_t node_count) const + { + std::size_t count = 0; + for (NodeIndex u = 0; u < node_count; ++u) + { + if constexpr (DIRECTED) + { + count += storage_[u].size(); + } + else + { + for (const auto& e : storage_[u]) + { + NodeIndex v = std::is_void_v ? e : e.first; + if (u < v) ++count; + } + } + } + return count; + } + + constexpr bool remove_edge(NodeIndex from, NodeIndex to) + { + bool removed = false; + auto& list = storage_[from]; + if constexpr (std::is_void_v) + { + auto it = std::find(list.begin(), list.end(), to); + if (it != list.end()) + { + list.erase(it); + removed = true; + } + } + else + { + auto it = std::find_if(list.begin(), list.end(), [to](const auto& p){ return p.first == to; }); + if (it != list.end()) + { + list.erase(it); + removed = true; + } + } + if constexpr (!DIRECTED) + { + if (removed) + { + auto& list2 = storage_[to]; + if constexpr (std::is_void_v) + { + auto it = std::find(list2.begin(), list2.end(), from); + if (it != list2.end()) list2.erase(it); + } + else + { + auto it = std::find_if(list2.begin(), list2.end(), [from](const auto& p){ return p.first == from; }); + if (it != list2.end()) list2.erase(it); + } + } + } + return removed; + } + + constexpr std::size_t degree(NodeIndex u) const + { + return storage_[u].size(); + } +}; + +template +class AdjacencyMatrixStorage +{ +private: + using MatrixElement = typename std::conditional::value, + bool, + std::optional>::type; + FixedVector, MAX_NODES> storage_; + +public: + constexpr AdjacencyMatrixStorage() + { + storage_.resize(MAX_NODES); + for (auto& row : storage_) + { + row.resize(MAX_NODES); + if constexpr (!std::is_void_v) + { + for (auto& e : row) e = std::nullopt; + } + } + } + + template + constexpr void add_edge(NodeIndex from, NodeIndex to, Args&&... args) + { + if constexpr (std::is_void_v) + { + storage_[from][to] = true; + } + else + { + storage_[from][to] = std::forward(args...); + } + if constexpr (!DIRECTED) + { + if constexpr (std::is_void_v) + { + storage_[to][from] = true; + } + else + { + storage_[to][from] = std::forward(args...); + } + } + } + + constexpr bool has_edge(NodeIndex from, NodeIndex to) const + { + if constexpr (std::is_void_v) + { + return storage_[from][to]; + } + else + { + return storage_[from][to].has_value(); + } + } + + constexpr FixedVector, NodeIndex, std::pair>::type, MAX_NODES> neighbors(NodeIndex node) const + { + FixedVector, NodeIndex, std::pair>::type, MAX_NODES> result{}; + for (NodeIndex v = 0; v < MAX_NODES; ++v) + { + if (has_edge(node, v)) + { + if constexpr (std::is_void_v) + { + result.push_back(v); + } + else + { + result.push_back({v, *storage_[node][v]}); + } + } + } + return result; + } + + constexpr std::size_t edge_count(std::size_t node_count) const + { + std::size_t count = 0; + for (NodeIndex u = 0; u < node_count; ++u) + { + for (NodeIndex v = 0; v < node_count; ++v) + { + if (has_edge(u, v)) + { + if constexpr (DIRECTED) + { + ++count; + } + else + { + if (u < v) ++count; + } + } + } + } + return count; + } + + constexpr bool remove_edge(NodeIndex from, NodeIndex to) + { + bool removed = has_edge(from, to); + if (removed) + { + if constexpr (std::is_void_v) + { + storage_[from][to] = false; + } + else + { + storage_[from][to] = std::nullopt; + } + if constexpr (!DIRECTED) + { + if constexpr (std::is_void_v) + { + storage_[to][from] = false; + } + else + { + storage_[to][from] = std::nullopt; + } + } + } + return removed; + } + + constexpr std::size_t degree(NodeIndex u) const + { + std::size_t deg = 0; + for (NodeIndex v = 0; v < MAX_NODES; ++v) + { + if (has_edge(u, v)) ++deg; + } + return deg; + } +}; + +template +class AdjacencyPoolStorage +{ +private: + using EdgeStorage = typename std::conditional::value, + NodeIndex, + std::pair>::type; + FixedVector storage_; + FixedVector, MAX_NODES> ranges_; + +public: + constexpr AdjacencyPoolStorage() + { + ranges_.resize(MAX_NODES, {0, 0}); + } + + template + constexpr void add_edge(NodeIndex from, NodeIndex to, Args&&... args) + { + if (storage_.size() >= MAX_TOTAL_EDGES) return; + size_t insert_pos = storage_.size(); + if constexpr (std::is_void_v) + { + storage_.push_back(to); + } + else + { + storage_.push_back({to, std::forward(args...)}); + } + if (ranges_[from].second == 0) { + ranges_[from].first = insert_pos; + } + ranges_[from].second = insert_pos + 1; + if constexpr (!DIRECTED) + { + if (storage_.size() >= MAX_TOTAL_EDGES) return; + insert_pos = storage_.size(); + if constexpr (std::is_void_v) + { + storage_.push_back(from); + } + else + { + storage_.push_back({from, std::forward(args...)}); + } + if (ranges_[to].second == 0) { + ranges_[to].first = insert_pos; + } + ranges_[to].second = insert_pos + 1; + } + } + + constexpr bool has_edge(NodeIndex from, NodeIndex to) const + { + auto [start, end] = ranges_[from]; + for (size_t i = start; i < end; ++i) + { + const auto& e = storage_[i]; + NodeIndex v; + if constexpr (std::is_void_v) + { + v = e; + } + else + { + v = e.first; + } + if (v == to) return true; + } + return false; + } + + constexpr FixedVector neighbors(NodeIndex node) const + { + FixedVector result{}; + auto [start, end] = ranges_[node]; + for (size_t i = start; i < end; ++i) + { + result.push_back(storage_[i]); + } + return result; + } + + constexpr std::size_t edge_count(std::size_t node_count) const + { + std::size_t count = 0; + for (NodeIndex u = 0; u < node_count; ++u) + { + auto [start, end] = ranges_[u]; + if constexpr (DIRECTED) + { + count += (end - start); + } + else + { + for (size_t i = start; i < end; ++i) + { + const auto& e = storage_[i]; + NodeIndex v; + if constexpr (std::is_void_v) + { + v = e; + } + else + { + v = e.first; + } + if (u < v) ++count; + } + } + } + return count; + } + + constexpr bool remove_edge(NodeIndex from, NodeIndex to) + { + (void)from; + (void)to; + // Removal from pool is complex due to contiguous storage, so not implemented + return false; + } + + constexpr std::size_t degree(NodeIndex u) const + { + auto [start, end] = ranges_[u]; + return end - start; + } +}; + +/** + * Fixed-capacity graph with maximum nodes and edges declared at compile-time. + * Supports directed and undirected graphs, weighted and unweighted edges. + * Properties: + * - constexpr + * - no dynamic allocations + * - adjacency list or matrix representation (configurable) + */ +template > +class FixedGraph +{ +public: + using node_type = NodeType; + using edge_type = EdgeType; + static constexpr std::size_t max_nodes = MAX_NODES; + static constexpr std::size_t max_edges_per_node = MAX_EDGES_PER_NODE; + static_assert(MAX_NODES > 0, "MAX_NODES must be > 0"); + +private: + using NodeIndex = std::size_t; + static constexpr NodeIndex INTERNAL_INVALID_INDEX = (std::numeric_limits::max)(); + + // For edges: if EdgeType is void, use NodeIndex, else pair + using EdgeStorage = typename std::conditional::value, + NodeIndex, + std::pair>::type; + + using NodeList = FixedVector, MAX_NODES>; + + // Storage + StorageType storage_; + + NodeList node_list_; + FixedVector index_to_node_; + + NodeIndex next_index_ = 0; + + +public: + // Public constant for invalid index so callers don't rely on internal naming + static constexpr NodeIndex INVALID_INDEX = INTERNAL_INVALID_INDEX; + +public: + constexpr FixedGraph() noexcept {} + + // Generate a complete graph (every node connected to every other node) + static constexpr FixedGraph create_complete_graph(std::size_t num_nodes) + { + FixedGraph graph{}; + for (std::size_t i = 0; i < num_nodes && i < MAX_NODES; ++i) + { + graph.add_node(static_cast(i)); + } + + for (std::size_t i = 0; i < num_nodes; ++i) + { + for (std::size_t j = i + 1; j < num_nodes; ++j) + { + graph.add_edge(i, j); + if constexpr (!DIRECTED) + graph.add_edge(j, i); + } + } + return graph; + } + + // Generate a cycle graph + static constexpr FixedGraph create_cycle_graph(std::size_t num_nodes) + { + FixedGraph graph{}; + for (std::size_t i = 0; i < num_nodes && i < MAX_NODES; ++i) + { + graph.add_node(static_cast(i)); + } + + for (std::size_t i = 0; i < num_nodes; ++i) + { + std::size_t next = (i + 1) % num_nodes; + graph.add_edge(i, next); + if constexpr (!DIRECTED) + graph.add_edge(next, i); + } + return graph; + } + + // Add a node, returns its index + constexpr NodeIndex add_node(const NodeType& node) + { + for (const auto& p : node_list_) + { + if (p.first == node) + { + return p.second; + } + } + if (node_list_.size() >= MAX_NODES) + { + return INVALID_INDEX; // Capacity exhausted + } + NodeIndex idx = next_index_++; + node_list_.push_back({node, idx}); + index_to_node_.push_back(node); + return idx; + } + + // Return true if the node exists + constexpr bool has_node(const NodeType& node) const noexcept + { + for (const auto& p : node_list_) { if (p.first == node) return true; } + return false; + } + + // Find node index or INVALID_INDEX if not present + constexpr NodeIndex find_node_index(const NodeType& node) const noexcept + { + for (const auto& p : node_list_) { if (p.first == node) return p.second; } + return INVALID_INDEX; + } + + // Add an edge + template + constexpr void add_edge(NodeIndex from, NodeIndex to, Args&&... args) + { + if (from >= next_index_ || to >= next_index_) + { + return; + } + storage_.add_edge(from, to, std::forward(args)...); + } + + // Check if edge exists + constexpr bool has_edge(NodeIndex from, NodeIndex to) const + { + if (from >= next_index_ || to >= next_index_) + { + return false; + } + return storage_.has_edge(from, to); + } + + // Get neighbors + constexpr void neighbors(NodeIndex node, FixedVector& out) const + { + if (node >= next_index_) { + out.clear(); + return; + } + out = storage_.neighbors(node); + } + + // Get node count + constexpr std::size_t node_count() const noexcept { return next_index_; } + + // Count edges (directed counts each directed edge; undirected counts each undirected edge once) + constexpr std::size_t edge_count() const noexcept + { + return storage_.edge_count(next_index_); + } + + // Get node by index + constexpr const NodeType& node_at(NodeIndex idx) const { return index_to_node_[idx]; } + + // Remove an edge; returns true if removed. For undirected graphs removes symmetric edge too. + constexpr bool remove_edge(NodeIndex from, NodeIndex to) + { + if (from >= next_index_ || to >= next_index_) return false; + return storage_.remove_edge(from, to); + } + + // BFS traversal + template + constexpr void bfs(NodeIndex start, Visitor visitor) const + { + if (start >= next_index_) return; + FixedVector visited{}; visited.resize(MAX_NODES, false); + FixedVector queue{}; queue.push_back(start); visited[start] = true; + std::size_t head = 0; + while (head < queue.size()) + { + NodeIndex current = queue[head++]; + visitor(current); + FixedVector neigh{}; + neighbors(current, neigh); + for (const auto& edge : neigh) + { + NodeIndex neighbor = get_neighbor(edge); + if (!visited[neighbor]) { visited[neighbor] = true; queue.push_back(neighbor); } + } + } + } + + // DFS traversal + template + constexpr void dfs(NodeIndex start, Visitor visitor) const + { + if (start >= next_index_) + return; + FixedVector visited{}; + visited.resize(MAX_NODES, false); + FixedVector stack{}; + stack.push_back(start); + while (!stack.empty()) + { + NodeIndex current = stack.back(); + stack.pop_back(); + if (!visited[current]) + { + visited[current] = true; + visitor(current); + FixedVector neigh{}; + neighbors(current, neigh); + for (const auto& edge : neigh) { + NodeIndex neighbor = get_neighbor(edge); + if (!visited[neighbor]) { + stack.push_back(neighbor); + } + } + } + } + } + + // Shortest path using BFS (for unweighted graphs) + constexpr void shortest_path(NodeIndex start, NodeIndex end, FixedVector& out) const + + // Topological sort (for directed graphs) + constexpr void topological_sort(FixedVector& out) const + requires(DIRECTED) + { + out.clear(); + FixedVector in_degree{}; + in_degree.resize(MAX_NODES, 0); + + // Calculate in-degrees + for (NodeIndex i = 0; i < next_index_; ++i) + { + FixedVector neigh{}; + neighbors(i, neigh); + for (const auto& edge : neigh) + { + NodeIndex neighbor = get_neighbor(edge); + if (neighbor < MAX_NODES) + in_degree[neighbor]++; + } + } + + // Find nodes with no incoming edges + FixedVector queue{}; + for (NodeIndex i = 0; i < next_index_; ++i) + { + if (in_degree[i] == 0) + queue.push_back(i); + } + std::size_t head = 0; + while (head < queue.size()) + { + NodeIndex current = queue[head++]; + out.push_back(current); + + // Reduce in-degree of neighbors + FixedVector neigh{}; + neighbors(current, neigh); + for (const auto& edge : neigh) + { + NodeIndex neighbor = get_neighbor(edge); + if (neighbor < MAX_NODES) + { + in_degree[neighbor]--; + if (in_degree[neighbor] == 0) + queue.push_back(neighbor); + } + } + } + + // Check for cycles (if not all nodes are included) + if (out.size() != next_index_) + out.clear(); // Cycle detected + } + + // Check if graph is connected (for undirected graphs) + constexpr bool is_connected() const + requires(!DIRECTED) + { + if (next_index_ == 0) return true; + if (next_index_ == 1) return true; + + FixedVector visited{}; + visited.resize(MAX_NODES, false); + FixedVector stack{}; + + // Start DFS from node 0 + stack.push_back(0); + visited[0] = true; + std::size_t visited_count = 1; + + while (!stack.empty()) + { + NodeIndex current = stack.back(); + stack.pop_back(); + + FixedVector neigh{}; + neighbors(current, neigh); + for (const auto& edge : neigh) + { + NodeIndex neighbor = get_neighbor(edge); + if (!visited[neighbor]) + { + visited[neighbor] = true; + visited_count++; + stack.push_back(neighbor); + } + } + } + + return visited_count == next_index_; + } + + // Dijkstra's shortest path algorithm for weighted graphs + template + constexpr void dijkstra_shortest_path(NodeIndex start, NodeIndex end, FixedVector& out) const + requires(!std::is_void_v && std::is_arithmetic_v) + { + out.clear(); + using DistanceType = WeightType; + constexpr DistanceType INF = std::numeric_limits::max() / 2; + + FixedVector distances{}; + distances.resize(MAX_NODES, INF); + FixedVector previous{}; + previous.resize(MAX_NODES, INVALID_INDEX); + FixedVector visited{}; + visited.resize(MAX_NODES, false); + + distances[start] = 0; + + // Simple priority queue implementation using FixedVector + auto find_min_distance = [&](const FixedVector& visited) -> NodeIndex { + DistanceType min_dist = INF; + NodeIndex min_node = INVALID_INDEX; + for (NodeIndex i = 0; i < next_index_; i++) { + if (!visited[i] && distances[i] < min_dist) { + min_dist = distances[i]; + min_node = i; + } + } + return min_node; + }; + + for (NodeIndex count = 0; count < next_index_; ++count) { + NodeIndex u = find_min_distance(visited); + if (u == INVALID_INDEX || distances[u] == INF) break; + + visited[u] = true; + + FixedVector neigh{}; + neighbors(u, neigh); + for (const auto& edge : neigh) { + NodeIndex v = get_neighbor(edge); + DistanceType weight = get_weight(edge); + if (!visited[v] && distances[u] != INF && distances[u] + weight < distances[v]) { + distances[v] = distances[u] + weight; + previous[v] = u; + } + } + } + + // Reconstruct path (reversed accumulation then reverse) + if (distances[end] == INF) return; // No path found + FixedVector rev{}; + for (NodeIndex at = end; at != INVALID_INDEX; at = previous[at]) { + rev.push_back(at); + } + for (std::size_t i = 0; i < rev.size(); ++i) { + out.push_back(rev[rev.size() - 1 - i]); + } + } + + // Bellman-Ford algorithm for graphs with negative weights + template + constexpr void bellman_ford_shortest_paths(NodeIndex start, FixedVector& distances, bool& has_negative_cycle) const + requires(!std::is_void_v && std::is_arithmetic_v) + { + distances.clear(); + distances.resize(MAX_NODES); + using DistanceType = WeightType; + constexpr DistanceType INF = std::numeric_limits::max() / 2; + + distances.resize(MAX_NODES, INF); + distances[start] = 0; + + // Relax edges |V|-1 times + for (NodeIndex i = 0; i < next_index_ - 1; ++i) { + for (NodeIndex u = 0; u < next_index_; ++u) { + FixedVector neigh{}; + neighbors(u, neigh); + for (const auto& edge : neigh) { + NodeIndex v = get_neighbor(edge); + DistanceType weight = get_weight(edge); + if (distances[u] != INF && distances[u] + weight < distances[v]) { + distances[v] = distances[u] + weight; + } + } + } + } + + // Check for negative cycles + has_negative_cycle = false; + for (NodeIndex u = 0; u < next_index_; ++u) { + FixedVector neigh{}; + neighbors(u, neigh); + for (const auto& edge : neigh) { + NodeIndex v = get_neighbor(edge); + DistanceType weight = get_weight(edge); + if (distances[u] != INF && distances[u] + weight < distances[v]) { + has_negative_cycle = true; + break; + } + } + if (has_negative_cycle) break; + } + } + + // Minimum spanning tree using Kruskal's algorithm + template + constexpr void kruskal_mst(FixedVector, MAX_NODES * MAX_NODES>& out) const + requires(!std::is_void_v && std::is_arithmetic_v && !DIRECTED) + { + out.clear(); + // Union-Find structure + FixedVector parent{}; + parent.resize(MAX_NODES); + for (NodeIndex i = 0; i < next_index_; ++i) parent[i] = i; + + auto find = [&](auto& self, NodeIndex x) -> NodeIndex { + return parent[x] == x ? x : parent[x] = self(self, parent[x]); + }; + + auto unite = [&](NodeIndex x, NodeIndex y) { + NodeIndex px = find(find, x), py = find(find, y); + if (px != py) parent[px] = py; + }; + + // Collect all edges + FixedVector, MAX_NODES * MAX_NODES> edges{}; + for (NodeIndex u = 0; u < next_index_; ++u) { + FixedVector neigh{}; + neighbors(u, neigh); + for (const auto& edge : neigh) { + NodeIndex v = get_neighbor(edge); + WeightType weight = get_weight(edge); + if (u < v) { // Avoid duplicates in undirected graph + edges.push_back({weight, u, v}); + } + } + } + + // Sort edges by weight + std::sort(edges.begin(), edges.end()); + + for (const auto& [weight, u, v] : edges) { + if (find(find, u) != find(find, v)) { + unite(u, v); + out.push_back({u, v}); + } + } + } + + // Strongly connected components using Kosaraju's algorithm + constexpr void strongly_connected_components(FixedVector, MAX_NODES>& out) const + requires(DIRECTED) + { + out.clear(); + FixedVector visited{}; + visited.resize(MAX_NODES, false); + FixedVector order{}; + + // First DFS to get finishing times + auto dfs1 = [&](auto& self, NodeIndex node) -> void { + visited[node] = true; + FixedVector neigh{}; + neighbors(node, neigh); + for (const auto& edge : neigh) { + NodeIndex neighbor = get_neighbor(edge); + if (!visited[neighbor]) { + self(self, neighbor); + } + } + order.push_back(node); + }; + + for (NodeIndex i = 0; i < next_index_; ++i) { + if (!visited[i]) { + dfs1(dfs1, i); + } + } + + // Create transpose graph + FixedGraph transpose{}; + create_transpose(transpose); + + // Reset visited + visited = FixedVector{}; + visited.resize(MAX_NODES, false); + + // Second DFS on transpose graph in decreasing finish time order + auto dfs2 = [&](auto& self, NodeIndex node, FixedVector& component) -> void { + visited[node] = true; + component.push_back(node); + FixedVector neigh{}; + transpose.neighbors(node, neigh); + for (const auto& edge : neigh) { + NodeIndex neighbor = get_neighbor(edge); + if (!visited[neighbor]) { + self(self, neighbor, component); + } + } + }; + + while (!order.empty()) { + NodeIndex node = order.back(); + order.pop_back(); + if (!visited[node]) { + FixedVector component{}; + dfs2(dfs2, node, component); + out.push_back(component); + } + } + } + + // Create transpose graph (reverse all edges) + constexpr void create_transpose(FixedGraph& out) const + requires(DIRECTED) + { + out = FixedGraph{}; + for (NodeIndex i = 0; i < next_index_; ++i) { + out.add_node(index_to_node_[i]); + } + + for (NodeIndex u = 0; u < next_index_; ++u) { + FixedVector neigh{}; + neighbors(u, neigh); + for (const auto& edge : neigh) { + NodeIndex v = get_neighbor(edge); + if constexpr (std::is_void_v) { + out.add_edge(v, u); + } else { + out.add_edge(v, u, get_weight(edge)); + } + } + } + } + + // Check if graph is bipartite + constexpr bool is_bipartite() const + { + FixedVector colors{}; + colors.resize(MAX_NODES, -1); // -1: uncolored, 0: color 0, 1: color 1 + + for (NodeIndex start = 0; start < next_index_; ++start) { + if (colors[start] == -1) { + FixedVector queue{}; queue.push_back(start); + colors[start] = 0; + std::size_t head = 0; + while (head < queue.size()) { + NodeIndex u = queue[head++]; + + FixedVector neigh{}; + neighbors(u, neigh); + for (const auto& edge : neigh) { + NodeIndex v = get_neighbor(edge); + if (colors[v] == -1) { + colors[v] = 1 - colors[u]; + queue.push_back(v); + } else if (colors[v] == colors[u]) { + return false; // Same color as parent + } + } + } + } + } + return true; + } + + // Graph coloring using greedy algorithm + constexpr void greedy_coloring(FixedVector& out) const + { + out.clear(); + out.resize(MAX_NODES, -1); + + for (NodeIndex u = 0; u < next_index_; ++u) { + FixedVector used_colors{}; + used_colors.resize(MAX_NODES, false); + + // Check colors of neighbors + FixedVector neigh{}; + neighbors(u, neigh); + for (const auto& edge : neigh) { + NodeIndex v = get_neighbor(edge); + if (out[v] != -1) { + used_colors[out[v]] = true; + } + } + + // Find first unused color + int color = 0; + while (color < static_cast(MAX_NODES) && used_colors[color]) { + ++color; + } + out[u] = color; + } + } + + // Degree centrality + constexpr void degree_centrality(FixedVector& out) const + { + out.clear(); + out.resize(MAX_NODES, 0); + + for (NodeIndex u = 0; u < next_index_; ++u) { + out[u] = storage_.degree(u); + if constexpr (!DIRECTED) { + // For undirected graphs, degree is already correct + } else { + // For directed graphs, we might want in-degree or out-degree + // Here we use out-degree + } + } + } + + // Betweenness centrality (approximate for large graphs) + constexpr void betweenness_centrality(FixedVector& out) const + { + out.clear(); + out.resize(MAX_NODES, 0.0); + + for (NodeIndex s = 0; s < next_index_; ++s) { + FixedVector sigma{}; + sigma.resize(MAX_NODES, 0.0); + sigma[s] = 1.0; + + FixedVector distance{}; + distance.resize(MAX_NODES, -1.0); + distance[s] = 0.0; + + FixedVector, MAX_NODES> predecessors{}; + predecessors.resize(MAX_NODES); + + FixedVector queue{}; queue.push_back(s); + std::size_t head = 0; + while (head < queue.size()) { + NodeIndex v = queue[head++]; + + FixedVector neigh{}; + neighbors(v, neigh); + for (const auto& edge : neigh) { + NodeIndex w = get_neighbor(edge); + if (distance[w] == -1.0) { + queue.push_back(w); + distance[w] = distance[v] + 1.0; + } + if (distance[w] == distance[v] + 1.0) { + sigma[w] += sigma[v]; + predecessors[w].push_back(v); + } + } + } + + FixedVector delta{}; + delta.resize(MAX_NODES, 0.0); + + while (!queue.empty()) queue.pop_back(); // Clear queue + for (NodeIndex v = 0; v < next_index_; ++v) { + if (distance[v] != -1.0) queue.push_back(v); + } + + while (!queue.empty()) { + NodeIndex w = queue.back(); + queue.pop_back(); + + for (NodeIndex v : predecessors[w]) { + delta[v] += (sigma[v] / sigma[w]) * (1.0 + delta[w]); + } + + if (w != s) { + out[w] += delta[w]; + } + } + } + } + + // Check if graph has Eulerian circuit + constexpr bool has_eulerian_circuit() const + { + if constexpr (DIRECTED) + { + // For directed graphs: strongly connected (when ignoring zero in/out nodes) & in-degree==out-degree per node + // Quick check: every node must have equal in/out degree and be reachable in undirected sense + // Build in-degrees + FixedVector in_degree{}; in_degree.resize(MAX_NODES, 0); + FixedVector has_any_edge{}; has_any_edge.resize(MAX_NODES, false); + for (NodeIndex u = 0; u < next_index_; ++u) + { + if (get_degree(u) > 0) has_any_edge[u] = true; + FixedVector neigh{}; + neighbors(u, neigh); + for (const auto& edge : neigh) + { + NodeIndex v = get_neighbor(edge); + in_degree[v]++; + has_any_edge[v] = true; + } + } + for (NodeIndex u = 0; u < next_index_; ++u) + { + if (has_any_edge[u]) + { + if (in_degree[u] != get_degree(u)) return false; + } + } + // Weak connectivity check: perform DFS treating edges as undirected on nodes that participate + // Find first node with an edge + NodeIndex start = INVALID_INDEX; + for (NodeIndex u = 0; u < next_index_; ++u) if (has_any_edge[u]) { start = u; break; } + if (start == INVALID_INDEX) return true; // Trivial (all isolated) + FixedVector visited{}; visited.resize(MAX_NODES, false); + FixedVector stack{}; stack.push_back(start); visited[start] = true; + while (!stack.empty()) + { + NodeIndex cur = stack.back(); stack.pop_back(); + // Out edges + FixedVector neigh_out{}; + neighbors(cur, neigh_out); + for (const auto& edge : neigh_out) { NodeIndex v = get_neighbor(edge); if (!visited[v]) { visited[v] = true; stack.push_back(v);} } + // In edges (scan all) - O(VE) worst case but bounded by template sizes + for (NodeIndex v = 0; v < next_index_; ++v) + { + FixedVector neigh_in{}; + neighbors(v, neigh_in); + for (const auto& e2 : neigh_in) { if (get_neighbor(e2) == cur && !visited[v]) { visited[v] = true; stack.push_back(v); } } + } + } + for (NodeIndex u = 0; u < next_index_; ++u) if (has_any_edge[u] && !visited[u]) return false; + return true; + } + else + { + if (!is_connected()) return false; + for (NodeIndex u = 0; u < next_index_; ++u) { + if (get_degree(u) % 2 != 0) return false; + } + return true; + } + } + + // Graph density + constexpr double density() const + { + std::size_t max_possible_edges = DIRECTED ? next_index_ * (next_index_ - 1) : next_index_ * (next_index_ - 1) / 2; + if (max_possible_edges == 0) return 0.0; + + std::size_t actual_edges = storage_.edge_count(next_index_); + + return static_cast(actual_edges) / max_possible_edges; + } + + // Graph diameter (longest shortest path) + constexpr std::size_t diameter() const + { + std::size_t max_distance = 0; + FixedVector distances{}; + for (NodeIndex start = 0; start < next_index_; ++start) { + bfs_distances(start, distances); + for (std::size_t dist : distances) { + if (dist != std::numeric_limits::max() && dist > max_distance) { + max_distance = dist; + } + } + } + return max_distance; + } + + // BFS distances from a source node + constexpr void bfs_distances(NodeIndex start, FixedVector& out) const + { + out.clear(); + out.resize(MAX_NODES, std::numeric_limits::max()); + FixedVector visited{}; + visited.resize(MAX_NODES, false); + FixedVector queue{}; queue.push_back(start); out[start] = 0; visited[start] = true; + std::size_t head = 0; + while (head < queue.size()) { + NodeIndex current = queue[head++]; + + FixedVector neigh{}; + neighbors(current, neigh); + for (const auto& edge : neigh) { + NodeIndex neighbor = get_neighbor(edge); + if (!visited[neighbor]) { + visited[neighbor] = true; + out[neighbor] = out[current] + 1; + queue.push_back(neighbor); + } + } + } + } + + // Clustering coefficient for undirected graphs + constexpr double clustering_coefficient(NodeIndex node) const + requires(!DIRECTED) + { + if (node >= next_index_) return 0.0; + + FixedVector neighbors_list{}; + neighbors(node, neighbors_list); + std::size_t degree = neighbors_list.size(); + if (degree < 2) return 0.0; + + std::size_t triangles = 0; + for (std::size_t i = 0; i < neighbors_list.size(); ++i) { + NodeIndex u = get_neighbor(neighbors_list[i]); + for (std::size_t j = i + 1; j < neighbors_list.size(); ++j) { + NodeIndex v = get_neighbor(neighbors_list[j]); + if (has_edge(u, v)) ++triangles; + } + } + + return 2.0 * triangles / (degree * (degree - 1)); + } + + // Average clustering coefficient + constexpr double average_clustering_coefficient() const + requires(!DIRECTED) + { + double sum = 0.0; + std::size_t count = 0; + for (NodeIndex i = 0; i < next_index_; ++i) { + if (get_degree(i) >= 2) { + sum += clustering_coefficient(i); + ++count; + } + } + return count > 0 ? sum / count : 0.0; + } + + // Graph complement + constexpr void complement(FixedGraph& out) const + { + out = FixedGraph{}; + for (NodeIndex i = 0; i < next_index_; ++i) { + out.add_node(index_to_node_[i]); + } + + for (NodeIndex u = 0; u < next_index_; ++u) { + for (NodeIndex v = 0; v < next_index_; ++v) { + if (u != v && !has_edge(u, v)) { + if constexpr (std::is_void_v) { + out.add_edge(u, v); + if constexpr (!DIRECTED) out.add_edge(v, u); + } else { + out.add_edge(u, v, EdgeType{}); + if constexpr (!DIRECTED) out.add_edge(v, u, EdgeType{}); + } + } + } + } + } + + // Graph union + constexpr void graph_union(const FixedGraph& other, FixedGraph& out) const + { + out = *this; + + // Add nodes from other graph + for (NodeIndex i = 0; i < other.next_index_; ++i) { + out.add_node(other.index_to_node_[i]); + } + + // Add edges from other graph + for (NodeIndex u = 0; u < other.next_index_; ++u) { + FixedVector neigh{}; + other.neighbors(u, neigh); + for (const auto& edge : neigh) { + NodeIndex v = other.get_neighbor(edge); + if constexpr (std::is_void_v) { + out.add_edge(u, v); + } else { + out.add_edge(u, v, other.get_weight(edge)); + } + } + } + } + + // Graph intersection + constexpr void graph_intersection(const FixedGraph& other, FixedGraph& out) const + { + out = FixedGraph{}; + + // Add common nodes + for (NodeIndex i = 0; i < next_index_; ++i) { + for (NodeIndex j = 0; j < other.next_index_; ++j) { + if (index_to_node_[i] == other.index_to_node_[j]) { + out.add_node(index_to_node_[i]); + break; + } + } + } + + // Add common edges + for (NodeIndex u = 0; u < out.next_index_; ++u) { + for (NodeIndex v = 0; v < out.next_index_; ++v) { + if (has_edge(u, v) && other.has_edge(u, v)) { + if constexpr (std::is_void_v) { + out.add_edge(u, v); + } else { + // Find the weight from this graph + FixedVector neigh{}; + neighbors(u, neigh); + for (const auto& edge : neigh) { + if (get_neighbor(edge) == v) { + out.add_edge(u, v, get_weight(edge)); + break; + } + } + } + } + } + } + } + + // Serialize graph to binary format + template + constexpr OutputIterator serialize(OutputIterator out) const + { + static_assert(std::is_trivially_copyable_v, "NodeType must be trivially copyable for serialize"); + if constexpr (!std::is_void_v) { static_assert(std::is_trivially_copyable_v, "EdgeType must be trivially copyable for serialize"); } + // Version byte (simple) then node count + *out++ = static_cast(1); // version + *out++ = static_cast(next_index_); + for (NodeIndex i = 0; i < next_index_; ++i) { + const NodeType& node = index_to_node_[i]; + std::memcpy(&*out, &node, sizeof(NodeType)); + out += sizeof(NodeType); + } + for (NodeIndex u = 0; u < next_index_; ++u) { + FixedVector list{}; + neighbors(u, list); + *out++ = static_cast(list.size()); + for (const auto& edge : list) { + if constexpr (std::is_void_v) { + NodeIndex v = get_neighbor(edge); + std::memcpy(&*out, &v, sizeof(NodeIndex)); + out += sizeof(NodeIndex); + } else { + std::pair pod{get_neighbor(edge), get_weight(edge)}; + std::memcpy(&*out, &pod, sizeof(pod)); + out += sizeof(pod); + } + } + } + return out; + } + + // Deserialize graph from binary format + template + constexpr InputIterator deserialize(InputIterator in) + { + static_assert(std::is_trivially_copyable_v, "NodeType must be trivially copyable for deserialize"); + if constexpr (!std::is_void_v) { static_assert(std::is_trivially_copyable_v, "EdgeType must be trivially copyable for deserialize"); } + // Version + (void)*in++; // ignore version for now + NodeIndex node_count = static_cast(*in++); + for (NodeIndex i = 0; i < node_count; ++i) { + NodeType node; std::memcpy(&node, &*in, sizeof(NodeType)); in += sizeof(NodeType); add_node(node); } + for (NodeIndex u = 0; u < node_count; ++u) { + std::size_t edge_count = static_cast(*in++); + for (std::size_t j = 0; j < edge_count; ++j) { + if constexpr (std::is_void_v) { + NodeIndex v; std::memcpy(&v, &*in, sizeof(NodeIndex)); in += sizeof(NodeIndex); add_edge(u, v); + } else { + std::pair pod; std::memcpy(&pod, &*in, sizeof(pod)); in += sizeof(pod); add_edge(u, pod.first, pod.second); + } + } + } + return in; + } + + // Check if graph has cycles + constexpr bool has_cycles() const + { + FixedVector visited{}; + visited.resize(MAX_NODES, false); + FixedVector rec_stack{}; + rec_stack.resize(MAX_NODES, false); + + for (NodeIndex i = 0; i < next_index_; ++i) + { + if (!visited[i] && has_cycles_helper(i, visited, rec_stack)) + return true; + } + return false; + } + +private: + NodeIndex get_neighbor(const EdgeStorage& edge) const + { + if constexpr (std::is_void_v) + { + return edge; + } + else + { + return edge.first; + } + } + + auto get_weight(const EdgeStorage& edge) const + { + if constexpr (std::is_void_v) + { + return 1; // Default weight for unweighted graphs + } + else + { + return edge.second; + } + } + + constexpr std::size_t get_degree(NodeIndex u) const + { + return storage_.degree(u); + } + + constexpr bool has_cycles_helper(NodeIndex node, + FixedVector& visited, + FixedVector& rec_stack) const + { + visited[node] = true; + rec_stack[node] = true; + + FixedVector neigh{}; + neighbors(node, neigh); + for (const auto& edge : neigh) + { + NodeIndex neighbor = get_neighbor(edge); + if (!visited[neighbor] && has_cycles_helper(neighbor, visited, rec_stack)) + return true; + else if (rec_stack[neighbor]) + return true; + } + + rec_stack[node] = false; + return false; + } +}; + +} // namespace fixed_containers \ No newline at end of file diff --git a/test/fixed_graph_test.cpp b/test/fixed_graph_test.cpp new file mode 100644 index 00000000..746ad74d --- /dev/null +++ b/test/fixed_graph_test.cpp @@ -0,0 +1,202 @@ +#include "fixed_containers/fixed_graph.hpp" +#include "fixed_containers/fixed_vector.hpp" + +#include + +using fixed_containers::FixedVector; + +namespace +{ +using Graph = fixed_containers::FixedGraph>; +using MatrixGraph = fixed_containers::FixedGraph>; +using PoolGraph = fixed_containers::FixedGraph>; +using EdgeStorage = std::size_t; // for void edges + +void test_basic() +{ + Graph g; + auto n0 = g.add_node(0); + auto n1 = g.add_node(1); + auto n2 = g.add_node(2); + + std::cout << "n0: " << n0 << ", n1: " << n1 << ", n2: " << n2 << std::endl; + std::cout << "node_count: " << g.node_count() << std::endl; + + g.add_edge(n0, n1); + g.add_edge(n1, n2); + + fixed_containers::FixedVector neigh{}; + g.neighbors(n0, neigh); + std::cout << "Neighbors of 0: " << neigh.size() << std::endl; + std::cout << "Has edge 0-1: " << g.has_edge(n0, n1) << std::endl; + std::cout << "Has edge 1-0: " << g.has_edge(n1, n0) << std::endl; + + std::cout << "BFS from 0: "; + g.bfs(n0, [](auto idx) { std::cout << idx << " "; }); + std::cout << std::endl; + + std::cout << "DFS from 0: "; + g.dfs(n0, [](auto idx) { std::cout << idx << " "; }); + std::cout << std::endl; + + FixedVector path; + g.shortest_path(n0, n2, path); + std::cout << "Shortest path 0 to 2: "; + for (auto p : path) std::cout << p << " "; + std::cout << std::endl; +} + +void test_new_features() +{ + std::cout << "\n=== Testing New Features ===\n"; + + // Test cycle detection + Graph g1; + auto a = g1.add_node(0); + auto b = g1.add_node(1); + auto c = g1.add_node(2); + g1.add_edge(a, b); + g1.add_edge(b, c); + g1.add_edge(c, a); // Creates a cycle + + std::cout << "Graph with cycle has cycles: " << g1.has_cycles() << std::endl; + + // Test acyclic graph + Graph g2; + auto x = g2.add_node(0); + auto y = g2.add_node(1); + auto z = g2.add_node(2); + g2.add_edge(x, y); + g2.add_edge(y, z); + // No cycle + + std::cout << "Graph without cycle has cycles: " << g2.has_cycles() << std::endl; + + // Test connectivity (for undirected graphs) + using UndirectedGraph = fixed_containers::FixedGraph>; + UndirectedGraph ug; + auto u1 = ug.add_node(0); + auto u2 = ug.add_node(1); + auto u3 = ug.add_node(2); + ug.add_edge(u1, u2); + ug.add_edge(u2, u3); + + std::cout << "Undirected graph is connected: " << ug.is_connected() << std::endl; + + // Test graph generators + std::cout << "\n=== Graph Generators ===\n"; + auto complete = Graph::create_complete_graph(4); + std::cout << "Complete graph (4 nodes) created with " << complete.node_count() << " nodes" << std::endl; + + auto cycle = Graph::create_cycle_graph(5); + std::cout << "Cycle graph (5 nodes) created with " << cycle.node_count() << " nodes" << std::endl; + + // Test advanced features + std::cout << "\n=== Advanced Graph Features ===\n"; + + // Test bipartite checking + using UndirectedGraphType = fixed_containers::FixedGraph>; + UndirectedGraphType bipartite_graph; + auto bp1 = bipartite_graph.add_node(0); + auto bp2 = bipartite_graph.add_node(1); + auto bp3 = bipartite_graph.add_node(2); + auto bp4 = bipartite_graph.add_node(3); + bipartite_graph.add_edge(bp1, bp2); + bipartite_graph.add_edge(bp1, bp4); + bipartite_graph.add_edge(bp2, bp3); + bipartite_graph.add_edge(bp3, bp4); + + std::cout << "Bipartite graph is bipartite: " << bipartite_graph.is_bipartite() << std::endl; + + // Test graph properties + std::cout << "Complete graph density: " << complete.density() << std::endl; + std::cout << "Cycle graph diameter: " << cycle.diameter() << std::endl; + + // Test degree centrality + FixedVector degrees; + complete.degree_centrality(degrees); + std::cout << "Degree centrality of node 0 in complete graph: " << degrees[0] << std::endl; + + // Test topological sort + using DirectedGraph = fixed_containers::FixedGraph>; + DirectedGraph dag_graph; + auto ts1 = dag_graph.add_node(0); + auto ts2 = dag_graph.add_node(1); + auto ts3 = dag_graph.add_node(2); + auto ts4 = dag_graph.add_node(3); + dag_graph.add_edge(ts1, ts2); + dag_graph.add_edge(ts1, ts3); + dag_graph.add_edge(ts2, ts4); + dag_graph.add_edge(ts3, ts4); + + FixedVector topo_order; + dag_graph.topological_sort(topo_order); + std::cout << "Topological sort: "; + for (auto node : topo_order) std::cout << node << " "; + std::cout << std::endl; + + // Test strongly connected components + DirectedGraph scc_graph; + auto scc1 = scc_graph.add_node(0); + auto scc2 = scc_graph.add_node(1); + auto scc3 = scc_graph.add_node(2); + auto scc4 = scc_graph.add_node(3); + scc_graph.add_edge(scc1, scc2); + scc_graph.add_edge(scc2, scc3); + scc_graph.add_edge(scc3, scc1); + scc_graph.add_edge(scc3, scc4); + + FixedVector, 10> sccs; + scc_graph.strongly_connected_components(sccs); + std::cout << "Number of strongly connected components: " << sccs.size() << std::endl; + + // Test graph coloring + FixedVector colors; + complete.greedy_coloring(colors); + std::cout << "Graph coloring used " << *std::max_element(colors.begin(), colors.end()) + 1 << " colors" << std::endl; +} + +void test_pool() +{ + std::cout << "\n=== Testing Pool Representation ===\n"; + + PoolGraph pg; + auto pn0 = pg.add_node(0); + auto pn1 = pg.add_node(1); + auto pn2 = pg.add_node(2); + + pg.add_edge(pn0, pn1); + pg.add_edge(pn1, pn2); + + std::cout << "Pool graph node_count: " << pg.node_count() << std::endl; + FixedVector pneigh{}; + pg.neighbors(pn0, pneigh); + std::cout << "Pool graph neighbors of 0: " << pneigh.size() << std::endl; + std::cout << "Pool graph has edge 0-1: " << pg.has_edge(pn0, pn1) << std::endl; + + std::cout << "Pool graph BFS from 0: "; + pg.bfs(pn0, [](auto idx) { std::cout << idx << " "; }); + std::cout << std::endl; + + std::cout << "Pool graph DFS from 0: "; + pg.dfs(pn0, [](auto idx) { std::cout << idx << " "; }); + std::cout << std::endl; + + FixedVector ppath; + pg.shortest_path(pn0, pn2, ppath); + std::cout << "Pool graph shortest path 0 to 2: "; + for (auto p : ppath) std::cout << p << " "; + std::cout << std::endl; + + std::cout << "Pool graph has cycles: " << pg.has_cycles() << std::endl; +} + +} // namespace + +int main() +{ + test_basic(); + test_new_features(); + test_pool(); + return 0; +} \ No newline at end of file