1857. Largest Color Value in a Directed Graph
Problem Description
You have a directed graph with n
nodes (numbered from 0
to n-1
) and m
edges. Each node has a color represented by a lowercase English letter, given in the string colors
where colors[i]
is the color of node i
.
The graph is defined by a 2D array edges
where each edges[j] = [aj, bj]
represents a directed edge from node aj
to node bj
.
A valid path in the graph is a sequence of nodes x1 -> x2 -> x3 -> ... -> xk
where there exists a directed edge from each node to the next one in the sequence.
For any valid path, the color value is calculated as the maximum frequency of any single color among all nodes in that path. For example, if a path contains nodes with colors "a", "b", "a", "a", "c", then the color value would be 3
(since "a" appears 3 times, which is the maximum).
Your task is to find the largest color value among all possible valid paths in the graph.
If the graph contains a cycle, return -1
since a cycle would allow infinite traversal and make the color value unbounded.
Example: If you have a path with nodes colored "r", "r", "b", "r", the color "r" appears 3 times and "b" appears 1 time, so the color value of this path is 3
.
Intuition
The key insight is that we need to track the maximum count of each color along all paths ending at each node. Since we want the largest color value across all valid paths, we need to explore paths systematically while avoiding cycles.
Think of this problem as asking: "For every possible path to reach a node, what's the maximum count of each color we can accumulate?" This naturally leads to a dynamic programming approach where we build up solutions from shorter paths to longer ones.
Why topological sort? In a directed acyclic graph (DAG), topological sort gives us an ordering where we process nodes only after all their predecessors have been processed. This ensures that when we reach a node, we already know the best color counts from all paths leading to it.
The cycle detection comes naturally from topological sort - if we can't process all n
nodes through topological sort, it means there's a cycle (some nodes have dependencies that can never be resolved).
For the dynamic programming part, we maintain a 2D array dp[node][color]
representing the maximum count of each color on any path ending at that node. When we process a node, we update all its neighbors by:
- Taking the color counts we've accumulated so far (from the current node)
- Adding 1 if the neighbor's color matches the color we're tracking
- Keeping the maximum value if multiple paths lead to the same neighbor
This way, we systematically build up the answer by processing nodes in topological order, ensuring we always have complete information about all incoming paths before processing a node. The final answer is the maximum value in the entire dp
array.
Learn more about Graph, Topological Sort, Memoization and Dynamic Programming patterns.
Solution Approach
The solution uses Topological Sort with Dynamic Programming to solve this problem efficiently.
Step 1: Build the Graph and Calculate In-degrees
- Create an adjacency list
g
to represent the directed graph - Track the in-degree of each node in array
indeg
- For each edge
[a, b]
, addb
tog[a]
's list and incrementindeg[b]
Step 2: Initialize Topological Sort
- Create a queue
q
and add all nodes with in-degree 0 (nodes with no incoming edges) - Initialize a 2D DP array
dp[n][26]
wheredp[i][j]
represents the maximum count of colorj
on any path ending at nodei
- For each starting node (in-degree 0), set
dp[i][c] = 1
wherec
is that node's color
Step 3: Process Nodes in Topological Order
- Use BFS to process nodes level by level
- For each node
i
dequeued:- Increment counter
cnt
(tracks processed nodes for cycle detection) - For each neighbor
j
of nodei
:- Decrement
indeg[j]
- If
indeg[j]
becomes 0, addj
to the queue - Update the DP values:
dp[j][k] = max(dp[j][k], dp[i][k] + (c == k))
- This means: take the maximum between the current value and the value from node
i
- Add 1 if color
k
matches nodej
's colorc
- This means: take the maximum between the current value and the value from node
- Track the maximum value seen so far in
ans
- Decrement
- Increment counter
Step 4: Check for Cycles and Return Result
- If
cnt < n
, not all nodes were processed, indicating a cycle exists - return-1
- Otherwise, return
ans
which holds the largest color value found
The algorithm runs in O(V + E)
time for the topological sort plus O(V * 26)
for the DP updates, giving overall O(V * 26 + E * 26)
time complexity, where V
is the number of nodes and E
is the number of edges.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Example Graph:
colors = "abaca"
(5 nodes with colors: 0β'a', 1β'b', 2β'a', 3β'c', 4β'a')edges = [[0,1], [0,2], [2,3], [3,4]]
This creates the graph:
0(a) β 1(b) β 2(a) β 3(c) β 4(a)
Step 1: Build Graph and Calculate In-degrees
- Adjacency list:
g[0] = [1,2]
,g[2] = [3]
,g[3] = [4]
- In-degrees:
indeg = [0, 1, 1, 1, 1]
(node 0 has no incoming edges)
Step 2: Initialize Topological Sort
- Queue
q = [0]
(only node 0 has in-degree 0) - Initialize DP array (showing only non-zero values):
dp[0][0] = 1
(node 0 has color 'a', which is index 0)
Step 3: Process Nodes in Topological Order
Processing node 0:
- Remove node 0 from queue,
cnt = 1
- Process neighbor 1:
indeg[1] = 0
, add to queue- Update DP:
dp[1][0] = max(0, 1+0) = 1
(inherits 'a' count) dp[1][1] = max(0, 0+1) = 1
(node 1 is 'b')
- Process neighbor 2:
indeg[2] = 0
, add to queue- Update DP:
dp[2][0] = max(0, 1+1) = 2
(node 2 is also 'a')
- Current max
ans = 2
Processing node 1:
- Remove node 1 from queue,
cnt = 2
- No neighbors, move on
Processing node 2:
- Remove node 2 from queue,
cnt = 3
- Process neighbor 3:
indeg[3] = 0
, add to queue- Update DP:
dp[3][0] = max(0, 2+0) = 2
(inherits 'a' count) dp[3][2] = max(0, 0+1) = 1
(node 3 is 'c')
- Current max
ans = 2
Processing node 3:
- Remove node 3 from queue,
cnt = 4
- Process neighbor 4:
indeg[4] = 0
, add to queue- Update DP:
dp[4][0] = max(0, 2+1) = 3
(node 4 is 'a', adds to count) dp[4][2] = max(0, 1+0) = 1
(inherits 'c' count)
- Current max
ans = 3
Processing node 4:
- Remove node 4 from queue,
cnt = 5
- No neighbors, done
Step 4: Check for Cycles and Return Result
cnt = 5 = n
, so no cycle exists- Return
ans = 3
The answer is 3, which comes from the path 0β2β3β4 where color 'a' appears 3 times (at nodes 0, 2, and 4).
Solution Implementation
1from collections import defaultdict, deque
2from typing import List
3
4class Solution:
5 def largestPathValue(self, colors: str, edges: List[List[int]]) -> int:
6 # Number of nodes in the graph
7 num_nodes = len(colors)
8
9 # Track in-degree for each node (for topological sort)
10 in_degree = [0] * num_nodes
11
12 # Build adjacency list representation of the graph
13 adjacency_list = defaultdict(list)
14 for source, destination in edges:
15 adjacency_list[source].append(destination)
16 in_degree[destination] += 1
17
18 # Initialize queue with nodes having zero in-degree (starting nodes)
19 queue = deque()
20
21 # DP table: dp[node][color] = max count of 'color' ending at 'node'
22 # 26 represents the 26 lowercase letters
23 color_count_dp = [[0] * 26 for _ in range(num_nodes)]
24
25 # Add all nodes with zero in-degree to queue and initialize their color count
26 for node_id, degree in enumerate(in_degree):
27 if degree == 0:
28 queue.append(node_id)
29 # Convert character to index (0-25) and increment count for this node's color
30 color_index = ord(colors[node_id]) - ord('a')
31 color_count_dp[node_id][color_index] += 1
32
33 # Track number of processed nodes (to detect cycles)
34 processed_nodes = 0
35 # Track the maximum color count found so far
36 max_color_count = 1
37
38 # Process nodes in topological order
39 while queue:
40 current_node = queue.popleft()
41 processed_nodes += 1
42
43 # Process all neighbors of current node
44 for neighbor in adjacency_list[current_node]:
45 in_degree[neighbor] -= 1
46
47 # If all dependencies processed, add to queue
48 if in_degree[neighbor] == 0:
49 queue.append(neighbor)
50
51 # Get the color index of the neighbor node
52 neighbor_color = ord(colors[neighbor]) - ord('a')
53
54 # Update DP values for the neighbor
55 for color_idx in range(26):
56 # If current color matches neighbor's color, add 1; otherwise, just propagate
57 increment = 1 if color_idx == neighbor_color else 0
58 color_count_dp[neighbor][color_idx] = max(
59 color_count_dp[neighbor][color_idx],
60 color_count_dp[current_node][color_idx] + increment
61 )
62 # Update global maximum
63 max_color_count = max(max_color_count, color_count_dp[neighbor][color_idx])
64
65 # If not all nodes were processed, there's a cycle
66 if processed_nodes < num_nodes:
67 return -1
68
69 return max_color_count
70
1class Solution {
2 public int largestPathValue(String colors, int[][] edges) {
3 int nodeCount = colors.length();
4
5 // Build adjacency list representation of the graph
6 List<Integer>[] adjacencyList = new List[nodeCount];
7 Arrays.setAll(adjacencyList, i -> new ArrayList<>());
8
9 // Track in-degree for each node (for topological sort)
10 int[] inDegree = new int[nodeCount];
11
12 // Construct the graph and calculate in-degrees
13 for (int[] edge : edges) {
14 int fromNode = edge[0];
15 int toNode = edge[1];
16 adjacencyList[fromNode].add(toNode);
17 inDegree[toNode]++;
18 }
19
20 // Queue for BFS-based topological sort
21 Deque<Integer> queue = new ArrayDeque<>();
22
23 // dp[node][color] = maximum count of 'color' in any path ending at 'node'
24 int[][] dp = new int[nodeCount][26];
25
26 // Initialize queue with nodes having zero in-degree (starting nodes)
27 for (int node = 0; node < nodeCount; node++) {
28 if (inDegree[node] == 0) {
29 queue.offer(node);
30 // Initialize DP for this node's color
31 int colorIndex = colors.charAt(node) - 'a';
32 dp[node][colorIndex] = 1;
33 }
34 }
35
36 // Track number of processed nodes (to detect cycles)
37 int processedNodes = 0;
38 // Track the maximum color value found across all paths
39 int maxColorValue = 1;
40
41 // Process nodes in topological order
42 while (!queue.isEmpty()) {
43 int currentNode = queue.pollFirst();
44 processedNodes++;
45
46 // Process all neighbors of current node
47 for (int neighbor : adjacencyList[currentNode]) {
48 // Reduce in-degree and add to queue if it becomes zero
49 if (--inDegree[neighbor] == 0) {
50 queue.offer(neighbor);
51 }
52
53 // Update DP values for the neighbor
54 int neighborColor = colors.charAt(neighbor) - 'a';
55 for (int color = 0; color < 26; color++) {
56 // Transfer color counts from current node to neighbor
57 // Add 1 if the neighbor has the same color
58 int increment = (color == neighborColor) ? 1 : 0;
59 dp[neighbor][color] = Math.max(dp[neighbor][color],
60 dp[currentNode][color] + increment);
61 // Update global maximum
62 maxColorValue = Math.max(maxColorValue, dp[neighbor][color]);
63 }
64 }
65 }
66
67 // If not all nodes were processed, there's a cycle
68 return processedNodes == nodeCount ? maxColorValue : -1;
69 }
70}
71
1class Solution {
2public:
3 int largestPathValue(string colors, vector<vector<int>>& edges) {
4 int numNodes = colors.size();
5
6 // Build adjacency list representation of the graph
7 vector<vector<int>> adjacencyList(numNodes);
8 vector<int> inDegree(numNodes);
9
10 for (auto& edge : edges) {
11 int fromNode = edge[0];
12 int toNode = edge[1];
13 adjacencyList[fromNode].push_back(toNode);
14 ++inDegree[toNode];
15 }
16
17 // Initialize queue for topological sort with nodes having no incoming edges
18 queue<int> processingQueue;
19
20 // dp[node][color] = maximum count of 'color' in any path ending at 'node'
21 vector<vector<int>> colorCountDP(numNodes, vector<int>(26));
22
23 // Find all nodes with no incoming edges (starting points for topological sort)
24 for (int node = 0; node < numNodes; ++node) {
25 if (inDegree[node] == 0) {
26 processingQueue.push(node);
27 // Initialize the color count for this node
28 int nodeColor = colors[node] - 'a';
29 colorCountDP[node][nodeColor]++;
30 }
31 }
32
33 int processedNodeCount = 0;
34 int maxColorValue = 1;
35
36 // Process nodes in topological order
37 while (!processingQueue.empty()) {
38 int currentNode = processingQueue.front();
39 processingQueue.pop();
40 ++processedNodeCount;
41
42 // Process all neighbors of the current node
43 for (int neighbor : adjacencyList[currentNode]) {
44 // Decrease in-degree and add to queue if it becomes 0
45 if (--inDegree[neighbor] == 0) {
46 processingQueue.push(neighbor);
47 }
48
49 // Update DP values for the neighbor
50 int neighborColor = colors[neighbor] - 'a';
51 for (int color = 0; color < 26; ++color) {
52 // Maximum count of 'color' reaching neighbor is either:
53 // - Current value at neighbor
54 // - Value from current node + 1 (if neighbor has this color)
55 int increment = (color == neighborColor) ? 1 : 0;
56 colorCountDP[neighbor][color] = max(colorCountDP[neighbor][color],
57 colorCountDP[currentNode][color] + increment);
58 maxColorValue = max(maxColorValue, colorCountDP[neighbor][color]);
59 }
60 }
61 }
62
63 // If all nodes were processed, graph is acyclic; otherwise, it contains a cycle
64 return (processedNodeCount == numNodes) ? maxColorValue : -1;
65 }
66};
67
1/**
2 * Finds the largest value of any path in a directed graph where nodes have colors.
3 * The value of a path is the number of nodes along that path that have the most frequent color.
4 * Returns -1 if the graph contains a cycle.
5 *
6 * @param colors - String where colors[i] represents the color of node i (lowercase letters)
7 * @param edges - Array of directed edges [from, to]
8 * @returns The largest path value, or -1 if there's a cycle
9 */
10function largestPathValue(colors: string, edges: number[][]): number {
11 const nodeCount = colors.length;
12
13 // Track incoming edges count for each node (for topological sort)
14 const inDegree = Array(nodeCount).fill(0);
15
16 // Build adjacency list representation of the graph
17 const adjacencyList: Map<number, number[]> = new Map();
18
19 for (const [from, to] of edges) {
20 if (!adjacencyList.has(from)) {
21 adjacencyList.set(from, []);
22 }
23 adjacencyList.get(from)!.push(to);
24 inDegree[to]++;
25 }
26
27 // Queue for topological sort (BFS approach)
28 const queue: number[] = [];
29
30 // DP table: dp[node][color] = max count of 'color' in any path ending at 'node'
31 // There are 26 possible colors (a-z)
32 const dp: number[][] = Array.from({ length: nodeCount }, () => Array(26).fill(0));
33
34 // Initialize queue with nodes that have no incoming edges
35 for (let node = 0; node < nodeCount; node++) {
36 if (inDegree[node] === 0) {
37 queue.push(node);
38 // Initialize the color count for this starting node
39 const colorIndex = colors.charCodeAt(node) - 97; // Convert 'a'-'z' to 0-25
40 dp[node][colorIndex]++;
41 }
42 }
43
44 // Track number of processed nodes (to detect cycles)
45 let processedNodes = 0;
46 let maxPathValue = 1;
47
48 // Process nodes in topological order
49 while (queue.length > 0) {
50 const currentNode = queue.pop()!;
51 processedNodes++;
52
53 // Process all neighbors of the current node
54 if (adjacencyList.has(currentNode)) {
55 for (const neighbor of adjacencyList.get(currentNode)!) {
56 inDegree[neighbor]--;
57
58 // If all incoming edges processed, add to queue
59 if (inDegree[neighbor] === 0) {
60 queue.push(neighbor);
61 }
62
63 // Update DP values for the neighbor
64 const neighborColorIndex = colors.charCodeAt(neighbor) - 97;
65
66 for (let colorIndex = 0; colorIndex < 26; colorIndex++) {
67 // If neighbor has the same color, increment count; otherwise, inherit count
68 const increment = (neighborColorIndex === colorIndex) ? 1 : 0;
69 dp[neighbor][colorIndex] = Math.max(
70 dp[neighbor][colorIndex],
71 dp[currentNode][colorIndex] + increment
72 );
73
74 // Track the maximum path value seen so far
75 maxPathValue = Math.max(maxPathValue, dp[neighbor][colorIndex]);
76 }
77 }
78 }
79 }
80
81 // If not all nodes were processed, there's a cycle
82 return processedNodes < nodeCount ? -1 : maxPathValue;
83}
84
Time and Space Complexity
Time Complexity: O((n + m) Γ 26)
or O((n + m) Γ |Ξ£|)
The algorithm uses topological sorting with dynamic programming. Breaking down the time complexity:
- Building the graph and calculating in-degrees takes
O(m)
time wherem
is the number of edges - The BFS traversal visits each node exactly once, taking
O(n)
time - For each node processed, we iterate through all its outgoing edges and update the DP table with 26 colors, which takes
O(degree(node) Γ 26)
time - The total work across all nodes is
O(m Γ 26)
since each edge is processed once with 26 color updates - Overall:
O(n + m Γ 26)
=O((n + m) Γ 26)
Space Complexity: O(m + n Γ 26)
or O(m + n Γ |Ξ£|)
The space usage consists of:
- Graph adjacency list
g
:O(m)
to store all edges - In-degree array
indeg
:O(n)
- Queue
q
:O(n)
in worst case - DP table
dp
:O(n Γ 26)
for storing the maximum count of each color for each node - Overall:
O(m + n Γ 26)
Where n
is the number of nodes, m
is the number of edges, and |Ξ£| = 26
represents the size of the alphabet (26 lowercase letters).
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect DP Initialization for Starting Nodes
The Pitfall: A common mistake is forgetting to initialize the DP values for nodes with zero in-degree (starting nodes). If you don't set dp[node][color] = 1
for the starting node's own color, the algorithm will incorrectly calculate all path color values as 0.
Why It Happens: When processing nodes in topological order, the DP propagation relies on having correct base values. Starting nodes have no predecessors, so their initial state must be manually set.
Solution: Always initialize the color count for starting nodes:
for node_id, degree in enumerate(in_degree):
if degree == 0:
queue.append(node_id)
color_index = ord(colors[node_id]) - ord('a')
color_count_dp[node_id][color_index] = 1 # Don't forget this!
2. Updating DP Values Before Checking In-Degree
The Pitfall: Updating the DP values for a neighbor node before verifying that all its dependencies have been processed (i.e., before its in-degree reaches 0) can lead to incorrect results.
Why It Happens: In topological sort with DP, a node's final DP value should only be computed after all possible paths leading to it have been considered. Premature updates may miss contributions from unprocessed predecessors.
Solution: Update DP values for all edges, but only add nodes to the queue when their in-degree reaches zero:
for neighbor in adjacency_list[current_node]:
in_degree[neighbor] -= 1
# Update DP regardless of in-degree
for color_idx in range(26):
increment = 1 if color_idx == neighbor_color else 0
color_count_dp[neighbor][color_idx] = max(
color_count_dp[neighbor][color_idx],
color_count_dp[current_node][color_idx] + increment
)
# Only add to queue when all dependencies are processed
if in_degree[neighbor] == 0:
queue.append(neighbor)
3. Not Tracking Maximum During DP Updates
The Pitfall: Computing the maximum color value only at the end by scanning the entire DP table can miss intermediate values for nodes that aren't leaf nodes in the DAG.
Why It Happens: The maximum color value might occur at an internal node rather than a terminal node. If you only check the final DP state, you might miss the actual maximum that occurred during processing.
Solution: Track the maximum value during each DP update:
max_color_count = max(max_color_count, color_count_dp[neighbor][color_idx])
4. Incorrect Cycle Detection
The Pitfall: Using the wrong condition for cycle detection, such as checking if the queue is empty instead of counting processed nodes.
Why It Happens: An empty queue doesn't necessarily mean all nodes were processedβit could mean some nodes formed a cycle and never had their in-degree reduced to zero.
Solution: Count the number of processed nodes and compare with total nodes:
if processed_nodes < num_nodes: return -1 # Cycle detected
Which technique can we use to find the middle of a linked list?
Recommended Readings
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
Topological Sort Topological Order Prereq Breadth First Search Review problems graph_bfs Directed Graph Before we get started on topological order let's talk about directed graphs first A graph is directed when its edges have directions these edges are also called arcs Suppose that v w are vertices in a directed
Memoization Prereq Backtracking problems backtracking Memoization is a fancy word for a simple concept so is the case for a lot of things we learn in school It means saving the previous function call result in a dictionary and reading from it when we do the exact same call again
Want a Structured Path to Master System Design Too? Donβt Miss This!