2374. Node With Highest Edge Score

MediumGraphHash Table
Leetcode Link

Problem Description

In this problem, we're given a directed graph consisting of n nodes. These nodes are uniquely labeled from 0 to n - 1. The special thing about this graph is that each node has exactly one outgoing edge, which means that every node points to exactly one other node. This setup creates a series of chains and possibly cycles within the graph.

The graph is described using an array edges of length n, where the value edges[i] represents the node that node i points to. The problem asks us to compute the "edge score" for each node. The edge score of a node is the sum of the labels of all nodes that point to it.

Our task is to identify the node with the highest edge score. If there happens to be a tie where multiple nodes have the same highest edge score, we should return the node with the smallest index among them.

To summarize, we are to calculate the edge scores, find the maximum, and return the node that obtains it, resolving ties by choosing the lowest-indexed node.

Intuition

To solve this problem, our approach involves two main steps:

  1. Calculating Edge Scores: To calculate the edge score of each node efficiently, we can traverse the graph once and increment the edge score for the target nodes. For instance, if there is an edge from node i to node j, we add i (the label of the starting node) to the edge score of node j (the destination node). We can keep a count of these edge scores using a data structure such as a Counter from Python's collections module, where the keys correspond to node indices and the values to their edge scores.

  2. Finding the Node with the Highest Edge Score: After we have all the edge scores, we iterate through them to find the maximum score. While doing this, we must also keep track of the node index because if multiple nodes have the same edge score, we need to return the one with the smallest index. An efficient way to tackle this is to initialize a variable (let's say ans) to store the index of the node with the current highest edge score. We iterate through all possible node indices, compare their edge scores with the current highest, and update ans when we find a higher score or if the score is the same but the node index is smaller.

This approach ensures we traverse the graph only once to compute the scores and a second time to find the maximum score with the smallest node index—both operations having linear time complexity, which is efficient.

Learn more about Graph patterns.

Solution Approach

To implement the solution as described in the intuition, we're using a counter and a for-loop construct. Here's the step-by-step breakdown of the implementation:

  1. Initializing the Counter: We start by initializing a Counter, which is a special dictionary that lets us keep a tally for each element. It is a part of Python's built-in collections module. In our case, each element corresponds to a node and its tally to the node's edge score.

    cnt = Counter()
  2. Calculating Edge Scores: We loop over each edge in the edges list with its index. At each step, we increment the edge score of the node pointed to by the current index. The index indicates the starting node (contributing to the edge score) and edges[i] the destination node. The score of the destination node is increased by the label of the starting node (which is its index i).

    for i, v in enumerate(edges):
        cnt[v] += i
  3. Finding the Node with the Highest Edge Score: We initialize a variable ans to keep track of the index of the node with the highest edge score found so far, starting with the first node (0).

    Then, we iterate over the range of node indices, using another for-loop. For each index, we compare its edge score (cnt[i]) with the edge score of the current answer (cnt[ans]). If we find a higher edge score, or if the edge scores are equal and the current index is less than ans (implying a smaller node index), we update ans.

    ans = 0
    for i in range(len(edges)):
        if cnt[ans] < cnt[i]:
            ans = i
  4. Return the Result: Finally, after finishing the iteration, the variable ans holds the index of the node with the highest edge score. We return ans as the final result.

    return ans

The combination of Counter to tally the score and a for-loop to determine the maximum ensures a straightforward and efficient implementation. It uses O(n) time for computing the edge scores and O(n) time to identify the node with the highest edge score, leading to an overall linear time complexity, where n is the number of nodes. The space complexity is also O(n) due to the storage needed for the Counter. This solution leverages the characteristics of our graph (each node pointing to exactly one other node) to maintain simplicity and efficiency.

Ready to land your dream job?

Unlock your dream job with a 2-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let’s walk through a small example to illustrate the solution approach.

Imagine a graph represented by the edges array: [1, 2, 3, 4, 0]. This array means:

  • Node 0 points to node 1
  • Node 1 points to node 2
  • Node 2 points to node 3
  • Node 3 points to node 4
  • Node 4 points to node 0

Now, following the step-by-step solution approach:

  1. Initializing the Counter:

    cnt = Counter()
  2. Calculating Edge Scores:

    As per our edges array:

    cnt[1] += 0  # node 0 points to node 1
    cnt[2] += 1  # node 1 points to node 2
    cnt[3] += 2  # node 2 points to node 3
    cnt[4] += 3  # node 3 points to node 4
    cnt[0] += 4  # node 4 points to node 0

    After this loop:

    cnt = {1: 0, 2: 1, 3: 2, 4: 3, 0: 4}
  3. Finding the Node with the Highest Edge Score:

    Initialize ans = 0. Then we compare:

    • cnt[0] is 4. ans remains 0.
    • cnt[1] is 0. No change since cnt[0] > cnt[1].
    • cnt[2] is 1. No change since cnt[0] > cnt[2].
    • cnt[3] is 2. No change since cnt[0] > cnt[3].
    • cnt[4] is 3. No change since cnt[0] > cnt[4].

    After comparing all, ans is still 0 as it has the highest score 4.

  4. Return the Result:

    return ans  # returns 0

With this example, we can see how the Counter was used to calculate edge scores by aggregating contributions from nodes that point to a specific node. Afterward, we iterated through the node indices, keeping track of the node with the current highest score and returned the one with the smallest index in case of a tie. The node with index 0 has the highest edge score of 4 in this example, so it is the answer.

Solution Implementation

1from collections import Counter  # Import Counter class from collections
2
3class Solution:
4    def edgeScore(self, edges: List[int]) -> int:
5        # Initialize a Counter to keep track of the scores for each node
6        node_scores = Counter()
7
8        # Iterate over the list of edges with their indices
9        for index, node in enumerate(edges):
10            # Accumulate the index values for each node to compute the edge score
11            node_scores[node] += index
12
13        # Initialize the variable that will hold the node with the highest edge score
14        max_score_node = 0
15
16        # Traverse the nodes to find the one with the highest edge score
17        # In case of a tie, the node with the lower index is selected
18        for node in range(len(edges)):
19            # Update the max_score_node if the current node has a higher score
20            if node_scores[max_score_node] < node_scores[node]:
21                max_score_node = node
22
23        # Return the node with the highest edge score
24        return max_score_node
25
1class Solution {
2    public int edgeScore(int[] edges) {
3        // Get the number of nodes in the graph.
4        int numNodes = edges.length;
5        // Create an array to keep track of the cumulative edge scores for each node.
6        long[] edgeScores = new long[numNodes];
7      
8        // Iterate over the array and accumulate edge scores.
9        for (int i = 0; i < numNodes; ++i) {
10            // Increment the edge score of the destination node by the index of the current node.
11            edgeScores[edges[i]] += i;
12        }
13      
14        // Initialize 'answer' to the first node's index (0) by default.
15        int answer = 0;
16        // Iterate over edgeScores to find the node with the highest edge score.
17        for (int i = 0; i < numNodes; ++i) {
18            // If the current node's edge score is higher than the score of the answer node,
19            // then update the answer to the current node's index.
20            if (edgeScores[answer] < edgeScores[i]) {
21                answer = i;
22            }
23        }
24      
25        // Return the node with the highest edge score.
26        return answer;
27    }
28}
29
1class Solution {
2public:
3    // Function that calculates the edge score for each node and returns the node with the highest score.
4    int edgeScore(vector<int>& edges) {
5        int numNodes = edges.size(); // The number of nodes is determined by the size of the edges array.
6        vector<long long> nodeScores(numNodes, 0); // Initialize a vector to store the score for each node.
7      
8        // Calculate the score for each node by adding the index of the node it points to its score.
9        for (int idx = 0; idx < numNodes; ++idx) {
10            nodeScores[edges[idx]] += idx;
11        }
12      
13        // The node with the highest score. Initialized to the first node (index 0).
14        int nodeWithMaxScore = 0;
15      
16        // Iterate through the node scores to find the node with the highest score.
17        // In case of a tie, the node with the lower index wins, which is naturally handled
18        // due to the non-decreasing traversal of the array.
19        for (int i = 0; i < numNodes; ++i) {
20            if (nodeScores[nodeWithMaxScore] < nodeScores[i]) {
21                nodeWithMaxScore = i; // Update the node with the maximum score.
22            }
23        }
24      
25        // Return the index of the node with the maximum score.
26        return nodeWithMaxScore;
27    }
28};
29
1function edgeScore(edges: number[]): number {
2    // Length of the 'edges' array
3    const numberOfNodes: number = edges.length;
4
5    // Initialize an array to store the sum of the indices for each edge
6    const edgeScores: number[] = new Array(numberOfNodes).fill(0);
7
8    // Iterate over the 'edges' array to calculate the sum of indices for each node
9    for (let index = 0; index < numberOfNodes; index++) {
10        // Update the score for the node pointed to by the current edge
11        edgeScores[edges[index]] += index;
12    }
13
14    // Variable to hold the index of the node with the highest score
15    let highestScoreNode: number = 0;
16
17    // Find the node with the maximum edge score
18    for (let node = 0; node < numberOfNodes; node++) {
19        // If the current node has a higher score than the highest recorded, update the highestScoreNode
20        if (edgeScores[highestScoreNode] < edgeScores[node]) {
21            highestScoreNode = node;
22        }
23    }
24
25    // Return the node with the highest edge score
26    return highestScoreNode;
27}
28

Time and Space Complexity

Time complexity

The given code consists primarily of two parts: a loop to accumulate the edge scores and another loop to find the node with the highest edge score. Let's analyze each part to determine the overall time complexity:

  1. The for i, v in enumerate(edges): iterates through the edges list once. The length of this list is n, where n is the number of nodes in the graph. Inside this loop, each iteration performs an O(1) operation, where it updates the Counter object. Therefore, this loop has a time complexity of O(n).

  2. The second loop for i in range(len(edges)): is also iterating n times for each node in the graph. For each iteration, it performs a comparison operation which is O(1). Hence, the time complexity of this loop is also O(n).

Combining both parts, the overall time complexity for the entire function is O(n) + O(n) which simplifies to O(n).

Space complexity

The space complexity is determined by the data structures used in the algorithm:

  1. The cnt variable is a Counter object which, in the worst case, will contain an entry for each unique node in edges. This means its size grows linearly with the number of nodes, contributing a space complexity of O(n).

  2. The ans variable is an integer, which occupies O(1) space.

As such, the total space complexity of the algorithm is O(n) for the Counter object plus O(1) for the integer, which results in an overall space complexity of O(n).

Learn more about how to find time and space complexity quickly using problem constraints.


Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:
Question 1 out of 10

What's the output of running the following function using input [30, 20, 10, 100, 33, 12]?

1def fun(arr: List[int]) -> List[int]:
2    import heapq
3    heapq.heapify(arr)
4    res = []
5    for i in range(3):
6        res.append(heapq.heappop(arr))
7    return res
8
1public static int[] fun(int[] arr) {
2    int[] res = new int[3];
3    PriorityQueue<Integer> heap = new PriorityQueue<>();
4    for (int i = 0; i < arr.length; i++) {
5        heap.add(arr[i]);
6    }
7    for (int i = 0; i < 3; i++) {
8        res[i] = heap.poll();
9    }
10    return res;
11}
12
1class HeapItem {
2    constructor(item, priority = item) {
3        this.item = item;
4        this.priority = priority;
5    }
6}
7
8class MinHeap {
9    constructor() {
10        this.heap = [];
11    }
12
13    push(node) {
14        // insert the new node at the end of the heap array
15        this.heap.push(node);
16        // find the correct position for the new node
17        this.bubble_up();
18    }
19
20    bubble_up() {
21        let index = this.heap.length - 1;
22
23        while (index > 0) {
24            const element = this.heap[index];
25            const parentIndex = Math.floor((index - 1) / 2);
26            const parent = this.heap[parentIndex];
27
28            if (parent.priority <= element.priority) break;
29            // if the parent is bigger than the child then swap the parent and child
30            this.heap[index] = parent;
31            this.heap[parentIndex] = element;
32            index = parentIndex;
33        }
34    }
35
36    pop() {
37        const min = this.heap[0];
38        this.heap[0] = this.heap[this.size() - 1];
39        this.heap.pop();
40        this.bubble_down();
41        return min;
42    }
43
44    bubble_down() {
45        let index = 0;
46        let min = index;
47        const n = this.heap.length;
48
49        while (index < n) {
50            const left = 2 * index + 1;
51            const right = left + 1;
52
53            if (left < n && this.heap[left].priority < this.heap[min].priority) {
54                min = left;
55            }
56            if (right < n && this.heap[right].priority < this.heap[min].priority) {
57                min = right;
58            }
59            if (min === index) break;
60            [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61            index = min;
62        }
63    }
64
65    peek() {
66        return this.heap[0];
67    }
68
69    size() {
70        return this.heap.length;
71    }
72}
73
74function fun(arr) {
75    const heap = new MinHeap();
76    for (const x of arr) {
77        heap.push(new HeapItem(x));
78    }
79    const res = [];
80    for (let i = 0; i < 3; i++) {
81        res.push(heap.pop().item);
82    }
83    return res;
84}
85

Recommended Readings

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


Load More