Whether a node is “good” depends only on the maximum value seen so far along the path from the root — that value can simply be carried down as an extra DFS argument, so each node is checked in O(1) without ever re-walking a path.
Brute Force (re-walk each path)
Time O(n^2)Space O(h)For every node, walk back up to the root (or, equivalently, track the full path down and re-scan it) to check whether any ancestor has a strictly greater value.
class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right
class Solution: def goodNodes(self, root: TreeNode) -> int: def count(node: TreeNode | None, path: list[int]) -> int: if node is None: return 0 is_good = 1 if node.val >= max(path, default=node.val) else 0 new_path = path + [node.val] return is_good + count(node.left, new_path) + count(node.right, new_path)
return count(root, [])Complexity: max(path) re-scans the whole path-so-far at every node, and paths can be O(n) long → O(n^2) time worst case (a skewed tree). Space for the path list plus recursion stack → O(h).
DFS Carrying Running Max
OptimalTime O(n)Space O(h)Pass the maximum value seen so far as a single running argument instead of the whole path. A node is good exactly when its value is at least that running max; the max passed to its children is then max(running_max, node.val).
class Solution: def goodNodes(self, root: TreeNode) -> int: def dfs(node: TreeNode | None, max_so_far: int) -> int: if node is None: return 0 is_good = 1 if node.val >= max_so_far else 0 new_max = max(max_so_far, node.val) return is_good + dfs(node.left, new_max) + dfs(node.right, new_max)
return dfs(root, root.val)Watch the single number that each call carries down: a node is judged against the max on its own root path, and the child receives that max updated with the node’s own value.
Decision at the current node
Start at root 3. Its incoming max is 3, so 3 >= 3 and it counts as good. Both children receive max_so_far = 3.
Complexity: each node does O(1) work with the running max instead of rescanning a path → O(n) time. Space is the recursion stack (the running max is a single O(1) value passed along it) → O(h).