| Aspect | Details |
|---|---|
| Source | Classic Graph Algorithm |
| Category | Graph Algorithms |
| Difficulty | Medium |
| Key Technique | Tarjan’s Algorithm with Low-Link Values |
| Time Complexity | O(V + E) |
| Space Complexity | O(V) |
After completing this analysis, you will be able to:
An articulation point (also called a cut vertex) is a vertex in an undirected graph whose removal disconnects the graph (or increases the number of connected components).
Original Graph: After removing vertex 2:
1---2---3 1 3
|
4 4
Vertex 2 is an articulation point because removing it
disconnects {1} from {3, 4}.
Problem: Given an undirected graph with n vertices and m edges, find all articulation points.
Input:
n and mm lines: two integers u and v describing an edgeOutput:
Constraints:
Input:
5 5
1 2
2 3
2 4
3 4
4 5
Output:
2 4
Explanation:
During DFS traversal, we assign each vertex two values:
disc[v]): When vertex v was first visitedlow[v]): The minimum discovery time reachable from the subtree rooted at vIf low[child] >= disc[current]:
"child" cannot reach any ancestor of "current" without going through "current"
Therefore, "current" is an articulation point
DFS Tree: Discovery & Low-Link Values:
1 (root) disc: [1, 2, 3, 4, 5]
| low: [1, 2, 2, 2, 5] (after propagation)
2
/ \ At vertex 2:
3---4 - Child 4 has low[4] = 2 >= disc[2] = 2
| - But there's a back edge 3-4, so low[3] = low[4] = 2
5
At vertex 4:
Back edge 3-4 allows - Child 5 has low[5] = 5 >= disc[4] = 4
low values to propagate - Vertex 4 is an articulation point
upward through the cycle
Condition 1 (Non-root vertex):
A non-root vertex u is an articulation point if it has a child v where:
low[v] >= disc[u]
This means v cannot reach any ancestor of u without passing through u.
Condition 2 (Root vertex): The DFS root is an articulation point if and only if it has two or more children in the DFS tree.
disc[] and low[] arrays to -1 (unvisited)u:
disc[u] = low[u] = timer++v:
v is unvisited: recurse, then low[u] = min(low[u], low[v])v is visited and not parent: low[u] = min(low[u], disc[v])function findArticulationPoints(graph):
timer = 0
disc = [-1] * n
low = [-1] * n
parent = [-1] * n
isArticulation = [false] * n
function dfs(u):
children = 0
disc[u] = low[u] = timer++
for each neighbor v of u:
if disc[v] == -1: # v not visited
children++
parent[v] = u
dfs(v)
low[u] = min(low[u], low[v])
# Condition 1: non-root with no back edge from subtree
if parent[u] != -1 and low[v] >= disc[u]:
isArticulation[u] = true
elif v != parent[u]: # Back edge
low[u] = min(low[u], disc[v])
# Condition 2: root with multiple children
if parent[u] == -1 and children > 1:
isArticulation[u] = true
for each vertex u:
if disc[u] == -1:
dfs(u)
return all u where isArticulation[u] is true
Graph:
1---2---3
| /|
| / |
4---5
Edges: (1,2), (2,3), (2,4), (3,4), (3,5), (4,5)
| Step | Current | Action | disc[] | low[] | Notes |
|---|---|---|---|---|---|
| 1 | 1 | Visit | [0,-,-,-,-] | [0,-,-,-,-] | Start DFS, timer=1 |
| 2 | 2 | Visit | [0,1,-,-,-] | [0,1,-,-,-] | From 1, timer=2 |
| 3 | 3 | Visit | [0,1,2,-,-] | [0,1,2,-,-] | From 2, timer=3 |
| 4 | 4 | Visit | [0,1,2,3,-] | [0,1,2,3,-] | From 3, timer=4 |
| 5 | 5 | Visit | [0,1,2,3,4] | [0,1,2,3,4] | From 4, timer=5 |
| 6 | 5 | Back edge to 3 | - | [0,1,2,3,2] | low[5]=min(4,disc[3])=2 |
| 7 | 5 | Back edge to 4 | - | [0,1,2,3,2] | low[5]=min(2,disc[4])=2 (no change) |
| 8 | 4 | Return from 5 | - | [0,1,2,2,2] | low[4]=min(3,low[5])=2 |
| 9 | 4 | Back edge to 2 | - | [0,1,2,1,2] | low[4]=min(2,disc[2])=1 |
| 10 | 3 | Return from 4 | - | [0,1,1,1,2] | low[3]=min(2,low[4])=1 |
| 11 | 2 | Return from 3 | - | [0,1,1,1,2] | low[2]=min(1,low[3])=1 |
| 12 | 2 | Check child 4 | - | - | Already visited, skip |
| 13 | 1 | Return from 2 | - | [0,0,1,1,2] | low[1]=min(0,low[2])=0 (no change) |
| Vertex | parent | children | Condition | Result |
|---|---|---|---|---|
| 1 | -1 (root) | 1 | Root needs 2+ children | Not AP |
| 2 | 1 | 2 (3,4 in DFS tree) | low[3]=1 >= disc[2]=1? Yes | AP |
| 3 | 2 | 1 (4) | low[4]=1 >= disc[3]=2? No | Not AP |
| 4 | 3 | 1 (5) | low[5]=2 >= disc[4]=3? No | Not AP |
| 5 | 4 | 0 | Leaf node | Not AP |
Result: Vertex 2 is the only articulation point.
Wait - let me recheck. In the DFS tree I traced:
Actually with the back edges 4-2, 5-3, 5-4, the low values propagate up allowing nodes below 2 to reach 2’s ancestors. But vertex 1 can only be reached through vertex 2.
Checking vertex 2:
Actually, the result depends on graph structure. In this particular fully connected subgraph {2,3,4,5}, removing vertex 2 would disconnect vertex 1. So vertex 2 is an articulation point.
# WRONG: Using low[v] instead of disc[v] for back edges
if v != parent[u]:
low[u] = min(low[u], low[v]) # INCORRECT!
# CORRECT: Use disc[v] for back edges (visited non-parent neighbors)
if v != parent[u]:
low[u] = min(low[u], disc[v]) # CORRECT!
Problem: Using low[v] can propagate incorrect values across back edges that don’t represent actual reachability.
# WRONG: Treating root the same as other nodes
if low[v] >= disc[u]:
is_articulation[u] = True # Wrong for root!
# CORRECT: Root is AP only if it has 2+ DFS children
if parent[u] == -1:
if children > 1:
is_articulation[u] = True
else:
if low[v] >= disc[u]:
is_articulation[u] = True
Problem: The root always satisfies low[v] >= disc[u] for its children, but it’s only an AP if removing it creates multiple disconnected subtrees.
# WRONG: Not excluding parent from back edge check
for v in graph[u]:
if disc[v] != -1:
low[u] = min(low[u], disc[v]) # Includes parent!
# CORRECT: Explicitly exclude parent
for v in graph[u]:
if disc[v] != -1 and v != parent[u]:
low[u] = min(low[u], disc[v])
Problem: The edge to parent is a tree edge, not a back edge. Including it gives incorrect low values.
# WRONG for multigraphs: Using simple parent check
if v != parent[u]:
...
# CORRECT for multigraphs: Track edge index or use edge count
# If there are multiple edges between u and parent,
# one can be a tree edge while others are back edges
| Case | Description | Handling |
|---|---|---|
| Single vertex | n=1, m=0 | No articulation points |
| Single edge | n=2, m=1 | Both vertices are articulation points |
| Complete graph | All vertices connected | No articulation points (except n<=2) |
| Linear chain | 1-2-3-4-5 | All middle vertices are articulation points |
| Star graph | Center connected to all | Only center is articulation point |
| Disconnected graph | Multiple components | Find APs in each component separately |
| Self-loops | Edge (u, u) | Ignore self-loops |
| Multiple edges | Parallel edges u-v | May affect AP status |
| Tree | n vertices, n-1 edges | All non-leaf vertices are APs |
import sys
from collections import defaultdict
def find_articulation_points(n, edges):
"""
Find all articulation points in an undirected graph.
Time: O(V + E)
Space: O(V + E) for adjacency list, O(V) for arrays
"""
# Build adjacency list
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
# Initialize arrays
disc = [-1] * (n + 1) # Discovery time
low = [-1] * (n + 1) # Low-link value
parent = [-1] * (n + 1) # Parent in DFS tree
is_ap = [False] * (n + 1) # Is articulation point?
timer = [0] # Use list for mutable closure
def dfs(u):
children = 0
disc[u] = low[u] = timer[0]
timer[0] += 1
for v in graph[u]:
if disc[v] == -1: # Not visited
children += 1
parent[v] = u
dfs(v)
# Update low-link from child
low[u] = min(low[u], low[v])
# Articulation point conditions
# Condition 1: u is root with 2+ children
if parent[u] == -1 and children > 1:
is_ap[u] = True
# Condition 2: u is not root and no back edge from v's subtree
if parent[u] != -1 and low[v] >= disc[u]:
is_ap[u] = True
elif v != parent[u]: # Back edge (not to parent)
low[u] = min(low[u], disc[v])
# Run DFS from each unvisited vertex (handles disconnected graphs)
for i in range(1, n + 1):
if disc[i] == -1:
dfs(i)
# Collect articulation points
return [i for i in range(1, n + 1) if is_ap[i]]
def solve():
input = sys.stdin.readline
n, m = map(int, input().split())
edges = []
for _ in range(m):
u, v = map(int, input().split())
edges.append((u, v))
result = find_articulation_points(n, edges)
if result:
print(len(result))
print(' '.join(map(str, sorted(result))))
else:
print(0)
if __name__ == "__main__":
sys.setrecursionlimit(200005)
solve()
| Operation | Complexity | Reason |
|---|---|---|
| Building adjacency list | O(E) | Process each edge once |
| DFS traversal | O(V + E) | Visit each vertex and edge once |
| Collecting results | O(V) | Check each vertex |
| Total | O(V + E) | Linear in graph size |
| Data Structure | Space | Purpose |
|---|---|---|
| Adjacency list | O(V + E) | Store graph |
| disc[] array | O(V) | Discovery times |
| low[] array | O(V) | Low-link values |
| parent[] array | O(V) | DFS tree parent |
| isAP[] array | O(V) | Result flags |
| Recursion stack | O(V) | DFS call stack |
| Total | O(V + E) | Dominated by adjacency list |
| Problem | Link | Connection |
|---|---|---|
| Building Roads | CSES 1666 | Finding connected components (prerequisite concept) |
| Flight Routes Check | CSES 1682 | Strong connectivity (directed graph variant) |
| Road Reparation | CSES 1675 | MST with connectivity (related infrastructure problem) |
| Road Construction | CSES 1676 | Dynamic connectivity tracking |
low[child] >= disc[node]Before moving on, ensure you can: