questions C
This commit is contained in:
198
backend/data/questions/count-good-nodes-in-binary-tree.yaml
Normal file
198
backend/data/questions/count-good-nodes-in-binary-tree.yaml
Normal file
@@ -0,0 +1,198 @@
|
||||
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.
|
||||
Reference in New Issue
Block a user