Jc-alt logo
jc

LeetCode: Graphs I DFS BFS

LeetCode: Graphs I DFS BFS
15 min read
data structures and algorithms

Graphs Intro

LeetCode problems with graph based solutions.

What is a Graph?

A graph is a data structure used to represent relationships between entities.

463. Island Perimeter ::2:: - Easy

Topics: Connected Component, Array, Depth First Search, Breadth First Search, Matrix, Grid

Intro

You are given row x col grid representing a map where grid[i][j] = 1 represents land and grid[i][j] = 0 represents water. Grid cells are connected horizontally/vertically (not diagonally). The grid is completely surrounded by water, and there is exactly one island (i.e., one or more connected land cells). The island doesn't have "lakes", meaning the water inside isn't connected to the water around the island. One cell is a square with side length 1. The grid is rectangular, width and height don't exceed 100. Determine the perimeter of the island.

Example InputOutput
grid = [[0,1,0,0],[1,1,1,0],[0,1,0,0],[1,1,0,0]]16
grid = [[1]]4
grid = [[1,0]]4

Constraints:

row == grid.length

col == grid[i].length

1 ≤ row, col ≤ 100

grid[i][j] is 0 or 1

There is exactly one island in grid

Abstraction

For every node in the single guaranteed connected component, count how many of its 4 sides touch water or the grid boundary. DFS and BFS can operate directly on the grid representation.

Pseudocode

Sol 1: DFS Cell Adjacent Counting
1. (rows, cols = width, height)
2. (perimeter = 0)
3. for (r, c) in range(rows, cols):
    a. if grid[r][c] == 1:
        perimeter += 4
        For neighbor if neighbor is == 1
            perimeter -= 1
4. return perimeter

Sol 2: BFS Iterative Track All Visited Land In Current Island
1. (rows, cols = width, height)
3. (start = any land cell (r, c))
4. (perimeter = 0)
5. (visited = {start})
6. (queue = deque([start]))
7. while queue:
   a. (r, c) = queue.popleft()
   b. For neighbor if neighbor == 0
        perimeter += 1 
        continue
   c. else neighbor (r, c) not in visited:
        visited.add(neighbor (r, c))
        queue.append(neighbor (r, c))
8. return perimeter

Solution 1: [DFS] DFS Cell Adjacent Counting - Graph/DFS Recursive Grid Exploration

    def islandPerimeter(self, grid: List[List[int]]) -> int:
        
        # Note:
        # Each land cell contributes 4 edges,
        # but if two land cells are adjacent they share one edge,
        # and that shared edge removes 2 perimeter edges,
        # one from each cell.
        # Thus, for each land cell start with 4 sides
        # and subtract 1 for each adjacent land neighbor.
        # Since each shared edge is counted twice, once per cell), 
        # and subtracting per neighbor naturally handles correct perimeter.
        
        # Grid Traversal
        rows = len(grid)
        cols = len(grid[0])
        
        # Total Perimeter
        perimeter = 0
        
        # Iterate through entire grid
        # tc: O(r*c)
        for r in range(rows):
            for c in range(cols):
                
                # Only process land cells, skip water cells
                if grid[r][c] == 1:
                    
                    # Starts each land cell with a value of 4, 
                    # we subtract 1 per land neighbor due to their shared edge
                    perimeter += 4
                    
                    # 4 Neighbors:
                    #   - top, bottom, left, right
                    if r-1 >= 0 and grid[r-1][c] == 1:
                        perimeter -= 1                    
                    if r+1 <= rows-1 and grid[r + 1][c] == 1:
                        perimeter -= 1                    
                    if c-1 >= 0 and grid[r][c-1] == 1:
                        perimeter -= 1                    
                    if c-1 <= cols-1 and grid[r][c+1] == 1:
                        perimeter -= 1
        
        # overall: tc O(r*c)
        # overall: sc O(1)
        return perimeter

Solution 2: [BFS] BFS Iterative Track All Visited Land In Current Island - Graph/BFS Iterative Grid Exploration Skip If Seen Already

    def islandPerimeter(self, grid: List[List[int]]) -> int:
        
        # Note:
        # Each land cell contributes 4 edges to the perimeter.        
        # If a land cell's neighbor is also land, that shared edge
        # is internal to the island and does not belong to the perimeter.
        # For each land cell, count only the sides that face "outward",
        # either off the grid entirely, or into a water cell (0).

        # Grid Traversal
        rows = len(grid)
        cols = len(grid[0])
        directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]

        # Pick any land cell to initialize the BFS
        start = None
        for r in range(rows):
            for c in range(cols):

                # Found some land, save coordinates
                if grid[r][c] == 1:
                    start = (r, c)
                    break
            if start:
                break

        # Total perimeter count
        perimeter = 0

        # Track land we have already visited to avoid 
        # sc: O(n)
        visited = {start}

        # BFS Iterative Queue:
        # holds land cells we have queued to expand
        queue = deque([start])

        while queue:

            # Pop land cell
            r, c = queue.popleft()

            # Examine all 4 neighbors
            for dr, dc in directions:

                # New Neighbor Coordinates
                nr, nc = r + dr, c + dc

                # Valid perimeter neighbor has either neighbor of:
                #   - out of bounds of the grid
                #   - water cell
                #   - out of bounds and water
                if nr < 0 or nr >= rows or 
                   nc < 0 or nc >= cols or 
                   grid[nr][nc] == 0:
                    perimeter += 1

                    # Early Pruning:
                    # no need to explore a perimeter neighbor
                    continue

                # Neighbor was inbounds and land, 
                # if has not been visited, enqueue it to explore:
                if (nr, nc) not in visited:
                    visited.add((nr, nc))
                    queue.append((nr, nc))

        # overall: tc O(r*c)
        # overall: sc O(r*c)
        return perimeter

733. Flood Fill ::2:: - Easy

Topics: Connected Component, Array, Depth First Search, Breadth First Search, Matrix, Grid

Intro

You are given an image represented by an m x n grid of integers image, where image[i][j] represents the pixel value of the image. You are also given three integers sr, sc, and color. Your task is to perform a flood fill on the image starting from the pixel image[sr][sc]. To perform a flood fill: Begin with the starting pixel and change its color to color. Perform the same process for each pixel that is directly adjacent (pixels that share a side with the original pixel, either horizontally or vertically) and shares the same color as the starting pixel. Keep repeating this process by checking neighboring pixels of the updated pixels and modifying their color if it matches the original color of the starting pixel. The process stops when there are no more adjacent pixels of the original color to update. Return the modified image after performing the flood fill.

Example InputOutput
image = [[1,1,1],[1,1,0],[1,0,1]], sr = 1, sc = 1, color = 2[[2,2,2],[2,2,0],[2,0,1]]
image = [[0,0,0],[0,0,0]], sr = 0, sc = 0, color = 0[[0,0,0],[0,0,0]]

Constraints:

m == image.length

n == image[i].length

1 ≤ m, n ≤ 50

0 ≤ image[i][j], color < 2^16

0 ≤ sr < m

0 ≤ sc < n

Abstraction

Change every node in the connected component containing the starting pixel (sr, sc)to the target color. Given a grid representation of a graph. DFS and BFS can operate directly on the grid representation.

Pseudocode

Sol 1: DFS Recursive Same Color Neighbor Fill
1. (rows, cols = width, height)
2. (from_color = image[start_row][start_column])
3. if from_color == to_color: 
    return image
4. dfs(r, c):
   a. if out of bounds: 
        return
   b. if image[r][c] != start_color: 
        return
   c. image[r][c] = from_color
        for neighbor:
            dfs(neighbor (r, c))
5. dfs(, sc)
6. return image

Sol 2: BFS Iterative Track All Repainted Pixels In Current Region
1. (rows, cols = width, height)
2. (from_color = image[start_row][start_column])
4. if from_color == to_color: 
    return image
5. queue = deque([(start_row, start_column)])
6. while queue:
   a. (r, c) = queue.popleft()
   b  image[r][c] = color
   c. for each neighbor (nr, nc):
        if out of bounds: 
            continue
        if image[nr][nc] != start_color: 
            continue
        queue.append((nr, nc))
8. return image

Solution 1: [DFS] DFS Recursive Same Color Neighbor Fill - Graph/DFS Recursive Grid Exploration

    def floodFill(self, image: List[List[int]], sr: int, sc: int, color: int) -> List[List[int]]:
        
        # Note:
        # Starting from (sr, sc), repaint every pixel that is
        # connected to it (up/down/left/right) and shares the
        # same original color. Recursion naturally stops once
        # we hit a pixel that doesn't match the original color,
        # including pixels we've already repainted (since after
        # repainting they no longer match the old color, unless
        # old color == new color, which we guard against).
        
        # Grid Traversal
        rows = len(image)
        cols = len(image[0])
        
        # Original color we are replacing
        start_color = image[sr][sc]
        
        # Edge case: if the new color is the same as the old color,
        # repainting would recurse forever (never "changes" color),
        # so just return the image unchanged.
        if start_color == color:
            return image
        
        def dfs(r, c):
            
            # Out of bounds, stop
            if r < 0 or r >= rows or c < 0 or c >= cols:
                return
            
            # Not the color we're flooding, stop
            if image[r][c] != start_color:
                return
            
            # Repaint current pixel
            image[r][c] = color
            
            # 4 Neighbors:
            #   - top, bottom, left, right
            dfs(r - 1, c)
            dfs(r + 1, c)
            dfs(r, c - 1)
            dfs(r, c + 1)
        
        # Kick off recursion from the starting pixel
        # tc: O(r*c)
        dfs(sr, sc)
        
        # overall: tc O(r*c)
        # overall: sc O(r*c) -- recursion stack in the worst case (all one color)
        return image

Solution 2: [BFS] BFS Iterative Track All Repainted Pixels In Current Region - Graph/BFS Iterative Grid Exploration Skip If Seen Already

    def floodFill(self, image: List[List[int]], sr: int, sc: int, color: int) -> List[List[int]]:
        
        # Note:
        # Starting from (sr, sc), repaint every pixel connected to it
        # (up/down/left/right) that shares the original color.
        # Use a queue to expand outward layer by layer, only enqueueing
        # pixels that still match the original color and haven't been
        # repainted yet.

        # Grid Traversal
        rows = len(image)
        cols = len(image[0])
        directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]

        # Original color we are replacing
        start_color = image[sr][sc]

        # Edge case: if the new color is the same as the old color,
        # there is nothing to do, and checking image[r][c] != color
        # below would never be true, so short-circuit here.
        if start_color == color:
            return image

        # BFS Iterative Queue:
        # holds pixels we have queued to expand
        queue = deque([(sr, sc)])

        # Repaint the starting pixel immediately so we never
        # re-enqueue it
        image[sr][sc] = color

        while queue:

            # Pop pixel
            r, c = queue.popleft()

            # Examine all 4 neighbors
            for dr, dc in directions:

                # New Neighbor Coordinates
                nr, nc = r + dr, c + dc

                # Valid neighbor to repaint has all of:
                #   - in bounds of the grid
                #   - still matches the original color
                if nr < 0 or nr >= rows or nc < 0 or nc >= cols:
                    continue
                if image[nr][nc] != start_color:
                    continue

                # Repaint and enqueue to expand from later
                image[nr][nc] = color
                queue.append((nr, nc))

        # overall: tc O(r*c)
        # overall: sc O(r*c)
        return image

133. Clone Graph ::2:: - Medium

Topics: Deep Copy, Connected Component, Hash Table, Depth First Search, Breadth First Search, Graph Theory, Adjacency List

Intro

Given a reference of a node in a connected undirected graph. Return a deep copy (clone) of the graph. Each node in the graph contains a value (int) and a list
(List[Node]) of its neighbors. class Node ( public int val; public List[Node] neighbors; ) Test case format: For simplicity, each node's value is the same as the node's index (1-indexed). For example, the first node with val == 1, the second node with val == 2, and so on. The graph is represented in the test case using an adjacency list. An adjacency list is a collection of unordered lists used to represent a finite graph. Each list describes the set of neighbors of a node in the graph. The given node will always be the first node with val = 1. You must return the copy of the given node as a reference to the cloned graph.

Example InputOutput
adjList = [[2,4],[1,3],[2,4],[1,3]][[2,4],[1,3],[2,4],[1,3]]
adjList = [[]][[]]
adjList = [][]

Constraints:

The number of nodes in the graph is in the range [0, 100].

1 ≤ Node.val ≤ 100

Node.val is unique for each node.

There are no repeated edges and no self-loops in the graph.

The Graph is connected and all nodes can be visited starting from the given node.

Abstraction

Given a graph represented by an adjacency list, return a deep copy.

Pseudocode

Sol 1: Recursive DFS
1. if not node: return None
2. clonedSeen = {}
3. dfs(n):
   a. copy = Node(n.val)
   b. clonedSeen[n] = copy
   c. for each neighbor in n.neighbors:
      if neighbor not in clonedSeen:
        copy.neighbors.append(dfs(neighbor))
      else:
        copy.neighbors.append(clonedSeen[neighbor])
   d. return copy
4. return dfs(node)

Sol 2: Iterative BFS
1. if not node: return None
2. clonedSeen = {node: Node(node.val)}
3. queue = deque([node])
4. while queue:
   a. current = queue.popleft()
   b. for each neighbor in current.neighbors:
      if neighbor not in clonedSeen:
        clonedSeen[neighbor] = Node(neighbor.val)
        queue.append(neighbor)
      clonedSeen[current].neighbors.append(clonedSeen[neighbor])
5. return clonedSeen[node]

Solution 1: [DFS] Recursive DFS - Graph/DFS Recursive Grid Exploration Skip If Seen Already

    def cloneGraph(self, node: Optional['Node']) -> Optional['Node']:
        
        # Note:
        # 1. Iterate over all nodes
        # 2. For each new node, clone and add to hashmap,
        #    if neighbors are not in seen set, explore
        # 3. HashMap() will act as clone hash set and tracking seen set
        
        # Empty Check:
        # nothing to copy
        if not node:
            return None

        # Deep copy for new list
        # sc: O(n)
        clonedSeen = {}

        # tc: O(V + E) visit each node and edge once
        # sc: O(V)  for hashmap + recursion stack
        def dfs(n) -> Node:

            # New Node Found:
            # clone and add to hashmap
            copy = Node(n.val)
            clonedSeen[n] = copy

            # If neighbors are not in seen set, explore
            for neighbor in n.neighbors:
                
                # Not Yet Seen:
                if neighbor not in clonedSeen:
                    copy.neighbors.append(dfs(neighbor))

                # Already seen, grab clone from hashmap:
                else:
                    copy.neighbors.append(clonedSeen[neighbor])

            # Pass back the cloned node
            return copy

        # Grab root of cloned graph
        cloneRoot = dfs(node)

        # overall: tc O(V + E) 
        # overall: sc O(V)
        return cloneRoot

Solution 2: [BFS] Iterative BFS - Graph/BFS Iterative Grid Exploration Skip If Seen

    def cloneGraph(self, node: Optional['Node']) -> Optional['Node']:
        
        # Note:
        # BFS Iterative Level By Level:
        # 1. Start at any node
        # 2. For each new node, make a clone, and
        #    check if neighbors are not in seen set, if so explore
        # 3. HashMap will serve as clone hashmap and seen set

        # Empty Check: 
        # graph has no nodes, nothing to clone
        if not node:
            return None

        # Deep copy for new list
        # sc: O(n)
        clonedSeen = {}
        clonedSeen[node] = Node(node.val)

        # BFS Iterative Queue:
        # sc: O(V)
        queue = deque()
        queue.append(node)

        # tc: O(V + E), each node and edge visited once
        # sc: O(V), for hashmap + queue
        while queue:

            # Pop node
            current = queue.popleft()

            # Explore neighbors
            for neighbor in current.neighbors:

                # Not yet cloned:
                if neighbor not in clonedSeen:
                    clonedSeen[neighbor] = Node(neighbor.val)
                    queue.append(neighbor)

                # Grab clone from hashmap:
                clonedSeen[current].neighbors.append(clonedSeen[neighbor])

        # Grab root of cloned graph
        cloneRoot = clonedSeen[node]

        # overall: tc O(V + E)
        # overall: sc O(V)
        return cloneRoot

329. Longest Increasing Path in a Matrix ::2:: - Hard

Topics: Connected Component, Array, Dynamic Programming, Depth First Search, Breadth First Search, Rule Based Graph, Graph Theory, Topological Sort, Memoization, Matrix, Directed Acyclic Graph

Intro

Given an m x n integers matrix, return the length of the longest increasing path in matrix. From each cell, you can either move in four directions: left, right, up, or down. You may not move diagonally or move outside the boundary (i.e., wrap-around is not allowed).

Example InputOutput
matrix = [[9,9,4],[6,6,8],[2,1,1]]4

Constraints:

m == matrix.length

n == matrix[i].length

1 ≤ m, n ≤ 200

0 ≤ matrix[i][j] ≤ 2^31 - 1

Abstraction

Given a tree represented as a matrix, find the longest increasing path where nodes are representing by cells in the graph and connections between nodes are represented by up, down, right, left neighbor connections.

Pseudocode

oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [DFS] DFS - Tree/DFS Post Order Traversal

Solution 2: [BFS] BFS Topological Sort - Tree/BFS Reverse Level Order Traversal