Adding DAG visitor and some unit tests around its function

This commit is contained in:
Ian Roddis
2021-06-07 11:54:50 -03:00
parent e36b3a2012
commit 2030368c22
6 changed files with 181 additions and 46 deletions

View File

@@ -23,8 +23,8 @@ namespace daggy {
// Vertices // Vertices
void addVertex(T id); void addVertex(T id);
void dropVertex(const T & id); void dropVertex(const T & id);
void setVertexVisited(const T & id, bool visited); const std::unordered_set<T> & getChildren(const T & id) const;
void setDAGVisited(bool visited); const std::unordered_set<T> & getRoots() const;
// Edges // Edges
void addEdge(const T & src, const T & dst); void addEdge(const T & src, const T & dst);
@@ -33,24 +33,14 @@ namespace daggy {
// Returns the path from {from} to {to} // Returns the path from {from} to {to}
std::deque<T> shortestPath(const T & from, const T & to); std::deque<T> shortestPath(const T & from, const T & to);
// Traversal // Attributes
void initTraversal(std::vector<const T&> ids = {}); size_t size() const;
const T & visitNextVertex(); bool empty() const;
private: private:
struct Vertex { std::unordered_map<T, std::unordered_set<T>> vertices_;
// It's a bit redundant to preserve both, but std::unordered_set<T> roots_;
// it makes it possible with and against the
// directions possible
std::unordered_set<T> parents;
std::unordered_set<T> children;
bool visited;
};
std::unordered_map<T, Vertex> vertices;
std::unordered_set<T> roots;
std::unordered_set<T> active_nodes;
}; };
#include "DAG.impl" #include "DAGImpl.hpp"
} }

View File

@@ -1,45 +1,45 @@
template<typename T>
size_t DAG<T>::size() const {
return vertices_.size();
}
template<typename T>
bool DAG<T>::empty() const {
return vertices_.empty();
}
template<typename T> template<typename T>
void DAG<T>::addVertex(T id) { void DAG<T>::addVertex(T id) {
if (vertices.find(id) != vertices.end()) if (vertices_.find(id) != vertices_.end())
throw std::runtime_error("Vertex already exists in graph"); throw std::runtime_error("Vertex already exists in graph");
vertices.emplace(id, Vertex{.visited = false}); vertices_[id];
roots.insert(id); roots_.insert(id);
} }
template<typename T> template<typename T>
void DAG<T>::dropVertex(const T & id) { void DAG<T>::dropVertex(const T & id) {
auto it = vertices.find(id); vertices_.extract(id);
auto vert = vertices.extract(id); roots_.extract(id);
for (auto cid : vert.children) dropEdge(id, cid);
for (auto pid : vert.parents) dropEdge(pid, id);
// Remove anything
roots.extract(id);
} }
template<typename T> template<typename T>
void DAG<T>::dropEdge(const T & from, const T & to) { void DAG<T>::dropEdge(const T & from, const T & to) {
auto & src = vertices.at(from); auto & src = vertices_.at(from);
auto & dst = vertices.at(to); src.extract(to);
roots_.extract(to);
src.children.extract(to);
dst.parents.extract(from);
roots.extract(to);
} }
template<typename T> template<typename T>
void DAG<T>::addEdge(const T & from, const T & to) { void DAG<T>::addEdge(const T & from, const T & to) {
auto & src = vertices.at(from); auto & src = vertices_.at(from);
auto & dst = vertices.at(to);
if (shortestPath(to, from).size() > 1) { if (shortestPath(to, from).size() > 1) {
throw std::runtime_error("Unable to add edge that would result in a cycle"); throw std::runtime_error("Unable to add edge that would result in a cycle");
} }
// Add the edge // Add the edge
src.children.insert(to); src.insert(to);
dst.parents.insert(from); roots_.extract(to);
} }
template<typename T> template<typename T>
@@ -48,16 +48,28 @@ std::deque<T> DAG<T>::shortestPath(const T & from, const T & to) {
if (from == to) return {to}; if (from == to) return {to};
auto & src = vertices.at(from); auto & src = vertices_.at(from);
for (const auto & cid : src.children) { for (const auto & cid : src) {
auto pth = shortestPath(cid, to); auto pth = shortestPath(cid, to);
if (subpath.size() == 0 or subpath.size() > pth.size()) if (subpath.size() == 0 or subpath.size() > pth.size())
subpath.swap(pth); subpath.swap(pth);
} }
if (subpath.size() == 0) return subpath;
subpath.push_front(from); subpath.push_front(from);
return subpath; return subpath;
} }
template<typename T>
const std::unordered_set<T> & DAG<T>::getChildren(const T & id) const {
return vertices_.at(id);
}
template<typename T>
const std::unordered_set<T> & DAG<T>::getRoots() const {
return roots_;
}

View File

@@ -0,0 +1,36 @@
#pragma once
#include <iostream>
#include <deque>
#include <stdexcept>
#include <unordered_map>
#include <unordered_set>
#include <iterator>
#include <functional>
#include "DAG.hpp"
/*
The DAG structure in daggy is just to ensure that tasks are run
in the correct dependent order.
*/
namespace daggy {
template<typename T>
class DAGVisitor {
public:
DAGVisitor(const DAG<T> & dag, std::unordered_set<T> roots = {});
std::optional<const T> visitNext(); // Get the next ID
void completeVisit(const T& id); // Mark the ID
bool isComplete() const; // True if the graph has been fully traversed
private:
const DAG<T> & dag_;
std::deque<T> queued_;
std::unordered_set<T> processing_;
std::unordered_set<T> complete_;
};
#include "DAGVisitorImpl.hpp"
}

View File

@@ -0,0 +1,42 @@
template<typename T>
DAGVisitor<T>::DAGVisitor(const DAG<T> & dag, std::unordered_set<T> roots)
: dag_(dag) {
if (roots.size() == 0) {
for (const auto & id : dag_.getRoots()) {
queued_.emplace_back(id);
}
} else {
for (auto & id : roots) {
queued_.push_back(id);
}
}
}
template<typename T>
std::optional<const T> DAGVisitor<T>::visitNext() {
if (queued_.empty()) return {};
const auto & id = queued_.front();
processing_.insert(id);
queued_.pop_front();
return id;
}; // Get the next ID
template<typename T>
void DAGVisitor<T>::completeVisit(const T& id) {
auto entry = processing_.extract(id);
complete_.insert(entry.value());
for (const auto & c : dag_.getChildren(id)) {
if (complete_.find(c) == complete_.end()) {
queued_.push_back(c);
}
}
}
template<typename T>
bool DAGVisitor<T>::isComplete() const {
bool complete = queued_.empty();
for (const auto & p : processing_) {
complete &= dag_.getChildren(p).empty();
}
return complete;
};

View File

@@ -1,9 +1,30 @@
#include <iostream> #include <iostream>
#include "daggy/DAG.hpp" #include "daggy/DAG.hpp"
#include "daggy/DAGVisitor.hpp"
#include "catch.hpp" #include "catch.hpp"
TEST_CASE("DAG Construction Tests", "[dag]") {
daggy::DAG<int> dag;
REQUIRE(dag.size() == 0);
REQUIRE(dag.empty());
REQUIRE_NOTHROW(dag.addVertex(0));
for (int i = 1; i < 10; ++i) {
dag.addVertex(i);
dag.addEdge(i-1, i);
}
REQUIRE(dag.getRoots().size() == 1);
REQUIRE(dag.size() == 10);
REQUIRE(! dag.empty());
// Cannot add an edge that would result in a cycle
REQUIRE_THROWS(dag.addEdge(9, 5));
}
TEST_CASE("DAG Basic Tests", "[dag]") { TEST_CASE("DAG Basic Tests", "[dag]") {
daggy::DAG<int> dag; daggy::DAG<int> dag;
@@ -13,10 +34,10 @@ TEST_CASE("DAG Basic Tests", "[dag]") {
dag.addEdge(i-1, i); dag.addEdge(i-1, i);
} }
REQUIRE(dag.shortestPath(0,9).size() == 10); SECTION("Pathing") {
REQUIRE(dag.shortestPath(0,9).size() == 10);
dag.addEdge(5, 9); dag.addEdge(5, 9);
REQUIRE(dag.shortestPath(0,9).size() == 7); REQUIRE(dag.shortestPath(0,9).size() == 7);
}
REQUIRE_THROWS(dag.addEdge(9, 5));
} }

View File

@@ -0,0 +1,34 @@
#include <iostream>
#include "daggy/DAGVisitor.hpp"
#include "catch.hpp"
TEST_CASE("DAG Visitor Tests", "[dagvisitor]") {
daggy::DAG<int> dag;
REQUIRE_NOTHROW(dag.addVertex(0));
for (int i = 1; i < 10; ++i) {
dag.addVertex(i);
dag.addEdge(i-1, i);
}
// Add an edge to make it interesting
dag.addEdge(5, 7);
// for (auto v : dag.getRoots()) std::cout << " " << v << std::endl;
SECTION("Basic traversal") {
daggy::DAGVisitor visitor(dag);
REQUIRE(! visitor.isComplete());
size_t i = 0;
while (! visitor.isComplete()) {
auto id = visitor.visitNext();
REQUIRE(id.has_value());
visitor.completeVisit(id.value());
++i;
}
REQUIRE(i == 10);
}
}