1129. Shortest Path with Alternating Colors
Problem Description
You have a directed graph with n
nodes labeled from 0
to n - 1
. The graph has two types of edges: red edges and blue edges. The graph may contain self-edges (edges from a node to itself) and parallel edges (multiple edges between the same pair of nodes).
You're given two arrays:
redEdges[i] = [ai, bi]
represents a directed red edge from nodeai
to nodebi
blueEdges[j] = [uj, vj]
represents a directed blue edge from nodeuj
to nodevj
Your task is to find the shortest path from node 0
to every other node in the graph, with a special constraint: the colors of edges must alternate along the path. This means if you traverse a red edge, the next edge must be blue, and if you traverse a blue edge, the next edge must be red.
Return an array answer
of length n
where:
answer[x]
is the length of the shortest alternating-color path from node0
to nodex
answer[x]
is-1
if no such alternating path exists from node0
to nodex
For example, if you start at node 0
and take a red edge to node 1
, then the next edge from node 1
(if you continue) must be a blue edge. The path length is counted as the number of edges traversed.
The key challenge is ensuring that edge colors alternate throughout the entire path while finding the minimum distance to each node.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem explicitly mentions a directed graph with
n
nodes labeled from0
ton-1
, with red and blue edges connecting these nodes.
Is it a tree?
- No: The problem states there can be self-edges and parallel edges, which means it's not a tree structure. Trees don't have cycles or multiple edges between nodes.
Is the problem related to directed acyclic graphs?
- No: The graph can have cycles (including self-edges), so it's not necessarily acyclic.
Is the problem related to shortest paths?
- Yes: We need to find the shortest path from node
0
to every other node with the constraint that edge colors must alternate.
Is the graph Weighted?
- No: All edges have an implicit weight of 1 (we're counting the number of edges in the path, not summing weights). Each edge contributes exactly 1 to the path length.
BFS
- Yes: Since we have an unweighted graph and need to find shortest paths, BFS is the appropriate algorithm.
Conclusion: The flowchart suggests using BFS (Breadth-First Search) for finding the shortest alternating-color paths. BFS is ideal here because:
- It explores nodes level by level, guaranteeing the shortest path in unweighted graphs
- It can handle the additional constraint of alternating colors by tracking the color of the last edge used
- It naturally finds the minimum distance from the source to all reachable nodes
Intuition
When we need to find shortest paths in an unweighted graph, BFS immediately comes to mind because it explores nodes level by level, guaranteeing we find the shortest distance first. However, this problem has a twist: we must alternate between red and blue edges.
The key insight is that we need to track not just which nodes we've visited, but also what color edge we used to reach them. Why? Because the color of the last edge determines what color we can use next. If we arrived at node 3
via a red edge, we can only leave via a blue edge.
This means the same node can be visited multiple times with different "states":
- Node
3
reached via a red edge (can only continue with blue) - Node
3
reached via a blue edge (can only continue with red)
These are essentially different states in our search space. So instead of the traditional BFS where we track (node)
, we need to track (node, last_color)
.
Another crucial observation: we need to consider both starting possibilities. From node 0
, we could take either a red edge or a blue edge as our first move. These lead to different alternating patterns:
- Starting with red: red → blue → red → blue...
- Starting with blue: blue → red → blue → red...
Both patterns might give us valid paths to different nodes, and we want the shortest among all valid paths.
The BFS approach naturally handles finding the minimum distance because when we first reach a node (regardless of the color state), we've found the shortest path to that node. We can track visited states as (node, color)
pairs to avoid revisiting the same state, which would create unnecessary cycles in our search.
By organizing the edges by color in separate adjacency lists (g[0]
for red, g[1]
for blue), we can easily toggle between colors using XOR operation (c ^= 1
), making the implementation clean and efficient.
Learn more about Breadth-First Search and Graph patterns.
Solution Approach
The solution implements BFS with state tracking to handle the alternating color constraint. Let's walk through the implementation step by step:
1. Graph Preprocessing
First, we organize the edges by color into separate adjacency lists:
g = [defaultdict(list), defaultdict(list)]
for i, j in redEdges:
g[0][i].append(j) # g[0] stores red edges
for i, j in blueEdges:
g[1][i].append(j) # g[1] stores blue edges
This structure allows us to easily access edges of a specific color: g[0]
for red edges and g[1]
for blue edges.
2. Initialization
We set up the necessary data structures:
ans = [-1] * n
: Initialize all distances to-1
(unreachable by default)vis = set()
: Track visited states as(node, color)
pairs to avoid revisitingq = deque([(0, 0), (0, 1)])
: Start BFS with both color possibilities from node0
(0, 0)
means starting at node0
with the ability to take red edges(0, 1)
means starting at node0
with the ability to take blue edges
d = 0
: Current distance level in BFS
3. BFS Traversal
The BFS proceeds level by level:
while q:
for _ in range(len(q)): # Process all nodes at current distance
i, c = q.popleft()
if ans[i] == -1:
ans[i] = d # First time reaching node i, record distance
vis.add((i, c)) # Mark this state as visited
c ^= 1 # Flip color: 0→1 or 1→0
for j in g[c][i]: # Explore edges of the required color
if (j, c) not in vis:
q.append((j, c))
d += 1 # Move to next distance level
Key Implementation Details:
-
Color Alternation: The
c ^= 1
operation flips the color. If we arrived via colorc
, we must leave via color1-c
. -
State Tracking: We track
(node, color)
pairs invis
to prevent revisiting the same state. This is crucial because visiting node3
via red edge is different from visiting it via blue edge. -
First Visit Optimization: When we first reach a node (checked by
ans[i] == -1
), we immediately record the distance. Since BFS explores level by level, this guarantees the shortest path. -
Dual Starting Points: By initializing the queue with both
(0, 0)
and(0, 1)
, we explore paths starting with either color, ensuring we find the shortest valid path regardless of the starting edge color.
The algorithm terminates when the queue is empty, meaning all reachable states have been explored. The final ans
array contains the shortest alternating-path distance to each node, or -1
if no such path exists.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Example Graph:
- n = 4
- redEdges = [[0,1], [1,2], [2,3]]
- blueEdges = [[0,2], [1,3]]
Visual Representation:
Red edges: 0 → 1 → 2 → 3 Blue edges: 0 → 2, 1 → 3
Step-by-Step BFS Execution:
Initialization:
- ans = [-1, -1, -1, -1]
- vis = {}
- q = [(0,0), (0,1)] // Start with both color options from node 0
- d = 0
Distance 0 (Processing node 0):
-
Process (0,0):
- ans[0] = 0 (first visit to node 0)
- Add (0,0) to vis
- Next color: 0^1 = 1 (blue)
- Check blue edges from 0: finds edge 0→2
- Add (2,1) to queue
-
Process (0,1):
- ans[0] already set, skip
- Add (0,1) to vis
- Next color: 1^1 = 0 (red)
- Check red edges from 0: finds edge 0→1
- Add (1,0) to queue
-
Queue after d=0: [(2,1), (1,0)]
-
ans = [0, -1, -1, -1]
Distance 1 (Processing nodes at distance 1):
-
d = 1
-
Process (2,1):
- ans[2] = 1 (first visit to node 2)
- Add (2,1) to vis
- Next color: 1^1 = 0 (red)
- Check red edges from 2: finds edge 2→3
- Add (3,0) to queue
-
Process (1,0):
- ans[1] = 1 (first visit to node 1)
- Add (1,0) to vis
- Next color: 0^1 = 1 (blue)
- Check blue edges from 1: finds edge 1→3
- Add (3,1) to queue
-
Queue after d=1: [(3,0), (3,1)]
-
ans = [0, 1, 1, -1]
Distance 2 (Processing nodes at distance 2):
-
d = 2
-
Process (3,0):
- ans[3] = 2 (first visit to node 3)
- Add (3,0) to vis
- Next color: 0^1 = 1 (blue)
- No blue edges from 3
-
Process (3,1):
- ans[3] already set, skip
- Add (3,1) to vis
- Next color: 1^1 = 0 (red)
- No red edges from 3
-
Queue empty, algorithm terminates
-
Final ans = [0, 1, 1, 2]
Path Analysis:
- Node 0: Distance 0 (starting point)
- Node 1: Distance 1 via path 0→1 (red edge)
- Node 2: Distance 1 via path 0→2 (blue edge)
- Node 3: Distance 2 via path 0→1→3 (red→blue) or 0→2→3 (blue→red)
The algorithm successfully finds the shortest alternating-color paths to all nodes!
Solution Implementation
1from collections import defaultdict, deque
2from typing import List
3
4class Solution:
5 def shortestAlternatingPaths(
6 self, n: int, redEdges: List[List[int]], blueEdges: List[List[int]]
7 ) -> List[int]:
8 # Build adjacency lists for both red and blue edges
9 # graph[0] stores red edges, graph[1] stores blue edges
10 graph = [defaultdict(list), defaultdict(list)]
11
12 # Populate red edges (index 0)
13 for source, destination in redEdges:
14 graph[0][source].append(destination)
15
16 # Populate blue edges (index 1)
17 for source, destination in blueEdges:
18 graph[1][source].append(destination)
19
20 # Initialize result array with -1 (unreachable)
21 shortest_distances = [-1] * n
22
23 # Track visited states as (node, last_color_used)
24 visited = set()
25
26 # BFS queue: start from node 0 with both color options
27 # (node_id, color) where color 0=red, 1=blue
28 queue = deque([(0, 0), (0, 1)])
29
30 # Current distance from source
31 distance = 0
32
33 # BFS traversal
34 while queue:
35 # Process all nodes at current distance level
36 for _ in range(len(queue)):
37 current_node, last_color = queue.popleft()
38
39 # Update shortest distance if not yet set
40 if shortest_distances[current_node] == -1:
41 shortest_distances[current_node] = distance
42
43 # Mark this state as visited
44 visited.add((current_node, last_color))
45
46 # Alternate color for next edge (XOR with 1 flips 0↔1)
47 next_color = last_color ^ 1
48
49 # Explore neighbors using the alternating color
50 for neighbor in graph[next_color][current_node]:
51 if (neighbor, next_color) not in visited:
52 queue.append((neighbor, next_color))
53
54 # Increment distance for next level
55 distance += 1
56
57 return shortest_distances
58
1class Solution {
2 public int[] shortestAlternatingPaths(int n, int[][] redEdges, int[][] blueEdges) {
3 // Create adjacency list for both red (index 0) and blue (index 1) edges
4 // graph[0] represents red edges, graph[1] represents blue edges
5 List<Integer>[][] graph = new List[2][n];
6 for (List<Integer>[] colorGraph : graph) {
7 Arrays.setAll(colorGraph, node -> new ArrayList<>());
8 }
9
10 // Build red edges graph (color index 0)
11 for (int[] edge : redEdges) {
12 graph[0][edge[0]].add(edge[1]);
13 }
14
15 // Build blue edges graph (color index 1)
16 for (int[] edge : blueEdges) {
17 graph[1][edge[0]].add(edge[1]);
18 }
19
20 // BFS queue storing [node, last_color]
21 // 0 = red, 1 = blue for last_color
22 Deque<int[]> queue = new ArrayDeque<>();
23 queue.offer(new int[] {0, 0}); // Start from node 0 with red as last color
24 queue.offer(new int[] {0, 1}); // Start from node 0 with blue as last color
25
26 // Track visited nodes for each color to avoid cycles
27 // visited[node][color] = true if we've visited this node with this color
28 boolean[][] visited = new boolean[n][2];
29
30 // Initialize result array with -1 (unreachable)
31 int[] result = new int[n];
32 Arrays.fill(result, -1);
33
34 // Track current distance from source
35 int distance = 0;
36
37 // BFS traversal
38 while (!queue.isEmpty()) {
39 // Process all nodes at current distance level
40 int levelSize = queue.size();
41 for (int i = 0; i < levelSize; i++) {
42 int[] current = queue.poll();
43 int currentNode = current[0];
44 int lastColor = current[1];
45
46 // Update shortest distance for this node if not yet set
47 if (result[currentNode] == -1) {
48 result[currentNode] = distance;
49 }
50
51 // Mark this node-color combination as visited
52 visited[currentNode][lastColor] = true;
53
54 // Switch to opposite color for next edge
55 // XOR with 1 toggles between 0 and 1
56 int nextColor = lastColor ^ 1;
57
58 // Explore all neighbors using the next color edges
59 for (int neighbor : graph[nextColor][currentNode]) {
60 // Only add to queue if this neighbor-color combo hasn't been visited
61 if (!visited[neighbor][nextColor]) {
62 queue.offer(new int[] {neighbor, nextColor});
63 }
64 }
65 }
66 // Increment distance for next level
67 distance++;
68 }
69
70 return result;
71 }
72}
73
1class Solution {
2public:
3 vector<int> shortestAlternatingPaths(int n, vector<vector<int>>& redEdges, vector<vector<int>>& blueEdges) {
4 // Build adjacency list for both red (0) and blue (1) edges
5 // graph[color][node] contains all nodes reachable from 'node' using edges of 'color'
6 vector<vector<vector<int>>> graph(2, vector<vector<int>>(n));
7
8 // Add red edges to the graph (red = 0)
9 for (auto& edge : redEdges) {
10 graph[0][edge[0]].push_back(edge[1]);
11 }
12
13 // Add blue edges to the graph (blue = 1)
14 for (auto& edge : blueEdges) {
15 graph[1][edge[0]].push_back(edge[1]);
16 }
17
18 // BFS queue storing pairs of (node, last_edge_color)
19 queue<pair<int, int>> bfsQueue;
20
21 // Start BFS from node 0 with both possible colors
22 bfsQueue.emplace(0, 0); // Start with red edge
23 bfsQueue.emplace(0, 1); // Start with blue edge
24
25 // Track visited states: visited[node][color] = true if we've visited 'node' with 'color' edge
26 bool visited[n][2];
27 memset(visited, false, sizeof(visited));
28
29 // Initialize result array with -1 (unreachable)
30 vector<int> result(n, -1);
31
32 // Current distance from source node
33 int distance = 0;
34
35 // BFS traversal
36 while (!bfsQueue.empty()) {
37 int levelSize = bfsQueue.size();
38
39 // Process all nodes at current distance level
40 for (int i = 0; i < levelSize; ++i) {
41 auto [currentNode, lastColor] = bfsQueue.front();
42 bfsQueue.pop();
43
44 // Update shortest distance to current node if not yet set
45 if (result[currentNode] == -1) {
46 result[currentNode] = distance;
47 }
48
49 // Mark this state as visited
50 visited[currentNode][lastColor] = true;
51
52 // Next edge must be opposite color (XOR toggles between 0 and 1)
53 int nextColor = lastColor ^ 1;
54
55 // Explore all neighbors using the opposite color edge
56 for (int& neighbor : graph[nextColor][currentNode]) {
57 if (!visited[neighbor][nextColor]) {
58 bfsQueue.emplace(neighbor, nextColor);
59 }
60 }
61 }
62
63 // Move to next distance level
64 ++distance;
65 }
66
67 return result;
68 }
69};
70
1/**
2 * Finds the shortest alternating path from node 0 to all other nodes
3 * using red and blue edges alternately.
4 *
5 * @param n - Number of nodes in the graph
6 * @param redEdges - Array of red edges where each edge is [from, to]
7 * @param blueEdges - Array of blue edges where each edge is [from, to]
8 * @returns Array where ans[i] is the shortest distance from node 0 to node i,
9 * or -1 if no path exists
10 */
11function shortestAlternatingPaths(
12 n: number,
13 redEdges: number[][],
14 blueEdges: number[][],
15): number[] {
16 // Type definitions for clarity
17 type AdjacencyList = Record<number, number[]>;
18 type ColorIndex = 0 | 1;
19 type QueueNode = [nodeId: number, lastColor: ColorIndex];
20
21 // Build adjacency lists for red (index 0) and blue (index 1) edges
22 const adjacencyLists: [AdjacencyList, AdjacencyList] = [{}, {}];
23
24 // Initialize result array with -1 (unreachable)
25 const shortestDistances = Array(n).fill(-1);
26
27 // Track visited nodes: visited[nodeId][colorIndex] indicates if we've visited
28 // nodeId with the last edge being of the specified color
29 const visited = Array.from({ length: n }, () => Array.from({ length: 2 }, () => false));
30
31 // BFS queue: stores [nodeId, lastEdgeColor]
32 // Start from node 0 with both color possibilities
33 let currentLevelQueue: QueueNode[] = [
34 [0, 0], // Start with red as last color
35 [0, 1], // Start with blue as last color
36 ];
37
38 // Mark starting node as visited for both colors
39 visited[0][0] = visited[0][1] = true;
40
41 // Current distance from source node
42 let currentDistance = 0;
43
44 // Build red edges adjacency list (color index 0)
45 for (const [fromNode, toNode] of redEdges) {
46 (adjacencyLists[0][fromNode] ??= []).push(toNode);
47 }
48
49 // Build blue edges adjacency list (color index 1)
50 for (const [fromNode, toNode] of blueEdges) {
51 (adjacencyLists[1][fromNode] ??= []).push(toNode);
52 }
53
54 // BFS to find shortest alternating paths
55 while (currentLevelQueue.length > 0) {
56 const nextLevelQueue: QueueNode[] = [];
57
58 // Process all nodes at current distance level
59 for (let [currentNode, lastColor] of currentLevelQueue) {
60 // Update shortest distance if not yet set
61 if (shortestDistances[currentNode] === -1) {
62 shortestDistances[currentNode] = currentDistance;
63 }
64
65 // Toggle color for next edge (0 -> 1, 1 -> 0)
66 const nextColor = (lastColor ^ 1) as ColorIndex;
67
68 // Explore neighbors using edges of the opposite color
69 for (const neighbor of adjacencyLists[nextColor][currentNode] ?? []) {
70 // Only visit if we haven't reached this node with this color pattern
71 if (!visited[neighbor][nextColor]) {
72 visited[neighbor][nextColor] = true;
73 nextLevelQueue.push([neighbor, nextColor]);
74 }
75 }
76 }
77
78 // Move to next distance level
79 currentLevelQueue = nextLevelQueue;
80 currentDistance++;
81 }
82
83 return shortestDistances;
84}
85
Time and Space Complexity
Time Complexity: O(n + m)
The algorithm uses BFS to traverse the graph. In the worst case:
- Each node can be visited at most twice (once with red color and once with blue color), contributing
O(n)
to the complexity - Each edge is processed at most once when exploring neighbors, contributing
O(m)
to the complexity wherem
is the total number of edges (len(redEdges) + len(blueEdges)
) - The initialization of the graph takes
O(m)
time to process all edges - The initialization of the answer array takes
O(n)
time
Therefore, the overall time complexity is O(n + m)
.
Space Complexity: O(n + m)
The space usage consists of:
- The graph structure
g
which stores all edges, requiringO(m)
space - The visited set
vis
which can contain at most2n
entries (each node with 2 possible colors), requiringO(n)
space - The queue
q
which in the worst case can containO(n)
nodes - The answer array
ans
which storesn
elements, requiringO(n)
space
Therefore, the overall space complexity is O(n + m)
, where n
is the number of nodes and m
is the total number of edges.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Not Tracking States Properly - Using Only Node ID for Visited Set
One of the most common mistakes is tracking only the node ID in the visited set instead of the (node, color)
pair. This leads to incorrect results because the same node can be reached via different color edges, and these represent different states in our alternating path problem.
Incorrect Implementation:
visited = set()
queue = deque([(0, 0), (0, 1)])
while queue:
for _ in range(len(queue)):
current_node, last_color = queue.popleft()
# WRONG: Only checking node, not the state
if current_node in visited:
continue
visited.add(current_node) # WRONG: Only adding node
# ... rest of the logic
Why This Fails: Consider a graph where node 2 can be reached from node 0 via:
- Path 1: Red edge (0→1) then Blue edge (1→2) - length 2
- Path 2: Blue edge (0→3) then Red edge (3→2) - length 2
If we only track node 2 as visited after the first path, we won't explore the second path. This could prevent us from finding valid alternating paths to other nodes that might only be reachable through the second path.
Correct Implementation:
visited = set()
queue = deque([(0, 0), (0, 1)])
while queue:
for _ in range(len(queue)):
current_node, last_color = queue.popleft()
# CORRECT: Check the complete state
if (current_node, last_color) in visited:
continue
visited.add((current_node, last_color)) # CORRECT: Add complete state
# ... rest of the logic
2. Incorrect Color Alternation Logic
Another pitfall is misunderstanding when and how to alternate colors. The color should be flipped BEFORE exploring neighbors, not after.
Incorrect Implementation:
for neighbor in graph[last_color][current_node]: # WRONG: Using same color if (neighbor, last_color) not in visited: queue.append((neighbor, last_color ^ 1)) # Flipping here is too late
Correct Implementation:
next_color = last_color ^ 1 # Flip color first for neighbor in graph[next_color][current_node]: # Use flipped color if (neighbor, next_color) not in visited: queue.append((neighbor, next_color))
3. Not Handling Both Starting Colors
Forgetting to initialize the queue with both color options from node 0 can lead to missing valid paths.
Incorrect Implementation:
queue = deque([(0, 0)]) # WRONG: Only starting with red edges
Correct Implementation:
queue = deque([(0, 0), (0, 1)]) # Start with both color options
This ensures we explore paths that begin with either a red or blue edge from node 0, finding the shortest valid alternating path regardless of the starting edge color.
Which of the following uses divide and conquer strategy?
Recommended Readings
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!