Largest Color Value in a Directed Graph
HardPrompt
There is a directed graph of n colored nodes and m edges. The nodes are numbered from 0 to n - 1.
You are given a string colors where colors[i] is a lowercase English letter representing the color of the ith node in this graph (0-indexed). You are also given a 2D array edges where edges[j] = [aj, bj] indicates that there is a directed edge from node aj to node bj.
A valid path in the graph is a sequence of nodes x1 -> x2 -> x3 -> ... -> xk such that there is a directed edge from xi to xi+1 for every 1 <= i < k. The color value of the path is the number of nodes that are colored the most frequently occurring color along that path.
Return the largest color value of any valid path in the given graph, or -1 if the graph contains a cycle.
Example 1:

"a" (red in the above image)Example 2:

Input: colors = "a", edges = [[0,0]]
Output: -1
Explanation: There is a cycle from 0 to 0.
Constraints:
n == colors.lengthm == edges.length1 <= n <= 1050 <= m <= 105colorsconsists of lowercase English letters.0 <= aj, bj < n
Approaches
2 approaches with complexity analysis and trade-offs.
This approach uses Depth First Search (DFS) combined with dynamic programming (memoization) to solve the problem. We define a state dp[u][c] as the maximum count of a specific color c on any path starting from node u. A recursive DFS function computes these DP values for all nodes. To handle cycles, which would lead to infinite paths, we keep track of the nodes currently in the recursion stack. If we encounter a node that's already in the stack, we've found a cycle.
Algorithm
- Build an adjacency list representation of the graph.
- Initialize a DP table
dp[n][26], wheredp[u][c]will store the maximum count of colorcon any path starting from nodeu. - Use two boolean arrays,
visited[n]to mark nodes for which the DP values have been computed, andpath[n]to track nodes in the current recursion stack for cycle detection. - Iterate through each node
ifrom0ton-1. Ifihas not been visited, start a DFS fromi. - The
dfs(u)function performs the following:- Mark
uas visiting (path[u] = true) and visited (visited[u] = true). - For each neighbor
vofu:- If
vis already in the currentpath, a cycle is detected. Return a value indicating a cycle. - If
vhas not been visited, recursively calldfs(v). Propagate the cycle detection signal if the recursive call finds one. - After the recursive call returns (and no cycle was found), update the DP values for
ubased onv:dp[u][c] = max(dp[u][c], dp[v][c])for all 26 colorsc.
- If
- After iterating through all neighbors, increment the count for the color of the current node
u:dp[u][colors.charAt(u) - 'a']++. - Unmark
ufrom the current path (path[u] = false).
- Mark
- If the DFS ever detects a cycle, terminate and return -1.
- After the DFS completes for all components, the
dptable is filled. The answer is the maximum value in the entiredptable.
Walkthrough
In this method, we aim to compute the answer for each subproblem, where a subproblem is finding the largest color value for all paths starting at a particular node u. We use a 2D array dp[n][26] for memoization, where dp[u][c] stores the maximum count of color c on any path originating from node u.
The computation is done via a recursive DFS function, let's call it dfs(u). This function is responsible for computing the values for dp[u]. To do this, it first recursively calls itself for all neighbors v of u. Once the dp values for all neighbors are computed, dp[u][c] can be determined by taking the maximum of dp[v][c] over all neighbors v, and then adding 1 if the color of node u is c.
To avoid recomputing for the same node multiple times, a visited array is used. More importantly, to detect cycles, we use a path (or recursionStack) array. Before exploring the neighbors of u, we mark path[u] = true. If we then try to visit a neighbor v that already has path[v] = true, we have found a back edge, indicating a cycle. If a cycle is found, we should immediately stop and return -1. After exploring all of u's neighbors, we backtrack by setting path[u] = false.
The final answer is the maximum value found in the dp table after the DFS has been run for all unvisited nodes.
class Solution { List<Integer>[] adj; int[][] dp; boolean[] visited; boolean[] path; String colors; public int largestColorValue(String colors, int[][] edges) { int n = colors.length(); this.colors = colors; adj = new ArrayList[n]; for (int i = 0; i < n; i++) { adj[i] = new ArrayList<>(); } for (int[] edge : edges) { adj[edge[0]].add(edge[1]); } dp = new int[n][26]; visited = new boolean[n]; path = new boolean[n]; int maxVal = 0; for (int i = 0; i < n; i++) { if (!visited[i]) { if (hasCycle(i)) { return -1; } } } for(int i=0; i<n; i++) { for(int j=0; j<26; j++) { maxVal = Math.max(maxVal, dp[i][j]); } } return maxVal; } private boolean hasCycle(int u) { visited[u] = true; path[u] = true; for (int v : adj[u]) { if (path[v]) { return true; // Cycle detected } if (!visited[v]) { if (hasCycle(v)) { return true; } } // Update dp table for u based on v for (int i = 0; i < 26; i++) { dp[u][i] = Math.max(dp[u][i], dp[v][i]); } } dp[u][colors.charAt(u) - 'a']++; path[u] = false; return false; }}Complexity
Time
O(N + M). Each node and edge is visited once by the DFS. For each node, we perform a constant number of operations (26 color updates from its children and one for itself). The work is proportional to the number of nodes and edges. More precisely, it's O(26 * (N + M)).
Space
O(N + M), where N is the number of nodes and M is the number of edges. This is because we store the adjacency list (O(N+M)), the DP table (O(N * 26)), the recursion stack (O(N) in the worst case), and visited/path arrays (O(N)). Since 26 is a constant, this simplifies to O(N + M).
Trade-offs
Pros
It's a conceptually clear extension of a standard DFS traversal.
Correctly solves the problem by handling path-dependent values and cycles simultaneously.
Cons
The recursive implementation can lead to a
StackOverflowErrorif the graph contains very long paths, although this is unlikely given the problem constraints.Requires managing two separate state arrays (
visitedandpath) for correctness, which can be slightly more complex than the iterative approach.
A more robust and typically preferred approach for problems on directed acyclic graphs (DAGs) is to use a topological sort. This method processes nodes in a linear order, such that for every directed edge from node u to node v, u comes before v in the ordering. We can use Kahn's algorithm, an iterative approach to topological sorting, to process nodes and propagate the color counts along the paths. If a topological sort cannot be completed (i.e., not all nodes are visited), it signifies the presence of a cycle.
Algorithm
- Build an adjacency list and compute the in-degree for every node.
- Initialize a queue and add all nodes with an in-degree of 0. These are the starting points of paths in the graph.
- Create a
counts[n][26]table.counts[u][c]will store the maximum count of colorcon any path from a source node tou. - For each source node
uadded to the queue, initialize its own color count:counts[u][colors.charAt(u) - 'a'] = 1. - Initialize a counter for processed nodes,
processedNodes = 0, and the answer,maxColorValue = 0. - Start a loop that continues as long as the queue is not empty:
- Dequeue a node
u. IncrementprocessedNodes. - Find the maximum value in
counts[u]and updatemaxColorValue = max(maxColorValue, max_in_counts_u). - For each neighbor
vofu:- Update the counts for
vbased onu. For each colorc,counts[v][c] = max(counts[v][c], counts[u][c] + (colors.charAt(v) - 'a' == c ? 1 : 0)). - Decrement the in-degree of
v. - If the in-degree of
vbecomes 0, enqueuev.
- Update the counts for
- Dequeue a node
- After the loop, if
processedNodes < n, it means some nodes were not visited. This is only possible if there is a cycle in the graph. In this case, return -1. - Otherwise, the graph is a DAG, and we can return the computed
maxColorValue.
Walkthrough
This approach reformulates the problem to fit an iterative, breadth-first-search-like traversal based on the graph's topology. It avoids recursion and its potential pitfalls.
-
Graph Representation and In-degrees: First, we construct an adjacency list for the graph and an array to store the in-degree of each node. An edge
u -> vincrements the in-degree ofv. -
Initialization: We use a
counts[n][26]table, wherecounts[u][c]will store the maximum count of colorcon any path ending at nodeu. A queue is initialized with all nodes having an in-degree of 0. For these source nodes, we initialize their own color count in thecountstable, e.g.,counts[u][color of u] = 1. -
Processing Nodes: We process nodes from the queue one by one. When a node
uis dequeued, we know that we have found the maximum color counts for all paths ending atu. We can then update the global maximum color value. For each neighborvofu, we propagate the counts fromu. The new count for a colorcon a path tovthroughuiscounts[u][c], plus one ifvitself has colorc. We updatecounts[v][c]by taking the maximum over all its incoming neighbors. After processingu's influence onv, we decrementv's in-degree. If it becomes 0,vis ready to be processed and is added to the queue. -
Cycle Detection: We keep track of the number of nodes processed. If this number is less than the total number of nodes
nafter the queue becomes empty, it means some nodes were part of a cycle and could never have their in-degree reduced to 0. In this case, we return -1. -
Result: If no cycle is detected, the maximum value found during the process is the answer.
class Solution { public int largestColorValue(String colors, int[][] edges) { int n = colors.length(); List<Integer>[] adj = new ArrayList[n]; int[] inDegree = new int[n]; for (int i = 0; i < n; i++) { adj[i] = new ArrayList<>(); } for (int[] edge : edges) { adj[edge[0]].add(edge[1]); inDegree[edge[1]]++; } int[][] counts = new int[n][26]; Queue<Integer> queue = new LinkedList<>(); for (int i = 0; i < n; i++) { if (inDegree[i] == 0) { queue.offer(i); counts[i][colors.charAt(i) - 'a'] = 1; } } int maxVal = 0; int processedNodes = 0; while (!queue.isEmpty()) { int u = queue.poll(); processedNodes++; int maxCountAtU = 0; for(int count : counts[u]) { maxCountAtU = Math.max(maxCountAtU, count); } maxVal = Math.max(maxVal, maxCountAtU); for (int v : adj[u]) { for (int i = 0; i < 26; i++) { int newCount = counts[u][i] + (colors.charAt(v) - 'a' == i ? 1 : 0); counts[v][i] = Math.max(counts[v][i], newCount); } inDegree[v]--; if (inDegree[v] == 0) { queue.offer(v); } } } if (processedNodes < n) { return -1; // Cycle detected } return maxVal; }}Complexity
Time
O(N + M). Building the graph and in-degrees takes O(N + M). Each node is enqueued and dequeued once. When processing a node, we iterate through its neighbors. Thus, each edge is processed once. For each edge, we do O(26) work. Finding the max at each node also takes O(26). The total time is O(N*26 + M*26), which simplifies to O(N + M).
Space
O(N + M). We store the adjacency list (O(N+M)), the `counts` table (O(N * 26)), the in-degree array (O(N)), and the queue (O(N) in the worst case). This simplifies to O(N + M).
Trade-offs
Pros
Iterative nature avoids recursion and potential stack overflow issues.
Cycle detection is a natural outcome of the algorithm, making the logic clean.
Generally considered more efficient in practice due to lower overhead than recursion.
Cons
Requires pre-computation of in-degrees and building the adjacency list, which adds some initial setup.
The logic of propagating counts iteratively might be slightly less intuitive to some compared to the recursive DP formulation.
Solutions
Solution
class Solution {public int largestPathValue(String colors, int[][] edges) { int n = colors.length(); List<Integer>[] g = new List[n]; Arrays.setAll(g, k->new ArrayList<>()); int[] indeg = new int[n]; for (int[] e : edges) { int a = e[0], b = e[1]; g[a].add(b); ++indeg[b]; } Deque<Integer> q = new ArrayDeque<>(); int[][] dp = new int[n][26]; for (int i = 0; i < n; ++i) { if (indeg[i] == 0) { q.offer(i); int c = colors.charAt(i) - 'a'; ++dp[i][c]; } } int cnt = 0; int ans = 1; while (!q.isEmpty()) { int i = q.pollFirst(); ++cnt; for (int j : g[i]) { if (--indeg[j] == 0) { q.offer(j); } int c = colors.charAt(j) - 'a'; for (int k = 0; k < 26; ++k) { dp[j][k] = Math.max(dp[j][k], dp[i][k] + (c == k ? 1 : 0)); ans = Math.max(ans, dp[j][k]); } } } return cnt == n ? ans : -1; }}class Solution {public: int largestPathValue(string colors, vector<vector<int>> &edges) { int n = colors.size(); vector<vector<int>> g(n); vector<int> indeg(n); for (auto &e : edges) { int a = e[0], b = e[1]; g[a].push_back(b); ++indeg[b]; } queue<int> q; vector<vector<int>> dp(n, vector<int>(26)); for (int i = 0; i < n; ++i) { if (indeg[i] == 0) { q.push(i); int c = colors[i] - 'a'; dp[i][c]++; } } int cnt = 0; int ans = 1; while (!q.empty()) { int i = q.front(); q.pop(); ++cnt; for (int j : g[i]) { if (--indeg[j] == 0) q.push(j); int c = colors[j] - 'a'; for (int k = 0; k < 26; ++k) { dp[j][k] = max(dp[j][k], dp[i][k] + (c == k)); ans = max(ans, dp[j][k]); } } } return cnt == n ? ans : -1; }};class Solution: def largestPathValue(self, colors: str, edges: List[List[int]]) -> int: n = len(colors) indeg = [0] * n g = defaultdict(list) for a, b in edges: g[a]. append(b) indeg[b] += 1 q = deque() dp = [[0] * 26 for _ in range(n)] for i, v in enumerate(indeg): if v == 0: q . append(i) c = ord(colors[i]) - ord('a') dp[i][c] += 1 cnt = 0 ans = 1 while q: i = q . popleft() cnt += 1 for j in g[i]: indeg[j] -= 1 if indeg[j] == 0: q . append(j) c = ord(colors[j]) - ord('a') for k in range(26): dp[j][k] = max(dp[j][k], dp[i][k] + (c == k)) ans = max(ans, dp[j][k]) return - 1 if cnt < n else ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.