๐Ÿ“– Chapter 3.11 โฑ๏ธ ~60 min ๐ŸŽฏ Intermediate

Chapter 3.11: Binary Trees

Prerequisites You should be familiar with: recursion (Chapter 2.3), pointers/structs in C++, and basic graph concepts (adjacency, nodes, edges). This chapter is a prerequisite for Chapter 5.1 (Graph Algorithms) and Chapter 5.3 (Trees & Special Graphs).

Binary trees are the foundation of some of the most important data structures in competitive programming โ€” from Binary Search Trees (BSTs) to Segment Trees to Heaps. A deep understanding of them will make graph algorithms, tree DP, and USACO Gold problems much more approachable.


3.11.1 Binary Tree Basics

A binary tree is a hierarchical data structure where:

  • Each node has at most 2 children: a left child and a right child
  • There is exactly one root node (no parent)
  • Every non-root node has exactly one parent
๐ŸŒณ
Core Terminology
Root โ€” The topmost node (depth 0)
Leaf โ€” A node with no children
Internal node โ€” A node with at least one child
Height โ€” The longest path from root to any leaf
Depth โ€” Distance from root to a given node
Subtree โ€” A node and all its descendants

Diagram

Binary Tree Structure

In this tree:

  • Height = 2 (longest root-to-leaf path: A โ†’ B โ†’ D)
  • Root = A, Leaves = D, E, F
  • B is the parent of D and E; D is B's left child, E is B's right child

C++ Node Definition

This chapter uses a unified struct TreeNode:

๐Ÿ“„ Unified `struct TreeNode` used throughout this chapter:
#include <bits/stdc++.h>
using namespace std;

struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;

    // Constructor: initialize with value, no children
    TreeNode(int v) : val(v), left(nullptr), right(nullptr) {}
};

๐Ÿ’ก Why raw pointers? Competitive programming typically uses manual memory management for speed. nullptr (C++11) is always safer than uninitialized pointers โ€” always initialize left = right = nullptr.

The three traversal orders visit the same tree but in completely different sequences โ€” each with unique use cases:

Binary Tree Traversals


3.11.2 Binary Search Trees (BST)

A Binary Search Tree is a binary tree with a key ordering property:

BST Property
Left subtree < Node < Right subtree
Search
O(log N) average
Insert
O(log N) average
Delete
O(log N) average
Worst Case
O(N)

BST Property: For every node v:

  • All values in the left subtree are strictly less than v.val
  • All values in the right subtree are strictly greater than v.val
       [5]          โ† Valid BST
      /    \
    [3]    [8]
   /   \   /  \
  [1] [4] [7] [10]

  Left of 5 = {1, 3, 4} โ€” all < 5  โœ“
  Right of 5 = {7, 8, 10} โ€” all > 5  โœ“
๐Ÿ“„ View Code: 3.11.2.1 BST Search
// BST Search โ€” O(log N) average, O(N) worst case
// Returns pointer to node with value 'target', or nullptr if not found
TreeNode* search(TreeNode* root, int target) {
    // Base case: empty tree or found target
    if (root == nullptr || root->val == target) {
        return root;
    }
    // BST property: target is smaller, go left
    if (target < root->val) {
        return search(root->left, target);
    }
    // target is larger, go right
    return search(root->right, target);
}

Iterative version (avoids stack overflow on large trees):

// BST Search โ€” iterative version
TreeNode* searchIterative(TreeNode* root, int target) {
    while (root != nullptr) {
        if (target == root->val) return root;       // Found
        else if (target < root->val) root = root->left;   // Go left
        else root = root->right;                     // Go right
    }
    return nullptr;  // Not found
}

3.11.2.2 BST Insert

๐Ÿ“„ View Code: 3.11.2.2 BST Insert
// BST Insert โ€” O(log N) average
// Returns the root of the subtree (possibly new)
TreeNode* insert(TreeNode* root, int val) {
    // Reached an empty spot โ€” create new node here
    if (root == nullptr) {
        return new TreeNode(val);
    }
    if (val < root->val) {
        root->left = insert(root->left, val);   // Recurse left
    } else if (val > root->val) {
        root->right = insert(root->right, val); // Recurse right
    }
    // val == root->val: duplicate, ignore (or handle as needed)
    return root;
}

// Usage:
// TreeNode* root = nullptr;
// root = insert(root, 5);
// root = insert(root, 3);
// root = insert(root, 8);

3.11.2.3 BST Delete

Deletion is the most complex BST operation, with 3 cases:

  1. Node has no children (leaf): simply remove it
  2. Node has one child: replace node with its child
  3. Node has two children: replace with in-order successor (smallest value in right subtree), then delete the successor
๐Ÿ“„ 3. **Node has two children**: replace with **in-order successor** (smallest in right subtree), then delete successor
// BST Delete โ€” O(log N) average
// Helper: find minimum node in subtree
TreeNode* findMin(TreeNode* node) {
    while (node->left != nullptr) node = node->left;
    return node;
}

// Delete node with value 'val' from tree rooted at 'root'
TreeNode* deleteNode(TreeNode* root, int val) {
    if (root == nullptr) return nullptr;  // Value not found

    if (val < root->val) {
        // Case: target is in left subtree
        root->left = deleteNode(root->left, val);
    } else if (val > root->val) {
        // Case: target is in right subtree
        root->right = deleteNode(root->right, val);
    } else {
        // Found the node to delete!

        // Case 1: No children (leaf node)
        if (root->left == nullptr && root->right == nullptr) {
            delete root;
            return nullptr;
        }
        // Case 2A: Only right child
        else if (root->left == nullptr) {
            TreeNode* temp = root->right;
            delete root;
            return temp;
        }
        // Case 2B: Only left child
        else if (root->right == nullptr) {
            TreeNode* temp = root->left;
            delete root;
            return temp;
        }
        // Case 3: Two children โ€” replace with in-order successor
        else {
            TreeNode* successor = findMin(root->right);  // Smallest in right subtree
            root->val = successor->val;                  // Copy successor's value
            root->right = deleteNode(root->right, successor->val);  // Delete successor
        }
    }
    return root;
}

3.11.2.4 BST Degeneration Problem

The diagram below shows BST insertion โ€” the search path follows BST property at each node until an empty spot is found:

BST Insert

BST Insert Step-by-Step Trace

โš ๏ธ Critical Issue: If you insert in sorted order (1, 2, 3, 4, 5...), the BST degenerates into a linked list:

[1]
  \
  [2]
    \
    [3]        โ† O(N) per operation, not O(log N)!
      \
      [4]
        \
        [5]

This is why balanced BSTs (AVL trees, Red-Black trees) exist. In C++, std::set and std::map use Red-Black trees โ€” always guaranteeing O(log N).

AVL Tree Rotations: Left & Right

๐Ÿ”— Key Takeaway: In competitive programming, use std::set / std::map instead of hand-written BSTs. They stay balanced always. Learning BST fundamentals is to understand why they work; use STL in contests (see Chapter 3.8).

3.11.3 Tree Traversals

Traversal = visiting every node exactly once. There are 4 fundamental traversals:

TraversalOrderUse Cases
Pre-orderRoot โ†’ Left โ†’ RightCopy tree, prefix expressions
In-orderLeft โ†’ Root โ†’ RightBST sorted output
Post-orderLeft โ†’ Right โ†’ RootDelete tree, postfix expressions
Level-orderBFS by levelShortest path, level operations

3.11.3.1 Pre-order Traversal

๐Ÿ“„ View Code: 3.11.3.1 Pre-order Traversal
// Pre-order Traversal โ€” O(N) time, O(H) space (H = height)
// Visit order: root, left subtree, right subtree
void preorder(TreeNode* root) {
    if (root == nullptr) return;   // Base case
    cout << root->val << " ";      // Process root first
    preorder(root->left);          // Then left subtree
    preorder(root->right);         // Then right subtree
}

// For tree:    [5]
//             /    \
//           [3]    [8]
//          /   \
//        [1]   [4]
// Pre-order: 5 3 1 4 8

Iterative pre-order (using a stack):

๐Ÿ“„ Full C++ Code
// Pre-order traversal โ€” iterative version
void preorderIterative(TreeNode* root) {
    if (root == nullptr) return;
    stack<TreeNode*> stk;
    stk.push(root);

    while (!stk.empty()) {
        TreeNode* node = stk.top(); stk.pop();
        cout << node->val << " ";    // Process current node

        // Push right first (so left is processed first โ€” LIFO!)
        if (node->right) stk.push(node->right);
        if (node->left)  stk.push(node->left);
    }
}

3.11.3.2 In-order Traversal

๐Ÿ“„ View Code: 3.11.3.2 In-order Traversal
// In-order Traversal โ€” O(N) time
// Visit order: left subtree, root, right subtree
// Key property: In-order traversal of a BST produces sorted output!
void inorder(TreeNode* root) {
    if (root == nullptr) return;
    inorder(root->left);           // Left subtree first
    cout << root->val << " ";      // Then root
    inorder(root->right);          // Then right subtree
}

// For BST (values {1, 3, 4, 5, 8}):
// In-order: 1 3 4 5 8  โ† Sorted! This is the most important BST property

๐Ÿ”‘ Core Insight: In-order traversal of any BST always produces a sorted sequence. This is why std::set iterates in sorted order โ€” it uses in-order traversal internally.

Iterative in-order (slightly more complex):

๐Ÿ“„ Full C++ Code
// In-order traversal โ€” iterative version
void inorderIterative(TreeNode* root) {
    stack<TreeNode*> stk;
    TreeNode* curr = root;

    while (curr != nullptr || !stk.empty()) {
        // Go as far left as possible
        while (curr != nullptr) {
            stk.push(curr);
            curr = curr->left;
        }
        // Process the leftmost unprocessed node
        curr = stk.top(); stk.pop();
        cout << curr->val << " ";

        // Move to right subtree
        curr = curr->right;
    }
}

3.11.3.3 Post-order Traversal

๐Ÿ“„ View Code: 3.11.3.3 Post-order Traversal
// Post-order Traversal โ€” O(N) time
// Visit order: left subtree, right subtree, root
// Used for: deleting trees, evaluating expression trees
void postorder(TreeNode* root) {
    if (root == nullptr) return;
    postorder(root->left);         // Left subtree first
    postorder(root->right);        // Then right subtree
    cout << root->val << " ";      // Root last
}

// โ”€โ”€ Use post-order to free memory โ”€โ”€
void deleteTree(TreeNode* root) {
    if (root == nullptr) return;
    deleteTree(root->left);   // Delete left subtree first
    deleteTree(root->right);  // Then right subtree
    delete root;              // Finally delete this node (safe: children already deleted)
}

3.11.3.4 Level-order Traversal (BFS)

๐Ÿ“„ View Code: 3.11.3.4 Level-order Traversal (BFS)
// Level-order Traversal (BFS) โ€” O(N) time, O(W) space (W = max level width)
// Uses a queue: processes nodes level by level
void levelOrder(TreeNode* root) {
    if (root == nullptr) return;

    queue<TreeNode*> q;
    q.push(root);

    while (!q.empty()) {
        int levelSize = q.size();  // Number of nodes at current level

        for (int i = 0; i < levelSize; i++) {
            TreeNode* node = q.front(); q.pop();
            cout << node->val << " ";

            if (node->left)  q.push(node->left);
            if (node->right) q.push(node->right);
        }
        cout << "\n";  // Newline between levels
    }
}

// For BST [5, 3, 8, 1, 4]:
// Level 0: 5
// Level 1: 3 8
// Level 2: 1 4

Traversal Summary

Tree:           [5]
               /    \
             [3]    [8]
            /   \   /
          [1]  [4] [7]

Pre-order:   5 3 1 4 8 7
In-order:    1 3 4 5 7 8    โ† Sorted!
Post-order:  1 4 3 7 8 5
Level-order: 5 | 3 8 | 1 4 7

3.11.4 Tree Height and Balance

3.11.4.1 Computing Tree Height

๐Ÿ“„ View Code: 3.11.4.1 Computing Tree Height
// Tree Height โ€” O(N) time, O(H) recursive stack space
// Height = length of the longest root-to-leaf path
// Convention: empty tree height = -1, leaf node height = 0
int height(TreeNode* root) {
    if (root == nullptr) return -1;  // Empty subtree height -1

    int leftHeight  = height(root->left);   // Left subtree height
    int rightHeight = height(root->right);  // Right subtree height

    return 1 + max(leftHeight, rightHeight);  // +1 for current node
}

3.11.4.2 Checking Balance

A balanced binary tree requires that the height difference between left and right subtrees of every node is at most 1.

๐Ÿ“„ Full C++ Code
// Check balanced BST โ€” O(N) time
// Returns -1 if unbalanced, otherwise returns subtree height
int checkBalanced(TreeNode* root) {
    if (root == nullptr) return 0;  // Empty tree is balanced, height 0

    int leftH = checkBalanced(root->left);
    if (leftH == -1) return -1;     // Left subtree unbalanced

    int rightH = checkBalanced(root->right);
    if (rightH == -1) return -1;    // Right subtree unbalanced

    // Check current node's balance: height difference at most 1
    if (abs(leftH - rightH) > 1) return -1;  // Unbalanced!

    return 1 + max(leftH, rightH);   // Return height when balanced
}

bool isBalanced(TreeNode* root) {
    return checkBalanced(root) != -1;
}

3.11.4.3 Node Counting

๐Ÿ“„ View Code: 3.11.4.3 Node Counting
// Node count โ€” O(N)
int countNodes(TreeNode* root) {
    if (root == nullptr) return 0;
    return 1 + countNodes(root->left) + countNodes(root->right);
}

// Count leaf nodes specifically
int countLeaves(TreeNode* root) {
    if (root == nullptr) return 0;
    if (root->left == nullptr && root->right == nullptr) return 1;  // Leaf!
    return countLeaves(root->left) + countLeaves(root->right);
}

3.11.5 Lowest Common Ancestor (LCA) โ€” Brute Force

The LCA of two nodes u and v in a rooted tree is their deepest common ancestor.

๐Ÿ“„ The **LCA** of two nodes `u` and `v` in a rooted tree is their deepest common ancestor.
          [1]
         /    \
       [2]    [3]
      /   \      \
    [4]   [5]   [6]
   /
  [7]

LCA(4, 5) = 2     (4 and 5 are both descendants of 2)
LCA(4, 6) = 1     (deepest common ancestor is root 1)
LCA(2, 4) = 2     (node 2 is an ancestor of 4, and also its own ancestor)

O(N) Brute Force LCA

๐Ÿ“„ View Code: O(N) Brute Force LCA
// LCA Brute Force โ€” O(N) per query
// Strategy: find path from root to each node, then find last common node

// Step 1: Find path from root to target node
bool findPath(TreeNode* root, int target, vector<int>& path) {
    if (root == nullptr) return false;

    path.push_back(root->val);  // Add current node to path

    if (root->val == target) return true;  // Found target!

    // Try left subtree first, then right
    if (findPath(root->left, target, path)) return true;
    if (findPath(root->right, target, path)) return true;

    path.pop_back();  // Backtrack: target not in this subtree
    return false;
}

// Step 2: Use two paths to find LCA
int lca(TreeNode* root, int u, int v) {
    vector<int> pathU, pathV;

    findPath(root, u, pathU);   // Path from root to u
    findPath(root, v, pathV);   // Path from root to v

    // Find last common node in both paths
    int result = root->val;
    int minLen = min(pathU.size(), pathV.size());

    for (int i = 0; i < minLen; i++) {
        if (pathU[i] == pathV[i]) {
            result = pathU[i];  // Still common
        } else {
            break;  // Diverged
        }
    }
    return result;
}
Brute Force
O(N) per query
Binary Lifting
O(log N) per query
Build Time
O(N log N)

๐Ÿ’ก USACO Note: For USACO Silver problems, O(N) brute force LCA is not always sufficient. With N โ‰ค 10^5 nodes and Q โ‰ค 10^5 queries, total O(NQ) = O(10^10) โ€” too slow. Only use brute force when N, Q โ‰ค 5000. Chapter 5.3 covers O(log N) binary lifting LCA for harder problems.


3.11.6 Complete BST Implementation

Here is a complete, contest-ready BST:

๐Ÿ“„ Complete contest-ready BST:
#include <bits/stdc++.h>
using namespace std;

struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int v) : val(v), left(nullptr), right(nullptr) {}
};

struct BST {
    TreeNode* root;
    BST() : root(nullptr) {}

    // โ”€โ”€ Insert โ”€โ”€
    TreeNode* _insert(TreeNode* node, int val) {
        if (!node) return new TreeNode(val);
        if (val < node->val) node->left  = _insert(node->left,  val);
        else if (val > node->val) node->right = _insert(node->right, val);
        return node;
    }
    void insert(int val) { root = _insert(root, val); }

    // โ”€โ”€ Search โ”€โ”€
    bool search(int val) {
        TreeNode* curr = root;
        while (curr) {
            if (val == curr->val) return true;
            curr = (val < curr->val) ? curr->left : curr->right;
        }
        return false;
    }

    // โ”€โ”€ In-order traversal (sorted output) โ”€โ”€
    void _inorder(TreeNode* node, vector<int>& result) {
        if (!node) return;
        _inorder(node->left, result);
        result.push_back(node->val);
        _inorder(node->right, result);
    }
    vector<int> getSorted() {
        vector<int> result;
        _inorder(root, result);
        return result;
    }

    // โ”€โ”€ Height โ”€โ”€
    int _height(TreeNode* node) {
        if (!node) return -1;
        return 1 + max(_height(node->left), _height(node->right));
    }
    int height() { return _height(root); }
};

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    BST bst;
    vector<int> vals = {5, 3, 8, 1, 4, 7, 10};
    for (int v : vals) bst.insert(v);

    cout << "Sorted output: ";
    for (int v : bst.getSorted()) cout << v << " ";
    cout << "\n";
    // Output: 1 3 4 5 7 8 10

    cout << "Height: " << bst.height() << "\n";  // 2
    cout << "Search 4: " << bst.search(4) << "\n";  // 1 (true)
    cout << "Search 6: " << bst.search(6) << "\n";  // 0 (false)

    return 0;
}

3.11.7 USACO-Style Practice Problems

Problem: "Cow Family Tree" (USACO Bronze Style)

Problem Statement:

FJ has N cows numbered 1 to N. Cow 1 is the ancestor of all cows (the "root"). For each cow i (2 โ‰ค i โ‰ค N), its parent is parent[i]. The depth of a cow is defined as the number of edges from the root (cow 1) to that cow (cow 1 has depth 0).

Given the tree and M queries, each asking "What is the depth of cow x?"

๐Ÿ“„ Given the tree and M queries, each asking "What is the depth of cow x?"
// Cow Family Tree โ€” Depth Queries
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100005;
vector<int> children[MAXN];  // Adjacency list: children[i] = list of i's children
int depth[MAXN];             // depth[i] = depth of node i

// DFS to compute depths
void dfs(int node, int d) {
    depth[node] = d;
    for (int child : children[node]) {
        dfs(child, d + 1);  // Children have depth +1
    }
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n, m;
    cin >> n >> m;

    for (int i = 2; i <= n; i++) {
        int par;
        cin >> par;
        children[par].push_back(i);  // par is i's parent
    }

    dfs(1, 0);  // Start DFS from root (cow 1) at depth 0

    while (m--) {
        int x;
        cin >> x;
        cout << depth[x] << "\n";
    }

    return 0;
}
// Time: O(N + M)
// Space: O(N)

3.11.8 Reconstructing a Tree from Traversals

Classic problem: given pre-order and in-order traversals, reconstruct the original tree.

Core Insight:

  • The first element of the pre-order array is always the root
  • In the in-order array, the root splits it into left and right subtrees
๐Ÿ“„ Full C++ Code
// Reconstruct tree from pre-order + in-order โ€” O(N^2) naive
TreeNode* build(vector<int>& pre, int preL, int preR,
                vector<int>& in,  int inL,  int inR) {
    if (preL > preR) return nullptr;

    int rootVal = pre[preL];  // First element of pre-order = root
    TreeNode* root = new TreeNode(rootVal);

    // Find root in in-order array
    int rootIdx = inL;
    while (in[rootIdx] != rootVal) rootIdx++;

    int leftSize = rootIdx - inL;  // Number of nodes in left subtree

    // Recursively build left and right subtrees
    root->left  = build(pre, preL+1, preL+leftSize, in, inL, rootIdx-1);
    root->right = build(pre, preL+leftSize+1, preR, in, rootIdx+1, inR);

    return root;
}

TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
    int n = preorder.size();
    return build(preorder, 0, n-1, inorder, 0, n-1);
}

โš ๏ธ Common Mistakes

Null pointer crash:

๐Ÿ“„ Full C++ Code
// โŒ Wrong: No null pointer check!
void inorder(TreeNode* root) {
    inorder(root->left);  // Crashes when root is null
    cout << root->val;
    inorder(root->right);
}

// โœ… Correct: Always check for null first
void inorder(TreeNode* root) {
    if (root == nullptr) return;  // โ† Critical!
    inorder(root->left);
    cout << root->val;
    inorder(root->right);
}

Stack overflow on large inputs:

๐Ÿ“„ Full C++ Code
// โŒ Dangerous: degenerate tree (skewed) with 10^5 nodes
// Recursion depth = 10^5, default stack ~8MB, overflows at ~10^4~10^5!

// โœ… Safe: use iterative for large trees
void dfsIterative(TreeNode* root) {
    stack<TreeNode*> stk;
    if (root) stk.push(root);
    while (!stk.empty()) {
        TreeNode* node = stk.top(); stk.pop();
        process(node);
        if (node->right) stk.push(node->right);
        if (node->left)  stk.push(node->left);
    }
}

Top 5 BST/Tree Bugs

  1. Forgetting nullptr base case โ€” causes immediate segfault
  2. Not returning the (possibly new) root after insert/delete โ€” corrupts tree structure
  3. Stack overflow โ€” use iterative traversal when N > 10^5
  4. Memory leak โ€” always delete removed nodes (or use smart pointers)
  5. Hand-writing BST when STL set suffices โ€” use std::set in contests

Chapter Summary

๐Ÿ“Œ Key Takeaways

ConceptKey PointTime Complexity
BST SearchGo left/right based on comparisonO(log N) avg, O(N) worst
BST InsertFind correct position, insert at empty spotO(log N) avg
BST Delete3 cases: leaf, one child, two childrenO(log N) avg
In-orderLeft โ†’ Root โ†’ RightO(N)
Pre-orderRoot โ†’ Left โ†’ RightO(N)
Post-orderLeft โ†’ Right โ†’ RootO(N)
Level-orderBFS by levelO(N)
Heightmax(left height, right height) + 1O(N)
LCA (brute force)Find paths then compareO(N) per query
LCA (binary lifting)Precompute 2^k ancestorsO(N log N) preprocess, O(log N) query
Euler TourDFS timestamps to flatten treeO(N) preprocess, O(1)~O(log N) subtree query

โ“ FAQ

Q1: When to use BST vs std::set?

A: In competitive programming, almost always use std::set. std::set is backed by a Red-Black tree (balanced BST), guaranteeing O(log N); hand-written BSTs can degenerate to O(N). Only consider hand-writing when you need custom BST behavior (e.g., tracking subtree sizes for "K-th largest" queries), or use __gnu_pbds::tree (policy-based tree).

Q2: What's the relationship between Segment Trees and BSTs?

A: Segment Trees (Chapter 3.9) are complete binary trees, but not BSTs โ€” nodes store interval aggregate values (like range sums), not ordered keys. Both are binary trees with similar structure, but completely different purposes. Understanding BST pointer/recursion patterns makes Segment Tree code easier to understand.

Q3: Which traversal is most commonly used in contests?

A: In-order is most important โ€” it outputs BST values in sorted order. Post-order is common for tree DP (process children before parent). Level-order (BFS) is used for level-by-level processing. Pre-order is less common but useful for tree serialization/deserialization.

Q4: Which is better โ€” recursive or iterative implementation?

A: Recursive code is cleaner and easier to understand (preferred in contests). But with N โ‰ฅ 10^5 and potentially degenerate trees, recursion risks stack overflow (default stack ~8MB, supports ~10^4~10^5 levels). USACO problems typically use non-degenerate trees, so recursion is usually fine; when uncertain, iterative is safer.

Q5: How important is LCA in competitive programming?

A: Very important! LCA is fundamental to tree DP and path queries. Appears occasionally in USACO Silver, almost always in USACO Gold. The brute force LCA in ยง3.11.5 handles N โ‰ค 5000; ยง5.5.1 Binary Lifting LCA handles large trees with N, Q โ‰ค 5ร—10^5, essential for contests.

๐Ÿ”— Connections to Other Chapters

  • Chapter 2.3 (Functions & Arrays): Recursion fundamentals โ€” binary tree traversal is the perfect application of recursion
  • Chapter 3.8 (Maps & Sets): std::set / std::map are backed by balanced BSTs; understanding BSTs helps you use them better
  • Chapter 3.9 (Segment Trees): Segment trees are complete binary trees; the recursive structure of build/query/update is identical to BST traversal
  • Chapter 5.2 (Graph Algorithms): Trees are special undirected graphs (connected, acyclic); all tree algorithms are special cases of graph algorithms
  • ยง5.5.1 LCA Binary Lifting + ยง5.5.2 Euler Tour: Directly build on tree traversals from this chapter, core techniques for Gold level

Practice Problems

Problem 3.11.1 โ€” BST Validation ๐ŸŸข Easy Given a binary tree (not necessarily a BST), determine if it satisfies the BST property.

Hint Common mistake: only checking `root->left->val < root->val` is insufficient. You need to pass down an allowed (minVal, maxVal) range.
โœ… Full Solution

Core Idea: Pass down an allowed (min, max) range; each node must be strictly within its range.

#include <bits/stdc++.h>
using namespace std;
struct TreeNode { int val; TreeNode *left, *right; };

bool isValidBST(TreeNode* root, long long lo, long long hi) {
    if (!root) return true;
    if (root->val <= lo || root->val >= hi) return false;
    return isValidBST(root->left, lo, root->val)
        && isValidBST(root->right, root->val, hi);
}
// Usage: isValidBST(root, LLONG_MIN, LLONG_MAX);

Why do we need min/max bounds? Because a node in the right subtree of the root, even if it's the left child of some ancestor, must still be > root. Passing only the direct parent is insufficient.

Complexity: O(N) time, O(H) recursive stack.


Problem 3.11.2 โ€” K-th Smallest in BST ๐ŸŸข Easy Find the K-th smallest element in a BST.

Hint In-order traversal visits nodes in sorted order; stop when you reach the K-th node.
โœ… Full Solution
int kthSmallest(TreeNode* root, int k) {
    stack<TreeNode*> st;
    TreeNode* cur = root;
    while (cur || !st.empty()) {
        while (cur) { st.push(cur); cur = cur->left; }
        cur = st.top(); st.pop();
        if (--k == 0) return cur->val;
        cur = cur->right;
    }
    return -1;
}

Complexity: O(H + K) โ€” much better than O(N) for small K.


Problem 3.11.3 โ€” Tree Diameter ๐ŸŸก Medium Find the longest path between any two nodes (doesn't need to pass through root).

Hint For each node, the longest path through it = left height + right height. Single DFS: return height while updating global diameter.
โœ… Full Solution

Core Idea: Post-order DFS. Each node computes: (a) its own height for the parent; (b) the best path through it (updates global answer).

int diameter = 0;
int height(TreeNode* root) {
    if (!root) return 0;
    int L = height(root->left);
    int R = height(root->right);
    diameter = max(diameter, L + R);  // Path through this node: L edges left + R edges right
    return 1 + max(L, R);              // Height returned to parent
}
// Answer: diameter (in edges). For node count, diameter+1.

Why does this work? The diameter must pass through some "apex" node โ€” the highest node on the path. That node's contribution = height(left) + height(right). We visit every node as a potential apex.

Complexity: O(N).


Problem 3.11.4 โ€” BST Flatten/Median ๐ŸŸก Medium Given a BST with N nodes, find the median score (the โŒˆN/2โŒ‰-th smallest value).

Hint In-order traversal gives a sorted array; return the element at index (N-1)/2.
โœ… Full Solution
void inorder(TreeNode* root, vector<int>& arr) {
    if (!root) return;
    inorder(root->left, arr);
    arr.push_back(root->val);
    inorder(root->right, arr);
}

int findMedian(TreeNode* root) {
    vector<int> arr;
    inorder(root, arr);
    return arr[(arr.size() - 1) / 2];  // Lower median for even N
}

Optimization for large trees: Use the K-th smallest method from Problem 3.11.2 directly โ€” no need to flatten: kthSmallest(root, (n+1)/2), saves O(N) memory.

Complexity: O(N) time and space (or O(H + N/2) with K-th smallest).


Problem 3.11.5 โ€” Maximum Path Sum ๐Ÿ”ด Hard Nodes may have negative values; find the path between any two nodes with maximum sum.

Hint For each node v: best path through it = max(0, left_max_down) + max(0, right_max_down) + v->val. Clamp negative branches to 0.
โœ… Full Solution

Core Idea: DFS returns "best single-sided path going down from this node". Global answer considers "best double-sided path with this node as apex". Negative sub-paths are clamped to 0 (don't include them).

int bestSum = INT_MIN;
int maxGain(TreeNode* root) {
    if (!root) return 0;
    // Clamp to 0: can choose not to include subtree (if it's negative)
    int L = max(0, maxGain(root->left));
    int R = max(0, maxGain(root->right));

    // Best path with root as turning point
    bestSum = max(bestSum, root->val + L + R);

    // Return single-sided path to parent (can only choose one branch)
    return root->val + max(L, R);
}
// Answer: bestSum after calling maxGain(root)

Key Insight: The path is "V"-shaped โ€” goes up to some apex, then comes down. Each node is considered as apex exactly once.

Complexity: O(N).



5.5.1 Advanced LCA: Binary Lifting O(log N)

This section upgrades the naive LCA from ยง3.11.5 to O(N log N) preprocessing + O(log N) queries, an essential technique for USACO Gold.

Core Idea

Naive LCA climbs up to O(N) steps per query โ€” too slow. Binary lifting precomputes:

anc[v][k] = the 2^k-th ancestor of v

Decomposing N steps into at most log N "jumps", each jumping a power of 2.

Building the anc table:

  • anc[v][0] = direct parent of v (recorded during DFS)
  • anc[v][k] = anc[anc[v][k-1]][k-1] (jumping 2^k = jumping twice by 2^(k-1))

Complete Implementation

๐Ÿ“„ View Code: Complete Binary Lifting LCA Implementation
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 500005, LOG = 20;
vector<int> adj[MAXN];
int depth[MAXN], anc[MAXN][LOG];

// DFS to build tree, computing anc[v][k]
void dfs(int u, int par, int d) {
    depth[u] = d;
    anc[u][0] = par;  // Direct parent
    for (int k = 1; k < LOG; k++)
        anc[u][k] = anc[anc[u][k-1]][k-1];  // Build by doubling
    for (int v : adj[u])
        if (v != par) dfs(v, u, d + 1);
}

// O(log N) LCA query
int lca(int u, int v) {
    // Step 1: Bring the deeper node up to the same depth
    if (depth[u] < depth[v]) swap(u, v);
    int diff = depth[u] - depth[v];
    for (int k = 0; k < LOG; k++)
        if ((diff >> k) & 1) u = anc[u][k];
    
    // Step 2: Both at same depth, jump together until they meet
    if (u == v) return u;  // One was already an ancestor of the other
    for (int k = LOG - 1; k >= 0; k--)
        if (anc[u][k] != anc[v][k]) {
            u = anc[u][k];
            v = anc[v][k];
        }
    return anc[u][0];  // Now u, v's parent is the LCA
}

int main() {
    ios_base::sync_with_stdio(false); cin.tie(NULL);
    int n, q; cin >> n >> q;
    for (int i = 0; i < n - 1; i++) {
        int u, v; cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    dfs(1, 1, 0);  // Root is 1, root's parent is itself
    while (q--) {
        int u, v; cin >> u >> v;
        cout << lca(u, v) << "\n";
    }
    return 0;
}

Key Understanding for Step 2: Enumerate from high bits to low bits; if jumping 2^k still gives different nodes, jump (otherwise might overshoot LCA). Finally u and v stop at direct children of LCA, and anc[u][0] is the LCA.

Complexity Comparison

MethodPreprocessingPer QueryUse Case
Naive climbing (ยง3.11.5)O(N)O(N)N โ‰ค 5000, simple code
Binary LiftingO(N log N)O(log N)N, Q โ‰ค 5ร—10^5, USACO Gold
Euler Tour + RMQO(N log N)O(1)Very high query frequency (beyond contest scope)

5.5.2 Euler Tour (DFS Timestamps)

The Euler Tour "flattens" a tree into a linear array, converting subtree queries into range queries, enabling O(log N) answers using Segment Trees or BITs.

Core Idea

During DFS, record entry time in[u] and exit time out[u] for each node:

          1
         / \
        2   3
       / \
      4   5

DFS order: 1(in=1) โ†’ 2(in=2) โ†’ 4(in=3,out=3) โ†’ 5(in=4,out=4) โ†’ 2(out=4) โ†’ 3(in=5,out=5) โ†’ 1(out=5)

in  = [_, 1, 2, 5, 3, 4]  (entry times for nodes 1~5)
out = [_, 5, 4, 5, 3, 4]  (exit times for nodes 1~5)

Subtree of node 2 = [in[2], out[2]] = [2, 4] = {nodes 2, 4, 5} โœ“

Key Property: The subtree of node u = the contiguous interval [in[u], out[u]] in the Euler Tour array.

๐Ÿ“„ View Code: Complete Euler Tour Implementation
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100005;
vector<int> children[MAXN];
int val[MAXN];
int in_time[MAXN], out_time[MAXN], timer_val = 0;
int euler_arr[MAXN];   // euler_arr[in_time[u]] = val[u]

void dfs_euler(int u, int parent) {
    in_time[u] = ++timer_val;       // Entry: record timestamp
    euler_arr[timer_val] = val[u];  // Record value in flattened array
    
    for (int v : children[u]) {
        if (v != parent) dfs_euler(v, u);
    }
    
    out_time[u] = timer_val;        // Exit: record final timestamp
}

// Query sum of all values in subtree of node u
// Using prefix sum array 'prefix' preprocessed from euler_arr
int subtree_sum(int u, int prefix[]) {
    return prefix[out_time[u]] - prefix[in_time[u] - 1];
}

int main() {
    int n; cin >> n;
    for (int i = 0; i < n - 1; i++) {
        int u, v; cin >> u >> v;
        children[u].push_back(v);
        children[v].push_back(u);
    }
    for (int i = 1; i <= n; i++) cin >> val[i];
    
    dfs_euler(1, -1);  // Start from root 1
    
    // Build prefix sums
    int prefix[MAXN] = {};
    for (int i = 1; i <= n; i++)
        prefix[i] = prefix[i-1] + euler_arr[i];
    
    // Query subtree sum of node u
    int u; cin >> u;
    cout << subtree_sum(u, prefix) << "\n";
    return 0;
}

Practical Applications: Subtree Update + Subtree Query

NeedToolComplexity after Euler Tour
Static subtree sumPrefix sumO(1) query
Dynamic point update + subtree sumBIT (Fenwick Tree)O(log N)
Range update + subtree querySegment Tree (lazy propagation)O(log N)

5.5 Additional Practice Problems

Problem 5.5.1 โ€” Subtree Sum (General Tree) ๐ŸŸข Easy

Problem: Read a rooted tree (root = node 1, N nodes), each node has a value. Output the sum of values in each node's subtree (including itself).

Sample:

Input: 5 nodes, values=[1,2,3,4,5], parent array=[_, 1,1,2,2]
Output: 15 11 3 4 5
(Node 1 subtree sum=1+2+3+4+5=15; Node 2 subtree=2+4+5=11; ...)
โœ… Full Solution

Approach: Post-order DFS, accumulate from leaves upward.

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios_base::sync_with_stdio(false); cin.tie(NULL);
    int n; cin >> n;
    vector<long long> val(n + 1);
    for (int i = 1; i <= n; i++) cin >> val[i];
    
    vector<vector<int>> children(n + 1);
    for (int i = 2; i <= n; i++) {
        int p; cin >> p;
        children[p].push_back(i);
    }
    
    vector<long long> sub(n + 1);
    function<void(int)> dfs = [&](int u) {
        sub[u] = val[u];
        for (int v : children[u]) { dfs(v); sub[u] += sub[v]; }
    };
    dfs(1);
    
    for (int i = 1; i <= n; i++) cout << sub[i] << " \n"[i==n];
    return 0;
}

Complexity: O(N) time and space.


Problem 5.5.2 โ€” Tree Diameter (General Tree, Two BFS) ๐ŸŸก Medium

Problem: Given an unweighted undirected tree with N nodes, find the diameter (length of the longest path between any two nodes).
Note: Problem 3.11.3 only handles binary tree structure. This problem handles general trees (each node can have any number of children).

Sample:

Input: 5
       1 2 / 1 3 / 3 4 / 3 5
Output: 3 (path 2-1-3-4 or 2-1-3-5)
โœ… Full Solution

Approach: Two BFS โ€” first find the farthest node u, then find the diameter from u.

#include <bits/stdc++.h>
using namespace std;

int n;
vector<int> adj[100005];

pair<int,int> bfs_far(int src) {
    vector<int> dist(n + 1, -1);
    queue<int> q;
    dist[src] = 0; q.push(src);
    int far = src;
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int v : adj[u]) {
            if (dist[v] == -1) {
                dist[v] = dist[u] + 1;
                q.push(v);
                if (dist[v] > dist[far]) far = v;
            }
        }
    }
    return {far, dist[far]};
}

int main() {
    ios_base::sync_with_stdio(false); cin.tie(NULL);
    cin >> n;
    for (int i = 0; i < n - 1; i++) {
        int u, v; cin >> u >> v;
        adj[u].push_back(v); adj[v].push_back(u);
    }
    auto [u, _] = bfs_far(1);
    auto [v, d] = bfs_far(u);
    cout << d << "\n";
    return 0;
}

Problem 5.5.3 โ€” LCA Queries (Binary Lifting) ๐ŸŸก Medium

Problem: Given a rooted tree (root is 1, N nodes) and Q queries, each giving two nodes u and v, output their LCA. N, Q โ‰ค 5ร—10^5.

โœ… Full Solution

Use the Binary Lifting LCA implementation from ยง5.5.1:

// See ยง5.5.1 complete implementation (dfs preprocessing + lca query function)
// In main():
int n, q; cin >> n >> q;
// Read tree, dfs(1, 1, 0), then q queries of lca(u, v)

Trace (tree: 1-2-3-4 chain, query lca(4,1)):

depth = [_, 0, 1, 2, 3]
anc[4][0]=3, anc[4][1]=1 (parent of parent of 3), anc[3][0]=2, ...

lca(4, 1): depth[4]=3 > depth[1]=0
  diff=3=0b11, k=0: (diff>>0)&1=1, u=anc[4][0]=3
  k=1: (diff>>1)&1=1, u=anc[3][1]=1
  Now depth[1]=depth[1]=0, u==v=1, return 1 โœ“

Problem 5.5.4 โ€” Euler Tour Subtree Sum (Static) ๐ŸŸก Medium

Problem: Rooted tree with N nodes, each with a value. Q queries, each asking for the sum of all values in the subtree rooted at node u.

โœ… Full Solution

Approach: Build Euler Tour, then use prefix sum array for O(1) per query.

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 100005;
vector<int> adj[MAXN];
long long val[MAXN];
int in_t[MAXN], out_t[MAXN], timer_v = 0;
long long ea[MAXN];  // Euler Tour flattened value array

void dfs(int u, int par) {
    in_t[u] = ++timer_v;
    ea[timer_v] = val[u];
    for (int v : adj[u])
        if (v != par) dfs(v, u);
    out_t[u] = timer_v;
}

int main() {
    ios_base::sync_with_stdio(false); cin.tie(NULL);
    int n, q; cin >> n;
    for (int i = 1; i <= n; i++) cin >> val[i];
    for (int i = 0; i < n - 1; i++) {
        int u, v; cin >> u >> v;
        adj[u].push_back(v); adj[v].push_back(u);
    }
    cin >> q;
    
    dfs(1, -1);
    
    // Prefix sums
    long long prefix[MAXN] = {};
    for (int i = 1; i <= n; i++) prefix[i] = prefix[i-1] + ea[i];
    
    while (q--) {
        int u; cin >> u;
        cout << prefix[out_t[u]] - prefix[in_t[u]-1] << "\n";
    }
    return 0;
}

Why is this correct? The Euler Tour guarantees that subtree nodes of u occupy exactly the interval [in_t[u], out_t[u]], and prefix sums answer range sum queries in O(1).


Problem 5.5.5 โ€” Minimum Spanning Tree (Kruskal) ๐Ÿ”ด Hard

Problem: Read a weighted undirected graph with N nodes and M edges, find the total weight of the minimum spanning tree (output IMPOSSIBLE if not connected).

โœ… Full Solution

Using Kruskal's algorithm (Union-Find covered in Chapter 5.6):

#include <bits/stdc++.h>
using namespace std;

vector<int> par, rnk;
int find(int x) { return par[x]==x ? x : par[x]=find(par[x]); }
bool unite(int x, int y) {
    x=find(x); y=find(y);
    if (x==y) return false;
    if (rnk[x]<rnk[y]) swap(x,y);
    par[y]=x; if(rnk[x]==rnk[y]) rnk[x]++;
    return true;
}

int main() {
    int n, m; cin >> n >> m;
    par.resize(n+1); rnk.assign(n+1,0);
    iota(par.begin(),par.end(),0);
    
    vector<tuple<int,int,int>> edges(m);
    for (auto& [w,u,v] : edges) cin >> u >> v >> w;
    sort(edges.begin(), edges.end());
    
    long long ans = 0; int cnt = 0;
    for (auto [w,u,v] : edges)
        if (unite(u,v)) { ans+=w; if(++cnt==n-1) break; }
    
    cout << (cnt==n-1 ? to_string(ans) : "IMPOSSIBLE") << "\n";
    return 0;
}

End of Chapter 5.5 (Complete Tree Algorithms) โ€” Next: Chapter 5.6: Union-Find