From a215f8729ec8a37514a38bdba5921218dcc9b7a6 Mon Sep 17 00:00:00 2001 From: Ashwin Date: Thu, 22 Oct 2026 11:50:58 +0530 Subject: [PATCH] graphs: fix articulation points and add doctests --- graphs/articulation_points.py | 116 +++++++++++++++++++++------------- 1 file changed, 72 insertions(+), 44 deletions(-) diff --git a/graphs/articulation_points.py b/graphs/articulation_points.py index 1de20e13752b..1933faa5b731 100644 --- a/graphs/articulation_points.py +++ b/graphs/articulation_points.py @@ -1,55 +1,83 @@ # Finding Articulation Points in Undirected Graph -def compute_ap(graph) -> None: +# https://en.wikipedia.org/wiki/Biconnected_component + + +def compute_ap(graph: dict[int, list[int]]) -> list[int]: + """ + Return the articulation points of an undirected graph in increasing order. + + An articulation point is a vertex whose removal disconnects part of the graph. + The graph is an adjacency list whose keys are the vertices 0 to n - 1. + + >>> compute_ap({0: [1, 2], 1: [0, 2], 2: [0, 1, 3, 5], 3: [2, 4], 4: [3], + ... 5: [2, 6, 8], 6: [5, 7], 7: [6, 8], 8: [5, 7]}) + [2, 3, 5] + >>> compute_ap({0: [5, 4], 1: [3], 2: [5, 3], 3: [1, 2], 4: [5, 0], + ... 5: [2, 4, 0]}) + [2, 3, 5] + >>> compute_ap({0: [1], 1: [0, 2], 2: [1]}) + [1] + >>> compute_ap({0: [1, 2], 1: [0, 2], 2: [0, 1]}) + [] + >>> compute_ap({0: [1], 1: [0], 2: [3, 4], 3: [2, 4], 4: [2, 3]}) + [] + >>> compute_ap({0: [1], 1: [0, 2], 2: [1], 3: [4], 4: [3, 5], 5: [4]}) + [1, 4] + >>> compute_ap({0: []}) + [] + >>> compute_ap({}) + [] + """ n = len(graph) - out_edge_count = 0 - low = [0] * n - visited = [False] * n + discovery = [-1] * n # order in which each vertex is first reached (-1: not yet) + low = [0] * n # smallest discovery number reachable from the vertex's branch is_art = [False] * n + timer = 0 - def dfs(root, at, parent, out_edge_count): - if parent == root: - out_edge_count += 1 - visited[at] = True - low[at] = at - + def dfs(at: int, parent: int) -> int: + """Visit ``at`` and return the number of branches the search starts from it.""" + nonlocal timer + discovery[at] = low[at] = timer + timer += 1 + children = 0 for to in graph[at]: if to == parent: - pass - elif not visited[to]: - out_edge_count = dfs(root, to, at, out_edge_count) + continue + if discovery[to] == -1: + children += 1 + dfs(to, at) low[at] = min(low[at], low[to]) - - # AP found via bridge - if at < low[to]: - is_art[at] = True - # AP found via cycle - if at == low[to]: + # a branch that cannot get back above ``at`` is cut off without it + if parent != -1 and low[to] >= discovery[at]: is_art[at] = True else: - low[at] = min(low[at], to) - return out_edge_count + low[at] = min(low[at], discovery[to]) + return children for i in range(n): - if not visited[i]: - out_edge_count = 0 - out_edge_count = dfs(i, i, -1, out_edge_count) - is_art[i] = out_edge_count > 1 - - for x in range(len(is_art)): - if is_art[x] is True: - print(x) - - -# Adjacency list of graph -graph = { - 0: [1, 2], - 1: [0, 2], - 2: [0, 1, 3, 5], - 3: [2, 4], - 4: [3], - 5: [2, 6, 8], - 6: [5, 7], - 7: [6, 8], - 8: [5, 7], -} -compute_ap(graph) + if discovery[i] == -1: + # the starting vertex is critical only if the search leaves it + # in two or more separate directions + is_art[i] = dfs(i, -1) > 1 + + return [i for i in range(n) if is_art[i]] + + +if __name__ == "__main__": + import doctest + + doctest.testmod() + + # Adjacency list of graph + graph = { + 0: [1, 2], + 1: [0, 2], + 2: [0, 1, 3, 5], + 3: [2, 4], + 4: [3], + 5: [2, 6, 8], + 6: [5, 7], + 7: [6, 8], + 8: [5, 7], + } + print(compute_ap(graph))