From 9243ded17ba0c1f836477d85e852b50e24d73235 Mon Sep 17 00:00:00 2001 From: Shaun Reed Date: Thu, 22 Jul 2021 11:15:44 -0400 Subject: [PATCH] Add templated graph example --- cpp/algorithms/graphs/CMakeLists.txt | 4 +- .../graphs/templated/CMakeLists.txt | 22 + cpp/algorithms/graphs/templated/graph.cpp | 163 ++++++ cpp/algorithms/graphs/templated/lib-graph.cpp | 12 + cpp/algorithms/graphs/templated/lib-graph.hpp | 464 ++++++++++++++++++ 5 files changed, 664 insertions(+), 1 deletion(-) create mode 100644 cpp/algorithms/graphs/templated/CMakeLists.txt create mode 100644 cpp/algorithms/graphs/templated/graph.cpp create mode 100644 cpp/algorithms/graphs/templated/lib-graph.cpp create mode 100644 cpp/algorithms/graphs/templated/lib-graph.hpp diff --git a/cpp/algorithms/graphs/CMakeLists.txt b/cpp/algorithms/graphs/CMakeLists.txt index 77a081a..a01c305 100644 --- a/cpp/algorithms/graphs/CMakeLists.txt +++ b/cpp/algorithms/graphs/CMakeLists.txt @@ -15,5 +15,7 @@ project ( LANGUAGES CXX ) -add_subdirectory(simple) add_subdirectory(object) +add_subdirectory(simple) +add_subdirectory(templated) +add_subdirectory(weighted) diff --git a/cpp/algorithms/graphs/templated/CMakeLists.txt b/cpp/algorithms/graphs/templated/CMakeLists.txt new file mode 100644 index 0000000..7b7e808 --- /dev/null +++ b/cpp/algorithms/graphs/templated/CMakeLists.txt @@ -0,0 +1,22 @@ +################################################################################ +## Author: Shaun Reed ## +## Legal: All Content (c) 2021 Shaun Reed, all rights reserved ## +## About: A basic CMakeLists to test templated graph implementation ## +## ## +## Contact: shaunrd0@gmail.com | URL: www.shaunreed.com | GitHub: shaunrd0 ## +################################################################################ +# + +cmake_minimum_required(VERSION 3.15) + +project( + #[[NAME]] TemplatedGraph + VERSION 1.0 + DESCRIPTION "Practice implementing and using templated graphs in C++" + LANGUAGES CXX +) + +#add_library(lib-graph-templated "lib-graph.cpp") + +add_executable(graph-test-templated "graph.cpp") +#target_link_libraries(graph-test-templated lib-graph-templated) diff --git a/cpp/algorithms/graphs/templated/graph.cpp b/cpp/algorithms/graphs/templated/graph.cpp new file mode 100644 index 0000000..ed22f64 --- /dev/null +++ b/cpp/algorithms/graphs/templated/graph.cpp @@ -0,0 +1,163 @@ +/*############################################################################## +## Author: Shaun Reed ## +## Legal: All Content (c) 2021 Shaun Reed, all rights reserved ## +## About: An example of a weighted graph implementation ## +## Algorithms in this example are found in MIT Intro to Algorithms ## +## ## +## Contact: shaunrd0@gmail.com | URL: www.shaunreed.com | GitHub: shaunrd0 ## +################################################################################ +*/ + +#include "lib-graph.hpp" + + +int main (const int argc, const char * argv[]) +{ + // We could initialize the graph with some localNodes... + std::vector> localNodes{ + {'a', {{'b', 0}, {'e', 0}}}, // Node a + {'b', {{'a', 0}, {'f', 0}}}, // Node b + {'c', {{'d', 0}, {'f', 0}, {'g', 0}}}, + {'d', {{'c', 0}, {'g', 0}, {'h', 0}}}, + {'e', {{'a', 0}}}, + {'f', {{'b', 0}, {'c', 0}, {'g', 0}}}, + {'g', {{'c', 0}, {'d', 0}, {'f', 0}, {'h', 0}}}, + {'h', {{'d', 0}, {'f', 0}}}, + }; + Graph bfsGraphInit(localNodes); + + + std::cout << "\n\n##### Breadth First Search #####\n"; + // Or we could use an initializer list... + // Initialize a example graph for Breadth First Search + Graph bfsGraph( + { + {'a', {{'b', 0}, {'e', 0}}}, // Node a + {'b', {{'a', 0}, {'f', 0}}}, // Node b + {'c', {{'d', 0}, {'f', 0}, {'g', 0}}}, + {'d', {{'c', 0}, {'g', 0}, {'h', 0}}}, + {'e', {{'a', 0}}}, + {'f', {{'b', 0}, {'c', 0}, {'g', 0}}}, + {'g', {{'c', 0}, {'d', 0}, {'f', 0}, {'h', 0}}}, + {'h', {{'d', 0}, {'f', 0}}}, + } + ); + // The graph traversed in this example is seen in MIT Intro to Algorithms + // + Chapter 22, Figure 22.3 on BFS + bfsGraph.BFS(bfsGraph.GetNodeCopy('b')); + + std::cout << "\nTesting finding a path between two nodes using BFS...\n"; + // Test finding a path between two nodes using BFS + auto path = bfsGraph.PathBFS( + bfsGraph.GetNodeCopy('a'), bfsGraph.GetNodeCopy('g') + ); + // If we were returned an empty path, it doesn't exist + if (path.empty()) std::cout << "No valid path found!\n"; + else { + // If we were returned a path, print it + std::cout << "\nValid path from " << path.front().GetData() + << " to " << path.back().GetData() << ": "; + for (const auto &node : path) { + std::cout << node.GetData() << " "; + } + std::cout << std::endl; + } + + + std::cout << "\n\n##### Depth First Search #####\n"; + // Initialize an example graph for Depth First Search + Graph dfsGraph( + { + {'a', {{'b', 0}, {'d', 0}}}, + {'b', {{'e', 0}}}, + {'c', {{'e', 0}, {'f', 0}}}, + {'d', {{'b', 0}}}, + {'e', {{'d', 0}}}, + {'f', {{'f', 0}}}, + } + ); + // The graph traversed in this example is seen in MIT Intro to Algorithms + // + Chapter 22, Figure 22.4 on DFS + dfsGraph.DFS(); + + + std::cout << "\n\n##### Topological Sort #####\n"; + // Initialize an example graph for Depth First Search + // + The order of initialization is important + // + To produce the same result as seen in the book + // ++ If the order is changed, other valid topological orders will be found + // The book starts on the 'shirt' node (with the number 6, in this example) + Graph topologicalGraph ( + { + {"undershorts", {{"pants", 0}, {"shoes", 0}}}, + {"socks", {{"shoes", 0}}}, + {"watch", {}}, + {"pants", {{"shoes", 0}, {"belt", 0}}}, + {"shoes", {}}, + {"shirt", {{"tie", 0}, {"belt", 0}}}, + {"belt", {{"jacket", 0}}}, + {"tie", {{"jacket", 0}}}, + {"jacket", {}}, + } + ); + + // The graph traversed in this example is seen in MIT Intro to Algorithms + // + Chapter 22, Figure 22.4 on DFS + // Unlike the simple-graph example, this final result matches MIT Algorithms + // + Aside from the placement of the watch node, which is not connected + // + This is because the node is visited after all other nodes are finished + std::vector> order = + topologicalGraph.TopologicalSort(topologicalGraph.GetNodeCopy("shirt")); + std::cout << "\nTopological order: "; + while (!order.empty()) { + std::cout << order.back().GetData() << " "; + order.pop_back(); + } + std::cout << std::endl << std::endl; + + // If we want the topological order to match what is seen in the book + // + We have to initialize the graph carefully to get this result - + Graph topologicalGraph2 ( + { + {"shirt", {{"tie", 0}, {"belt", 0}}}, + {"tie", {{"jacket", 0}}}, + {"belt", {{"jacket", 0}}}, + {"jacket", {}}, + {"watch", {}}, + {"undershorts", {{"pants", 0}, {"shoes", 0}}}, + {"pants", {{"shoes", 0}, {"belt", 0}}}, + {"shoes", {}}, + {"socks", {{"shoes", 0}}}, + } + ); + auto order2 = topologicalGraph2.TopologicalSort(*topologicalGraph2.NodeBegin()); + std::cout << "\nTopological order: "; + while (!order2.empty()) { + std::cout << order2.back().GetData() << " "; + order2.pop_back(); + } + std::cout << std::endl; + + + std::cout << "\n\n##### Minimum Spanning Trees #####\n"; + // This example graph is seen in MIT Algorithms chapter 23, figure 23.4 + // + The result we produce is the same in total weight + // + Differs only in the connection of nodes (b->c) *instead of* (h->a) + // ++ Both of these edges have the same weight, and we do not create a cycle + Graph graphMST( + { + {'a', {{'b', 4}}}, + {'b', {{'c', 8}}}, + {'c', {{'d', 7}}}, + {'d', {{'e', 9}}}, + {'e', {{'f', 10}}}, + {'f', {{'c', 4}, {'d', 14}, {'g', 2}}}, + {'g', {{'h', 1}}}, + {'h', {{'a', 8}, {'b', 11}, {'i', 7}}}, + {'i', {{'c', 2}, {'g', 6}}} + } + ); + InfoMST resultMST = graphMST.KruskalMST(); + std::cout << "Finding MST using Kruskal's...\n\n"; + resultMST.Print(); +} diff --git a/cpp/algorithms/graphs/templated/lib-graph.cpp b/cpp/algorithms/graphs/templated/lib-graph.cpp new file mode 100644 index 0000000..5fffd73 --- /dev/null +++ b/cpp/algorithms/graphs/templated/lib-graph.cpp @@ -0,0 +1,12 @@ +/*############################################################################## +## Author: Shaun Reed ## +## Legal: All Content (c) 2021 Shaun Reed, all rights reserved ## +## About: Driver program to test object graph implementation ## +## ## +## Contact: shaunrd0@gmail.com | URL: www.shaunreed.com | GitHub: shaunrd0 ## +################################################################################ +*/ + +#include "lib-graph.hpp" + + diff --git a/cpp/algorithms/graphs/templated/lib-graph.hpp b/cpp/algorithms/graphs/templated/lib-graph.hpp new file mode 100644 index 0000000..1f83cc9 --- /dev/null +++ b/cpp/algorithms/graphs/templated/lib-graph.hpp @@ -0,0 +1,464 @@ +/*############################################################################## +## Author: Shaun Reed ## +## Legal: All Content (c) 2021 Shaun Reed, all rights reserved ## +## About: An example of an object graph implementation ## +## Algorithms in this example are found in MIT Intro to Algorithms ## +## ## +## Contact: shaunrd0@gmail.com | URL: www.shaunreed.com | GitHub: shaunrd0 ## +################################################################################ +*/ +#ifndef LIB_GRAPH_HPP +#define LIB_GRAPH_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + + +/******************************************************************************/ +// Base struct for storing traversal information on all nodes + +template struct Node; + +// Color represents the discovery status of any given node +// + White is undiscovered, Gray is in progress, Black is fully discovered +enum Color {White, Gray, Black}; + +// Information used in all searches +struct SearchInfo { + // Coloring of the nodes is used in both DFS and BFS + Color discovered = White; +}; + + +/******************************************************************************/ +// BFS search information structs + +// Information that is only used in BFS +template +struct BFS : SearchInfo { + // Used to represent distance from start node + int distance = 0; + // Used to represent the parent node that discovered this node + // + If we use this node as the starting point, this will remain a nullptr + const Node *predecessor = nullptr; +}; + + +/******************************************************************************/ +// DFS search information structs + +// Information that is only used in DFS +struct DFS : SearchInfo { + // Create a pair to track discovery / finish time + // + Discovery time is the iteration the node is first discovered + // + Finish time is the iteration the node has been checked completely + // ++ A finished node has considered all adjacent nodes + std::pair discoveryFinish; +}; + + +/******************************************************************************/ +// Alias types for storing search information structures + +// Store search information in unordered_maps so we can pass it around easily +// + Allows each node to store relative information on the traversal +template using InfoBFS = std::unordered_map>; +template using InfoDFS = std::unordered_map; +// Edges stored as multimap> +template using Edges = std::multimap>; + + +/******************************************************************************/ +// MST search information structs + +struct MST : SearchInfo { + int32_t parent = INT32_MIN; + int rank = 0; +}; + +template +struct InfoMST { + template friend class Graph; + + explicit InfoMST(const std::vector> &nodes) { + for (const auto &node : nodes){ + // Initialize the default values for forest tracked by this struct + // + This data is used in KruskalMST() to find the MST + MakeSet(node.data_); + for (const auto adj : node.adjacent_) { + // node.number is the number that represents this node + // adj.first is the node number that is connected to this node + // adj.second is the weight of the connected edge + edges_.emplace(adj.second, std::make_pair(node.data_, adj.first)); + // So we initialize the multimap> + // + Since a multimap sorts by key, we have sorted our edges by weight + } + } + } + + void Print() + { + std::cout << "MST result: \n"; + for (const auto &edge : edgesMST_) { + std::cout << "Connected nodes: " << edge.second.first << "->" + << edge.second.second << " with weight of " << edge.first << "\n"; + } + std::cout << "Total MST weight: " << weightMST_ << std::endl; + } + + void MakeSet(T x) + { + searchInfo[x].parent = x; + searchInfo[x].rank = 0; + } + + void Union(T x, T y) + { + Link(FindSet(x), FindSet(y)); + } + + void Link(T x, T y) + { + if (searchInfo[x].rank > searchInfo[y].rank) { + searchInfo[y].parent = x; + } + else { + searchInfo[x].parent = y; + if (searchInfo[x].rank == searchInfo[y].rank) { + searchInfo[y].rank += 1; + } + } + } + + T FindSet(T x) + { + if (x != searchInfo[x].parent) { + searchInfo[x].parent = FindSet(searchInfo[x].parent); + } + return searchInfo[x].parent; + } + +private: + std::unordered_map searchInfo; + // All of the edges within our graph + // + Since each node stores its own edges, this is initialized in InfoMST ctor + Edges edges_; + + // A multimap of the edges found for our MST + Edges edgesMST_; + // The total weight of our resulting MST + int weightMST_ = 0; +}; + + +/******************************************************************************/ +// Node structure for representing a graph + +template +struct Node { +public: + template friend class Graph; + template friend class InfoMST; + + // Constructors + Node(const Node &rhs) = default; + Node & operator=(Node rhs) { + if (this == &rhs) return *this; + swap(*this, rhs); + return *this; + } + Node(T data, const std::vector> &adj) + : data_(data) + { + // Place each adjacent node in vector into our unordered_map of edges + for (const auto &i : adj) adjacent_.emplace(i.first, i.second); + } + + friend void swap(Node &a, Node &b) { + std::swap(a.data_, b.data_); + std::swap(a.adjacent_, b.adjacent_); + } + + // Operators + // Define operator== for std::find; And comparisons between nodes + bool operator==(const Node &b) const { return this->data_ == b.data_;} + // Define an operator!= for comparing nodes for inequality + bool operator!=(const Node &b) const { return this->data_ != b.data_;} + + // Accessors + inline T GetData() const { return data_;} + inline std::unordered_map GetAdjacent() const { return adjacent_;} + +private: + T data_; + // Adjacent stored in an unordered_map + std::unordered_map adjacent_; +}; + + +/******************************************************************************/ +// Templated graph class + +template +class Graph { +public: + // Constructor + Graph(std::vector> nodes) : nodes_(std::move(nodes)) {} + + // Breadth First Search + InfoBFS BFS(const Node& startNode) const; + + std::deque> PathBFS(const Node &start, const Node &finish) const; + + // Depth First Search + InfoDFS DFS() const; + // An alternate DFS that checks each node of the graph beginning at startNode + InfoDFS DFS(const Node &startNode) const; + // Visit function is used in both versions of DFS + void DFSVisit(int &time, const Node& startNode, InfoDFS &searchInfo) const; + // Topological sort, using DFS + std::vector> TopologicalSort(const Node &startNode) const; + // Kruskal's MST + InfoMST KruskalMST() const; + + // Returns a copy of a node with the number i within the graph + // + This uses the private, non-const accessor GetNode() and returns a copy + inline Node GetNodeCopy(T i) { return GetNode(i);} + // Return a constant iterator for reading node values + inline typename std::vector>::const_iterator NodeBegin() + { return nodes_.cbegin();} + +private: + // A non-const accessor for direct access to a node with the number value i + inline Node & GetNode(T i) + { return *std::find(nodes_.begin(), nodes_.end(), Node(i, {}));} + // For grabbing a const qualified node + inline const Node & GetNode(T i) const + { return *std::find(nodes_.begin(), nodes_.end(), Node(i, {}));} + + std::vector> nodes_; +}; + + +/******************************************************************************/ +// Graph class member function definitions + +template +InfoBFS Graph::BFS(const Node &startNode) const +{ + // Create local object to track the information gathered during traversal + InfoBFS searchInfo; + + // Create a queue to visit discovered nodes in FIFO order + std::queue *> visitQueue; + + // Mark the startNode as in progress until we finish checking adjacent nodes + searchInfo[startNode.data_].discovered = Gray; + + // Visit the startNode + visitQueue.push(&startNode); + + // Continue to visit nodes until there are none left in the graph + while (!visitQueue.empty()) { + // Remove thisNode from the visitQueue, storing its vertex locally + const Node * thisNode = visitQueue.front(); + visitQueue.pop(); + std::cout << "Visiting node " << thisNode->data_ << std::endl; + + // Check if we have already discovered all the adjacentNodes to thisNode + for (const auto &adjacent : thisNode->adjacent_) { + if (searchInfo[adjacent.first].discovered == White) { + std::cout << "Found undiscovered adjacentNode: " << adjacent.first + << "\n"; + // Mark the adjacent node as in progress + searchInfo[adjacent.first].discovered = Gray; + searchInfo[adjacent.first].distance = + searchInfo[thisNode->data_].distance + 1; + searchInfo[adjacent.first].predecessor = + &GetNode(thisNode->data_); + + // Add the discovered node the the visitQueue + visitQueue.push(&GetNode(adjacent.first)); + } + } + // We are finished with this node and the adjacent nodes; Mark it discovered + searchInfo[thisNode->data_].discovered = Black; + } + + // Return the information gathered from this search, JIC caller needs it + return searchInfo; +} + +template +std::deque> Graph::PathBFS(const Node &start, + const Node &finish) const +{ + // Store the path as copies of each node + // + If the caller modifies these, it will not impact the graph's data + std::deque> path; + + InfoBFS searchInfo = BFS(start); + const Node * next = searchInfo[finish.data_].predecessor; + bool isValid = false; + do { + // If we have reached the start node, we have found a valid path + if (*next == Node(start)) isValid = true; + + // Add the node to the path as we check each node + // + Use emplace_front to call the Node copy constructor + path.emplace_front(*next); + + // Move to the next node + next = searchInfo[next->data_].predecessor; + } while (next != nullptr); + // Use emplace_back to call Node copy constructor + path.emplace_back(finish); + + // If we never found a valid path, erase all contents of the path + if (!isValid) path.erase(path.begin(), path.end()); + + // Return the path, the caller should handle empty paths accordingly + return path; +} + +template +InfoDFS Graph::DFS() const +{ + // Track the nodes we have discovered + InfoDFS searchInfo; + int time = 0; + + // Visit each node in the graph + for (const auto& node : nodes_) { + std::cout << "Visiting node " << node.data_ << std::endl; + // If the node is undiscovered, visit it + if (searchInfo[node.data_].discovered == White) { + std::cout << "Found undiscovered node: " << node.data_ << std::endl; + // Visiting the undiscovered node will check it's adjacent nodes + DFSVisit(time, node, searchInfo); + } + } + + return searchInfo; +} + +template +InfoDFS Graph::DFS(const Node &startNode) const +{ + // Track the nodes we have discovered + InfoDFS searchInfo; + int time = 0; + + auto startIter = std::find(nodes_.begin(), nodes_.end(), + Node(startNode.data_, {}) + ); + + // beginning at startNode, visit each node in the graph until we reach the end + while (startIter != nodes_.end()) { + std::cout << "Visiting node " << startIter->data_ << std::endl; + // If the startIter is undiscovered, visit it + if (searchInfo[startIter->data_].discovered == White) { + std::cout << "Found undiscovered node: " << startIter->data_ << std::endl; + // Visiting the undiscovered node will check it's adjacent nodes + DFSVisit(time, *startIter, searchInfo); + } + startIter++; + } + + // Once we reach the last node, check the beginning for unchecked nodes + startIter = nodes_.begin(); + + // Once we reach the initial startNode, we have checked all nodes + while (*startIter != startNode) { + std::cout << "Visiting node " << startIter->data_ << std::endl; + // If the startIter is undiscovered, visit it + if (searchInfo[startIter->data_].discovered == White) { + std::cout << "Found undiscovered node: " << startIter->data_ << std::endl; + // Visiting the undiscovered node will check it's adjacent nodes + DFSVisit(time, *startIter, searchInfo); + } + startIter++; + } + + return searchInfo; +} + +template +void Graph::DFSVisit(int &time, const Node& startNode, + InfoDFS &searchInfo) const +{ + searchInfo[startNode.data_].discovered = Gray; + time++; + searchInfo[startNode.data_].discoveryFinish.first = time; + + // Check the adjacent nodes of the startNode + for (const auto &adjacent : startNode.adjacent_) { + auto iter = std::find(nodes_.begin(), nodes_.end(), + Node(adjacent.first, {})); + // If the adjacentNode is undiscovered, visit it + // + Offset by 1 to account for 0 index of discovered vector + if (searchInfo[iter->data_].discovered == White) { + std::cout << "Found undiscovered adjacentNode: " + << GetNode(adjacent.first).data_ << std::endl; + // Visiting the undiscovered node will check it's adjacent nodes + DFSVisit(time, *iter, searchInfo); + } + } + searchInfo[startNode.data_].discovered = Black; + time++; + searchInfo[startNode.data_].discoveryFinish.second = time; +} + +template +std::vector> Graph::TopologicalSort(const Node &startNode) const +{ + InfoDFS topological = DFS(GetNode(startNode.data_)); + + std::vector> order(nodes_); + + auto comp = [&topological](const Node &a, const Node &b) { + return (topological[a.data_].discoveryFinish.second < + topological[b.data_].discoveryFinish.second); + }; + + std::sort(order.begin(), order.end(), comp); + + // The topologicalOrder is read right-to-left in the final result + // + Output is handled in main as FILO, similar to a stack + return order; +} + +template +InfoMST Graph::KruskalMST() const +{ + InfoMST searchInfo(nodes_); + // The ctor for InfoMST initializes all edges within the graph into a multimap + // + Key for multimap is edge weight, so they're already sorted in ascending + + // For each edge in the graph, check if they are part of the same tree + // + Since we do not want to create a cycle in the MST forest - + // + we don't connect nodes that are part of the same tree + for (const auto &edge : searchInfo.edges_) { + // Two integers representing the node.number for the connected nodes + const int u = edge.second.first; + const int v = edge.second.second; + // Check if the nodes are of the same tree + if (searchInfo.FindSet(u) != searchInfo.FindSet(v)) { + // If they are not, add the edge to our MST + searchInfo.edgesMST_.emplace(edge); + searchInfo.weightMST_ += edge.first; + // Update the forest to reflect this change + searchInfo.Union(u, v); + } + } + + return searchInfo; +} + +#endif // LIB_GRAPH_HPP