1129. Shortest Path with Alternating Colors - Explanation
Prerequisites
Before attempting this problem, you should be comfortable with:
- Graph Representation (Adjacency Lists) - Building and traversing graphs using adjacency lists for directed edges
- Breadth-First Search (BFS) - Using BFS to find shortest paths in unweighted graphs
- State-Space Search - Tracking visited states as (node, additional_info) pairs rather than just nodes
- Hash Sets - Efficiently checking and storing visited states to avoid revisiting
1. Breadth First Search - I
Intuition
We need to find shortest paths from node 0 to all other nodes, but with a twist: the path must alternate between red and blue edges. The key insight is that reaching a node via a red edge is different from reaching it via a blue edge, because it affects which color we can use next. So we track states as (node, last_edge_color) pairs. BFS naturally finds shortest paths in unweighted graphs, and since we want the first time we reach each node, we record the distance on first visit.
Algorithm
- Build adjacency lists for red and blue edges separately.
- Initialize the answer array with
-1for all nodes. - Start
BFSfrom node0with no previous edge color (allowing either color first). - For each state
(node, length, edgeColor):- If this node has not been visited before, record
lengthas its answer. - If the last edge was not red, explore all red edges to neighbors.
- If the last edge was not blue, explore all blue edges to neighbors.
- Track visited states as
(node, color)to avoid cycles.
- If this node has not been visited before, record
- Return the answer array.
class Solution:
def shortestAlternatingPaths(self, n: int, redEdges: list[list[int]], blueEdges: list[list[int]]) -> list[int]:
red, blue = defaultdict(list), defaultdict(list)
for src, dst in redEdges:
red[src].append(dst)
for src, dst in blueEdges:
blue[src].append(dst)
answer = [-1 for _ in range(n)]
q = deque()
q.append((0, 0, None)) # [node, length, prev_edge_color]
visit = set()
visit.add((0, None))
while q:
node, length, edgeColor = q.popleft()
if answer[node] == -1:
answer[node] = length
if edgeColor != "RED":
for nei in red[node]:
if (nei, "RED") not in visit:
visit.add((nei, "RED"))
q.append((nei, length + 1, "RED"))
if edgeColor != "BLUE":
for nei in blue[node]:
if (nei, "BLUE") not in visit:
visit.add((nei, "BLUE"))
q.append((nei, length + 1, "BLUE"))
return answerTime & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of vertices and is the number of edges.
2. Breadth First Search - II
Intuition
This approach uses a cleaner state representation. Instead of tracking the edge color explicitly, we use 0 for red and 1 for blue, and toggle between them using XOR (color ^ 1). We maintain a 2D distance array where dist[node][color] stores the shortest distance to reach node when the last edge used was color. Starting from node 0 with both colors as valid starting points, we relax edges only when we find a shorter path.
Algorithm
- Build separate adjacency lists for red (index
0) and blue (index1) edges. - Create a 2D distance array initialized to infinity, with
dist[0][0] = dist[0][1] = 0. - Start
BFSwith two initial states:(0, 0)and(0, 1)representing starting with red or blue. - For each state
(node, color):- Explore neighbors through edges of the current color.
- If
dist[neighbor][opposite_color] > dist[node][color] + 1, update it and enqueue.
- The answer for each node is the minimum of
dist[node][0]anddist[node][1]. - Return
-1for nodes where both distances remain infinity.
class Solution:
def shortestAlternatingPaths(self, n: int, redEdges: list[list[int]], blueEdges: list[list[int]]) -> list[int]:
def buildGraph(edges):
adj = [[] for _ in range(n)]
for u, v in edges:
adj[u].append(v)
return adj
red, blue = buildGraph(redEdges), buildGraph(blueEdges)
adj = [red, blue]
INF = float("inf")
dist = [[INF] * 2 for _ in range(n)]
dist[0][0] = dist[0][1] = 0
q = deque([(0, 0), (0, 1)])
while q:
node, color = q.popleft()
for nei in adj[color][node]:
if dist[nei][color ^ 1] > dist[node][color] + 1:
dist[nei][color ^ 1] = dist[node][color] + 1
q.append((nei, color ^ 1))
answer = [0] + [-1] * (n - 1)
for i in range(1, n):
answer[i] = min(dist[i][0], dist[i][1])
if answer[i] == INF:
answer[i] = -1
return answerTime & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of vertices and is the number of edges.
3. Depth First Search
Intuition
While BFS is typically preferred for shortest path problems, DFS can also work here because we are tracking distances and only updating when we find a shorter path. The recursion naturally handles the alternating color constraint. We start DFS from node 0 twice: once beginning with red edges and once with blue. Whenever we find a shorter path to a node with a particular ending color, we update the distance and continue exploring.
Algorithm
- Build separate adjacency lists for red and blue edges.
- Initialize a 2D distance array with infinity, setting
dist[0][0] = dist[0][1] = 0. - Run
DFSstarting from node0with color0(red), then again with color1(blue). - In
DFSfor state(node, color):- For each neighbor reachable via the current color:
- If
dist[neighbor][opposite_color] > dist[node][color] + 1:- Update the distance.
- Recursively call
DFSon the neighbor with the opposite color.
- If
- For each neighbor reachable via the current color:
- Build the answer by taking the minimum of both color distances for each node.
- Return
-1for unreachable nodes.
class Solution:
def shortestAlternatingPaths(self, n: int, redEdges: list[list[int]], blueEdges: list[list[int]]) -> list[int]:
def buildGraph(edges):
adj = [[] for _ in range(n)]
for u, v in edges:
adj[u].append(v)
return adj
red, blue = buildGraph(redEdges), buildGraph(blueEdges)
adj = [red, blue]
INF = float("inf")
dist = [[INF] * 2 for _ in range(n)]
dist[0][0] = dist[0][1] = 0
def dfs(node, color):
for nei in adj[color][node]:
if dist[nei][color ^ 1] > dist[node][color] + 1:
dist[nei][color ^ 1] = dist[node][color] + 1
dfs(nei, color ^ 1)
dfs(0, 0)
dfs(0, 1)
answer = [0] + [-1] * (n - 1)
for i in range(1, n):
answer[i] = min(dist[i][0], dist[i][1])
if answer[i] == INF:
answer[i] = -1
return answerTime & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of vertices and is the number of edges.
Common Pitfalls
Tracking Only Node Visits Instead of (Node, Color) Pairs
A common mistake is marking nodes as visited based solely on the node ID, ignoring the color of the edge used to reach it. Since reaching node X via a red edge allows different future moves than reaching it via a blue edge, you must track visited states as (node, last_edge_color) pairs. Without this, you may prematurely block valid paths that use a different edge color.
Forgetting to Allow Both Starting Colors
Since there is no edge leading into node 0, both red and blue edges are valid first moves. Failing to initialize the BFS with both color options (or using a sentinel value like None or -1 for "no previous color") means you might miss the shortest path if it requires starting with a specific color.
Not Recording Distance on First Visit
In BFS for shortest paths, the first time you reach a node is guaranteed to be via the shortest path. A pitfall is updating the answer every time a node is encountered rather than only on the first visit. This can lead to incorrect results if you overwrite an earlier (shorter) distance with a later (longer) one when the node is reached through a different color sequence.