source == destination
, return True
, as no path is needed when starting at the destination.defaultdict(list)
called graph
. For each edge [u, v]
in edges
, append v
to graph[u]
and u
to graph[v]
, forming an undirected graph with bidirectional connections.seen
and add the source
node to mark it as visited, preventing cycles during traversal.This solution uses a recursive depth-first search to explore the graph, diving deep into each branch before backtracking.
dfs(i)
that takes a node i
:
i == destination
, return True
, indicating the target is reached.nei_node
in graph[i]
, if not in seen
:
nei_node
to seen
.dfs(nei_node)
. If it returns True
, propagate True
.False
.dfs(source)
and return its result.seen
set ensures nodes are visited once, preventing infinite loops in the undirected graph. It’s concise but may face recursion depth issues for very deep graphs.
This solution uses an iterative depth-first search with a stack, mimicking the recursive approach without recursion overhead.
source
node.stack
is not empty:
node
) from the stack.node == destination
, return True
.nei_node
in graph[node]
, if not in seen
:
nei_node
to seen
.nei_node
onto the stack.False
.seen
set prevents cycles.
This solution uses a breadth-first search with a queue, exploring nodes level by level from the source.
deque
(q
) and append the source
node.q
is not empty:
node
) using popleft()
.node == destination
, return True
.nei_node
in graph[node]
, if not in seen
:
nei_node
to seen
.nei_node
to the queue.False
.seen
set prevents cycles, and the queue manages level-by-level traversal, making it ideal for finding paths (potentially the shortest in terms of edges).
The task is to determine whether there is a path between two nodes (source and destination) in an undirected graph. This is a fundamental graph traversal problem that can be solved using depth-first search (DFS), breadth-first search (BFS), or Union-Find. Since the graph is undirected, an edge [u, v] means both u is connected to v and v is connected to u.
The core idea is to traverse the graph starting from the source node and see if we can reach the destination. To do this, we first represent the graph using an adjacency list. For each edge [u, v], we record both u → v and v → u in a dictionary to capture the undirected nature of the graph.
After building the graph, we use a standard DFS approach.
A stack is initialized with the source node and a set is used to track visited nodes to prevent cycles or repeated work.
At each step, we pop a node from the stack and check if it matches the destination.
If it does, we return true
.
If not, we push all unvisited neighbors of the current node onto the stack and continue.
If the traversal ends without finding the destination, we return false
.
- If the source is equal to the destination, we return true
immediately, as the path trivially exists.
- If there are no edges or the source node has no connections, we cannot proceed, and false
is returned.
Time Complexity: O(V + E), where V is the number of nodes and E is the number of edges. We potentially visit every node and edge once.
Space Complexity: O(V + E) for storing the graph and visited set, and O(V) for the recursion stack or explicit stack used in DFS.
This problem is a classic example of graph traversal. DFS is a natural choice when we want to explore paths from one node to another, especially when we do not need the shortest path. The key to an efficient solution is using an adjacency list and tracking visited nodes to avoid unnecessary work.