Trees, rooting and basic properties
- Module
- M5.8
- Lesson
- 1 of 1
- Reading time
- 5 min
In this lesson
- Distinguish trees from general graphs and identify their properties.
- Root a tree at a node and compute parent and depth arrays.
- Build order arrays and perform tree traversals.
- Compute tree diameter using two BFS passes.
- Prune leaves iteratively and compute subtree aggregates.
A tree is a connected graph with no cycles. It has nodes and exactly edges. Every pair of nodes has exactly one unique path between them. This simple structure underpins many contest problems.
Unlike general graphs, trees are simpler to work with. You can root a tree at any node and process it recursively or iteratively. Parent-child relationships give you structure. You can compute distances between nodes in linear time. You can prune branches or find the longest path in .
Rooting a tree and parent arrays
To root a tree at a node, run BFS or DFS from that node and treat the traversal as a hierarchy. Each node's first visitor becomes its parent. Depth is the distance from the root.
from collections import deque
def root_tree(adj, root): """Root a tree at a node, compute parent and depth.""" n = len(adj) parent = [-1] * n depth = [-1] * n depth[root] = 0 queue = deque([root])
while queue: u = queue.popleft() for v in adj[u]: if depth[v] == -1: depth[v] = depth[u] + 1 parent[v] = u queue.append(v)
return parent, depth
adj = [ [1, 2], [0, 3, 4], [0, 5], [1], [1], [2]]
parent, depth = root_tree(adj, 0)print("Parent:", parent)print("Depth:", depth)Output
Parent: [-1, 0, 0, 1, 1, 2]
Depth: [0, 1, 1, 2, 2, 2]The parent array lets you trace paths from any node to the root. The depth array shows how far each node is from the root. These arrays are building blocks for more complex algorithms.
Rooting matters because it transforms a tree from a symmetrical, undirected structure into a hierarchical one. Once rooted, you can ask questions like "is X an ancestor of Y?" by checking if you reach X while tracing Y's parent pointers toward the root. You can compute the lowest common ancestor (LCA) of two nodes by finding where their paths to the root meet. These capabilities are essential for tree DP and range-query problems.
Tree diameter: the longest path
The tree diameter is the longest path between any two nodes. Find it with two BFS passes: first from any node to find one end of the diameter, then from that end to find the farthest node.
from collections import deque
def bfs_farthest(adj, start): """BFS from start, return (farthest_node, distance).""" n = len(adj) dist = [-1] * n dist[start] = 0 queue = deque([start]) farthest = start max_dist = 0
while queue: u = queue.popleft() for v in adj[u]: if dist[v] == -1: dist[v] = dist[u] + 1 queue.append(v) if dist[v] > max_dist: max_dist = dist[v] farthest = v
return farthest, max_dist
def tree_diameter(adj): """Find tree diameter using two BFS passes.""" end1, _ = bfs_farthest(adj, 0) end2, diameter = bfs_farthest(adj, end1) return end1, end2, diameter
adj = [ [1, 2], [0, 3], [0, 4, 5], [1], [2], [2]]
end1, end2, diam = tree_diameter(adj)print(f"Diameter endpoints: {end1}, {end2}")print(f"Diameter: {diam}")Output
Diameter endpoints: 3, 4
Diameter: 4This two-pass technique works because in a tree, one end of the diameter must be the farthest node from any starting point. The second pass finds the farthest node from that end, which is the other end of the diameter.
Diameter is useful when you need to know the longest distance between any two nodes in the tree. Some problems ask for the minimum distance you need to add to make all nodes close together, which uses diameter as a starting point. Others ask which nodes lie on the diameter path, which requires retracing the path using parent pointers after finding both ends.
Pruning leaves with a queue
Repeatedly removing leaves shrinks the tree. Use a queue: add all current leaves, remove them, and mark their parents as new leaves if they have degree 1 afterward. This processes the tree layer by layer from the outside in.
from collections import deque
def prune_leaves(adj): """Prune leaves layer by layer.""" n = len(adj) degree = [len(adj[i]) for i in range(n)] queue = deque([i for i in range(n) if degree[i] <= 1]) layers = []
while queue: layer = [] for _ in range(len(queue)): u = queue.popleft() layer.append(u) for v in adj[u]: degree[v] -= 1 if degree[v] == 1: queue.append(v) layers.append(layer)
return layers
adj = [ [1, 2], [0, 3, 4], [0, 5], [1], [1], [2]]
layers = prune_leaves(adj)for i, layer in enumerate(layers): print(f"Layer {i}: {layer}")Output
Layer 0: [3, 4, 5]
Layer 1: [1, 2]
Layer 2: [0]Pruning is useful when you need to find the core of a tree or compute how many edges must be removed to satisfy some property.
Computing subtree aggregates
Often you need a value for each node that depends on its subtree: subtree size, maximum depth, sum of child values. Use postorder DFS to process children before parents.
def compute_subtree_aggregates(adj, root): """Compute subtree size and max depth using postorder DFS.""" n = len(adj) subtree_size = [0] * n max_depth = [0] * n visited = [False] * n
def dfs(u): visited[u] = True subtree_size[u] = 1 max_depth[u] = 0
for v in adj[u]: if not visited[v]: dfs(v) subtree_size[u] += subtree_size[v] max_depth[u] = max(max_depth[u], max_depth[v] + 1)
dfs(root) return subtree_size, max_depth
adj = [ [1, 2], [0, 3, 4], [0, 5], [1], [1], [2]]
size, depth = compute_subtree_aggregates(adj, 0)print("Subtree sizes:", size)print("Max depths:", depth)Output
Subtree sizes: [6, 3, 2, 1, 1, 1]
Max depths: [2, 1, 1, 0, 0, 0]Postorder ensures that when you process a node, its children are already computed. You can then combine their results into the parent's value. This example recurses directly, which is fine on the small tree shown here. A tree with a long chain and a hundred thousand nodes needs the iterative marker technique from the previous module instead, since a recursive postorder DFS can still hit Python's recursion limit.
Why pruning matters
Pruning is used in problems where you need to remove nodes or edges under certain constraints. For example, a problem might say "remove all degree-1 vertices that don't have a certain property". Pruning layer by layer lets you remove these nodes efficiently. Start with all leaves, process them, and their former neighbors may become new leaves. This continues until either no more leaves exist or you have processed enough layers.
The queue-based approach ensures you process all current leaves before moving inward. This is much faster than repeatedly scanning for leaves, which would be in a naive implementation.
A pruning example
Consider a tree where some nodes are marked as "important". You want to remove all unmarked leaves (nodes with degree 1 that are not important). Start by adding all unmarked degree-1 nodes to the queue. For each node removed, check its neighbors. If any become degree-1 and are unmarked, add them. Continue until the queue is empty. The remaining tree contains only marked nodes and paths between them. This takes time because each node is added to the queue at most once.
Cost analysis
All tree algorithms discussed here run in time where is the number of nodes. BFS or DFS visits each node once and each edge twice (once from each direction). Computing parent and depth arrays during rooting is part of the traversal. Pruning adds each node to the queue at most once, so it is also linear. Diameter requires two BFS passes, so . Subtree aggregates in postorder also visit each node once.
The space cost is for the arrays and for the stack or queue, which is at most in the worst case (a chain). These linear costs make tree algorithms efficient even for .
Common mistakes
- Confusing tree diameter with depth: diameter is the longest path between any two nodes. Depth is the distance from the root to one specific node.
- Assuming edges are undirected: trees in contests are often given as undirected. Check that your adjacency list has an entry for both directions of each edge.
- Forgetting to mark the starting node: When doing DFS or BFS to root a tree, mark the root as visited first, or you will revisit it through its parent pointer.
- Integer overflow on large sums: If summing subtree values, use 64-bit integers. With nodes and large weights, sums can exceed 32 bits.
- Off-by-one in parent arrays: After rooting a tree, the root's parent is -1 by convention. Check boundary conditions carefully when tracing paths to the root.
Recap
Trees have edges and unique paths between nodes. Rooting a tree gives parent-child structure. Diameter is found in with two BFS passes. Leaves can be pruned layer by layer. Subtree aggregates are computed in postorder to process children before parents.
Practice
Try these on the judges. Each link opens the problem on WMOJ or DMOJ.
- 2016 S3Phonomenal Reviews (opens on DMOJ in a new tab) DMOJ
Find which subtrees to keep and minimize the total edge count.
Why DMOJ: An older tree problem, solvable with the traversal and pruning techniques in this module, that still makes good practice.
- 2025 S4Floor is Lava (opens on WMOJ in a new tab) WMOJ
A tree-shaped special case is worth partial credit before the full graph problem.