What L5 looks like
- Explains why BFS (not DFS) gives the shortest path, and counts blocks off by none, including the
Snext toDcase. - Encodes the ranking as a tuple comparison instead of nested ifs.
- Notes the tighter
O(R * C + k)bound, and moves cleanly to Dijkstra when weights or mode switches are added.
Approach
Each digit cell belongs to exactly one mode, and you may not change modes mid-trip. So a trip in mode m is a path from S to D that only steps on S, D and cells labelled m. Every block in mode m costs the same time[m] and cost[m]. That means the trip's time and cost are both blocks * time[m] and blocks * cost[m], where blocks is the number of mode cells on the path. Fewer blocks is better on both counts. The question for each mode becomes: what is the shortest unweighted path from S to D through that mode's cells?
Brute force. DFS over all simple paths for each mode and keep the shortest. This is exponential. Mention it only to dismiss it.
BFS per mode. For each mode m, run BFS from S. A neighbour is passable if it is D or a cell labelled m. X, other digits and out-of-bounds cells are walls. The first time BFS reaches D, the distance d counts the steps. The blocks travelled are the d - 1 cells strictly between S and D. Score the mode as (blocks * time[m], blocks * cost[m], m) and take the minimum tuple. That ranks by fastest first, then cheapest, then lowest mode number. Modes that never reach D are skipped. Return -1 if no mode works.
Why it is cheaper than it looks. A naive bound is O(k * R * C). But BFS for mode m only enters cells labelled m (plus D). Each cell has a single label, so across all modes every cell is dequeued at most once, apart from S and D. The total work is O(R * C + k). You can also skip modes with no cell adjacent to S before running BFS.
Invariants.
- BFS dequeues cells in non-decreasing distance order, so the first arrival at
Dis the shortest path. - A cell is marked visited when it is enqueued, so it is never enqueued twice in one mode's search.
-
SandDare passable for every mode.Dis a goal and is never expanded.
Code
from collections import deque
from typing import List, Optional, Sequence, Tuple
DIRS = ((1, 0), (-1, 0), (0, 1), (0, -1))
def _find(grid: Sequence[str], ch: str) -> Tuple[int, int]:
for r, row in enumerate(grid):
c = row.find(ch)
if c != -1:
return r, c
raise ValueError(f"grid has no {ch!r}")
def _blocks_for_mode(grid: Sequence[str], mode: str,
start: Tuple[int, int]) -> Optional[int]:
"""Fewest mode cells between S and D using only `mode`, or None."""
rows, cols = len(grid), len(grid[0])
seen = {start}
queue = deque([(start[0], start[1], 0)])
while queue:
r, c, dist = queue.popleft()
for dr, dc in DIRS:
nr, nc = r + dr, c + dc
if not (0 <= nr < rows and 0 <= nc < cols) or (nr, nc) in seen:
continue
cell = grid[nr][nc]
if cell == "D":
return dist # dist mode cells were stepped on before D
if cell == mode:
seen.add((nr, nc))
queue.append((nr, nc, dist + 1))
return None
def optimal_commute(grid: Sequence[str], time: List[int],
cost: List[int]) -> int:
"""Return the 1-based mode with least total time, ties by cost then
lower mode number. Return -1 if no single mode reaches D."""
if len(time) != len(cost):
raise ValueError("time and cost must have the same length")
if not grid or not grid[0]:
return -1
start = _find(grid, "S")
best: Optional[Tuple[int, int, int]] = None
for m in range(1, len(time) + 1):
blocks = _blocks_for_mode(grid, str(m), start)
if blocks is None:
continue
key = (blocks * time[m - 1], blocks * cost[m - 1], m)
if best is None or key < best:
best = key
return best[2] if best else -1
if __name__ == "__main__":
city = [
"3333D",
"3XX1X",
"S1112",
"2XX2X",
"2222X",
]
# Modes 1 and 2 dead-end; mode 3 reaches D in 5 blocks.
assert optimal_commute(city, [1, 1, 9], [1, 1, 9]) == 3
assert optimal_commute(["S1D"], [5, 1], [1, 1]) == 1
assert optimal_commute(["S2D", "1X1", "111"], [1, 9], [9, 1]) == 1
assert optimal_commute(["S2D", "1X1", "111"], [9, 2], [1, 1]) == 2
assert optimal_commute(["S11", "22D"], [2, 2], [3, 1]) == 2
assert optimal_commute(["SD"], [3, 1], [1, 1]) == 1
assert optimal_commute(["SX", "XD"], [1], [1]) == -1
assert optimal_commute(["S12D"], [1, 1], [1, 1]) == -1
print("ok")
Complexity
- Time:
O(R * C + k). Each mode's BFS only enters cells with that mode's label, so every cell is processed by at most one search (Sis the start of allksearches). - Space:
O(R * C)for the visited set and queue of the largest single mode.
Test cases
-
["S1D"],time=[5,1],cost=[1,1]→1. Mode 2 has no cells, so only mode 1 reachesD. -
["S2D","1X1","111"],time=[1,9],cost=[9,1]→1. Mode 1 uses 5 blocks for time 5. Mode 2 uses 1 block for time 9. - Same grid,
time=[9,2],cost=[1,1]→2. Mode 2 takes 2, mode 1 takes 45. -
["S11","22D"],time=[2,2],cost=[3,1]→2. Mode 1 dead-ends, so mode 2 wins outright. -
["SD"],time=[3,1],cost=[1,1]→1. Zero blocks, so every mode ties at(0, 0)and the lowest mode number wins. -
["SX","XD"],time=[1],cost=[1]→-1. Fully blocked. -
["S12D"],time=[1,1],cost=[1,1]→-1. ReachingDneeds a mode switch, which is not allowed. - Time tie: two modes with equal
blocks * time→ the lowerblocks * costwins.
Follow-ups
-
Return the route too. Store a parent pointer per cell in the winning mode's BFS and walk back from
D. This addsO(R * C)space. -
Mode switches allowed, with a transfer penalty. Run Dijkstra on states
(r, c, mode). Moving within a mode coststime[mode]. Switching costs a fixed penalty. Use a lexicographic(time, cost)priority. That isO(R * C * log(R * C)), because each cell has one label and so only a constant number of states. - Per-cell weights (traffic, hills). Plain BFS is no longer correct. Use Dijkstra per mode, or 0-1 BFS if weights are 0/1.
-
Many queries on the same map (different
S/Dpairs or price lists). Precompute connected components per mode with union-find to answer "can mode m connect them?" inO(1). Cache BFS distances from popular sources. -
Optimise a weighted blend or a budget ("fastest under $X"). Each mode's path is fixed-length, so filter modes by
blocks * cost <= Xand then take the fastest. With mode switching this becomes a constrained shortest path. Discuss Pareto labels per state.
Interviewer expectations
Mid-level (L4)
- Parses the grid and finds
Scorrectly. Writes a correct BFS with bounds and visited checks. - Sees that a fixed mode makes time and cost proportional to path length.
- Handles the unreachable case and the tie-break, with a few hand-traced tests.
Senior (L5)
- Explains why BFS (not DFS) gives the shortest path, and counts blocks off by none, including the
Snext toDcase. - Encodes the ranking as a tuple comparison instead of nested ifs.
- Notes the tighter
O(R * C + k)bound, and moves cleanly to Dijkstra when weights or mode switches are added.
Staff (L6+)
- Asks the ambiguous questions early: are
S/Dcounted as blocks, may modes switch, what to return on a tie or no path. - Designs the switching follow-up as a state-space graph and reasons about its size.
- Discusses reuse across queries (component precompute, caching) and multi-objective trade-offs (Pareto fronts) without over-building the base answer.
Make it yours
Remix it: write each part yourself, grade it against this answer, then let the coach ask you follow-ups.
