Facebook Pixel

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Taking the color counts we've accumulated so far (from the current node)
  2. Adding 1 if the neighbor's color matches the color we're tracking
  3. 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], add b to g[a]'s list and increment indeg[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] where dp[i][j] represents the maximum count of color j on any path ending at node i
  • For each starting node (in-degree 0), set dp[i][c] = 1 where c 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 node i:
      • Decrement indeg[j]
      • If indeg[j] becomes 0, add j 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 node j's color c
      • Track the maximum value seen so far in ans

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 Evaluator

Example 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 where m 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
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which technique can we use to find the middle of a linked list?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More