Jc-alt logo
jc
Hackerrank: Graphs II BFS Multi Source

Hackerrank: Graphs II BFS Multi Source

··
7 min read
·data structures and algorithms

Multi Source BFS Algorithm Intro

Intro

Multi Source BFS is an extension of Breadth First Search where we start BFS simultaneously from multiple source nodes.

3. Post Office Placement Add One — Hard

Topics: Multi Source BFS, Binary Search, Matrix, Chebyshev Distance Companies: Amazon

Intro

Given a 2D grid where 1 represents a post office and 0 represents an empty cell, you may add at most one new post office anywhere in the grid. Using 9-direction movement (8 neighbors + Chebyshev metric), find the minimum possible value of the maximum distance from any cell to its nearest post office after adding that one office.

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

Constraints:

m == grid.length

n == grid[i].length

1 ≤ m, n ≤ 100

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

At least one cell contains a 1.

Abstraction

Given a grid with existing post offices, place one additional post office to minimize the worst-case (max) Chebyshev distance from any cell to its nearest post office.

Pseudocode

1. multi-source BFS from all existing 1-cells using 8-directional moves
    -> dist[r][c] = distance to nearest EXISTING post office
2. binary search on answer d (0 .. max possible distance):
    collect all cells where dist[r][c] > d (cells the new office must cover)
    if none -> feasible (existing offices already good enough)
    else compute bounding box of those cells
    (minR, maxR, minC, maxC)
    feasible if (maxR - minR) <= 2d AND (maxC - minC) <= 2d
    (a single point can Chebyshev-cover a box iff the box fits in a (2d+1)x(2d+1) window)
3. shrink d while feasible, return smallest feasible d

Solution 1: [BFS] [Binary Search] Multi Source BFS with Bounding Box Feasibility - Graph/Greedy

    def minMaxDistance(self, grid: List[List[int]]) -> int:

        # Multi Source BFS (Chebyshev Distance) + Binary Search on Answer
        # Determine minimum possible value of the max distance to nearest
        # post office, after optimally placing ONE new post office

        # Idea:
        # - Step 1: BFS from ALL existing post offices simultaneously
        #           using 8-directional moves -> gives Chebyshev distance
        #           from every cell to nearest EXISTING office
        # - Step 2: Binary search on the answer d. A candidate d is
        #           feasible if a single new office can cover every cell
        #           whose existing distance is > d, within radius d
        # - Step 3: Feasibility check uses the fact that a Chebyshev ball
        #           of radius d is a (2d+1) x (2d+1) square, so a set of
        #           points is coverable by one point iff its bounding box
        #           fits inside that square

        # Edge Case + Setup

        # boundaries
        # sc: O(1)
        m, n = len(grid), len(grid[0])

        # Iterative Queue Holds: (row, col, dist)
        # sc: O(m*n)
        queue = deque()

        # dist grid initialized to -1 (unvisited)
        # sc: O(m*n)
        dist = [[-1] * n for _ in range(m)]

        # Multi Source Setup:
        #   - put all existing post offices in BFS queue at dist 0
        # tc: O(m*n)
        for r in range(m):
            for c in range(n):
                if grid[r][c] == 1:
                    dist[r][c] = 0
                    queue.append((r, c, 0))

        # 8 directions (9-direction movement minus "stay in place")
        directions = [(-1,-1), (-1,0), (-1,1),
                      (0,-1),          (0,1),
                      (1,-1),  (1,0),  (1,1)]

        # BFS Traversal:
        # Fills dist[r][c] = distance to nearest EXISTING post office
        # tc: O(m*n) each cell processed once
        while queue:

            # Process current root
            r, c, d = queue.popleft()

            # Process Choices:
            # 8 direction spread
            for dr, dc in directions:
                nr, nc = r + dr, c + dc

                # Early Pruning:
                # Valid bounds and unvisited cell
                if 0 <= nr < m and 0 <= nc < n and dist[nr][nc] == -1:

                    # Record distance immediately
                    dist[nr][nc] = d + 1

                    # Add next frontier cell to queue
                    queue.append((nr, nc, d + 1))

        # Feasibility Check Helper:
        # Given candidate d, can ONE new office cover all cells
        # whose existing dist > d, within Chebyshev radius d?
        # tc: O(m*n) per call
        def feasible(d):

            minR, maxR = float('inf'), float('-inf')
            minC, maxC = float('inf'), float('-inf')
            found = False

            # Scan all cells still uncovered at radius d
            for r in range(m):
                for c in range(n):
                    if dist[r][c] > d:
                        found = True
                        minR, maxR = min(minR, r), max(maxR, r)
                        minC, maxC = min(minC, c), max(maxC, c)

            # No cell left uncovered -> trivially feasible
            if not found:
                return True

            # Bounding box must fit inside a (2d+1) x (2d+1) window
            return (maxR - minR) <= 2 * d and (maxC - minC) <= 2 * d

        # Binary Search Setup:
        # Search space for d: 0 .. max possible Chebyshev distance
        # sc: O(1)
        lo, hi = 0, max(m, n)

        # Binary Search on Answer:
        # Shrink toward smallest feasible d
        # tc: O(log(m+n)) iterations * O(m*n) feasibility check
        while lo < hi:

            mid = (lo + hi) // 2

            # Try smaller d if feasible, else need bigger d
            if feasible(mid):
                hi = mid
            else:
                lo = mid + 1

        # overall: tc O(m*n*log(m+n))
        # overall: sc O(m*n)
        return lo

Solution 2: [Brute Force] Candidate Cell Simulation - Graph/Simulation

    def minMaxDistance(self, grid: List[List[int]]) -> int:

        # Brute Force Candidate Placement
        # Try placing the new office at EVERY empty cell, recompute the
        # resulting max distance, and take the best over all candidates

        # Idea:
        # - Step 1: BFS from ALL existing post offices to get
        #           dist[r][c] = distance to nearest EXISTING office
        # - Step 2: For every candidate cell (r0, c0), the resulting max
        #           distance is max over all cells of
        #           min(dist[r][c], chebyshev(r, c, r0, c0))
        # - Step 3: Take the candidate that minimizes that max distance

        # Edge Case + Setup

        # boundaries
        # sc: O(1)
        m, n = len(grid), len(grid[0])

        # Iterative Queue Holds: (row, col, dist)
        # sc: O(m*n)
        queue = deque()

        # dist grid initialized to -1 (unvisited)
        # sc: O(m*n)
        dist = [[-1] * n for _ in range(m)]

        # Multi Source Setup:
        #   - put all existing post offices in BFS queue at dist 0
        # tc: O(m*n)
        for r in range(m):
            for c in range(n):
                if grid[r][c] == 1:
                    dist[r][c] = 0
                    queue.append((r, c, 0))

        # 8 directions (9-direction movement minus "stay in place")
        directions = [(-1,-1), (-1,0), (-1,1),
                      (0,-1),          (0,1),
                      (1,-1),  (1,0),  (1,1)]

        # BFS Traversal:
        # Fills dist[r][c] = distance to nearest EXISTING post office
        # tc: O(m*n) each cell processed once
        while queue:

            r, c, d = queue.popleft()

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

                if 0 <= nr < m and 0 <= nc < n and dist[nr][nc] == -1:
                    dist[nr][nc] = d + 1
                    queue.append((nr, nc, d + 1))

        # Best answer found across all candidate placements
        # sc: O(1)
        best = float('inf')

        # Try Every Candidate Cell:
        # Simulate placing the new office at (r0, c0)
        # tc: O(m*n) candidates
        for r0 in range(m):
            for c0 in range(n):

                # Track worst-case distance for THIS candidate
                # sc: O(1)
                curMax = 0

                # Recompute max distance if office placed here
                # tc: O(m*n) per candidate
                for r in range(m):
                    for c in range(n):

                        # Chebyshev distance to the candidate office
                        chebyshev = max(abs(r - r0), abs(c - c0))

                        # Nearest of existing offices vs this candidate
                        best_for_cell = min(dist[r][c], chebyshev)

                        curMax = max(curMax, best_for_cell)

                # Keep best (smallest) worst-case seen so far
                best = min(best, curMax)

        # overall: tc O((m*n)^2)
        # overall: sc O(m*n)
        return best