8.10 Subtree Queries

Many tree problems ask questions about an entire subtree rather than an individual node.

8.10 Subtree Queries

Many tree problems ask questions about an entire subtree rather than an individual node.

Examples include:

  • What is the sum of all values in this subtree?
  • How many descendants does this node have?
  • What is the maximum value below this node?
  • How many active users belong to this organizational branch?
  • What is the total storage used by a directory and its descendants?

A naive solution traverses the subtree every time a query arrives. This works for small trees, but quickly becomes impractical when the number of queries grows.

The key observation is that subtrees have structure. Once that structure is exploited, many subtree queries become simple range queries on arrays.

This recipe shows how to preprocess a tree so that subtree queries can be answered efficiently.

Problem

You need to answer repeated queries over entire subtrees.

A straightforward DFS per query is too expensive.

Solution

Flatten the tree using an Euler Tour and compute subtree intervals.

Consider this tree:

          0
        /   \\n       1     2
      / \   / \\n     3   4 5   6

Assign values:

Node Value
0 10
1 5
2 8
3 3
4 4
5 6
6 2

Perform a DFS.

package main

var timer int

func flatten(
    tree [][]int,
    node int,
    tin []int,
    size []int,
    order []int,
) {
    tin[node] = timer
    order[timer] = node
    timer++

    size[node] = 1

    for _, child := range tree[node] {
        flatten(
            tree,
            child,
            tin,
            size,
            order,
        )

        size[node] += size[child]
    }
}

Possible DFS order:

0 1 3 4 2 5 6

Subtree ranges become:

Node Range
0 [0,6]
1 [1,3]
2 [4,6]
3 [2,2]
4 [3,3]
5 [5,5]
6 [6,6]

Every subtree becomes a contiguous interval.

Discussion

The Euler Tour creates an important invariant:

Every descendant of a node appears in a consecutive block.

For node 1:

1
├── 3
└── 4

The DFS order is:

1 3 4

These elements occupy:

[1,3]

in the flattened array.

Therefore:

Subtree Query

becomes:

Range Query

This transformation allows powerful array-based data structures to be used on trees.

Subtree Size Queries

The simplest subtree query is subtree size.

The DFS already computes it:

size[node] += size[child]

For the sample tree:

Node Subtree Size
0 7
1 3
2 3
3 1
4 1
5 1
6 1

Query complexity:

O(1)

No traversal is required after preprocessing.

Subtree Sum Queries

Suppose the flattened order contains:

Index  Node  Value

0      0     10
1      1      5
2      3      3
3      4      4
4      2      8
5      5      6
6      6      2

Build prefix sums.

func prefixSums(values []int) []int {
    prefix := make([]int, len(values)+1)

    for i := range values {
        prefix[i+1] =
            prefix[i] +
            values[i]
    }

    return prefix
}

Range sum:

func rangeSum(
    prefix []int,
    left int,
    right int,
) int {
    return prefix[right+1] - prefix[left]
}

Subtree of node 1:

Range = [1,3]

Calculation:

5 + 3 + 4 = 12

Query time:

O(1)

after preprocessing.

Dynamic Updates

Prefix sums work only when values remain unchanged.

Suppose:

Node 4 += 100

Rebuilding prefix sums after every update becomes expensive.

A Fenwick Tree solves this problem.

type Fenwick struct {
    tree []int
}

Operations:

Point update
Range sum

both run in:

O(log n)

The subtree range:

[tin[node],
 tin[node]+size[node]-1]

becomes a Fenwick Tree query.

The tree disappears from the algorithm.

Only intervals remain.

Subtree Maximum Queries

Replace sums with maximum values.

A Segment Tree becomes appropriate.

Suppose:

Maximum value in subtree(1)

The subtree interval remains:

[1,3]

The Segment Tree answers:

max(5,3,4)

Result:

5

Query complexity:

O(log n)

Update complexity:

O(log n)

The underlying tree structure never needs to be traversed again.

Counting Active Nodes

Consider a user hierarchy.

CEO
├── Team A
└── Team B

Each node contains:

1 = active
0 = inactive

The question:

How many active users
exist below manager X?

becomes:

Sum over subtree interval.

The solution is identical to subtree sum queries.

Many real-world hierarchy systems use exactly this pattern.

Subtree Updates

Sometimes an entire subtree must change.

Example:

Increase all salaries
in a department by 1000.

After Euler Tour:

Update interval.

Instead of:

Visit every descendant.

A Segment Tree with Lazy Propagation performs:

Range update

in:

O(log n)

This technique scales to very large trees.

Offline Subtree Queries

Suppose updates never occur.

In that case, preprocessing can answer many queries immediately.

Examples:

Subtree size
Subtree sum
Subtree minimum
Subtree maximum

The DFS computes everything once.

Future queries become simple table lookups.

Many production systems use this approach when the hierarchy changes infrequently but queries are frequent.

Aggregation During DFS

Not every subtree query requires Euler Tour.

Some values can be computed directly during DFS.

Subtree sums:

func subtreeSum(
    tree [][]int,
    values []int,
    node int,
    sums []int,
) {
    sums[node] = values[node]

    for _, child := range tree[node] {
        subtreeSum(
            tree,
            values,
            child,
            sums,
        )

        sums[node] += sums[child]
    }
}

The recurrence is:

node value
+
sum of child subtrees

After preprocessing:

sums[node]

contains the answer.

Query complexity:

O(1)

This is often simpler than building additional data structures.

Subtree Query Design Pattern

Many subtree algorithms follow the same workflow.

Step 1

Flatten the tree.

Tree
→
Array

Step 2

Identify subtree interval.

[start,end]

Step 3

Choose an interval data structure.

Examples:

Query Type Structure
Sum Prefix Sum
Dynamic Sum Fenwick Tree
Min/Max Segment Tree
Updates Lazy Segment Tree

Step 4

Answer queries on intervals instead of traversing trees.

This pattern appears repeatedly throughout advanced tree algorithms.

Complexity Analysis

Euler Tour preprocessing:

Operation Complexity
DFS flattening O(n)
Subtree sizes O(n)
Entry times O(n)

Prefix sums:

Operation Complexity
Build O(n)
Query O(1)

Fenwick Tree:

Operation Complexity
Update O(log n)
Query O(log n)

Segment Tree:

Operation Complexity
Update O(log n)
Query O(log n)

The choice depends on update frequency and query type.

Common Mistakes

A common mistake is recomputing subtree values for every query.

For:

q queries

this often leads to:

O(nq)

behavior.

Another mistake is forgetting:

end =
tin[node] + size[node] - 1

The missing subtraction creates interval errors.

A third mistake is using a Fenwick Tree for operations that require minimum or maximum values. Fenwick Trees work naturally with invertible operations such as sums, but not with arbitrary range aggregates.

Finally, some implementations flatten the tree correctly but forget to reorder node values into Euler order. The resulting intervals become meaningless.

Takeaway

Subtree queries become efficient when the tree is flattened into an array. Euler Tour converts every subtree into a contiguous interval, allowing array-based data structures to replace repeated tree traversals. Once this transformation is understood, a wide range of hierarchy, aggregation, and update problems reduce to familiar range-query techniques. This idea forms the foundation for many advanced tree algorithms and large-scale hierarchical systems.