From 5a785272fe673a9e269fc6b58de6c0ed9a987b00 Mon Sep 17 00:00:00 2001 From: TripleCamellya Date: Wed, 19 Mar 2025 15:23:24 +0800 Subject: [PATCH] feat: Add Bron Kerbosch algorithm to find maximal cliques --- .../pydatastructs/graphs/algorithms.rst | 4 +- pydatastructs/graphs/__init__.py | 3 +- pydatastructs/graphs/algorithms.py | 93 +++++++++++++++++-- pydatastructs/graphs/tests/test_algorithms.py | 38 +++++++- 4 files changed, 126 insertions(+), 12 deletions(-) diff --git a/docs/source/pydatastructs/graphs/algorithms.rst b/docs/source/pydatastructs/graphs/algorithms.rst index c508421a..3e4478c7 100644 --- a/docs/source/pydatastructs/graphs/algorithms.rst +++ b/docs/source/pydatastructs/graphs/algorithms.rst @@ -21,4 +21,6 @@ Algorithms .. autofunction:: pydatastructs.topological_sort_parallel -.. autofunction:: pydatastructs.find_bridges \ No newline at end of file +.. autofunction:: pydatastructs.find_bridges + +.. autofunction:: pydatastructs.find_maximal_cliques \ No newline at end of file diff --git a/pydatastructs/graphs/__init__.py b/pydatastructs/graphs/__init__.py index 21e0a5f3..232ce793 100644 --- a/pydatastructs/graphs/__init__.py +++ b/pydatastructs/graphs/__init__.py @@ -22,7 +22,8 @@ topological_sort, topological_sort_parallel, max_flow, - find_bridges + find_bridges, + find_maximal_cliques ) __all__.extend(algorithms.__all__) diff --git a/pydatastructs/graphs/algorithms.py b/pydatastructs/graphs/algorithms.py index 334f522c..372f656d 100644 --- a/pydatastructs/graphs/algorithms.py +++ b/pydatastructs/graphs/algorithms.py @@ -24,7 +24,8 @@ 'topological_sort', 'topological_sort_parallel', 'max_flow', - 'find_bridges' + 'find_bridges', + 'find_maximal_cliques' ] Stack = Queue = deque @@ -1169,7 +1170,6 @@ def _job(graph: Graph, u: str): raise ValueError("Graph is not acyclic.") return L - def _breadth_first_search_max_flow(graph: Graph, source_node, sink_node, flow_passed, for_dinic=False): bfs_queue = Queue() parent, currentPathC = {}, {} @@ -1191,7 +1191,6 @@ def _breadth_first_search_max_flow(graph: Graph, source_node, sink_node, flow_pa bfs_queue.append(next_node.name) return (0, parent) - def _max_flow_edmonds_karp_(graph: Graph, source, sink): m_flow = 0 flow_passed = {} @@ -1209,7 +1208,6 @@ def _max_flow_edmonds_karp_(graph: Graph, source, sink): new_flow, parent = _breadth_first_search_max_flow(graph, source, sink, flow_passed) return m_flow - def _depth_first_search_max_flow_dinic(graph: Graph, u, parent, sink_node, flow, flow_passed): if u == sink_node: return flow @@ -1233,7 +1231,6 @@ def _depth_first_search_max_flow_dinic(graph: Graph, u, parent, sink_node, flow, return path_flow return 0 - def _max_flow_dinic_(graph: Graph, source, sink): max_flow = 0 flow_passed = {} @@ -1253,7 +1250,6 @@ def _max_flow_dinic_(graph: Graph, source, sink): return max_flow - def max_flow(graph, source, sink, algorithm='edmonds_karp', **kwargs): raise_if_backend_is_not_python( max_flow, kwargs.get('backend', Backend.PYTHON)) @@ -1266,7 +1262,6 @@ def max_flow(graph, source, sink, algorithm='edmonds_karp', **kwargs): "performing max flow on graphs.") return getattr(algorithms, func)(graph, source, sink) - def find_bridges(graph): """ Finds all bridges in an undirected graph using Tarjan's Algorithm. @@ -1368,3 +1363,87 @@ def dfs(u): bridges.append((b, a)) bridges.sort() return bridges + +def _bron_kerbosc(graph: Graph, set_r: set, set_p: set, set_x: set, cliques: list): + if not set_p and not set_x: + cliques.append(sorted(list(set_r))) + return + + for v in list(set_p): + neighbor_nodes = graph.neighbors(v) + neighbors = set(n.name for n in neighbor_nodes) + _bron_kerbosc(graph, set_r.union({v}), set_p.intersection(neighbors), + set_x.intersection(neighbors), cliques) + set_p.remove(v) + set_x.add(v) + +def _find_maximal_cliques_bron_kerbosc_adjacency_list(graph: Graph) -> list: + cliques = [] + vertices = set(graph.vertices) + _bron_kerbosc(graph, set(), vertices, set(), cliques) + return sorted(cliques) + +_find_maximal_cliques_bron_kerbosc_adjacency_matrix = \ + _find_maximal_cliques_bron_kerbosc_adjacency_list + +def find_maximal_cliques(graph: Graph, algorithm: str, **kwargs) -> list: + """ + Finds maximal cliques for an undirected graph. + + Parameters + ========== + + graph: Graph + The graph under consideration. + algorithm: str + The algorithm to be used. Currently, the following algorithms + are implemented, + + 'bron_kerbosc' -> Bron Kerbosc algorithm as given in [1]. + backend: pydatastructs.Backend + The backend to be used. + Optional, by default, the best available + backend is used. + + Returns + ======= + + cliques: list + Python list with each element as list of vertices. + + Examples + ======== + + >>> from pydatastructs import Graph, AdjacencyListGraphNode + >>> from pydatastructs import find_maximal_cliques + >>> V1 = AdjacencyListGraphNode('V1') + >>> V2 = AdjacencyListGraphNode('V2') + >>> V3 = AdjacencyListGraphNode('V3') + >>> V4 = AdjacencyListGraphNode('V4') + >>> G = Graph(V1, V2, V3, V4) + >>> G.add_edge('V1', 'V2') + >>> G.add_edge('V2', 'V1') + >>> G.add_edge('V2', 'V3') + >>> G.add_edge('V3', 'V2') + >>> G.add_edge('V1', 'V3') + >>> G.add_edge('V3', 'V1') + >>> G.add_edge('V1', 'V4') + >>> G.add_edge('V4', 'V1') + >>> find_maximal_cliques(G, 'bron_kerbosc') + [['V1', 'V2', 'V3'], ['V1', 'V4']] + + References + ========== + + .. [1] https://en.wikipedia.org/wiki/Bron%E2%80%93Kerbosch_algorithm + """ + raise_if_backend_is_not_python( + find_maximal_cliques, kwargs.get('backend', Backend.PYTHON)) + + import pydatastructs.graphs.algorithms as algorithms + func = "_find_maximal_cliques_" + algorithm + "_" + graph._impl + if not hasattr(algorithms, func): + raise NotImplementedError( + f"Currently {algorithm} algorithm isn't implemented for " + "finding maximal cliques on graphs.") + return getattr(algorithms, func)(graph) diff --git a/pydatastructs/graphs/tests/test_algorithms.py b/pydatastructs/graphs/tests/test_algorithms.py index 553377f3..3dcfd898 100644 --- a/pydatastructs/graphs/tests/test_algorithms.py +++ b/pydatastructs/graphs/tests/test_algorithms.py @@ -2,7 +2,7 @@ breadth_first_search_parallel, minimum_spanning_tree, minimum_spanning_tree_parallel, strongly_connected_components, depth_first_search, shortest_paths,all_pair_shortest_paths, topological_sort, -topological_sort_parallel, max_flow, find_bridges) +topological_sort_parallel, max_flow, find_bridges, find_maximal_cliques) from pydatastructs.utils.raises_util import raises def test_breadth_first_search(): @@ -382,8 +382,8 @@ def _test_topological_sort(func, ds, algorithm, threads=None): _test_topological_sort(topological_sort, "List", "kahn") _test_topological_sort(topological_sort_parallel, "List", "kahn", 3) - def test_max_flow(): + def _test_max_flow(ds, algorithm): import pydatastructs.utils.misc_util as utils GraphNode = getattr(utils, "Adjacency" + ds + "GraphNode") @@ -451,8 +451,8 @@ def _test_max_flow(ds, algorithm): _test_max_flow("List", "dinic") _test_max_flow("Matrix", "dinic") - def test_find_bridges(): + def _test_find_bridges(ds): import pydatastructs.utils.misc_util as utils GraphNode = getattr(utils, "Adjacency" + ds + "GraphNode") @@ -504,3 +504,35 @@ def _test_find_bridges(ds): _test_find_bridges("List") _test_find_bridges("Matrix") + +def test_maximal_cliques(): + + def _test_maximal_cliques(ds, algorithm): + import pydatastructs.utils.misc_util as utils + GraphNode = getattr(utils, "Adjacency" + ds + "GraphNode") + a, b, c, d, e, f = \ + [GraphNode(chr(x)) for x in range(ord('a'), ord('f') + 1)] + + graph = Graph(a, b, c, d, e, f) + + graph.add_edge(a.name, b.name) + graph.add_edge(a.name, e.name) + graph.add_edge(b.name, a.name) + graph.add_edge(b.name, c.name) + graph.add_edge(b.name, e.name) + graph.add_edge(c.name, b.name) + graph.add_edge(c.name, d.name) + graph.add_edge(d.name, c.name) + graph.add_edge(d.name, e.name) + graph.add_edge(d.name, f.name) + graph.add_edge(e.name, a.name) + graph.add_edge(e.name, b.name) + graph.add_edge(e.name, d.name) + graph.add_edge(f.name, d.name) + + cliques = find_maximal_cliques(graph, algorithm) + expected_cliques = [['a', 'b', 'e'], ['b', 'c'], ['c', 'd'], ['d', 'e'], ['d', 'f']] + assert cliques == expected_cliques + + _test_maximal_cliques("List", "bron_kerbosc") + _test_maximal_cliques("Matrix", "bron_kerbosc")