diff --git a/pydatastructs/graphs/_backend/cpp/Algorithms.hpp b/pydatastructs/graphs/_backend/cpp/Algorithms.hpp index a427e744..f006511b 100644 --- a/pydatastructs/graphs/_backend/cpp/Algorithms.hpp +++ b/pydatastructs/graphs/_backend/cpp/Algorithms.hpp @@ -3,10 +3,11 @@ #include #include #include +#include +#include "GraphEdge.hpp" #include "AdjacencyList.hpp" #include "AdjacencyMatrix.hpp" - static PyObject* breadth_first_search_adjacency_list(PyObject* self, PyObject* args, PyObject* kwargs) { PyObject* graph_obj; const char* source_name; @@ -153,3 +154,151 @@ static PyObject* breadth_first_search_adjacency_matrix(PyObject* self, PyObject* Py_RETURN_NONE; } + +static PyObject* minimum_spanning_tree_prim_adjacency_list(PyObject* self, PyObject* args, PyObject* kwargs) { + + PyObject* graph_obj; + static const char* kwlist[] = {"graph", nullptr}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!", const_cast(kwlist), + &AdjacencyListGraphType, &graph_obj)) { + return nullptr; + } + + AdjacencyListGraph* graph = reinterpret_cast(graph_obj); + + struct EdgeTuple { + std::string source; + std::string target; + std::variant value; + DataType value_type; + + bool operator>(const EdgeTuple& other) const { + if (value_type != other.value_type) + return value_type > other.value_type; + if (std::holds_alternative(value)) + return std::get(value) > std::get(other.value); + if (std::holds_alternative(value)) + return std::get(value) > std::get(other.value); + if (std::holds_alternative(value)) + return std::get(value) > std::get(other.value); + return false; + } + }; + + std::priority_queue, std::greater<>> pq; + std::unordered_set visited; + + PyObject* mst_graph = PyObject_CallObject(reinterpret_cast(&AdjacencyListGraphType), nullptr); + AdjacencyListGraph* mst = reinterpret_cast(mst_graph); + + std::string start = graph->node_map.begin()->first; + visited.insert(start); + + AdjacencyListGraphNode* start_node = graph->node_map[start]; + + Py_INCREF(start_node); + mst->nodes.push_back(start_node); + mst->node_map[start] = start_node; + + for (const auto& [adj_name, _] : start_node->adjacent) { + std::string key = make_edge_key(start, adj_name); + GraphEdge* edge = graph->edges[key]; + EdgeTuple et; + et.source = start; + et.target = adj_name; + et.value_type = edge->value_type; + + switch (edge->value_type) { + case DataType::Int: + et.value = std::get(edge->value); + break; + case DataType::Double: + et.value = std::get(edge->value); + break; + case DataType::String: + et.value = std::get(edge->value); + break; + default: + et.value = std::monostate{}; + } + + pq.push(et); + } + + while (!pq.empty()) { + EdgeTuple edge = pq.top(); + pq.pop(); + + if (visited.count(edge.target)) continue; + visited.insert(edge.target); + + for (const std::string& name : {edge.source, edge.target}) { + if (!mst->node_map.count(name)) { + AdjacencyListGraphNode* node = graph->node_map[name]; + Py_INCREF(node); + mst->nodes.push_back(node); + mst->node_map[name] = node; + } + } + + AdjacencyListGraphNode* u = mst->node_map[edge.source]; + AdjacencyListGraphNode* v = mst->node_map[edge.target]; + + Py_INCREF(v); + Py_INCREF(u); + u->adjacent[edge.target] = reinterpret_cast(v); + v->adjacent[edge.source] = reinterpret_cast(u); + + std::string key_uv = make_edge_key(edge.source, edge.target); + GraphEdge* new_edge = PyObject_New(GraphEdge, &GraphEdgeType); + PyObject_Init(reinterpret_cast(new_edge), &GraphEdgeType); + new (&new_edge->value) std::variant(edge.value); + new_edge->value_type = edge.value_type; + Py_INCREF(u); + Py_INCREF(v); + new_edge->source = reinterpret_cast(u); + new_edge->target = reinterpret_cast(v); + mst->edges[key_uv] = new_edge; + + std::string key_vu = make_edge_key(edge.target, edge.source); + GraphEdge* new_edge_rev = PyObject_New(GraphEdge, &GraphEdgeType); + PyObject_Init(reinterpret_cast(new_edge_rev), &GraphEdgeType); + new (&new_edge_rev->value) std::variant(edge.value); + new_edge_rev->value_type = edge.value_type; + Py_INCREF(u); + Py_INCREF(v); + new_edge_rev->source = reinterpret_cast(v); + new_edge_rev->target = reinterpret_cast(u); + mst->edges[key_vu] = new_edge_rev; + + AdjacencyListGraphNode* next_node = graph->node_map[edge.target]; + + for (const auto& [adj_name, _] : next_node->adjacent) { + if (visited.count(adj_name)) continue; + std::string key = make_edge_key(edge.target, adj_name); + GraphEdge* adj_edge = graph->edges[key]; + EdgeTuple adj_et; + adj_et.source = edge.target; + adj_et.target = adj_name; + adj_et.value_type = adj_edge->value_type; + + switch (adj_edge->value_type) { + case DataType::Int: + adj_et.value = std::get(adj_edge->value); + break; + case DataType::Double: + adj_et.value = std::get(adj_edge->value); + break; + case DataType::String: + adj_et.value = std::get(adj_edge->value); + break; + default: + adj_et.value = std::monostate{}; + } + + pq.push(adj_et); + } + } + return reinterpret_cast(mst); +} diff --git a/pydatastructs/graphs/_backend/cpp/algorithms.cpp b/pydatastructs/graphs/_backend/cpp/algorithms.cpp index 9f3f0150..f4e044b1 100644 --- a/pydatastructs/graphs/_backend/cpp/algorithms.cpp +++ b/pydatastructs/graphs/_backend/cpp/algorithms.cpp @@ -6,6 +6,7 @@ static PyMethodDef AlgorithmsMethods[] = { {"bfs_adjacency_list", (PyCFunction)breadth_first_search_adjacency_list, METH_VARARGS | METH_KEYWORDS, "Run BFS on adjacency list with callback"}, {"bfs_adjacency_matrix", (PyCFunction)breadth_first_search_adjacency_matrix, METH_VARARGS | METH_KEYWORDS, "Run BFS on adjacency matrix with callback"}, + {"minimum_spanning_tree_prim_adjacency_list", (PyCFunction)minimum_spanning_tree_prim_adjacency_list, METH_VARARGS | METH_KEYWORDS, "Run Prim's algorithm on adjacency list"}, {NULL, NULL, 0, NULL} }; diff --git a/pydatastructs/graphs/algorithms.py b/pydatastructs/graphs/algorithms.py index 393eb25b..47869e34 100644 --- a/pydatastructs/graphs/algorithms.py +++ b/pydatastructs/graphs/algorithms.py @@ -338,16 +338,20 @@ def minimum_spanning_tree(graph, algorithm, **kwargs): should be used only for such graphs. Using with other types of graphs may lead to unwanted results. """ - raise_if_backend_is_not_python( - minimum_spanning_tree, kwargs.get('backend', Backend.PYTHON)) - import pydatastructs.graphs.algorithms as algorithms - func = "_minimum_spanning_tree_" + algorithm + "_" + graph._impl - if not hasattr(algorithms, func): - raise NotImplementedError( - "Currently %s algoithm for %s implementation of graphs " - "isn't implemented for finding minimum spanning trees." - %(algorithm, graph._impl)) - return getattr(algorithms, func)(graph) + backend = kwargs.get('backend', Backend.PYTHON) + if backend == Backend.PYTHON: + import pydatastructs.graphs.algorithms as algorithms + func = "_minimum_spanning_tree_" + algorithm + "_" + graph._impl + if not hasattr(algorithms, func): + raise NotImplementedError( + "Currently %s algoithm for %s implementation of graphs " + "isn't implemented for finding minimum spanning trees." + %(algorithm, graph._impl)) + return getattr(algorithms, func)(graph) + else: + from pydatastructs.graphs._backend.cpp._algorithms import minimum_spanning_tree_prim_adjacency_list + if graph._impl == "adjacency_list" and algorithm == 'prim': + return minimum_spanning_tree_prim_adjacency_list(graph) def _minimum_spanning_tree_parallel_kruskal_adjacency_list(graph, num_threads): mst = _generate_mst_object(graph) diff --git a/pydatastructs/graphs/tests/test_adjacency_list.py b/pydatastructs/graphs/tests/test_adjacency_list.py index 6f82763a..3a9cdb14 100644 --- a/pydatastructs/graphs/tests/test_adjacency_list.py +++ b/pydatastructs/graphs/tests/test_adjacency_list.py @@ -67,16 +67,16 @@ def test_adjacency_list(): g2.add_vertex(v) g2.add_edge('v_4', 'v', 0) g2.add_edge('v_5', 'v', 0) - g2.add_edge('v_6', 'v', 0) + g2.add_edge('v_6', 'v', "h") assert g2.is_adjacent('v_4', 'v') is True assert g2.is_adjacent('v_5', 'v') is True assert g2.is_adjacent('v_6', 'v') is True e1 = g2.get_edge('v_4', 'v') e2 = g2.get_edge('v_5', 'v') e3 = g2.get_edge('v_6', 'v') - assert (str(e1)) == "('v_4', 'v')" - assert (str(e2)) == "('v_5', 'v')" - assert (str(e3)) == "('v_6', 'v')" + assert (str(e1)) == "('v_4', 'v', 0)" + assert (str(e2)) == "('v_5', 'v', 0)" + assert (str(e3)) == "('v_6', 'v', h)" g2.remove_edge('v_4', 'v') assert g2.is_adjacent('v_4', 'v') is False g2.remove_vertex('v') diff --git a/pydatastructs/graphs/tests/test_algorithms.py b/pydatastructs/graphs/tests/test_algorithms.py index 4cfab7e5..f898bbc7 100644 --- a/pydatastructs/graphs/tests/test_algorithms.py +++ b/pydatastructs/graphs/tests/test_algorithms.py @@ -185,6 +185,46 @@ def _test_minimum_spanning_tree(func, ds, algorithm, *args): for k, v in mst.edge_weights.items(): assert (k, v.value) in expected_mst + def _test_minimum_spanning_tree_cpp(ds, algorithm, *args): + if (ds == 'List' and algorithm == "prim"): + a1 = AdjacencyListGraphNode('a', 0, backend = Backend.CPP) + b1 = AdjacencyListGraphNode('b', 0, backend = Backend.CPP) + c1 = AdjacencyListGraphNode('c', 0, backend = Backend.CPP) + d1 = AdjacencyListGraphNode('d', 0, backend = Backend.CPP) + e1 = AdjacencyListGraphNode('e', 0, backend = Backend.CPP) + g = Graph(a1, b1, c1, d1, e1, backend = Backend.CPP) + g.add_edge(a1.name, c1.name, 10) + g.add_edge(c1.name, a1.name, 10) + g.add_edge(a1.name, d1.name, 7) + g.add_edge(d1.name, a1.name, 7) + g.add_edge(c1.name, d1.name, 9) + g.add_edge(d1.name, c1.name, 9) + g.add_edge(d1.name, b1.name, 32) + g.add_edge(b1.name, d1.name, 32) + g.add_edge(d1.name, e1.name, 23) + g.add_edge(e1.name, d1.name, 23) + mst = minimum_spanning_tree(g, "prim", backend = Backend.CPP) + expected_mst = ["('a', 'd', 7)", "('d', 'c', 9)", "('e', 'd', 23)", "('b', 'd', 32)", + "('d', 'a', 7)", "('c', 'd', 9)", "('d', 'e', 23)", "('d', 'b', 32)"] + assert str(mst.get_edge('a', 'd')) in expected_mst + assert str(mst.get_edge('e', 'd')) in expected_mst + assert str(mst.get_edge('d', 'c')) in expected_mst + assert str(mst.get_edge('b', 'd')) in expected_mst + assert mst.num_edges() == 8 + a=AdjacencyListGraphNode('0', 0, backend = Backend.CPP) + b=AdjacencyListGraphNode('1', 0, backend = Backend.CPP) + c=AdjacencyListGraphNode('2', 0, backend = Backend.CPP) + d=AdjacencyListGraphNode('3', 0, backend = Backend.CPP) + g2 = Graph(a,b,c,d,backend = Backend.CPP) + g2.add_edge('0', '1', 74) + g2.add_edge('1', '0', 74) + g2.add_edge('0', '3', 55) + g2.add_edge('3', '0', 55) + g2.add_edge('1', '2', 74) + g2.add_edge('2', '1', 74) + mst2=minimum_spanning_tree(g2, "prim", backend = Backend.CPP) + assert mst2.num_edges() == 6 + fmst = minimum_spanning_tree fmstp = minimum_spanning_tree_parallel _test_minimum_spanning_tree(fmst, "List", "kruskal") @@ -193,6 +233,7 @@ def _test_minimum_spanning_tree(func, ds, algorithm, *args): _test_minimum_spanning_tree(fmstp, "List", "kruskal", 3) _test_minimum_spanning_tree(fmstp, "Matrix", "kruskal", 3) _test_minimum_spanning_tree(fmstp, "List", "prim", 3) + _test_minimum_spanning_tree_cpp("List", "prim") def test_strongly_connected_components(): diff --git a/pydatastructs/utils/_backend/cpp/GraphEdge.hpp b/pydatastructs/utils/_backend/cpp/GraphEdge.hpp index 757bd89b..4008db17 100644 --- a/pydatastructs/utils/_backend/cpp/GraphEdge.hpp +++ b/pydatastructs/utils/_backend/cpp/GraphEdge.hpp @@ -4,6 +4,8 @@ #define PY_SSIZE_T_CLEAN #include #include +#include +#include #include "GraphNode.hpp" extern PyTypeObject GraphEdgeType; @@ -12,56 +14,123 @@ typedef struct { PyObject_HEAD PyObject* source; PyObject* target; - PyObject* value; + std::variant value; + DataType value_type; } GraphEdge; static void GraphEdge_dealloc(GraphEdge* self) { Py_XDECREF(self->source); Py_XDECREF(self->target); - Py_XDECREF(self->value); Py_TYPE(self)->tp_free(reinterpret_cast(self)); } static PyObject* GraphEdge_new(PyTypeObject* type, PyObject* args, PyObject* kwds) { - GraphEdge* self; - self = reinterpret_cast(type->tp_alloc(type, 0)); + GraphEdge* self = PyObject_New(GraphEdge, &GraphEdgeType); if (!self) return NULL; + new (&self->value) std::variant(); + self->value_type = DataType::None; + static char* kwlist[] = {"node1", "node2", "value", NULL}; PyObject* node1; PyObject* node2; PyObject* value = Py_None; if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|O", kwlist, &node1, &node2, &value)) { - PyErr_SetString(PyExc_ValueError, "Invalid arguments: Expected (GraphNode, GraphNode, optional value)"); + PyErr_SetString(PyExc_ValueError, "Expected (GraphNode, GraphNode, optional value)"); return NULL; } Py_INCREF(node1); Py_INCREF(node2); - Py_INCREF(value); - self->source = node1; self->target = node2; - self->value = value; + + if (value == Py_None) { + self->value_type = DataType::None; + self->value = std::monostate{}; + } else if (PyLong_Check(value)) { + self->value_type = DataType::Int; + self->value = static_cast(PyLong_AsLongLong(value)); + } else if (PyFloat_Check(value)) { + self->value_type = DataType::Double; + self->value = PyFloat_AsDouble(value); + } else if (PyUnicode_Check(value)) { + const char* str = PyUnicode_AsUTF8(value); + self->value_type = DataType::String; + self->value = std::string(str); + } else { + PyErr_SetString(PyExc_TypeError, "Unsupported edge value type (must be int, float, str, or None)"); + return NULL; + } return reinterpret_cast(self); } static PyObject* GraphEdge_str(GraphEdge* self) { - std::string source_name = (reinterpret_cast(self->source))->name; - std::string target_name = (reinterpret_cast(self->target))->name; - - if (source_name.empty() || target_name.empty()) { - PyErr_SetString(PyExc_AttributeError, "Both nodes must have a 'name' attribute."); - return NULL; + std::string src = reinterpret_cast(self->source)->name; + std::string tgt = reinterpret_cast(self->target)->name; + std::string val_str; + + switch (self->value_type) { + case DataType::Int: + val_str = std::to_string(std::get(self->value)); + break; + case DataType::Double: + val_str = std::to_string(std::get(self->value)); + break; + case DataType::String: + val_str = std::get(self->value); + break; + case DataType::None: + default: + val_str = "None"; + break; } - PyObject* str_repr = PyUnicode_FromFormat("('%s', '%s')", source_name.c_str(), target_name.c_str()); + return PyUnicode_FromFormat("('%s', '%s', %s)", src.c_str(), tgt.c_str(), val_str.c_str()); +} - return str_repr; +static PyObject* GraphEdge_get_value(GraphEdge* self, void* closure) { + switch (self->value_type) { + case DataType::Int: + return PyLong_FromLongLong(std::get(self->value)); + case DataType::Double: + return PyFloat_FromDouble(std::get(self->value)); + case DataType::String: + return PyUnicode_FromString(std::get(self->value).c_str()); + case DataType::None: + default: + Py_RETURN_NONE; + } } +static int GraphEdge_set_value(GraphEdge* self, PyObject* value) { + if (value == Py_None) { + self->value_type = DataType::None; + self->value = std::monostate{}; + } else if (PyLong_Check(value)) { + self->value_type = DataType::Int; + self->value = static_cast(PyLong_AsLongLong(value)); + } else if (PyFloat_Check(value)) { + self->value_type = DataType::Double; + self->value = PyFloat_AsDouble(value); + } else if (PyUnicode_Check(value)) { + const char* str = PyUnicode_AsUTF8(value); + self->value_type = DataType::String; + self->value = std::string(str); + } else { + PyErr_SetString(PyExc_TypeError, "Edge value must be int, float, str, or None."); + return -1; + } + return 0; +} + +static PyGetSetDef GraphEdge_getsetters[] = { + {"value", (getter)GraphEdge_get_value, (setter)GraphEdge_set_value, "Get or set edge value", NULL}, + {NULL} +}; + inline PyTypeObject GraphEdgeType = { /* tp_name */ PyVarObject_HEAD_INIT(NULL, 0) "GraphEdge", /* tp_basicsize */ sizeof(GraphEdge), @@ -91,7 +160,7 @@ inline PyTypeObject GraphEdgeType = { /* tp_iternext */ 0, /* tp_methods */ 0, /* tp_members */ 0, - /* tp_getset */ 0, + /* tp_getset */ GraphEdge_getsetters, /* tp_base */ 0, /* tp_dict */ 0, /* tp_descr_get */ 0, diff --git a/pydatastructs/utils/_extensions.py b/pydatastructs/utils/_extensions.py index 7aae04ef..bdf351b2 100644 --- a/pydatastructs/utils/_extensions.py +++ b/pydatastructs/utils/_extensions.py @@ -1,6 +1,8 @@ from setuptools import Extension import os import sys +import os +import sys project = 'pydatastructs' module = 'utils' @@ -10,17 +12,35 @@ nodes = '.'.join([project, module, backend, cpp, '_nodes']) nodes_sources = [os.path.join(project, module, backend, cpp, 'nodes.cpp')] +nodes_sources = [os.path.join(project, module, backend, cpp, 'nodes.cpp')] + graph_utils = '.'.join([project, module, backend, cpp, '_graph_utils']) graph_utils_sources = [os.path.join(project, module, backend, cpp, 'graph_utils.cpp')] extra_compile_args = ["-std=c++17"] +if sys.platform == "darwin": + extra_compile_args.append("-mmacosx-version-min=10.13") +elif sys.platform == "win32": + extra_compile_args = ["/std:c++17"] +graph_utils_sources = [os.path.join(project, module, backend, cpp, 'graph_utils.cpp')] + +extra_compile_args = ["-std=c++17"] + if sys.platform == "darwin": extra_compile_args.append("-mmacosx-version-min=10.13") elif sys.platform == "win32": extra_compile_args = ["/std:c++17"] extensions = [ - Extension(nodes, sources=nodes_sources, language="c++", extra_compile_args=extra_compile_args), - Extension(graph_utils, sources=graph_utils_sources, language="c++", extra_compile_args=extra_compile_args), + Extension( + nodes, + sources=nodes_sources, + extra_compile_args=["-std=c++17"] + ), + Extension( + graph_utils, + sources=graph_utils_sources, + extra_compile_args=["-std=c++17"] + ) ] diff --git a/pydatastructs/utils/tests/test_misc_util.py b/pydatastructs/utils/tests/test_misc_util.py index 2f8810dc..13ba2ec8 100644 --- a/pydatastructs/utils/tests/test_misc_util.py +++ b/pydatastructs/utils/tests/test_misc_util.py @@ -57,7 +57,7 @@ def test_GraphEdge(): h_1 = AdjacencyListGraphNode('h_1', 1, backend = Backend.CPP) h_2 = AdjacencyListGraphNode('h_2', 2, backend = Backend.CPP) e2 = GraphEdge(h_1, h_2, value = 2, backend = Backend.CPP) - assert str(e2) == "('h_1', 'h_2')" + assert str(e2) == "('h_1', 'h_2', 2)" def test_BinomialTreeNode(): b = BinomialTreeNode(1,1)