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:
- Building an adjacency list from the given edges to represent the tree.
- 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).
- 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
). - 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:
-
Constructing the Adjacency List:
- We use a dictionary
g
(created usingdefaultdict(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.
- We use a dictionary
-
DFS Function:
- Define a function
dfs(a, fa)
wherea
is the current node andfa
is the parent node. This function calculates the size of the subtree rooted at nodea
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 to1
to represent the current node.ok
is set to1
to signify that the node is good initially.
- Traverse through all neighbors
b
of nodea
. For each neighbor not equal tofa
, recursively compute the size of the subtree usingdfs(b, a)
and add the result tocnt
. - Compare
cur
(the size of the current child's subtree) withpre
. Ifpre
is still-1
, updatepre
tocur
. Ifpre
is not equal tocur
, it means the subtree sizes are not uniform, setok
to0
, indicatinga
is not a good node. - Add the value of
ok
toans
which keeps track of the total number of good nodes.
- Define a function
-
Calculating the Result:
- Initialize
ans
to0
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.
- Initialize
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 EvaluatorExample 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
-
Start the DFS from the root node 0.
dfs(0, -1)
:pre = -1
,cnt = 1
,ok = 1
-
Explore neighbor 1 of node 0:
- Enter
dfs(1, 0)
:pre = -1
,cnt = 1
,ok = 1
- Enter
-
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
- Enter
-
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, sook = 1
. - Return
cnt = 3
for node 1 as it's good andok = 1
.
- Enter
-
Back in
dfs(0, -1)
,cur = 3
,pre = 3
-
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, sook = 0
for node 0.
- Enter
-
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 returns1
.
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.
Which type of traversal does breadth first search do?
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https algomonster s3 us east 2 amazonaws com cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!