Facebook Pixel

3249. Count the Number of Good Nodes


Problem Description

There is an undirected tree with n nodes labeled from 0 to n - 1, and rooted at node 0. You are given a 2D integer array edges of length n - 1, where edges[i] = [ai, bi] indicates that there is an edge between nodes ai and bi in the tree.

A node is good if all the subtrees rooted at its children have the same size.

Return the number of good nodes in the given tree.

A subtree of a node is a tree consisting of that node and all of its descendants.

Intuition

To solve the problem, we employ a Depth-First Search (DFS) approach. The key observation is that a node is considered good if all its child subtrees are of equal size. The DFS allows us to explore each subtree recursively and gather information about the size of each child's subtree.

The process includes:

  1. Building an adjacency list from the given edges to represent the tree.
  2. Implementing a DFS function that traverses the tree starting from the root node (0). During this traversal, we keep track of:
    • pre: The size of the subtree of the first child, used for comparison.
    • cnt: The cumulative size of the subtree (including the node itself).
    • ok: A flag indicating whether the node is good; initially set to 1 (true).
  3. For each node, we compare the sizes of all its child subtrees using pre. If any subtree has a different size, we mark the node as not good (ok = 0).
  4. We sum up all nodes flagged as good and return this count.

This approach efficiently determines the number of good nodes in the given tree by recursively calculating subtree sizes and ensuring uniformity among sibling subtrees at each node.

Learn more about Tree and Depth-First Search patterns.

Solution Approach

The solution to the problem is constructed using a Depth-First Search (DFS) approach, which is implemented with the following steps:

  1. Constructing the Adjacency List:

    • We use a dictionary g (created using defaultdict(list)) to store the adjacency list of the tree. Each node points to a list of its adjacent nodes.
    • We populate this list using the provided edges array, ensuring that the tree structure is appropriately represented for bidirectional traversal.
  2. DFS Function:

    • Define a function dfs(a, fa) where a is the current node and fa is the parent node. This function calculates the size of the subtree rooted at node a and also counts the number of good nodes.
    • Initialize three local variables:
      • pre is set to -1, used for storing the size of the first child subtree.
      • cnt is initialized to 1 to represent the current node.
      • ok is set to 1 to signify that the node is good initially.
    • Traverse through all neighbors b of node a. For each neighbor not equal to fa, recursively compute the size of the subtree using dfs(b, a) and add the result to cnt.
    • Compare cur (the size of the current child's subtree) with pre. If pre is still -1, update pre to cur. If pre is not equal to cur, it means the subtree sizes are not uniform, set ok to 0, indicating a is not a good node.
    • Add the value of ok to ans which keeps track of the total number of good nodes.
  3. Calculating the Result:

    • Initialize ans to 0 before invoking the DFS from the root node (dfs(0, -1)).
    • Once the DFS completes, ans contains the total count of good nodes, which is then returned as the final result.

This method leverages depth-first traversal to efficiently assess each node's qualification as a "good" node and counts them based on the uniformity of their child subtrees' sizes.

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 illustrate the solution approach with a small example.

Consider a tree with 5 nodes and the following edges: edges = [[0, 1], [0, 2], [1, 3], [1, 4]].

This defines the following tree structure, rooted at node 0:

     0
    / \
   1   2
  / \
 3   4

Step 1: Constructing the Adjacency List

We construct an adjacency list to represent the tree:

g = {
    0: [1, 2],
    1: [0, 3, 4],
    2: [0],
    3: [1],
    4: [1]
}

Step 2: DFS Function Execution

  1. Start the DFS from the root node 0. dfs(0, -1):

    • pre = -1, cnt = 1, ok = 1
  2. Explore neighbor 1 of node 0:

    • Enter dfs(1, 0): pre = -1, cnt = 1, ok = 1
  3. Explore neighbor 3 of node 1:

    • Enter dfs(3, 1): pre = -1, cnt = 1, ok = 1
    • Node 3 has no children apart from its parent (1), so return cnt = 1.
    • Back in dfs(1, 0), cur = 1, pre = 1
  4. Explore neighbor 4 of node 1:

    • Enter dfs(4, 1): pre = -1, cnt = 1, ok = 1
    • Node 4 has no children apart from its parent (1), so return cnt = 1.
    • Back in dfs(1, 0), cur = 1, pre = 1, remains the same, so ok = 1.
    • Return cnt = 3 for node 1 as it's good and ok = 1.
  5. Back in dfs(0, -1), cur = 3, pre = 3

  6. Explore neighbor 2 of node 0:

    • Enter dfs(2, 0): pre = -1, cnt = 1, ok = 1
    • Node 2 has no children apart from its parent (0), so return cnt = 1.
    • Back in dfs(0, -1), cur = 1, pre = 3, not the same, so ok = 0 for node 0.
  7. The DFS traversal is complete. Only node 1 is marked as good since it had uniform child subtree sizes.

Step 3: Calculating the Result

  • The total count of good nodes is 1. Thus, the function returns 1.

Solution Implementation

1from typing import List
2from collections import defaultdict
3
4class Solution:
5    def countGoodNodes(self, edges: List[List[int]]) -> int:
6        # Depth First Search function to explore the tree
7        def dfs(node: int, parent: int) -> int:
8            # Initialize variables
9            previous_count = -1  # Stores count of subtree nodes
10            count = 1            # Start with current node
11            is_good_tree = 1     # Flag to check if current subtree is uniform
12          
13            # Iterate over all connected nodes (children)
14            for child in graph[node]:
15                if child != parent:  # Ensure not going back to the parent node
16                    # Recursively calculate subtree node count
17                    current = dfs(child, node)
18                    count += current
19                  
20                    if previous_count < 0:
21                        # First child processed, store its count
22                        previous_count = current
23                    elif previous_count != current:
24                        # If any subtree has different count, mark as not uniform
25                        is_good_tree = 0
26          
27            # Update global answer if subtree is uniform
28            nonlocal answer
29            answer += is_good_tree
30          
31            # Return total nodes under this subtree including itself
32            return count
33
34        # Initialize graph as adjacency list
35        graph = defaultdict(list)
36        for a, b in edges:
37            graph[a].append(b)
38            graph[b].append(a)
39      
40        # Initialize answer and call dfs starting from node 0
41        answer = 0
42        dfs(0, -1)
43      
44        return answer
45
1class Solution {
2    private int ans; // To count the number of good nodes
3    private List<Integer>[] graph; // To represent the graph using adjacency list
4
5    public int countGoodNodes(int[][] edges) {
6        int n = edges.length + 1; // Number of nodes is number of edges + 1
7        graph = new List[n]; // Initialize the adjacency list for the graph
8        Arrays.setAll(graph, k -> new ArrayList<>()); // Assign a new ArrayList to each node in the graph
9
10        // Build the graph from the given edges
11        for (int[] edge : edges) {
12            int a = edge[0], b = edge[1];
13            graph[a].add(b); // Add b to the adjacency list of a
14            graph[b].add(a); // Add a to the adjacency list of b
15        }
16
17        dfs(0, -1); // Start DFS from the root node 0, with a parent of -1
18        return ans; // Return the count of good nodes
19    }
20
21    private int dfs(int node, int parent) {
22        int previousChildValue = -1; // To store the value of previously visited child node
23        int subtreeNodeCount = 1; // Count the node itself
24        int isGoodNode = 1; // Initially assume the current node is a good node
25
26        // Iterate over all adjacent nodes (children)
27        for (int child : graph[node]) {
28            if (child != parent) { // Ensure that we don't revisit the parent node
29                int currentChildValue = dfs(child, node); // Perform DFS on child
30                subtreeNodeCount += currentChildValue; // Increment subtree count
31
32                // Compare subtree sizes for children to determine if it's a good node
33                if (previousChildValue < 0) {
34                    previousChildValue = currentChildValue; // Set for the first child
35                } else if (previousChildValue != currentChildValue) {
36                    isGoodNode = 0; // This node is not good if subtree sizes differ
37                }
38            }
39        }
40        ans += isGoodNode; // Increase count if it's a good node
41        return subtreeNodeCount; // Return the number of nodes in the subtree rooted at this node
42    }
43}
44
1#include <vector>
2using namespace std;
3
4class Solution {
5public:
6    // Function to count good nodes in a tree represented by its edges
7    int countGoodNodes(vector<vector<int>>& edges) {
8        int n = edges.size() + 1; // Number of nodes in the graph
9        vector<int> graph[n];
10
11        // Build adjacency list representation of the graph from edge list
12        for (const auto& edge : edges) {
13            int nodeA = edge[0], nodeB = edge[1];
14            graph[nodeA].push_back(nodeB); // Add nodeB as neighbor of nodeA
15            graph[nodeB].push_back(nodeA); // Add nodeA as neighbor of nodeB for undirected graph
16        }
17
18        int goodNodesCount = 0; // Initialize count of good nodes
19        // Define a recursive lambda function for Depth First Search (DFS)
20        function<int(int, int)> dfs = [&](int current, int parent) -> int {
21            int previousSubTreeSize = -1;
22            int subTreeSize = 1;
23            bool isGoodNode = true;
24
25            // Iterate through every connected node
26            for (int neighbor : graph[current]) {
27                if (neighbor != parent) { // Ensure not to go back to parent node
28                    int currentSubTreeSize = dfs(neighbor, current);
29                    subTreeSize += currentSubTreeSize;
30
31                    // Check if previous subtree size is different
32                    if (previousSubTreeSize < 0) {
33                        previousSubTreeSize = currentSubTreeSize;
34                    } else if (previousSubTreeSize != currentSubTreeSize) {
35                        isGoodNode = false;
36                    }
37                }
38            }
39
40            goodNodesCount += isGoodNode; // Increment good node count if true
41            return subTreeSize; // Return total subtree size rooted at current node
42        };
43
44        dfs(0, -1); // Call DFS starting from node 0 with no parent
45        return goodNodesCount; // Return the count of good nodes
46    }
47};
48
1// Function to count the number of "good nodes" in a tree graph.
2function countGoodNodes(edges: number[][]): number {
3    // Calculate the number of nodes. It's one more than the number of edges, since the structure is a tree.
4    const n = edges.length + 1;
5
6    // Initialize an adjacency list to represent the graph.
7    const graph: number[][] = Array.from({ length: n }, () => []);
8
9    // Populate the graph with edges.
10    for (const [a, b] of edges) {
11        graph[a].push(b);
12        graph[b].push(a);
13    }
14
15    // Variable to keep track of the count of "good nodes".
16    let goodNodeCount = 0;
17
18    // Depth-First Search function to traverse the graph.
19    const dfs = (node: number, parent: number): number => {
20        // Initialize `pre` to -1 to track if all child nodes return the same value, `cnt` to 1 to count nodes including `node`, and `ok` to 1 assuming it's a "good node".
21        let [pre, totalNodes, isGood] = [-1, 1, 1];
22
23        // Visit adjacent nodes.
24        for (const neighbor of graph[node]) {
25            if (neighbor !== parent) {
26                // Perform DFS on the subtree rooted at this neighbor.
27                const subtreeNodeCount = dfs(neighbor, node);
28                totalNodes += subtreeNodeCount;
29
30                // Check if the "good node" condition is violated.
31                if (pre < 0) {
32                    pre = subtreeNodeCount;
33                } else if (pre !== subtreeNodeCount) {
34                    isGood = 0; // Not a "good node" if subtree sizes differ.
35                }
36            }
37        }
38
39        // Increase the global count of "good nodes" if this is a "good node".
40        goodNodeCount += isGood;
41      
42        // Return the total count of nodes in this subtree.
43        return totalNodes;
44    };
45
46    // Start DFS from node 0 (assuming node 0 is the root).
47    dfs(0, -1);
48
49    // Return the total number of "good nodes".
50    return goodNodeCount;
51}
52

Time and Space Complexity

The time complexity of the code is O(n), where n is the number of nodes in the graph. This complexity arises because each node and edge are visited exactly once during the depth-first search (DFS) traversal of the entire graph. The function dfs is called once for each node, and within each call, all child nodes are processed, leading to a total of O(n + m), where m is the number of edges. In a connected graph like a tree, m = n - 1, simplifying the complexity to O(n).

The space complexity of the code is also O(n). This is due to the space required to store the adjacency list representation of the graph using the defaultdict(list), which stores the edges. Additionally, the recursive DFS function dfs utilizes stack space proportional to the height of the tree, which in the worst case of a skewed tree is O(n).

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


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

Which type of traversal does breadth first search do?


Recommended Readings

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


Load More