A graph is a finite set of vertices and connected by a set of edges.
(1:M)---(0:E)
| \ |
| \ |
| \ |
(2:B)---(3:L)
\
\
(4:P)
Types of graphs:
- Undirected Graph: nodes are connected by edges that are all bidirectional.
- Directed Graph: nodes are connected by directed edges – they only go in one direction.
Graph Adjacency List Representation
- The size of the array is equal to the number of nodes.
- A single index, array[i] represents the list of nodes adjacent to the ith node.
0 -> 1 -> 3#
1 -> 0 -> 2 -> 3#
2 -> 1 -> 3 -> 4#
3 -> 0 -> 1 -> 2#
4 -> 2#
Graph Adjacency Matrix Representation
- An Adjacency Matrix is a 2D array of size V x V where V is the number of nodes in a graph.
- A slot matrix[i][j] = 1 indicates that there is an edge from node i to node j.
| 0 | 1 | 2 | 3 | 4 | |
|---|---|---|---|---|---|
| 0 | 0 | 1 | 0 | 1 | 0 |
| 1 | 1 | 0 | 1 | 1 | 0 |
| 2 | 0 | 1 | 0 | 1 | 1 |
| 3 | 1 | 1 | 1 | 0 | 0 |
| 4 | 0 | 0 | 1 | 0 | 0 |
Graph Python Implementation
graph = {
'A': ['B', 'C'], # A -> B, A -> C
'B': ['C', 'D'], # B -> C, B -> D
'C': ['D'], # C -> D
'D': ['C'], # D -> C
'E': ['F'], # E -> F
'F': ['C'] # F -> C
}Depth-first search is an algorithm for traversing or searching tree or graph data structures. The algorithm starts at the root node and explores as far as possible along each branch before backtracking.
visited = set()
def dfs(node):
# base case
if not node:
return
# already visited
if node in visited:
return
# process current node
process(node.val)
# add to visited
visited.add(node)
# process neighbors / children
for child in node.children:
dfs(child)Island Problem
def island(self, grid: List[List[int]]):
# length of row and column
m, n = len(grid), len(grid[0])
def dfs(r, c):
# base case: grid[r][c] is out of bound
if not inArea(r, c):
return
# current node is ocean, or it's already visited
if grid[r][c] == 0 or grid[r][c] == -1:
return
# mark as visited
grid[r][c] = -1
# visit neighbor nodes
dfs(r+1, c) # UP
dfs(r-1, c) # DOWN
dfs(r, c-1) # LEFT
dfs(r, c+1) # RIGHT
def inArea(r, c):
return 0 <= r < m and 0 <= c < n
for r in range(m):
for c in range(n):
# start dfs for each element in grid
dfs(r, c)Leetcode Problems
- 200. Number of Islands
- 695. Max Area of Island
- 463. Island Perimeter
- 827. Making A Large Island
- 36. Valid Sudoku
- 37. Sudoku Solver
Breadth-first search is an algorithm for searching a tree data structure for a node that satisfies a given property. It starts at the tree root and explores all nodes at the present depth prior to moving on to the nodes at the next depth level.
def bfs(root):
queue = collections.deque([root])
visited = set()
# Loop until queue is empty
while queue:
# get current node from queue
node = queue.popleft()
# already visited
if node in visited:
continue
# process current node
process(node.val)
# add to visited
visited.add(node)
# process neighbors / children
for child in node.children:
queue.append(child)Level Order
def levelOrder(root):
queue = collections.deque([root])
visited = set()
res = []
# Loop until queue is empty
while queue:
# process all nodes from the current level
level_nodes = []
for _ in range(len(queue)):
# get current node from queue
node = queue.popleft()
# check if the node is already visited
if node in visited:
continue
# process current node
level_nodes.append(node.val)
# add to visited
visited.add(node)
# process children
if node.children:
for child in node.children:
if child not in visited:
queue.append(child)
res.append(level_nodes)
return resShortest Path
def shortestPath(start, target): # any two nodes, doesn't have to start from the root
queue = collections.deque([start])
visited = set([start])
step = 0
# Loop until queue is empty
while queue:
# spread the search from the current level
for _ in range(len(queue)):
# get current node from queue
node = queue.popleft()
# see if we reach the target
if node is target:
return step
# process children
if node.children:
for child in node.children:
if child not in visited:
queue.append(child)
visited.add(child)
step += 1
return 0 # not foundBidirectional BFS
def biBfs(source, target):
sourceQueue = collections.deque([source])
targetQueue = collections.deque([target])
visited = set([source])
step = 0
while sourceQueue and targetQueue:
# choose the smaller queue to spread
if len(sourceQueue) > len(targetQueue):
sourceQueue, targetQueue = targetQueue, sourceQueue
for _ in range(len(sourceQueue)):
node = sourceQueue.popleft()
for child in node.children:
# source and target meet
if child in targetQueue:
return step + 1
if child not in visited:
sourceQueue.append(child)
visited.add(child)
step += 1
return 0 # not foundLeetcode Problems
- 111. Minimum Depth of Binary Tree
- 515. Find Largest Value in Each Tree Row
- 127. Word Ladder
- 126. Word Ladder II
- 752. Open the Lock
- 433. Minimum Genetic Mutation
- 529. Minesweeper
- 773. Sliding Puzzle
A-star is a graph traversal and path search algorithm, which is often used in computer science due to its completeness, optimality, and optimal efficiency. One major practical drawback is its O space complexity, as it stores all generated nodes in memory.
def AstarSearch(graph, start, end):
pq = collections.priority_queue() # priority —> heuristic function
pq.append([start])
visited.add(start)
while pq:
node = pq.pop() # can we add more intelligence here?
visited.add(node)
process(node)
nodes = generate_related_nodes(node)
unvisited = [node for node in nodes if node not in visited]
pq.push(unvisited)Leetcode Problems