From f4a8d84b5c43ac424753f77efec93d17471c671a Mon Sep 17 00:00:00 2001 From: Rayan Syed Date: Sat, 28 Mar 2026 04:22:46 -0400 Subject: [PATCH] problem 2 autograder --- ci_cd/.gitlab-ci.yml | 10 ++ ci_cd/problem2.yml | 48 +++++++ tests/Makefile | 32 +++++ tests/impl/Vertex.cpp | 8 ++ tests/include/DirectedGraph.h | 231 ++++++++++++++++++++++++++++++++ tests/include/README.txt | 1 + tests/include/UndirectedGraph.h | 64 +++++++++ tests/include/Vertex.h | 163 ++++++++++++++++++++++ tests/include/VertexGraph.h | 133 ++++++++++++++++++ tests/testMaxPlanarSubgraph.cpp | 87 ++++++++++++ 10 files changed, 777 insertions(+) create mode 100644 ci_cd/.gitlab-ci.yml create mode 100644 ci_cd/problem2.yml create mode 100644 tests/Makefile create mode 100644 tests/impl/Vertex.cpp create mode 100644 tests/include/DirectedGraph.h create mode 100644 tests/include/README.txt create mode 100644 tests/include/UndirectedGraph.h create mode 100644 tests/include/Vertex.h create mode 100644 tests/include/VertexGraph.h create mode 100644 tests/testMaxPlanarSubgraph.cpp diff --git a/ci_cd/.gitlab-ci.yml b/ci_cd/.gitlab-ci.yml new file mode 100644 index 0000000..bc1c0c6 --- /dev/null +++ b/ci_cd/.gitlab-ci.yml @@ -0,0 +1,10 @@ +stages: + - prebuild + - compile + - test + +include: + - local: 'ci_cd/problem2.yml' # Planar Subgraph + +default: + timeout: 5m diff --git a/ci_cd/problem2.yml b/ci_cd/problem2.yml new file mode 100644 index 0000000..3e8917d --- /dev/null +++ b/ci_cd/problem2.yml @@ -0,0 +1,48 @@ +prebuild_problem_2: + stage: prebuild + script: + - | + # Check if source files exist + if [ ! -f "impl/MaxPlanarSubgraph.cpp" ]; then + echo "impl/MaxPlanarSubgraph.cpp does not exist"; + exit 1; + fi + - git clone https://agile.bu.edu/gitlab/configs/ec330/homeworks/homeworksix.git hw6 + artifacts: + paths: + - hw6/ + rules: + - if: '$CI_COMMIT_REF_NAME == "problem2"' + tags: [c++-17] + +compile_problem_2: + stage: compile + needs: + - job: prebuild_problem_2 + artifacts: true + script: + - ls -l hw6/tests/ + - cp impl/MaxPlanarSubgraph.cpp hw6/tests/impl/ + - cd hw6/tests + - make problem2 + artifacts: + paths: + - hw6/ + rules: + - if: '$CI_COMMIT_REF_NAME == "problem2"' + tags: [c++-17] + +exec_problem_2: + stage: test + needs: + - job: compile_problem_2 + artifacts: true + script: + - cd hw6/tests + - ./problem2 + artifacts: + paths: + - hw6/ + rules: + - if: '$CI_COMMIT_REF_NAME == "problem2"' + tags: [c++-17] diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 0000000..741e1a8 --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,32 @@ +# Makefile, generated with support of ChatGPT + +# Compiler and flags +CXX = g++ -O2 +CXXFLAGS = -std=c++17 + +# Source files +PROBLEM2_SRCS = testMaxPlanarSubgraph.cpp impl/MaxPlanarSubgraph.cpp impl/Vertex.cpp +SRCS = $(PROBLEM2_SRCS) + +# Object files +PROBLEM2_OBJS = testMaxPlanarSubgraph.o impl/MaxPlanarSubgraph.o impl/Vertex.o +OBJS = $(PROBLEM2_OBJS) + +# Executable name +PROBLEM2_EXEC = problem2 +EXECS = $(PROBLEM2_EXEC) + +# Default target to build the executable +all: $(PROBLEM2_EXEC) + +# Rule to build the executable from object files +$(PROBLEM2_EXEC): $(PROBLEM2_OBJS) Makefile + $(CXX) $(CXXFLAGS) -o $(PROBLEM2_EXEC) $(PROBLEM2_OBJS) + +# Rules to build object files from source files +%.o: %.cpp Makefile + $(CXX) $(CXXFLAGS) -c $< -o $@ + +# Clean target to remove compiled files +clean: + rm -f $(OBJS) $(EXECS) \ No newline at end of file diff --git a/tests/impl/Vertex.cpp b/tests/impl/Vertex.cpp new file mode 100644 index 0000000..e0e96bf --- /dev/null +++ b/tests/impl/Vertex.cpp @@ -0,0 +1,8 @@ +// +// Created by Ari on 3/25/26. +// +#include "../include/Vertex.h" + +// static variables +int Vertex::ID_COUNT = 0; +set> Vertex::locs; \ No newline at end of file diff --git a/tests/include/DirectedGraph.h b/tests/include/DirectedGraph.h new file mode 100644 index 0000000..90424eb --- /dev/null +++ b/tests/include/DirectedGraph.h @@ -0,0 +1,231 @@ +// +// Created by Ari on 3/25/26. +// + +#ifndef HW6_ADMIN_DIRECTEDGRAPH_H +#define HW6_ADMIN_DIRECTEDGRAPH_H + +#include +#include +#include +#include +#include +#include + +using namespace std; + +/** + * A generic directed graph whose vertices are referenced rather than copied. + * + * The public API uses vertex references, while the internal representation + * stores pointers to vertices. This allows the graph to work with vertex + * types that are movable but not copyable. + * + * @tparam VLT Vertex label type + * @tparam ELT Edge label type + */ +template +class DirectedGraph { +public: + // TYPES + using VertexPtr = const VLT *; + using NeighborMap = unordered_multimap; + + // MANIPULATORS + /** + * Adds a vertex to the graph. + */ + void addVertex(const VLT &v) { vLabels.insert(&v); } + + /** + * Adds a directed edge v1 -> v2 with label weight. + * Missing endpoints are inserted automatically. + */ + virtual void addEdge(const VLT &v1, const VLT &v2, ELT weight) { + addVertex(v1); + addVertex(v2); + eLabels[&v1].emplace(&v2, weight); + } + + // INFORMATIONAL + /** + * @return true iff the graph contains at least one edge v1 -> v2. + */ + [[nodiscard]] bool hasEdge(const VLT &v1, const VLT &v2) const { + auto outer = eLabels.find(&v1); + if (outer == eLabels.end()) + return false; + return outer->second.find(&v2) != outer->second.end(); + } + + /** + * Returns the ii-th parallel edge label from v1 to v2, if it exists. + */ + [[nodiscard]] optional getEdge(const VLT &v1, const VLT &v2, const size_t ii = 0) const { + auto outer = eLabels.find(&v1); + if (outer == eLabels.end()) + return nullopt; + + const auto &inner = outer->second; + auto range = inner.equal_range(&v2); + + size_t count = 0; + for (auto it = range.first; it != range.second; ++it) { + if (count == ii) + return it->second; + ++count; + } + return nullopt; + } + + /** + * @return the number of parallel edges from v1 to v2. + */ + [[nodiscard]] size_t numEdges(const VLT &v1, const VLT &v2) const { + auto outer = eLabels.find(&v1); + if (outer == eLabels.end()) + return 0; + return outer->second.count(&v2); + } + + /** + * @return the total number of edges in the graph. + */ + [[nodiscard]] size_t numEdges() const { + size_t total = 0; + for (const auto &[u, inner]: eLabels) { + (void) u; + total += inner.size(); + } + return total; + } + + /** + * @return the out-degree of v. + */ + [[nodiscard]] size_t outDegree(const VLT &v) const { + auto outer = eLabels.find(&v); + if (outer == eLabels.end()) + return 0; + return outer->second.size(); + } + + /** + * @return the in-degree of v. + */ + [[nodiscard]] size_t inDegree(const VLT &v) const { + size_t count = 0; + for (const auto &[u, inner]: eLabels) { + (void) u; + count += inner.count(&v); + } + return count; + } + + /** + * @return the number of vertices in the graph. + */ + [[nodiscard]] size_t numVerts() const { return vLabels.size(); } + + /** + * Read-only access to the set of stored vertex pointers. + */ + [[nodiscard]] const set &getVerts() const { return vLabels; } + + /** + * Read-only access to the edge map. + */ + [[nodiscard]] const unordered_map &getEdges() const { return eLabels; } + + /** + * @return the outgoing neighbor multimap of v, or an empty map if none. + */ + [[nodiscard]] const NeighborMap &neighbors(const VLT &v) const { + static const NeighborMap empty; + auto outer = eLabels.find(&v); + return outer == eLabels.end() ? empty : outer->second; + } + + /** + * Returns the subgraph induced by the given vertex-pointer set. + */ + [[nodiscard]] virtual DirectedGraph induced(const set &verts) const { + DirectedGraph result; + + for (VertexPtr v: verts) + result.vLabels.insert(v); + + for (VertexPtr u: verts) { + auto outer = eLabels.find(u); + if (outer == eLabels.end()) + continue; + + for (const auto &[v, w]: outer->second) { + if (verts.count(v)) + result.eLabels[u].emplace(v, w); + } + } + + return result; + } + + /** + * String representation of the graph. + */ + [[nodiscard]] string toString() const { + ostringstream oss; + oss << "Graph:\n"; + + for (VertexPtr u: vLabels) { + oss << " " << *u << " -> "; + + auto outer = eLabels.find(u); + if (outer == eLabels.end() || outer->second.empty()) { + oss << "{}\n"; + continue; + } + + vector> edges(outer->second.begin(), outer->second.end()); + + sort(edges.begin(), edges.end(), [](const auto &a, const auto &b) { + if (!(*a.first == *b.first)) + return *a.first < *b.first; + return a.second < b.second; + }); + + oss << "{ "; + for (size_t i = 0; i < edges.size(); ++i) { + const auto &[v, w] = edges[i]; + if (i > 0) + oss << ", "; + oss << "(" << *v << ", " << w << ")"; + } + oss << " }\n"; + } + + return oss.str(); + } + + /** + * Stream access to printing the graph. + */ + friend ostream &operator<<(ostream &os, const DirectedGraph &g) { return os << g.toString(); } + + /** + * Virtual destructor, to make sure all inherited destructors are called. + */ + virtual ~DirectedGraph() = default; + +protected: + /** + * The vertex set. + */ + set vLabels; + + /** + * Maps each source vertex to its outgoing labeled edges. + */ + unordered_map eLabels; +}; + +#endif // HW6_ADMIN_DIRECTEDGRAPH_H diff --git a/tests/include/README.txt b/tests/include/README.txt new file mode 100644 index 0000000..d132f31 --- /dev/null +++ b/tests/include/README.txt @@ -0,0 +1 @@ +Some code in this directory was generated with light assistance or review by ChatGPT. \ No newline at end of file diff --git a/tests/include/UndirectedGraph.h b/tests/include/UndirectedGraph.h new file mode 100644 index 0000000..1f75dda --- /dev/null +++ b/tests/include/UndirectedGraph.h @@ -0,0 +1,64 @@ +// +// Created by Ari Trachtenberg on 3/25/26. +// + +#ifndef HW6_ADMIN_UNDIRECTEDGRAPH_H +#define HW6_ADMIN_UNDIRECTEDGRAPH_H + +#include "DirectedGraph.h" + +/** + * An undirected version of DirectedGraph that allows self-loops + * and parallel edges. + */ +template +class UndirectedGraph : public DirectedGraph { +public: + // TYPES + using Base = DirectedGraph; + using VertexPtr = typename Base::VertexPtr; + + // MANIPULATORS + /** + * Converts a directed graph into an undirected one by ignoring edge directions. + * @param Gdir The DirectedGraph to convert. + */ + static UndirectedGraph makeUndirected(const DirectedGraph &Gdir) { + UndirectedGraph result; + + for (VertexPtr v: Gdir.getVerts()) { + result.addVertex(*v); + } + + for (const auto &[u, inner]: Gdir.getEdges()) { + for (const auto &[v, w]: inner) { + result.addEdge(*u, *v, w); + } + } + + return result; + } + + /** + * Adds an undirected edge between v1 and v2. + * For a self-loop, only one directed copy is stored. + */ + void addEdge(const VLT &v1, const VLT &v2, ELT weight) override { + Base::addEdge(v1, v2, weight); + if (&v1 != &v2) + Base::addEdge(v2, v1, weight); + } + + // INFORMATIONAL + /** + * @return the number of incident edges incident on v. + */ + [[nodiscard]] size_t degree(const VLT &v) const { return Base::inDegree(v); } + + /** + * Virtual destructor, to make sure all inherited destructors are called. + */ + virtual ~UndirectedGraph() = default; +}; + +#endif // HW6_ADMIN_UNDIRECTEDGRAPH_H diff --git a/tests/include/Vertex.h b/tests/include/Vertex.h new file mode 100644 index 0000000..05f59a1 --- /dev/null +++ b/tests/include/Vertex.h @@ -0,0 +1,163 @@ +// +// Created by Ari on 3/25/26. +// + +#ifndef HW6_ADMIN_VERTEX_H +#define HW6_ADMIN_VERTEX_H + +#include +#include +#include +using namespace std; + +/** + * A class that represents a Vertex with Integer coordinates on a plane. + * The coordinates of the Vertex can be adjusted through member functions. + * @note No two vertices can have the same coordinates. + * @note A vertex cannot be copied, only moved. + */ +class Vertex { +public: + // CONSTRUCTORS + + /** + * Creates a new vertex at location (theXX, theYY). + * @throws a runtime error if there is also a vertex at that location. + * @param theXX The x coordinate of the new vertex. + * @param theYY The y coordinate of the next vertex. + */ + Vertex(const int theXX, const int theYY) : xx(theXX), yy(theYY), ID(ID_COUNT++) { + if (inLocs(theXX, theYY)) + throw runtime_error("Vertex already exists at that location"); + locs.insert({xx, yy}); + } + + /** + * Move constructor + */ + Vertex(Vertex &&other) noexcept : xx(other.xx), yy(other.yy), ID(other.ID) { + // update locs + locs.erase({other.xx, other.yy}); + locs.insert({xx, yy}); + } + + // MANIPULATORS + // ... disable copying + Vertex(const Vertex &) = delete; + Vertex &operator=(const Vertex &) = delete; + + /** + * Move assignment + */ + Vertex &operator=(Vertex &&other) noexcept { + if (this != &other) { + // remove current location + locs.erase({xx, yy}); + + xx = other.xx; + yy = other.yy; + ID = other.ID; + + // transfer location + locs.erase({other.xx, other.yy}); + locs.insert({xx, yy}); + } + return *this; + } + + /** + * Attempts to move this \ref Vertex to a new location. + * @param newXX The x coordinate of the new location. + * @param newYY The y coordinate of the new location. + * @throws runtime error if the new location is already taken. + */ + void moveVertex(const int newXX, const int newYY) { + if (inLocs(newXX, newYY)) { + throw runtime_error("Move location is already taken!"); + } + else { + locs.erase({xx, yy}); // out with the old + xx = newXX; + yy = newYY; + locs.insert({xx, yy}); // in with the new + } + } + + // INFORMATIONAL + + /** + * getter for xx + */ + [[nodiscard]] int getX() const { return xx; } + + /** + * getter for yy + */ + [[nodiscard]] int getY() const { return yy; } + + /** + * Equality check of two vertices. + */ + bool operator==(const Vertex &other) const { return ID == other.ID; } + + /** + * Checks whether this \ref Vertex comes before other + * in some arbitrary (but stable) ordering. + */ + bool operator<(const Vertex &other) const { return ID < other.ID; } + + /** + * + * Stream output of a \ref Vertex + */ + friend std::ostream &operator<<(std::ostream &os, const Vertex &v) { + return os << "(" << v.xx << "," << v.yy << ")"; + } + + /** + * @return the ID of this vertex + */ + [[nodiscard]] size_t getID() const { return ID; } + + +private: + // FIELDS + + /** + * planar coordinates of this \ref Vertex + */ + int xx, yy; + + /** + * The unique ID of this \ref Vertex. + */ + int ID; + + /** + * Counts the number of Vertex IDs generated so far. + */ + static int ID_COUNT; + + /** + * The current location of all vertices. + */ + static set> locs; + + /** + * @param xx The x coordinate of the point in question. + * @param yy The y coordinate of the point in question. + * @return true iff (xx,yy) is already taken in the \ref locs map. + */ + static bool inLocs(int xx, int yy) { return locs.find(make_pair(xx, yy)) != locs.end(); } +}; + +/** + * A \ref Vertex hash function, for use in sets of Vertices, for example. + */ +template<> +struct std::hash { + size_t operator()(const Vertex &v) const noexcept { + return v.getID(); // simple sum + } +}; +#endif // HW6_ADMIN_VERTEX_H diff --git a/tests/include/VertexGraph.h b/tests/include/VertexGraph.h new file mode 100644 index 0000000..c87e706 --- /dev/null +++ b/tests/include/VertexGraph.h @@ -0,0 +1,133 @@ +// +// Created by Ari on 3/25/26. +// + +#ifndef HW6_ADMIN_VERTEXGRAPH_H +#define HW6_ADMIN_VERTEXGRAPH_H + +#include + +#include "UndirectedGraph.h" +#include "Vertex.h" + +/** + * Represents a Graph of Vertices on the 2-dimensional Integer plane. + */ +class VertexGraph : public UndirectedGraph { +public: + // TYPES + using Base = UndirectedGraph; + using VertexPtr = const Vertex *; + + // MANIPULATORS + /** + * Adds an undirected edge between two vertices. + */ + void addEdge(const Vertex &v1, const Vertex &v2) { Base::addEdge(v1, v2, true); } + + // INFORMATIONAL + /** + * @return true iff this graph has no crossing edges on the plane. + */ + [[nodiscard]] bool isPlanarDrawingQ() const { + for (const auto &[v1, adj1]: getEdges()) { + for (const auto &[v2, label1]: adj1) { + (void) label1; + + for (const auto &[v3, adj2]: getEdges()) { + for (const auto &[v4, label2]: adj2) { + (void) label2; + + // Ignore the very same undirected edge. + if ((v1 == v3 && v2 == v4) || (v1 == v4 && v2 == v3)) + continue; + + // Edges meeting at a shared endpoint are allowed. + if (v1 == v3 || v1 == v4 || v2 == v3 || v2 == v4) + continue; + + if (segmentsIntersect(v1, v2, v3, v4)) + return false; + } + } + } + } + return true; + } + + /** + * Converts a directed graph into a VertexGraph. + */ + static VertexGraph toVertexGraph(const DirectedGraph &Gdir) { + VertexGraph result; + + for (const VertexPtr v: Gdir.getVerts()) { + result.addVertex(*v); + } + + for (const auto &[u, inner]: Gdir.getEdges()) { + for (const auto &[v, w]: inner) { + if (w) + result.addEdge(*u, *v); + } + } + + return result; + } + + + /** + * @return the set of vertices adjacent to v. + */ + [[nodiscard]] set adjacent(const Vertex &v) const { + set result; + for (const auto &[neighbor, value]: neighbors(v)) { + (void) value; + result.insert(neighbor); + } + return result; + } + + /** + * Virtual destructor, to make sure all inherited destructors are called. + */ + virtual ~VertexGraph() = default; + +private: + // HELPER FUNCTIONS for determining edge crossings + static long long cross(const VertexPtr a, const VertexPtr b, const VertexPtr c) { + return 1LL * (b->getX() - a->getX()) * (c->getY() - a->getY()) - + 1LL * (b->getY() - a->getY()) * (c->getX() - a->getX()); + } + + static bool between(const int a, const int b, const int x) { return min(a, b) <= x && x <= max(a, b); } + + static bool onSegment(const VertexPtr a, const VertexPtr b, const VertexPtr p) { + return cross(a, b, p) == 0 && between(a->getX(), b->getX(), p->getX()) && + between(a->getY(), b->getY(), p->getY()); + } + + /** + * @return true iff segment (v1,v2) intersects with segment (v3,v4), + * including if they touch at one point or overlap. + */ + static bool segmentsIntersect(const VertexPtr v1, const VertexPtr v2, const VertexPtr v3, const VertexPtr v4) { + const long long c1 = cross(v1, v2, v3); + const long long c2 = cross(v1, v2, v4); + const long long c3 = cross(v3, v4, v1); + const long long c4 = cross(v3, v4, v2); + + if (c1 == 0 && onSegment(v1, v2, v3)) + return true; + if (c2 == 0 && onSegment(v1, v2, v4)) + return true; + if (c3 == 0 && onSegment(v3, v4, v1)) + return true; + if (c4 == 0 && onSegment(v3, v4, v2)) + return true; + + return (c1 * c2 < 0) && (c3 * c4 < 0); + } +}; + +#endif // HW6_ADMIN_VERTEXGRAPH_H diff --git a/tests/testMaxPlanarSubgraph.cpp b/tests/testMaxPlanarSubgraph.cpp new file mode 100644 index 0000000..d50b466 --- /dev/null +++ b/tests/testMaxPlanarSubgraph.cpp @@ -0,0 +1,87 @@ +// +// Minimal tests based on main.cpp -- adapted from prev HW's test files by Rayan +// +#include +#include +#include + +#include "include/VertexGraph.h" + +using namespace std; + +set maxPlanarSubgraph(const VertexGraph& G); + +bool failExample(const char* testName, const string& msg, + const string& expected, const string& got) { + cerr << "[" << testName << " FAILED] " + << msg << ": expected " << expected + << ", got " << got << "\n"; + return false; +} + +bool test_0() { + const char* T = "MaxPlanarSubgraph examples"; + + Vertex v1(0,0), v2(0,10), v3(10,10), v4(10,0); + + { + VertexGraph Gplanar; + Gplanar.addEdge(v1,v2); + Gplanar.addEdge(v2,v3); + Gplanar.addEdge(v3,v4); + Gplanar.addEdge(v4,v1); + + if (!Gplanar.isPlanarDrawingQ()) { + return failExample(T, + "square graph should be planar", + "true", "false"); + } + } + + { + VertexGraph K4; + K4.addEdge(v1,v2); + K4.addEdge(v1,v3); + K4.addEdge(v1,v4); + K4.addEdge(v2,v3); + K4.addEdge(v2,v4); + K4.addEdge(v3,v4); + + if (K4.isPlanarDrawingQ()) { + return failExample(T, + "K4 drawn as square with both diagonals should not be planar", + "false", "true"); + } + + set chosen = maxPlanarSubgraph(K4); + VertexGraph induced = VertexGraph::toVertexGraph(K4.induced(chosen)); + + if (!induced.isPlanarDrawingQ()) { + return failExample(T, + "induced graph from maxPlanarSubgraph(K4) should be planar", + "true", "false"); + } + + if (chosen.size() != 3) { + return failExample(T, + "K4 example should return 3 vertices in the planar induced subgraph", + "3", to_string(chosen.size())); + } + } + + return true; +} + +int main() { + bool results[] = { test_0() }; + + bool allPassed = true; + for (size_t ii = 0; ii < std::size(results); ii++) { + cout << "Test of problem " << to_string(ii) << ": " + << (results[ii] ? "passed" : "failed") << endl; + allPassed &= results[ii]; + } + + if (allPassed) exit(0); + else exit(-1); +} \ No newline at end of file -- GitLab