diff --git a/pydatastructs/graphs/_backend/cpp/Algorithms.hpp b/pydatastructs/graphs/_backend/cpp/Algorithms.hpp index 2bb362c34..e63c055a7 100644 --- a/pydatastructs/graphs/_backend/cpp/Algorithms.hpp +++ b/pydatastructs/graphs/_backend/cpp/Algorithms.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "GraphEdge.hpp" #include "AdjacencyList.hpp" #include "AdjacencyMatrix.hpp" @@ -159,6 +160,305 @@ static PyObject* breadth_first_search_adjacency_matrix(PyObject* self, PyObject* Py_RETURN_NONE; } +static PyObject* depth_first_search_adjacency_list(PyObject* self, PyObject* args, PyObject* kwargs) { + PyObject* graph_obj; + const char* source_name; + PyObject* operation; + PyObject* varargs = nullptr; + PyObject* kwargs_dict = nullptr; + static const char* kwlist[] = {"graph", "source_node", "operation", "args", "kwargs", nullptr}; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!sO|OO", const_cast(kwlist), + &AdjacencyListGraphType, &graph_obj, + &source_name, &operation, + &varargs, &kwargs_dict)) { + return nullptr; + } + + AdjacencyListGraph* cpp_graph = reinterpret_cast(graph_obj); + auto it = cpp_graph->node_map.find(source_name); + AdjacencyListGraphNode* start_node = it->second; + std::unordered_set visited; + std::stack dfs_stack; + dfs_stack.push(start_node); + visited.insert(start_node->name); + + while (!dfs_stack.empty()) { + AdjacencyListGraphNode* node = dfs_stack.top(); + dfs_stack.pop(); + + for (const auto& [adj_name, adj_obj] : node->adjacent) { + if (visited.count(adj_name)) continue; + if (get_type_tag(adj_obj) != NodeType_::AdjacencyListGraphNode) continue; + + AdjacencyListGraphNode* adj_node = reinterpret_cast(adj_obj); + + PyObject* node_pyobj = reinterpret_cast(node); + PyObject* adj_node_pyobj = reinterpret_cast(adj_node); + + PyObject* final_args; + + if (varargs && PyTuple_Check(varargs)) { + Py_ssize_t varargs_size = PyTuple_Size(varargs); + if (varargs_size == 1) { + PyObject* extra_arg = PyTuple_GetItem(varargs, 0); + final_args = PyTuple_Pack(3, node_pyobj, adj_node_pyobj, extra_arg); + } else { + PyObject* base_args = PyTuple_Pack(2, node_pyobj, adj_node_pyobj); + if (!base_args) + return nullptr; + final_args = PySequence_Concat(base_args, varargs); + Py_DECREF(base_args); + } + } else { + final_args = PyTuple_Pack(2, node_pyobj, adj_node_pyobj); + } + if (!final_args) + return nullptr; + + PyObject* result = PyObject_Call(operation, final_args, kwargs_dict); + Py_DECREF(final_args); + + if (!result) + return nullptr; + + Py_DECREF(result); + visited.insert(adj_name); + dfs_stack.push(adj_node); + } + } + + if (PyErr_Occurred()) { + return nullptr; + } + + Py_RETURN_NONE; +} + +static PyObject* depth_first_search_adjacency_matrix(PyObject* self, PyObject* args, PyObject* kwargs) { + PyObject* graph_obj; + const char* source_name; + PyObject* operation; + PyObject* varargs = nullptr; + PyObject* kwargs_dict = nullptr; + + static const char* kwlist[] = {"graph", "source_node", "operation", "args", "kwargs", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!sO|OO", const_cast(kwlist), + &AdjacencyMatrixGraphType, &graph_obj, + &source_name, &operation, + &varargs, &kwargs_dict)) { + return nullptr; + } + + AdjacencyMatrixGraph* cpp_graph = reinterpret_cast(graph_obj); + + auto it = cpp_graph->node_map.find(source_name); + if (it == cpp_graph->node_map.end()) { + PyErr_SetString(PyExc_KeyError, "Source node not found in graph"); + return nullptr; + } + AdjacencyMatrixGraphNode* start_node = it->second; + + std::unordered_set visited; + std::stack dfs_stack; + + dfs_stack.push(start_node); + visited.insert(source_name); + + while (!dfs_stack.empty()) { + AdjacencyMatrixGraphNode* node = dfs_stack.top(); + dfs_stack.pop(); + + std::string node_name = reinterpret_cast(node)->name; + auto& neighbors = cpp_graph->matrix[node_name]; + + for (const auto& [adj_name, connected] : neighbors) { + if (!connected || visited.count(adj_name)) continue; + + auto adj_it = cpp_graph->node_map.find(adj_name); + if (adj_it == cpp_graph->node_map.end()) continue; + + AdjacencyMatrixGraphNode* adj_node = adj_it->second; + + PyObject* base_args = PyTuple_Pack(2, + reinterpret_cast(node), + reinterpret_cast(adj_node)); + if (!base_args) return nullptr; + + PyObject* final_args; + if (varargs && PyTuple_Check(varargs)) { + final_args = PySequence_Concat(base_args, varargs); + Py_DECREF(base_args); + if (!final_args) return nullptr; + } else { + final_args = base_args; + } + + PyObject* result = PyObject_Call(operation, final_args, kwargs_dict); + Py_DECREF(final_args); + if (!result) return nullptr; + Py_DECREF(result); + + visited.insert(adj_name); + dfs_stack.push(adj_node); + } + } + + if (PyErr_Occurred()) { + return nullptr; + } + + Py_RETURN_NONE; +} + +static PyObject* shortest_paths_bellman_ford_adjacency_list(PyObject* self, PyObject* args, PyObject* kwargs) { + PyObject* graph_obj; + const char* source_name; + const char* target_name = ""; + + static const char* kwlist[] = {"graph", "source_node", "target_node", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!s|s", const_cast(kwlist), + &AdjacencyListGraphType, &graph_obj, + &source_name, &target_name)) { + return nullptr; + } + + AdjacencyListGraph* graph = reinterpret_cast(graph_obj); + const size_t V = graph->node_map.size(); + + std::vector>> adj(V); + for (const auto& [edge_key, edge] : graph->edges) { + size_t delim = edge_key.find('_'); + std::string u_name = edge_key.substr(0, delim); + std::string v_name = edge_key.substr(delim + 1); + int u = graph->name_to_id[u_name]; + int v = graph->name_to_id[v_name]; + + double weight = 0.0; + if (edge->value_type == DataType::Int) + weight = static_cast(std::get(edge->value)); + else if (edge->value_type == DataType::Double) + weight = std::get(edge->value); + else + continue; + + adj[u].emplace_back(v, weight); + } + + std::vector dist(V, std::numeric_limits::infinity()); + std::vector pred(V, -1); + std::vector visited(V, false); + std::vector cnts(V, 0); + + int source_id = graph->name_to_id[source_name]; + dist[source_id] = 0.0; + + std::queue que; + que.push(source_id); + visited[source_id] = true; + + while (!que.empty()) { + int u = que.front(); + que.pop(); + visited[u] = false; + + for (const auto& [v, weight] : adj[u]) { + if (dist[u] != std::numeric_limits::infinity() && + dist[u] + weight < dist[v]) { + dist[v] = dist[u] + weight; + pred[v] = u; + cnts[v] = cnts[u] + 1; + + if (cnts[v] >= static_cast(V)) { + PyErr_SetString(PyExc_ValueError, "Graph contains a negative weight cycle."); + return nullptr; + } + + if (!visited[v]) { + que.push(v); + visited[v] = true; + } + } + } + } + + PyObject* dist_dict = PyDict_New(); + PyObject* pred_dict = PyDict_New(); + if (!dist_dict || !pred_dict) { + Py_XDECREF(dist_dict); + Py_XDECREF(pred_dict); + return nullptr; + } + + for (int id = 0; id < static_cast(V); ++id) { + const std::string& name = graph->id_to_name[id]; + + PyObject* dval = PyFloat_FromDouble(dist[id]); + if (!dval || PyDict_SetItemString(dist_dict, name.c_str(), dval) < 0) { + Py_XDECREF(dval); + Py_DECREF(dist_dict); + Py_DECREF(pred_dict); + return nullptr; + } + Py_DECREF(dval); + + PyObject* py_pred; + if (pred[id] == -1) { + Py_INCREF(Py_None); + py_pred = Py_None; + } else { + py_pred = PyUnicode_FromString(graph->id_to_name[pred[id]].c_str()); + if (!py_pred) { + Py_DECREF(dist_dict); + Py_DECREF(pred_dict); + return nullptr; + } + } + + if (PyDict_SetItemString(pred_dict, name.c_str(), py_pred) < 0) { + Py_DECREF(py_pred); + Py_DECREF(dist_dict); + Py_DECREF(pred_dict); + return nullptr; + } + Py_DECREF(py_pred); + } + + if (strlen(target_name) > 0) { + int target_id = graph->name_to_id[target_name]; + PyObject* out = PyTuple_New(2); + if (!out) { + Py_DECREF(dist_dict); + Py_DECREF(pred_dict); + return nullptr; + } + + PyObject* dist_val = PyFloat_FromDouble(dist[target_id]); + if (!dist_val) { + Py_DECREF(out); + Py_DECREF(dist_dict); + Py_DECREF(pred_dict); + return nullptr; + } + + PyTuple_SetItem(out, 0, dist_val); + PyTuple_SetItem(out, 1, pred_dict); + Py_DECREF(dist_dict); + return out; + } + + PyObject* result = PyTuple_New(2); + if (!result) { + Py_DECREF(dist_dict); + Py_DECREF(pred_dict); + return nullptr; + } + + PyTuple_SetItem(result, 0, dist_dict); + PyTuple_SetItem(result, 1, pred_dict); + return result; +} + static PyObject* minimum_spanning_tree_prim_adjacency_list(PyObject* self, PyObject* args, PyObject* kwargs) { PyObject* graph_obj; static const char* kwlist[] = {"graph", nullptr}; diff --git a/pydatastructs/graphs/_backend/cpp/graph.cpp b/pydatastructs/graphs/_backend/cpp/graph.cpp index 67b1b4572..fb5cfabba 100644 --- a/pydatastructs/graphs/_backend/cpp/graph.cpp +++ b/pydatastructs/graphs/_backend/cpp/graph.cpp @@ -12,8 +12,11 @@ static PyMethodDef GraphMethods[] = { {"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"}, + {"dfs_adjacency_list", (PyCFunction)depth_first_search_adjacency_list, METH_VARARGS | METH_KEYWORDS, "Run DFS on adjacency list with callback"}, + {"dfs_adjacency_matrix", (PyCFunction)depth_first_search_adjacency_matrix, METH_VARARGS | METH_KEYWORDS, "Run DFS 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"}, {"shortest_paths_dijkstra_adjacency_list", (PyCFunction)shortest_paths_dijkstra_adjacency_list, METH_VARARGS | METH_KEYWORDS, "Dijkstra's algorithm for adjacency list graphs"}, + {"shortest_paths_bellman_ford_adjacency_list", (PyCFunction)shortest_paths_bellman_ford_adjacency_list, METH_VARARGS | METH_KEYWORDS, "Bellman-Ford algorithm for adjacency list graphs"}, // Add this line {NULL, NULL, 0, NULL} }; diff --git a/pydatastructs/graphs/algorithms.py b/pydatastructs/graphs/algorithms.py index 6c2182fed..d1f077cac 100644 --- a/pydatastructs/graphs/algorithms.py +++ b/pydatastructs/graphs/algorithms.py @@ -708,16 +708,24 @@ def depth_first_search( .. [1] https://en.wikipedia.org/wiki/Depth-first_search """ - raise_if_backend_is_not_python( - depth_first_search, kwargs.get('backend', Backend.PYTHON)) - import pydatastructs.graphs.algorithms as algorithms - func = "_depth_first_search_" + graph._impl - if not hasattr(algorithms, func): - raise NotImplementedError( - "Currently depth first search isn't implemented for " - "%s graphs."%(graph._impl)) - return getattr(algorithms, func)( - graph, source_node, operation, *args, **kwargs) + backend = kwargs.get('backend', Backend.PYTHON) + if backend == Backend.PYTHON: + import pydatastructs.graphs.algorithms as algorithms + func = "_depth_first_search_" + graph._impl + if not hasattr(algorithms, func): + raise NotImplementedError( + "Currently depth first search isn't implemented for " + "%s graphs."%(graph._impl)) + return getattr(algorithms, func)( + graph, source_node, operation, *args, **kwargs) + else: + from pydatastructs.graphs._backend.cpp._graph import dfs_adjacency_list, dfs_adjacency_matrix + if (graph._impl == "adjacency_list"): + extra_args = args if args else () + return dfs_adjacency_list(graph, source_node, operation, extra_args) + if (graph._impl == "adjacency_matrix"): + extra_args = args if args else () + return dfs_adjacency_matrix(graph, source_node, operation, extra_args) def _depth_first_search_adjacency_list( graph, source_node, operation, *args, **kwargs): @@ -814,9 +822,11 @@ def shortest_paths(graph: Graph, algorithm: str, "finding shortest paths in graphs."%(algorithm)) return getattr(algorithms, func)(graph, source, target) else: - from pydatastructs.graphs._backend.cpp._graph import shortest_paths_dijkstra_adjacency_list + from pydatastructs.graphs._backend.cpp._graph import (shortest_paths_dijkstra_adjacency_list, shortest_paths_bellman_ford_adjacency_list) if graph._impl == "adjacency_list" and algorithm == 'dijkstra': return shortest_paths_dijkstra_adjacency_list(graph, source, target) + elif graph._impl == "adjacency_list" and algorithm == 'bellman_ford': + return shortest_paths_bellman_ford_adjacency_list(graph, source, target) def _bellman_ford_adjacency_list(graph: Graph, source: str, target: str) -> tuple: distances, predecessor, visited, cnts = {}, {}, {}, {} diff --git a/pydatastructs/graphs/tests/test_algorithms.py b/pydatastructs/graphs/tests/test_algorithms.py index 7dd2e1b78..41644da65 100644 --- a/pydatastructs/graphs/tests/test_algorithms.py +++ b/pydatastructs/graphs/tests/test_algorithms.py @@ -278,6 +278,8 @@ def _test_depth_first_search(ds): G1 = Graph(V1, V2, V3) + assert G1.num_vertices() == 3 + edges = [ (V1.name, V2.name), (V2.name, V3.name), @@ -287,6 +289,8 @@ def _test_depth_first_search(ds): for edge in edges: G1.add_edge(*edge) + assert G1.num_edges() == len(edges) + parent = {} def dfs_tree(curr_node, next_node, parent): if next_node != "": @@ -297,6 +301,32 @@ def dfs_tree(curr_node, next_node, parent): assert (parent[V3.name] == V1.name and parent[V2.name] == V1.name) or \ (parent[V3.name] == V2.name and parent[V2.name] == V1.name) + if (ds=='List'): + parent = {} + V9 = AdjacencyListGraphNode("9",0,backend = Backend.CPP) + V10 = AdjacencyListGraphNode("10",0,backend = Backend.CPP) + V11 = AdjacencyListGraphNode("11",0,backend = Backend.CPP) + G2 = Graph(V9, V10, V11,implementation = 'adjacency_list', backend = Backend.CPP) + assert G2.num_vertices()==3 + G2.add_edge("9", "10") + G2.add_edge("10", "11") + depth_first_search(G2, "9", dfs_tree, parent, backend = Backend.CPP) + assert parent[V10] == V9 + assert parent[V11] == V10 + + if (ds == 'Matrix'): + parent3 = {} + V12 = AdjacencyMatrixGraphNode("12", 0, backend = Backend.CPP) + V13 = AdjacencyMatrixGraphNode("13", 0, backend = Backend.CPP) + V14 = AdjacencyMatrixGraphNode("14", 0, backend = Backend.CPP) + G3 = Graph(V12, V13, V14, implementation = 'adjacency_matrix', backend = Backend.CPP) + assert G3.num_vertices() == 3 + G3.add_edge("12", "13") + G3.add_edge("13", "14") + depth_first_search(G3, "12", dfs_tree, parent3, backend = Backend.CPP) + assert parent3[V13] == V12 + assert parent3[V14] == V13 + V4 = GraphNode(0) V5 = GraphNode(1) V6 = GraphNode(2) @@ -316,6 +346,8 @@ def dfs_tree(curr_node, next_node, parent): for edge in edges: G2.add_edge(*edge) + assert G2.num_edges() == len(edges) + path = [] def path_finder(curr_node, next_node, dest_node, parent, path): if next_node != "": @@ -383,7 +415,32 @@ def _test_shortest_paths_positive_edges(ds, algorithm): assert dist2 == {'S': 6, 'C': 2, 'SLC': 0, 'SF': 6, 'D': 3} assert pred2 == {'S': 'C', 'C': 'SLC', 'SLC': None, 'SF': 'D', 'D': 'SLC'} - + if (ds == 'List' and algorithm == 'bellman_ford'): + vertices3 = [AdjacencyListGraphNode('S', 0, backend = Backend.CPP), + AdjacencyListGraphNode('C', 0, backend = Backend.CPP), + AdjacencyListGraphNode('SLC', 0, backend = Backend.CPP), + AdjacencyListGraphNode('SF', 0, backend = Backend.CPP), + AdjacencyListGraphNode('D', 0, backend = Backend.CPP)] + graph3 = Graph(*vertices3, backend = Backend.CPP) + graph3.add_edge('S', 'SLC', 2) + graph3.add_edge('C', 'S', 4) + graph3.add_edge('C', 'D', 2) + graph3.add_edge('SLC', 'C', 2) + graph3.add_edge('SLC', 'D', 3) + graph3.add_edge('SF', 'SLC', 2) + graph3.add_edge('SF', 'S', 2) + graph3.add_edge('D', 'SF', 3) + (dist3, pred3) = shortest_paths(graph3, algorithm, 'SLC', backend = Backend.CPP) + assert dist3 == {'S': 6, 'C': 2, 'SLC': 0, 'SF': 6, 'D': 3} + assert pred3 == {'S': 'C', 'C': 'SLC', 'SLC': None, 'SF': 'D', 'D': 'SLC'} + + (dist4, pred4) = shortest_paths(graph3, algorithm, 'SLC', 'SF', backend = Backend.CPP) + assert dist4 == 6 + assert pred4 == {'S': 'C', 'C': 'SLC', 'SLC': None, 'SF': 'D', 'D': 'SLC'} + + graph3.remove_edge('SLC', 'D') + graph3.add_edge('D', 'SLC', -10) + assert raises(ValueError, lambda: shortest_paths(graph3, 'bellman_ford', 'SLC', backend = Backend.CPP)) def _test_shortest_paths_negative_edges(ds, algorithm): import pydatastructs.utils.misc_util as utils