Jc-alt logo
jc

LeetCode: Graphs II BFS 01 Shortest Path Weighted Graph

LeetCode: Graphs II BFS 01 Shortest Path Weighted Graph
11 min read
data structures and algorithms

Zero One BFS Algorithm Intro

Intro

01 BFS is an efficient shortest path algorithm for graphs where edge weights are only 0 or 1

Special case of BFS/Dijkstras

Instead of using a standard priority queue, it uses a deque to achieve O(V + E) time

Efficiently computes shortest distances from a single source in such graphs

Graph Requirements

  1. Weighted graph with edge weights only 0 or 1 only
  2. Directed or Undirected
  3. Represented Using:
    • Adjacency List

Output

Shortest distance from the source node to all other nodes Optionally, the path itself

Video Animation

written: https://codeforces.com/blog/entry/22276

Pseudo Code

    from collections import deque

    def zero_one_bfs(graph, source):

        n = len(graph)
        dist = [float('inf')] * n
        dist[source] = 0

        dq = deque()
        dq.append(source)

        while dq:
            node = dq.popleft()
            for neighbor, weight in graph[node]:
                if dist[node] + weight < dist[neighbor]:
                    dist[neighbor] = dist[node] + weight
                    if weight == 0:
                        dq.appendleft(neighbor)  # 0-weight edges prioritized
                    else:
                        dq.append(neighbor)      # 1-weight edges go to back

        return dist

Time Complexity

Each vertex is processed at most once Each edge is considered at most once

Must faster than Dijkstras for this specific case O(E + V) instead of O(E log V)

Space Complexity

Distance Array: O(V) Deque: O(V) in worst case

IRL Use Case

  • Special Graphs Slide (0 cost) vs Step (1 cost)

  • Network Propagation With Free/Paid Edges 0 for instance transfer, 1 for delayed transfer

2290. Minimum Obstacle Removal to Reach Corner ::1:: - Hard

Topics: Array, Breadth First Search, Graph Theory, Heap (Priority Queue), Matrix, Shortest Path

Intro

You are given a 0-indexed 2D integer array grid of size m x n. Each cell has one of two values: 0 represents an empty cell, 1 represents an obstacle that may be removed. You can move up, down, left, or right from and to an empty cell. Return the minimum number of obstacles to remove so you can move from the upper left corner (0, 0) to the lower right corner (m - 1, n - 1).

Example InputOutput
grid = [[0,1,1],[1,1,0],[1,1,0]]2
grid = [[0,1,0,0,0],[0,1,0,1,0],[0,0,0,1,0]]0

Constraints:

m == grid.length

n == grid[i].length

1 ≤ m, n ≤ 10^5

2 ≤ m*n ≤ 10^5

grid[i][j] is either 0 or 1

grid[0][0] == grid[m-1][n-1] == 0

Abstraction

Given a graph full of empty cell or obstacles, determine the minimum number of obstacles to remove to move from top left to bottom right

Pseudocode

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

Solution 1: [01 BFS] 01 BFS Approach - Advanced Graphs/Advanced Graphs

    def minimumObstacles(self, grid: List[List[int]]) -> int:
        # Grid dimensions
        m, n = len(grid), len(grid[0])

        # Directions: up, down, left, right
        directions = [(1,0), (-1,0), (0,1), (0,-1)]

        # distance matrix: min obstacles removed to reach each cell
        dist = [[float('inf')] * n for _ in range(m)]
        dist[0][0] = grid[0][0]  # starting cost (remove if starting on 1)

        # deque for 0-1 BFS
        dq = deque()
        dq.append((0,0))

        while dq:

            r, c = dq.popleft()

            # Explore neighbors
            for dr, dc in directions:
                nr, nc = r + dr, c + dc

                # Valid boundaries
                if 0 <= nr < m and 0 <= nc < n:

                    # New distance = current + cost to enter neighbor
                    new_dist = dist[r][c] + grid[nr][nc]

                    # Relaxation
                    if new_dist < dist[nr][nc]:
                        dist[nr][nc] = new_dist

                        # 0-cost move → appendleft
                        if grid[nr][nc] == 0:
                            dq.appendleft((nr, nc))
                        # 1-cost move → append to back
                        else:
                            dq.append((nr, nc))

        # Result: min obstacles to remove to reach bottom-right
        return dist[m-1][n-1]

1368. Minimum Cost to Make at Least One Valid Path in a Grid ::1:: - Hard

Topics: Array, Breadth First Search, Graph Theory, Heap (Priority Queue), Matrix, Shortest Path

Intro

Given an m x n grid. Each cell of the grid has a sign pointing to the next cell you should visit if you are currently in this cell. The sign of grid[i][j] can be: 1 which means go to the cell to the right. (i.e go from grid[i][j] to grid[i][j + 1]) 2 which means go to the cell to the left. (i.e go from grid[i][j] to grid[i][j - 1]) 3 which means go to the lower cell. (i.e go from grid[i][j] to grid[i + 1][j]) 4 which means go to the upper cell. (i.e go from grid[i][j] to grid[i - 1][j]) Notice that there could be some signs on the cells of the grid that point outside the grid. You will initially start at the upper left cell (0, 0). A valid path in the grid is a path that starts from the upper left cell (0, 0) and ends at the bottom-right cell (m - 1, n - 1) following the signs on the grid. The valid path does not have to be the shortest. You can modify the sign on a cell with cost = 1. You can modify the sign on a cell one time only. Return the minimum cost to make the grid have at least one valid path.

Example InputOutput
grid = [[1,1,1,1],[2,2,2,2],[1,1,1,1],[2,2,2,2]]3
grid = [[1,1,3],[3,2,2],[1,1,4]]0
grid = [[1,2],[4,3]]1

Constraints:

m == grid.length

n == grid[i].length

1 ≤ m, n ≤ 100

1 ≤ grid[i][j] ≤ 4

Abstraction

What the heck!

Pseudocode

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

Solution 1: [01 BFS] 01 BFS Approach - Advanced Graphs/Advanced Graphs

    def minCost(self, grid):
        m, n = len(grid), len(grid[0])
        
        # directions: right, left, down, up
        dirs = [(0,1), (0,-1), (1,0), (-1,0)]
        
        # visited[i][j] = minimum cost to reach (i,j)
        visited = [[float('inf')] * n for _ in range(m)]
        visited[0][0] = 0
        
        dq = deque()
        dq.append((0,0,0))  # i, j, cost
        
        while dq:
            i, j, cost = dq.popleft()
            
            for d, (dx, dy) in enumerate(dirs, start=1):
                ni, nj = i + dx, j + dy
                
                if 0 <= ni < m and 0 <= nj < n:
                    # If we follow the arrow, cost = 0; otherwise cost = 1
                    new_cost = cost if d == grid[i][j] else cost + 1
                    
                    if new_cost < visited[ni][nj]:
                        visited[ni][nj] = new_cost
                        if d == grid[i][j]:
                            dq.appendleft((ni, nj, new_cost))  # 0-cost → front
                        else:
                            dq.append((ni, nj, new_cost))      # 1-cost → back
        
        return visited[m-1][n-1]

1263. Minimum Moves to Move a Box to Their Target Location ::1:: - Hard

Topics: Array, Breadth First Search, Heap (Priority Queue), Matrix, Shortest Path, Graph Theory

Intro

A storekeeper is a game in which the player pushes boxes around in a warehouse trying to get them to target locations. The game is represented by an m x n grid of characters grid where each element is a wall, floor, or box. Your task is to move the box 'B' to the target position 'T' under the following rules: The character 'S' represents the player. The player can move up, down, left, right in grid if it is a floor (empty cell). The character '.' represents the floor which means a free cell to walk. The character '#' represents the wall which means an obstacle (impossible to walk there). There is only one box 'B' and one target cell 'T' in the grid. The box can be moved to an adjacent free cell by standing next to the box and then moving in the direction of the box. This is a push. The player cannot walk through the box. Return the minimum number of pushes to move the box to the target. If there is no way to reach the target, return -1.

Example InputOutput
grid, look at LeetCode3

Constraints:

m == grid.length

n == grid[i].length

1 ≤ m, n ≤ 20

grid contains only characters '.', '#', 'S', 'T', or 'B'.

There is only one character 'S', 'B', and 'T' in the grid.

Abstraction

What the heck!

Pseudocode

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

Solution 1: [01 BFS] 01 BFS Approach - Advanced Graphs/Advanced Graphs

    def minPushBox(self, grid: List[List[str]]) -> int:
        
        # 01 BFS over the (box position, player position) state graph

        # Unlike a plain grid BFS, the real state here isn't just a
        # single cell, it's the pair (box position, player position),
        # since where the box can be pushed next depends on whether the
        # player can maneuver to the correct side of it. Two kinds of
        # edges exist between states: the player repositioning without
        # touching the box (free, cost 0), and the player pushing the
        # box into an adjacent cell (a real move, cost 1). 01 BFS uses
        # a deque instead of a plain queue so 0-cost edges are explored
        # before 1-cost edges, keeping the frontier processed in true
        # non-decreasing cost order without needing a full priority queue.

        m, n = len(grid), len(grid[0])
        dirs = [(-1, 0), (1, 0), (0, -1), (0, 1)]

        # Locate Key Cells:
        # scan once to find the starting player, box, and target positions
        for r in range(m):
            for c in range(n):
                if grid[r][c] == 'S':
                    player = (r, c)
                elif grid[r][c] == 'B':
                    box = (r, c)
                elif grid[r][c] == 'T':
                    target = (r, c)

        def isFree(r, c):
            # In Bounds And Walkable:
            # a cell is usable if it's on the grid and not a wall
            return 0 <= r < m and 0 <= c < n and grid[r][c] != '#'

        # State Encoding:
        # a state bundles box position and player position together,
        # since the same box location can have different reachable
        # pushes depending on where the player currently stands
        start = (box[0], box[1], player[0], player[1])
        dist = {start: 0}
        dq = deque([start])
        done = set()

        while dq:

            # Pop a state from the front of the deque
            state = dq.popleft()

            # Early Pruning:
            # skip if this state was already finalized with an equal or
            # lower cost through an earlier pop, since 01 BFS guarantees
            # the first finalization of a state is optimal
            if state in done:
                continue
            done.add(state)

            br, bc, pr, pc = state
            cost = dist[state]

            # Check:
            # if the box has reached target, we are guaranteed to have
            # the minimum number of pushes
            if (br, bc) == target:
                return cost

            # Player Repositioning (cost 0):
            # the player can step to any free adjacent cell that isn't
            # occupied by the box, without counting as a push
            for dr, dc in dirs:
                npr, npc = pr + dr, pc + dc
                if isFree(npr, npc) and (npr, npc) != (br, bc):
                    nstate = (br, bc, npr, npc)
                    if nstate not in dist or dist[nstate] > cost:
                        dist[nstate] = cost
                        dq.appendleft(nstate)

            # Box Pushing (cost 1):
            # a push in direction (dr, dc) requires the player standing
            # on the opposite side of the box, and the destination cell
            # for the box must itself be free
            for dr, dc in dirs:
                ppos = (br - dr, bc - dc)
                nbr, nbc = br + dr, bc + dc
                if isFree(nbr, nbc) and isFree(*ppos) and ppos == (pr, pc):
                    nstate = (nbr, nbc, br, bc)
                    ncost = cost + 1
                    if nstate not in dist or dist[nstate] > ncost:
                        dist[nstate] = ncost
                        dq.append(nstate)

        # overall: tc O(m^2 * n^2), since each of the O(m*n) box
        #   positions can pair with each of the O(m*n) player positions,
        #   and every state is expanded a constant number of times
        # overall: sc O(m^2 * n^2), for the dist map and done set storing
        #   every explored (box, player) state pair
        return -1