Recall that we have the following tree traversals:
Similarly, we can traverse a graph using BFS and DFS. Just like in trees, both BFS and DFS color each node in three colors in the traversal process:
In the “waterfront” analogy, you can imagine white nodes being dry areas, gray nodes being the waterfront, and the black areas being completely submerged in water. Note this analogy works slightly better for BFS (and its derivatives such as Dijkstra) than for DFS.
BFS for graph is exactly like BFS for tree, except that now you need to check whether a node has been visited before when you try to add it to the queue. You only want to add a “white” node that has never been seen before to the queue.
def bfs(n, edges): # full BFS procedure for the whole graph
def _BFS(v): # do one BFS from v
= [v] # start node
queue for u in queue: # pop u logically but not physically from queue
# BFS visit order
order.append(u) for w in adjlist[u]: # u->w
if color[w] == 0: # only if gray->white (tree edge)
queue.append(w)= 1 # gray
color[w] = 2 # black: i'm done
color[u]
= edges2adjlist(edges) # convert edges to adjaency list
adjlist = defaultdict(int) # default 0: white
color = [] # output
order for v in range(n):
if color[v] == 0: # only do BFS on white nodes
# another BFS tree in BFS forest
_BFS(v) return order
Caveats:
_BFS(v)
may
only reach part of the graph reachable from v
(and produces
a BFS tree rooted at v
). In order to make sure every node
is visited, we need this extra for loop (the set of BFS trees is called
the BFS forest, see below).deque
class which is double-ended queue (so that you can
pop/push on both ends in deque
is an overkill since we just need a queue.
Can you use something simpler, e.g., an array? But while array supports
push-right (append) in head += 1
, which is a
“logical” but not “physical” pop. The for loop to traverse the elements
in the (dynamic) queue becomes while head < len(queue):
and head == len(queue)
means the queue is empty. This
is the standard implementation found in most textbooks.for u in queue:
and it works even if
queue
is expanding on the right end! This the
recommended solution.For example,
0 --> 6 --> 2 --> 3 <-- 7 --> 8
/ | v
1 +---> 4 --> 5
>>> print(bfs(9, [(0,6), (1,6), (6,2), (2,3), (6,4), (4,5), (3,5), (7,3), (7,8)]))
0, 6, 2, 4, 3, 5, 1, 7, 8] [
Note that there are three BFS trees in this BFS forest:
0) BFS(1) BFS(7)
BFS(0 1 7
| |
6 8
/ \
2 4
| |
3 5
The time complexity for BFS is
BFS can be used to find the single-source shortest-path(s) in
unweighted graphs (where each edge has a unit cost), which is also known
as “uniform cost” search in AI. Basically, the first time you add a node
By contrast, in a general weighted graph, nodes in the queue can still be updated to better distances, and therefore you need to use a priority queue instead of a queue, and this becomes the Dijkstra algorithm. So Dijkstra is a generalization of BFS on weighted graphs.
DFS becomes more interesting on graphs, especially directed graphs. Unlike BFS, here we use a stack, and like BFS, we only push a node to the stack if it’s white. In real implementation, like in trees, we use recursion which maintains the implicit stack for you. The following DFS code also detects cycles as a by-product:
def dfs(n, edges): # DFS for the whole graph
def _DFS(v): # recursive
= 1 # gray
color[v]
order.append(v)for u in adjlist[v]: # v->u
if color[u] == 0: # tree edge (gray->white)
_DFS(u)= 2 # black
color[v]
= edges2adjlist(edges) # convert edges to adjacency list
adjlist = defaultdict(int) # default 0: white
color = [] # output
order for v in range(n): # no need for this loop if there is "God" node
if color[v] == 0:
# another DFS tree
_DFS(v)
return order
Note that
dfs(God)
.For example,
0 --> 6 --> 2 --> 3 <-- 7 --> 8
/ | v
1 +---> 4 --> 5
>>> print(dfs(9, [(0,6), (1,6), (6,2), (2,3), (6,4), (4,5), (3,5), (7,3), (7,8)]))
0, 6, 2, 3, 5, 4, 1, 7, 8] # different from BFS order [
Here is the DFS forest; as you can see DFS trees (being “depth”-first) are indeed “deeper” than BFS ones:
0) DFS(1) DFS(7)
DFS(0 1 7
| |
6 8
/ \
2 4
|
3
|
5
A very nice byproduct of DFS is cycle detection. In order to form a
cycle
Small caveat: for undirected graphs, visiting your parent is not a
possible back edge (so a back edge in an undirected graph must be
visting your grandparent or above), but for directed graph it is
possible (
Just add these two lines after if color[u]==0:
block:
if color[u] == 0: # tree edge (gray->white)
_DFS(u)
elif color[u] == 1: # gray: active; back edge (gray->gray)
print("cycle detected %d->%d" % (v, u))
For example, if we add two edges, 5->1
and
5->7
, to the above graph to form cycles:
0 --> 6 --> 2 --> 3 <-- 7 --> 8
/ | v ^
1 +---> 4 --> 5 ----+
^ |
---------------+
\
>>> print(dfs(9, [(0,6), (1,6), (6,2), (2,3), (6,4), (4,5), (3,5), (7,3), (7,8), (5,1), (5,7)]))
1->6
cycle detected 7->3
cycle detected 0, 6, 2, 3, 5, 1, 7, 8, 4] [
DFS detects cycle twice, producing this DFS forest with a single, deep, tree (with back edges shown in dotted edges):
0)
DFS(0
|
> 6
..../ \
. 2 4
. |
. 3 <..
. | .
. 5 .
. / \ .
. .1 7..
.|
8
Note that our code can only detect the existence of cycles. If you
also want to print a cycle (6->2->3->5->1->6
and 3->5->7
) then you do need to maintain the stack.
Note there are many other “related” cycles that are not detected in DFS,
such as 6->4->5->1->6
.
Like BFS, the time complexity for DFS is also
Above we used gray-to-white edges (tree edges) to construct the DFS tree and gray-to-gray edges (back edges) to detect cycles. But are there other cases such as gray-to-black edges? Well, yes and no – it all depends on the directedness of the graph.
On undirected graphs, DFS classifies each edge into two classes:
Why you can never rediscover a black node? Let’s prove by
contradiction. If current node
However, on directed graphs, DFS classifies each edge into four classes:
These classifications are not just useful for detecting cycles, but also useful in other more advanced connectivity/component problems such as detecting strongly connected components in directed graphs and detecting “bridges” in undirected graphs.
You can also use either BFS or DFS to figure out the topological ordering, which we’ll discuss next.