Subtree Queries
Problem Overview
| Attribute | Value |
|---|---|
| Difficulty | Medium |
| Category | Tree Algorithms |
| Time Limit | 1 second |
| Key Technique | Euler Tour + BIT/Segment Tree |
| CSES Link | https://cses.fi/problemset/task/1137 |
Learning Goals
After solving this problem, you will be able to:
- Flatten a tree into an array using Euler tour (DFS order)
- Map subtree operations to range operations on arrays
- Use Binary Indexed Tree (BIT) or Segment Tree for point updates and range queries
- Understand why subtrees form contiguous ranges in Euler tour order
Problem Statement
Problem: Given a rooted tree with n nodes, each node has a value. Process two types of queries:
- Update: Change the value of node s to x
- Query: Find the sum of all values in the subtree rooted at node s
Input:
- Line 1: n q (number of nodes and queries)
- Line 2: v1, v2, …, vn (initial values of nodes)
- Next n-1 lines: edges a b (connecting nodes a and b)
- Next q lines: queries in format “1 s x” (update) or “2 s” (query sum)
Output:
- For each type 2 query, print the sum of values in the subtree
Constraints:
- 1 <= n, q <= 2 * 10^5
- 1 <= vi, x <= 10^9
- Tree is rooted at node 1
Example
Input:
5 3
4 2 5 2 1
1 2
1 3
3 4
3 5
2 3
1 5 3
2 3
Output:
8
10
Explanation:
- Initial tree: Node 1 is root with children 2, 3. Node 3 has children 4, 5.
- Query “2 3”: Subtree of node 3 contains {3, 4, 5} with values {5, 2, 1}. Sum = 8.
- Update “1 5 3”: Change node 5’s value from 1 to 3.
- Query “2 3”: Subtree of node 3 now has values {5, 2, 3}. Sum = 10.
Intuition: How to Think About This Problem
Pattern Recognition
Key Question: How can we convert tree operations into efficient array operations?
The crucial insight is that performing DFS on a tree visits each subtree as a contiguous segment. When we enter a node, all its descendants are visited before we backtrack. This means if we record the entry time of each node during DFS, nodes in the same subtree have consecutive entry times.
Breaking Down the Problem
- What are we looking for? Sum of values in a subtree, with support for updates.
- What information do we have? Tree structure and node values.
- What’s the relationship between input and output? Subtree = contiguous range in DFS order.
Analogies
Think of this problem like organizing a company directory. When you do a depth-first traversal of an org chart, everyone in a department (subtree) appears consecutively. If you want to know the total salary of a department, you just need to sum a contiguous range in your flattened list.
Solution 1: Brute Force
Idea
For each query, perform DFS from the queried node and sum all values in its subtree.
Algorithm
- Build adjacency list from edges
- For each query, run DFS from the query node
- Sum values of all visited nodes
Code
def solve_brute_force(n, q, values, edges, queries):
"""
Brute force: DFS for each query.
Time: O(q * n)
Space: O(n)
"""
from collections import defaultdict
graph = defaultdict(list)
for a, b in edges:
graph[a].append(b)
graph[b].append(a)
def subtree_sum(root, parent=-1):
total = values[root]
for child in graph[root]:
if child != parent:
total += subtree_sum(child, root)
return total
results = []
for query in queries:
if query[0] == 1: # Update
s, x = query[1], query[2]
values[s] = x
else: # Query
s = query[1]
# Need to find parent to avoid going back up
# For simplicity, we root at 1 and track parents
results.append(subtree_sum(s, -1)) # Simplified
return results
Complexity
| Metric | Value | Explanation |
|---|---|---|
| Time | O(q * n) | Each query traverses up to n nodes |
| Space | O(n) | Adjacency list and recursion stack |
Why This Works (But Is Slow)
Correctness is guaranteed because DFS visits exactly the nodes in the subtree. However, with q queries and n nodes, the worst case is O(q * n) = O(4 * 10^10) operations, far too slow.
Solution 2: Optimal Solution (Euler Tour + BIT)
Key Insight
The Trick: Euler tour flattens the tree so each subtree becomes a contiguous range. Use BIT for O(log n) updates and range queries.
Euler Tour Concept
| Term | Meaning |
|---|---|
in_time[v] |
The DFS entry time of node v (0-indexed position in tour) |
out_time[v] |
The DFS exit time of node v (last position in its subtree) |
euler[i] |
The node value at position i in the flattened array |
In plain English: When we DFS from node v, all nodes in v’s subtree are visited between in_time[v] and out_time[v]. So subtree sum = range sum from in_time[v] to out_time[v].
Why Euler Tour Works
Tree: Euler Tour (DFS order):
1 Position: 0 1 2 3 4
/ \ Node: 1 2 3 4 5
2 3 Value: 4 2 5 2 1
/ \
4 5 in_time: 1->0, 2->1, 3->2, 4->3, 5->4
out_time: 1->4, 2->1, 3->4, 4->3, 5->4
Subtree of node 3 = positions [2, 4] = nodes {3, 4, 5}.
Algorithm
- Build tree: Create adjacency list
- Euler tour: DFS to compute in_time and out_time for each node
- Initialize BIT: Store node values in DFS order
- Process queries:
- Update: Point update in BIT at position
in_time[s] - Query: Range sum from
in_time[s]toout_time[s]
- Update: Point update in BIT at position
Dry Run Example
Let’s trace through the example:
Input:
n=5, q=3
values: [4, 2, 5, 2, 1] (1-indexed: node 1=4, node 2=2, etc.)
edges: (1,2), (1,3), (3,4), (3,5)
queries: (2,3), (1,5,3), (2,3)
Step 1: Build Tree (rooted at 1)
1 (val=4)
/ \
2 3 (val=2, val=5)
/ \
4 5 (val=2, val=1)
Step 2: Euler Tour (DFS from node 1)
Visit node 1: in_time[1] = 0
Visit node 2: in_time[2] = 1, out_time[2] = 1
Visit node 3: in_time[3] = 2
Visit node 4: in_time[4] = 3, out_time[4] = 3
Visit node 5: in_time[5] = 4, out_time[5] = 4
out_time[3] = 4
out_time[1] = 4
Euler array (by in_time order):
Position: 0 1 2 3 4
Node: 1 2 3 4 5
Value: 4 2 5 2 1
Summary:
Node | in_time | out_time | Subtree Range
-----|---------|----------|---------------
1 | 0 | 4 | [0, 4]
2 | 1 | 1 | [1, 1]
3 | 2 | 4 | [2, 4]
4 | 3 | 3 | [3, 3]
5 | 4 | 4 | [4, 4]
Step 3: Initialize BIT with values [4, 2, 5, 2, 1]
BIT supports: point update, prefix sum query
Step 4: Process Queries
Query 1: "2 3" (sum of subtree rooted at 3)
Range: [in_time[3], out_time[3]] = [2, 4]
Sum = prefix_sum(4) - prefix_sum(1)
= (4+2+5+2+1) - (4+2)
= 14 - 6 = 8
Output: 8
Query 2: "1 5 3" (update node 5 to value 3)
Position in euler array: in_time[5] = 4
Old value: 1, New value: 3, Delta: +2
BIT.update(4, +2)
Now euler array represents: [4, 2, 5, 2, 3]
Query 3: "2 3" (sum of subtree rooted at 3)
Range: [2, 4]
Sum = prefix_sum(4) - prefix_sum(1)
= (4+2+5+2+3) - (4+2)
= 16 - 6 = 10
Output: 10
Final Output: 8, 10
Visual Diagram
Tree Structure: Euler Tour Mapping:
1(4) Position: 0 1 2 3 4
/ \ | | | | |
2(2) 3(5) Node: 1 2 3 4 5
/ \ Value: 4 2 5 2 1
4(2) 5(1) ^--------------^
| |
Subtree of 1: [0,4] sum=14
^----^
| |
Subtree of 2: [1,1] sum=2
^---------^
| |
Subtree of 3: [2,4] sum=8
Code (Python)
import sys
from collections import defaultdict
input = sys.stdin.readline
def solve():
n, q = map(int, input().split())
values = [0] + list(map(int, input().split())) # 1-indexed
# Build adjacency list
graph = defaultdict(list)
for _ in range(n - 1):
a, b = map(int, input().split())
graph[a].append(b)
graph[b].append(a)
# Euler tour using iterative DFS (avoid recursion limit)
in_time = [0] * (n + 1)
out_time = [0] * (n + 1)
euler = [0] * (n + 1) # euler[i] = value at position i
timer = 0
stack = [(1, -1, False)] # (node, parent, visited)
while stack:
node, parent, visited = stack.pop()
if visited:
out_time[node] = timer - 1
else:
in_time[node] = timer
euler[timer] = values[node]
timer += 1
stack.append((node, parent, True))
for child in graph[node]:
if child != parent:
stack.append((child, node, False))
# Binary Indexed Tree (1-indexed internally)
bit = [0] * (n + 2)
def bit_update(i, delta):
i += 1 # Convert to 1-indexed
while i <= n + 1:
bit[i] += delta
i += i & (-i)
def bit_query(i):
i += 1 # Convert to 1-indexed
total = 0
while i > 0:
total += bit[i]
i -= i & (-i)
return total
def range_sum(l, r):
if l == 0:
return bit_query(r)
return bit_query(r) - bit_query(l - 1)
# Initialize BIT with euler tour values
for i in range(n):
bit_update(i, euler[i])
# Process queries
results = []
for _ in range(q):
query = list(map(int, input().split()))
if query[0] == 1: # Update
s, x = query[1], query[2]
pos = in_time[s]
old_val = euler[pos]
euler[pos] = x
bit_update(pos, x - old_val)
else: # Query subtree sum
s = query[1]
l, r = in_time[s], out_time[s]
results.append(range_sum(l, r))
print('\n'.join(map(str, results)))
solve()
Complexity
| Metric | Value | Explanation |
|---|---|---|
| Time | O((n + q) log n) | Euler tour O(n), each query O(log n) |
| Space | O(n) | BIT, Euler arrays, adjacency list |
Common Mistakes
Mistake 1: Recursion Depth Exceeded
# WRONG - May cause stack overflow for n = 200000
def dfs(node, parent):
in_time[node] = timer
for child in graph[node]:
if child != parent:
dfs(child, node) # Deep recursion
out_time[node] = timer - 1
Problem: Python’s default recursion limit (~1000) is too small.
Fix: Use iterative DFS with explicit stack, or increase limit with sys.setrecursionlimit(300000).
Mistake 2: Wrong BIT Indexing
# WRONG - BIT is 1-indexed but forgetting conversion
def bit_update(i, delta):
while i <= n: # Missing i += 1 for 0-indexed input
bit[i] += delta
i += i & (-i)
Problem: BIT operations assume 1-indexed arrays. Fix: Add 1 to index before BIT operations, or consistently use 1-indexed arrays.
Mistake 3: Integer Overflow
Problem: Values up to 10^9, n up to 210^5, total sum can exceed 210^14.
Fix: Use long long for BIT and sum variables.
Mistake 4: Incorrect out_time Calculation
# WRONG
def dfs(node, parent):
in_time[node] = timer
timer += 1
for child in graph[node]:
if child != parent:
dfs(child, node)
out_time[node] = timer # Should be timer - 1
Problem: out_time should point to the last valid index, not one past it.
Fix: out_time[node] = timer - 1 (inclusive range).
Edge Cases
| Case | Input | Expected Behavior | Why |
|---|---|---|---|
| Single node | n=1 | Subtree of 1 = just node 1’s value | Tree with only root |
| Linear tree | 1-2-3-4-5 | Subtree of 1 = all nodes | Chain tree |
| Star tree | 1 connected to 2,3,4,5 | Subtree of 2 = just node 2 | All leaves |
| Large values | v_i = 10^9 | Use long long | Sum can overflow int |
| Many updates | q updates to same node | Each update O(log n) | BIT handles efficiently |
| Query root | Query subtree of 1 | Sum of all values | Root’s subtree = entire tree |
When to Use This Pattern
Use This Approach When:
- You need subtree queries (sum, min, max, count)
- You need to update individual node values
- The tree structure is static (edges don’t change)
- Multiple queries on the same tree
Don’t Use When:
- Tree structure changes dynamically (use Link-Cut Trees)
- You need path queries instead of subtree queries (use Heavy-Light Decomposition)
- Only one query is needed (simple DFS is enough)
Pattern Recognition Checklist:
- Subtree operation? --> Consider Euler Tour
- Need updates? --> Euler Tour + BIT/Segment Tree
- Static queries only? --> Euler Tour + Prefix Sums
- Path queries? --> Consider HLD or LCA techniques
Related Problems
Easier (Do These First)
| Problem | Why It Helps |
|---|---|
| Subordinates (CSES) | Basic subtree counting with DFS |
| Tree Matching (CSES) | Tree DP fundamentals |
Similar Difficulty
| Problem | Key Difference |
|---|---|
| Path Queries (CSES) | Path sum instead of subtree sum |
| Range Update Queries (CSES) | BIT with lazy propagation |
| Distinct Colors (CSES) | Subtree distinct count using small-to-large |
Harder (Do These After)
| Problem | New Concept |
|---|---|
| Path Queries II (CSES) | Heavy-Light Decomposition |
| Subtree Queries with XOR | Euler tour with segment tree and XOR |
| Tree and Queries | Mo’s algorithm on trees |
Key Takeaways
- The Core Idea: Euler tour converts subtree operations to range operations
- Time Optimization: BIT provides O(log n) updates and queries vs O(n) brute force
- Space Trade-off: O(n) extra space for BIT enables efficient queries
- Pattern: Euler Tour + Data Structure = Efficient Tree Queries
Practice Checklist
Before moving on, make sure you can:
- Implement Euler tour without looking at reference
- Explain why subtrees become contiguous ranges
- Write BIT from scratch with update and query
- Handle 0-indexed vs 1-indexed correctly
- Identify when Euler tour applies to a tree problem
Additional Resources
- CP-Algorithms: Euler Tour Technique
- CP-Algorithms: Binary Indexed Tree
- CSES Subtree Queries - Euler tour with range queries
- USACO Guide: Tree Euler Tour