Chapter 3.11: Binary Trees
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
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
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 initializeleft = right = nullptr.
The three traversal orders visit the same tree but in completely different sequences โ each with unique use cases:
3.11.2 Binary Search Trees (BST)
A Binary Search Tree is a binary tree with a key ordering property:
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 โ
3.11.2.1 BST Search
๐ 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:
- Node has no children (leaf): simply remove it
- Node has one child: replace node with its child
- 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:
โ ๏ธ 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).
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:
| Traversal | Order | Use Cases |
|---|---|---|
| Pre-order | Root โ Left โ Right | Copy tree, prefix expressions |
| In-order | Left โ Root โ Right | BST sorted output |
| Post-order | Left โ Right โ Root | Delete tree, postfix expressions |
| Level-order | BFS by level | Shortest 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::setiterates 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;
}
๐ก 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
- Forgetting
nullptrbase case โ causes immediate segfault - Not returning the (possibly new) root after insert/delete โ corrupts tree structure
- Stack overflow โ use iterative traversal when N > 10^5
- Memory leak โ always
deleteremoved nodes (or use smart pointers) - Hand-writing BST when STL set suffices โ use
std::setin contests
Chapter Summary
๐ Key Takeaways
| Concept | Key Point | Time Complexity |
|---|---|---|
| BST Search | Go left/right based on comparison | O(log N) avg, O(N) worst |
| BST Insert | Find correct position, insert at empty spot | O(log N) avg |
| BST Delete | 3 cases: leaf, one child, two children | O(log N) avg |
| In-order | Left โ Root โ Right | O(N) |
| Pre-order | Root โ Left โ Right | O(N) |
| Post-order | Left โ Right โ Root | O(N) |
| Level-order | BFS by level | O(N) |
| Height | max(left height, right height) + 1 | O(N) |
| LCA (brute force) | Find paths then compare | O(N) per query |
| LCA (binary lifting) | Precompute 2^k ancestors | O(N log N) preprocess, O(log N) query |
| Euler Tour | DFS timestamps to flatten tree | O(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::setis backed by a Red-Black tree (balanced BST), guaranteeingO(log N); hand-written BSTs can degenerate toO(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::mapare 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
| Method | Preprocessing | Per Query | Use Case |
|---|---|---|---|
| Naive climbing (ยง3.11.5) | O(N) | O(N) | N โค 5000, simple code |
| Binary Lifting | O(N log N) | O(log N) | N, Q โค 5ร10^5, USACO Gold |
| Euler Tour + RMQ | O(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
| Need | Tool | Complexity after Euler Tour |
|---|---|---|
| Static subtree sum | Prefix sum | O(1) query |
| Dynamic point update + subtree sum | BIT (Fenwick Tree) | O(log N) |
| Range update + subtree query | Segment 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