2Brute2Best
πŸ”₯0

Tree DFS Pattern: Algorithmic Intuition, Sub-patterns & C++ Guide

Solve a tree by asking each child for an answer (recursion), then combine. Decide what each call returns and what it receives.

6 sub-patterns Β· 26 questions

Remember this Β· Tree DFS
Trigger
Paths, heights, diameters, validating structure, ancestors, anything "for every subtree".
Think
If my left and right children already gave me their answers, how do I build mine?
Key question
What does my recursive function return, and what do I pass down?
A

What is this pattern?

Tree DFS goes as deep as possible down one branch before coming back, usually with recursion. Almost every tree question is solved by writing one small function and trusting it on the two subtrees: "if f already works for my left child and my right child, how do I get my own answer?"

The whole pattern is deciding which way the information flows:

  • Top-down: the parent passes something down as a parameter (the sum so far, the max on the path, the allowed range in a BST). Leaves check it.
  • Bottom-up: each child returns a summary of its subtree (height, best path, "did I find p?"). The parent combines the two, and a global best is updated along the way.

Every node is visited once, so it is O(n) time. The recursion stack holds one root-to-node path: O(h) space, where h is the height (log n when balanced, n when the tree is a chain).

A manager asking a team for a report. Top-down: the manager hands each team lead a budget ("you have 12 left"), and each lead passes what's left to their own team. Bottom-up: every team lead first collects their team's totals and sends one number up. The manager never looks at individual workers, only at the two numbers from the two leads.

B

See it move

Blue = the current path, red = finished (left the path), purple = a leaf whose path hits the target. The note under a node is the sum from the root down to it.

Top-down: carry the path sum to the leavesroot = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22step 1/15
tree
54117281341
State
target
22
path
[ ]
Top-down: pass the sum to the children

Each call receives the sum of the path above it. The state flows from the root down, and each node only adds its own value.

Blue = combining now, green = finished (note = height it returned), purple = where the best path bends.

Bottom-up: return height, update the bestroot = [1,2,null,4,5,8,null,null,6,null,null,null,7]step 1/9
tree
1248567
Combine
best
0
Bottom-up: children answer first

A node can't know its height until both children have told it theirs, so the work happens on the way back up (post-order). The note under each finished node is the height it returned.

C

How do I recognise it?

  • "Maximum depth", "height", "diameter", "balanced": answers built from both subtrees.

  • "Root-to-leaf path" with a sum, digits, a max so far, or a direction: carry it down.

  • "Any path" / "maximum path sum": each node returns its best one-sided path; the best two-sided path is a global.

  • A binary search tree: left < node < right. Search one side; inorder is sorted; pass a valid range down.

  • "Lowest common ancestor": the first node where the two targets end up on different sides.

  • Compare, invert, flatten, build, serialize a tree: a recursive walk that handles one node and trusts the recursion for the rest.

Looks like Tree DFS, but isn't

Anything "per level", "right side view", "minimum depth"
β†’ Tree BFS

The answer is grouped by depth, and BFS visits depth by depth. Minimum depth also lets BFS stop at the first leaf instead of exploring every path.

Count paths with sum K that can start anywhere (Path Sum III)
β†’ Prefix Sum (on the root-to-node path)

A path that starts anywhere on the way down is a subarray of the current root-to-node path. A prefix-sum hash map counts them in O(n), instead of starting a DFS from every node.

Choose nodes with a rule between neighbours (House Robber III)
β†’ Tree DFS, returning a small DP pair

It is still bottom-up DFS, but each node returns two answers (take it / skip it), like a DP state, so no node is solved twice.

Many words searched by their prefixes
β†’ Trie

A trie is a tree you build from the words. The DFS happens on it, but the idea is sharing common prefixes.

D

How would I discover it from scratch?

Don't memorise β€œuse tree dfs”. Ask these questions in order β€” the pattern falls out.

Pretend the recursion already works for both children. What do you need from them, or what do they need from you? "My height = 1 + max of theirs" is bottom-up. "My remaining sum = what my parent had βˆ’ my value" is top-down.

E

Sub-patterns

Same core idea, different rule for β€œvalid”. Learn them in this order.

F

Don't mix these up

Top-down (parameter) vs Bottom-up (return value)

Does a node need something from above (the sum so far, the max on the path, a BST range)? Pass it down. Does it need something from below (heights, best path, found or not)? Return it up. Some questions use both: Count Good Nodes passes the max down and returns the count up.

The return value vs The global answer

When the best path can bend at any node, a node returns only what its parent can extend (one side). The two-sided value is compared with a global at every node. Returning both sides upward would build a path that forks.

Check at a leaf vs Check at null

A root-to-leaf path ends at a node with no children. Checking at null treats the empty side of a one-child node as a path end: Path Sum gets a false hit, and Minimum Depth (the min version) returns 1 on a chain.

BST: range from all ancestors vs Compare with children only

[5,4,6,null,null,3,7] passes every parent-child check, but 3 sits in 5's right subtree. Each node must be inside the range from every ancestor, which the (low, high) parameters carry.

LCA in a BST (compare values) vs LCA in a binary tree (search both sides)

The value rule only works because a BST is ordered. On a plain binary tree the values say nothing about where p and q are, so you need the post-order "what did each side find" search.

G

Code templates (C++)

Traversals and walking two trees

Pre-, in- and post-order differ only in where you handle the node. The iterative inorder goes left as far as it can, pops, visits, then goes right. Two trees: compare the pair, then recurse on matching children.

traversal-template.cpp
// Recursive DFS: the three orders differ only in WHERE you handle the node.
void dfs(TreeNode* node, vector<int>& out) {
    if (!node) return;
    // preorder:  out.push_back(node->val);   (node before its children)
    dfs(node->left, out);
    out.push_back(node->val);                 // inorder: between the two children
    dfs(node->right, out);
    // postorder: out.push_back(node->val);  (node after both children)
}

// Iterative inorder with your own stack: go left as far as possible, then visit, then go right.
vector<int> inorderIterative(TreeNode* root) {
    vector<int> out;
    stack<TreeNode*> st;
    TreeNode* cur = root;
    while (cur || !st.empty()) {
        while (cur) { st.push(cur); cur = cur->left; }   // everything on the way down waits its turn
        cur = st.top(); st.pop();
        out.push_back(cur->val);                         // its whole left side is already done
        cur = cur->right;
    }
    return out;
}

// Two trees at once: compare the pair, then recurse on matching pairs of children.
bool isSameTree(TreeNode* p, TreeNode* q) {
    if (!p || !q) return p == q;                         // both null β†’ same; one null β†’ different
    return p->val == q->val && isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}

Top-down: carry the path state

Pass the state as a parameter; check at a leaf. A shared path vector is pushed on entry and popped on exit, so siblings never see each other's nodes.

path-state-template.cpp
// Top-down: carry the path's state as a parameter; check it at the leaves.
// Here: every root-to-leaf path whose sum is target (Path Sum II).
void walk(TreeNode* node, int remaining, vector<int>& path, vector<vector<int>>& out) {
    if (!node) return;
    path.push_back(node->val);                           // enter: add this node to the path
    remaining -= node->val;
    if (!node->left && !node->right) {                   // a leaf: the path is complete
        if (remaining == 0) out.push_back(path);
    } else {
        walk(node->left, remaining, path, out);
        walk(node->right, remaining, path, out);
    }
    path.pop_back();                                     // leave: undo, so the parent's path is clean
}

vector<vector<int>> pathSum(TreeNode* root, int target) {
    vector<vector<int>> out;
    vector<int> path;
    walk(root, target, path, out);
    return out;
}

Bottom-up: return a summary, update a global

Each call returns what its parent can extend (one side only). The best answer bends at some node and uses both sides, so it is kept in a separate global.

subtree-answer-template.cpp
// Bottom-up: each call returns a summary of its subtree; the global answer is
// updated while combining the two children. Here: diameter (longest path, in edges).
int best = 0;

int height(TreeNode* node) {                             // returns: nodes on the longest downward path
    if (!node) return 0;
    int lh = height(node->left);
    int rh = height(node->right);
    best = max(best, lh + rh);                           // the longest path that bends at this node
    return 1 + max(lh, rh);                              // a parent can only extend ONE side
}

int diameterOfBinaryTree(TreeNode* root) {
    best = 0;
    height(root);
    return best;
}

BST: range check and one-sided search

Every node must sit inside (low, high) from its ancestors. Use long long bounds so values equal to INT_MIN / INT_MAX still pass.

bst-range-template.cpp
// BST: every node must fit inside the (low, high) range set by its ancestors.
// long long bounds so a node equal to INT_MIN or INT_MAX still fits.
bool valid(TreeNode* node, long long low, long long high) {
    if (!node) return true;
    if (node->val <= low || node->val >= high) return false;
    return valid(node->left, low, node->val)             // left side: everything below node->val
        && valid(node->right, node->val, high);          // right side: everything above node->val
}

bool isValidBST(TreeNode* root) { return valid(root, LLONG_MIN, LLONG_MAX); }

// Searching only needs one side: the comparison tells you where the value must be.
TreeNode* searchBST(TreeNode* root, int target) {
    while (root && root->val != target)
        root = target < root->val ? root->left : root->right;
    return root;
}

Lowest common ancestor in one pass

Return the node itself if it is p or q; if both sides return something, this node is where they split. In a BST, compare values instead: both smaller β†’ left, both larger β†’ right.

lca-template.cpp
// Lowest common ancestor in a plain binary tree, in one post-order pass.
// Each call returns p or q if it found one below (or is one), else nullptr.
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
    if (!root || root == p || root == q) return root;
    TreeNode* left = lowestCommonAncestor(root->left, p, q);
    TreeNode* right = lowestCommonAncestor(root->right, p, q);
    if (left && right) return root;                      // p and q split here: this is the LCA
    return left ? left : right;                          // pass up whichever side found something
}

Build from preorder + inorder

Preorder gives the next root; its index in inorder (from a hash map) splits the two subtrees. Build the left subtree first, because preorder lists root, left, right.

build-template.cpp
// Build from preorder + inorder: preorder gives the next root, its position
// in inorder splits the left and right subtrees.
TreeNode* build(vector<int>& pre, int& next, int lo, int hi, unordered_map<int, int>& pos) {
    if (lo > hi) return nullptr;                         // empty inorder range β†’ no subtree
    int val = pre[next++];
    TreeNode* node = new TreeNode(val);
    int mid = pos[val];
    node->left = build(pre, next, lo, mid - 1, pos);     // left first: preorder is root, left, right
    node->right = build(pre, next, mid + 1, hi, pos);
    return node;
}

TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
    unordered_map<int, int> pos;                         // value β†’ index in inorder (values are unique)
    for (int i = 0; i < (int)inorder.size(); i++) pos[inorder[i]] = i;
    int next = 0;
    return build(preorder, next, 0, (int)inorder.size() - 1, pos);
}