A node is good when no larger value exists on the path from the root to that node. The key is to thread a pathMax variable down through the DFS — each node compares itself to that running maximum in O(n) time.
Given a binary tree, count nodes that are good: a node X is good if there is no node with a value greater than X.val on the path from the root down to X (the node itself is included in the path).
Concrete example: tree 3,1,4,3,null,1,5 (level-order). The root 3 is trivially good (nothing above it). Its left child 1 is not good because 3 > 1 is on the path. The right child 4 is good (4 ≥ 3). Left grandchild 3 is good (3 ≥ 3). Right subtree of 4: left child 1 is not good (1 < 4), right child 5 is good (5 ≥ 4). Answer: 4.
pathMax through a DFS — each recursive call passes Math.max(pathMax, node.val) to its children. No extra data structure needed; the call stack is the path.node is null, return 0 — no node, no contribution.node.val >= pathMax means this node qualifies; count it as 1, otherwise 0.newMax = Math.max(pathMax, node.val). Pass newMax — not pathMax — to children, so they see the highest value on the extended path.dfs(root, -Infinity) so the root is always considered good (every finite value ≥ −∞).0 instead of -Infinity. If the root has a negative value like -5, seeding with 0would incorrectly mark it as "not good" — the root is always good by definition. Use -Infinity (or Number.MIN_SAFE_INTEGER for languages without infinity) to guarantee the root passes the check.An explicit stack of { node, pathMax } tuples works identically — useful if the interview asks for an iterative solution or if stack overflow is a concern on very deep trees.
pathMax = -∞ down the tree.1function goodNodes(root: TreeNode | null): number {2 function dfs(node: TreeNode | null, pathMax: number): number {3 if (!node) return 0;45 const isGood = node.val >= pathMax;6 const newMax = Math.max(pathMax, node.val);78 return (isGood ? 1 : 0)9 + dfs(node.left, newMax)10 + dfs(node.right, newMax);11 }1213▶ return dfs(root, -Infinity);14}
function goodNodes(root: TreeNode | null): number {
function dfs(node: TreeNode | null, pathMax: number): number {
if (!node) return 0;
const isGood = node.val >= pathMax;
const newMax = Math.max(pathMax, node.val);
return (isGood ? 1 : 0)
+ dfs(node.left, newMax)
+ dfs(node.right, newMax);
}
return dfs(root, -Infinity);
}dfs. Accepts the current node and pathMax — the largest value seen from the root down to (but not including) this node. Nesting it inside goodNodes keeps the interface clean.null node contributes zero good nodes. This naturally handles empty trees and the children of leaf nodes.node.val to pathMax. Because we seed with -Infinity, the root always passes (any finite number ≥ −∞). The boolean is cast to 1 or 0 via the ternary.newMax is the max the children will see. We pass newMax (not the old pathMax) so children correctly know the highest ancestor value on their extended path. The return value is the sum of this node's count plus both subtree counts — no global state needed.dfs(root, -Infinity) — -Infinity ensures the root is always good regardless of its value.[node, pathMax] tuples onto an explicit stack and process them in a loop — identical logic, no recursion.pathMin and check node.val <= pathMin; seed with Infinity.[min, max] window; this problem threads a single pathMax.function goodNodes(root: TreeNode | null): number {
function dfs(node: TreeNode | null, pathMax: number): number {
if (!node) return 0;
const isGood = node.val >= pathMax;
const newMax = Math.max(pathMax, node.val);
return (isGood ? 1 : 0)
+ dfs(node.left, newMax)
+ dfs(node.right, newMax);
}
return dfs(root, -Infinity);
}Visit every node, and for each one independently search from the root to gather the values on the path down to it, then mark it good if its value is >= the max of that path. No running maximum is threaded down.
function goodNodes(root: TreeNode | null): number {
// Find the path (values) from root down to 'target'; null if not found.
function pathTo(node: TreeNode | null, target: TreeNode, acc: number[]): number[] | null {
if (!node) return null;
acc.push(node.val);
if (node === target) return [...acc];
const left = pathTo(node.left, target, acc);
if (left) { acc.pop(); return left; }
const right = pathTo(node.right, target, acc);
acc.pop();
return right;
}
let good = 0;
function visit(node: TreeNode | null): void {
if (!node) return;
const path = pathTo(root, node, [])!; // O(n) search per node
if (node.val >= Math.max(...path)) good++;
visit(node.left);
visit(node.right);
}
visit(root);
return good;
}O(n) root-to-node search just to recover the path it already sits on, making the whole count O(n²). Threading the running pathMax down the single DFS (the optimal) removes the repeated work for O(n).| "good node" or path-maximum condition on a tree | DFS threading pathMax |
| count nodes satisfying a root-to-node property | preorder DFS with parameter |
| tree + running max/min/sum on path | thread value as DFS argument |
| validate BST / path sum / count good nodes | pass bounds/accumulator into recursive call |
function goodNodes(root: TreeNode | null): number {
function dfs(node: TreeNode | null, pathMax: number): number {
if (!node) return 0;
const isGood = node.val >= pathMax;
const newMax = Math.max(pathMax, node.val);
return (isGood ? 1 : 0)
+ dfs(node.left, newMax)
+ dfs(node.right, newMax);
}
return dfs(root, -Infinity);
}[3,1,4,3,null,1,5] (level-order), what is the count of good nodes?-7: how many good nodes?