1129. Shortest Path with Alternating Colors - Explanation

Problem Link



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

  1. Build adjacency lists for red and blue edges separately.
  2. Initialize the answer array with -1 for all nodes.
  3. Start BFS from node 0 with no previous edge color (allowing either color first).
  4. For each state (node, length, edgeColor):
    • If this node has not been visited before, record length as 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.
  5. 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 answer

Time & Space Complexity

  • Time complexity: O(V+E)O(V + E)
  • Space complexity: O(V+E)O(V + E)

Where VV is the number of vertices and EE 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

  1. Build separate adjacency lists for red (index 0) and blue (index 1) edges.
  2. Create a 2D distance array initialized to infinity, with dist[0][0] = dist[0][1] = 0.
  3. Start BFS with two initial states: (0, 0) and (0, 1) representing starting with red or blue.
  4. 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.
  5. The answer for each node is the minimum of dist[node][0] and dist[node][1].
  6. Return -1 for 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 answer

Time & Space Complexity

  • Time complexity: O(V+E)O(V + E)
  • Space complexity: O(V+E)O(V + E)

Where VV is the number of vertices and EE is the number of edges.


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

  1. Build separate adjacency lists for red and blue edges.
  2. Initialize a 2D distance array with infinity, setting dist[0][0] = dist[0][1] = 0.
  3. Run DFS starting from node 0 with color 0 (red), then again with color 1 (blue).
  4. In DFS for 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 DFS on the neighbor with the opposite color.
  5. Build the answer by taking the minimum of both color distances for each node.
  6. Return -1 for 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 answer

Time & Space Complexity

  • Time complexity: O(V+E)O(V + E)
  • Space complexity: O(V+E)O(V + E)

Where VV is the number of vertices and EE 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.