title: Count Good Nodes in Binary Tree slug: count-good-nodes-in-binary-tree difficulty: medium leetcode_id: 1448 leetcode_url: https://leetcode.com/problems/count-good-nodes-in-binary-tree/ categories: - trees patterns: - dfs - tree-traversal description: | Given a binary tree `root`, a node *X* in the tree is named **good** if in the path from root to *X* there are no nodes with a value *greater than* X. Return the number of **good** nodes in the binary tree. constraints: | - The number of nodes in the binary tree is in the range `[1, 10^5]` - Each node's value is between `[-10^4, 10^4]` examples: - input: "root = [3,1,4,3,null,1,5]" output: "4" explanation: "Root node (3) is always good. Node 4 -> path (3,4) has max 4. Node 5 -> path (3,4,5) has max 5. Node 3 -> path (3,1,3) has max 3. These 4 nodes are good." - input: "root = [3,3,null,4,2]" output: "3" explanation: "Node 2 -> path (3,3,2) is not good because 3 is greater than 2." - input: "root = [1]" output: "1" explanation: "The root is always considered good." explanation: intuition: | Imagine you're hiking down a mountain with multiple branching paths. At each point along a path, you record the highest altitude you've reached so far. A node is "good" if its altitude is at least as high as the highest point you've seen on the path leading to it. The key insight is that **you need to track the maximum value seen along the path from the root to the current node**. As you traverse the tree, you carry this "path maximum" with you. When you arrive at a node, you simply compare: is this node's value greater than or equal to the maximum seen so far? Think of it like this: the root is always good (there's no ancestor to compare against). As you move down, each node either meets or exceeds the current path maximum (making it good) or falls short. The path maximum only increases as you descend — it never decreases because we're tracking the *maximum* value seen. This naturally suggests a **depth-first search (DFS)** where we pass the current path maximum down to each child. The traversal order doesn't matter (preorder, inorder, postorder all work) because we're counting nodes, not processing them in any specific order. approach: | We solve this using a **DFS with Path Maximum Tracking**: **Step 1: Define the recursive function** - Create a helper function `dfs(node, max_so_far)` that takes the current node and the maximum value seen on the path from root to this node - The function returns the count of good nodes in the subtree rooted at `node`   **Step 2: Handle the base case** - If `node` is `None`, return `0` — there are no good nodes in an empty subtree   **Step 3: Check if current node is good** - Compare `node.val` with `max_so_far` - If `node.val >= max_so_far`, this node is good — add `1` to our count - Update `max_so_far` to be the maximum of itself and `node.val` for child traversals   **Step 4: Recursively count good nodes in subtrees** - Call `dfs(node.left, max_so_far)` to count good nodes in the left subtree - Call `dfs(node.right, max_so_far)` to count good nodes in the right subtree - Add both counts to the current node's count (0 or 1)   **Step 5: Start the traversal** - Call `dfs(root, root.val)` — the root is always good since there's no ancestor with a greater value - Alternatively, start with `dfs(root, float('-inf'))` so the root naturally qualifies as good common_pitfalls: - title: Modifying Path Maximum Incorrectly description: | A common mistake is updating `max_so_far` before checking if the current node is good. For example: ```python max_so_far = max(max_so_far, node.val) # Wrong order! if node.val >= max_so_far: # Always true now ``` You should check if the node is good **first**, then update the maximum for child calls. wrong_approach: "Update max before checking if node is good" correct_approach: "Check if good first, then update max for children" - title: Not Passing Updated Maximum to Children description: | When a node has a higher value than the current path maximum, you must pass this new maximum to its children. Forgetting to update means children might incorrectly be marked as good. For instance, with path `[3, 5, 4]`, if you don't update the max at node 5, node 4 would compare against 3 instead of 5 and incorrectly be marked good. wrong_approach: "Pass the same max_so_far to all children regardless of current node" correct_approach: "Pass max(max_so_far, node.val) to children" - title: Forgetting the Root is Always Good description: | The root node has no ancestors, so by definition there are no nodes with values greater than it on its path. The root is always good. Initialize with `max_so_far = float('-inf')` or `max_so_far = root.val` to ensure the root counts as good. Don't start with an arbitrary value like `0` which could incorrectly exclude negative root values. wrong_approach: "Starting with max_so_far = 0" correct_approach: "Start with float('-inf') or root.val" key_takeaways: - "**Path-based problems need state passing**: When a condition depends on the path from root to node, pass relevant state (like max/min/sum) down through recursion" - "**DFS naturally handles paths**: Unlike BFS which explores level-by-level, DFS follows paths from root to leaf, making it ideal for path-based conditions" - "**Order of operations matters**: Check conditions before updating state to avoid logical errors" - "**Foundation for similar problems**: This pattern extends to problems like 'Longest Univalue Path', 'Binary Tree Maximum Path Sum', and 'Path Sum' variants" time_complexity: "O(n). We visit each node exactly once during the DFS traversal, where `n` is the number of nodes in the tree." space_complexity: "O(h). The recursion stack can grow up to the height of the tree `h`. In the worst case (skewed tree), this is O(n). For a balanced tree, it's O(log n)." solutions: - approach_name: DFS with Path Maximum is_optimal: true code: | class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def good_nodes(root: TreeNode) -> int: def dfs(node: TreeNode, max_so_far: int) -> int: # Base case: empty node contributes 0 good nodes if not node: return 0 # Check if current node is good good = 1 if node.val >= max_so_far else 0 # Update the path maximum for children new_max = max(max_so_far, node.val) # Count good nodes in both subtrees left_count = dfs(node.left, new_max) right_count = dfs(node.right, new_max) return good + left_count + right_count # Start DFS from root with negative infinity # so root is always considered good return dfs(root, float('-inf')) explanation: | **Time Complexity:** O(n) — We visit each node exactly once. **Space Complexity:** O(h) — Recursion stack depth equals tree height. We traverse the tree using DFS, carrying the maximum value seen on the path from root. At each node, we check if it's good (value >= path max), then recursively process children with the updated maximum. The count bubbles up through the recursion. - approach_name: BFS with Path Maximum is_optimal: false code: | from collections import deque class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def good_nodes(root: TreeNode) -> int: if not root: return 0 count = 0 # Queue stores (node, max_value_on_path_to_node) queue = deque([(root, float('-inf'))]) while queue: node, max_so_far = queue.popleft() # Check if current node is good if node.val >= max_so_far: count += 1 # Update max for children new_max = max(max_so_far, node.val) # Add children to queue with updated max if node.left: queue.append((node.left, new_max)) if node.right: queue.append((node.right, new_max)) return count explanation: | **Time Complexity:** O(n) — We visit each node exactly once. **Space Complexity:** O(w) — Queue size equals maximum width of tree. For a complete binary tree, this is O(n/2) = O(n). BFS traverses level-by-level, but we still track the path maximum by storing it alongside each node in the queue. This approach uses iterative traversal instead of recursion, which can be preferable for very deep trees to avoid stack overflow.