Count Good Nodes in Binary Tree
A node in a binary tree is good if no node on the path from the root to it has a value greater than it. Count the good nodes.
Open on LeetCode ↗Intuition
The trap is comparing each node only against its immediate PARENT. A node is good when nothing on the ENTIRE root-to-node path beats it, not just the one node directly above -- a node can lose to a grandparent or higher ancestor even while beating its own parent. The fix is to thread the running maximum value seen so far down through the recursion as a parameter, updating it at each step, rather than only looking one level up. A node is good exactly when its value is greater than or equal to that running maximum.
Thread the running maximum down through the recursion. Each node compares against the largest value on its root-to-here path, and passes the updated maximum to its children. Top-down state passing is the counterpart to postorder's bottom-up returns.
Approach
Carry the path max as a recursion parameter
dfs(node, maxSoFar) receives the largest value seen anywhere from the root down to node's parent -- not just the parent's own value.
Compare against the whole path, not the parent
node is good if node.val >= maxSoFar. If so, increment the count. This correctly handles cases where the parent is small but a grandparent was large.
Update the max before recursing into children
Compute newMax = max(maxSoFar, node.val) and pass newMax into both dfs(node.left, newMax) and dfs(node.right, newMax) -- each child sees the true best-so-far, not a stale value.
Solution & live demo
Common pitfalls
Using a shared mutable maximum
self.maxSoFar = max(self.maxSoFar, node.val) dfs(node.left); dfs(node.right)
newMax = max(maxSoFar, node.val) count += dfs(node.left, newMax)
A single field leaks the left subtree's maximum into the right subtree, where those nodes aren't ancestors. Passing the value as a parameter gives each branch its own path history automatically.
Using strict greater-than
count = 1 if node.val > maxSoFar else 0
count = 1 if node.val >= maxSoFar else 0
A node equal to the path maximum still has nothing greater above it, so it qualifies. Strict comparison also fails the root against itself if the initial maximum is the root's own value.
Seeding with zero
return dfs(root, 0)
return dfs(root, float('-inf'))Node values can be negative, so a zero seed disqualifies every negative root. Negative infinity guarantees the root always counts, which it must.
Edge cases
Always good -- its path is just itself, so the initial maxSoFar starts at negative infinity (or the root's own value).
Threading the max (not just parent.val) catches this correctly -- comparing to parent alone would wrongly mark it good.
Every node on that path is good, since each exceeds everything before it.
One good node -- the root.