LeetCode 2445 - Number of Nodes With Value One
The tree in this problem is not given explicitly through an edge list. Instead, the structure is defined mathematically. Every node v has parent floor(v / 2), which creates the same structure as a binary heap.
Difficulty: 🟡 Medium
Topics: Array, Tree, Depth-First Search, Breadth-First Search, Binary Tree
Solution
Problem Understanding
The tree in this problem is not given explicitly through an edge list. Instead, the structure is defined mathematically. Every node v has parent floor(v / 2), which creates the same structure as a binary heap.
For example:
- Node
2and node3are children of node1 - Node
4and node5are children of node2 - Node
6and node7are children of node3
The tree contains nodes labeled from 1 to n.
Initially, every node stores the value 0.
Each query tells us to flip every value inside the subtree rooted at a specific node. Flipping means:
0 -> 11 -> 0
After processing all queries, we must return how many nodes contain value 1.
The key detail is that queries affect entire subtrees, not just individual nodes. Since a node may belong to multiple queried subtrees, its value depends on how many times it was flipped:
- flipped even number of times -> final value
0 - flipped odd number of times -> final value
1
The constraints are important:
n <= 10^5queries.length <= 10^5
A naive approach that explicitly traverses every subtree for every query can become extremely slow. In the worst case, a subtree can contain almost all nodes, producing time complexity near O(n * q).
The problem guarantees:
- The tree is valid and connected
- Node labels are continuous from
1ton - Every query refers to an existing node
Important edge cases include:
- A query on the root node, which flips the entire tree
- Repeated queries on the same node
- Queries on leaf nodes
- Very deep nodes near
n - Trees that are incomplete because
nmay not be of the form2^k - 1
Approaches
Brute Force Approach
The most direct solution is to explicitly simulate every query.
For each query:
- Find all nodes in the subtree rooted at the queried node
- Flip every node inside that subtree
We can discover subtree nodes using DFS or BFS. Since the tree follows heap indexing rules:
- left child of
xis2 * x - right child of
xis2 * x + 1
We can recursively traverse all descendants.
This approach is correct because it directly performs the operations exactly as described in the problem statement.
However, it is too slow.
Consider:
n = 100000100000queries- many queries targeting large subtrees
Each query may visit almost all nodes, resulting in roughly O(n * q) operations, which can reach 10^10.
Optimal Approach
The important observation is that we do not actually need to flip subtree nodes immediately.
Instead, we can think from the perspective of each node:
A node's final value depends on how many queried subtrees contain that node.
A node belongs to the subtree of all its ancestors, including itself.
So for a node x, its value changes once for every queried ancestor of x.
This leads to a much more efficient strategy:
- Count how many times each node appears in queries
- Perform a DFS from the root
- While traversing downward, maintain the cumulative number of flips inherited from ancestors
- If the cumulative flip count is odd, the current node ends as
1
This works because subtree flips propagate naturally downward through ancestor relationships.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n * q) | O(n) | Explicitly flips every subtree |
| Optimal | O(n + q) | O(n) | Uses cumulative flip propagation during DFS |
Algorithm Walkthrough
- Create an array called
flip_countof sizen + 1.
flip_count[x] stores how many queries directly target node x.
2. Process all queries.
For each query node q, increment:
flip_count[q] += 1
We do not flip anything yet. We only record how many flips originate at each node.
3. Start DFS from the root node 1.
During DFS, maintain a variable called current_flips.
This represents how many flips affect the current node from all ancestors encountered so far.
4. When visiting node x, compute:
total_flips = current_flips + flip_count[x]
This includes:
- flips inherited from ancestors
- flips initiated at the current node
- Determine the final value of node
x.
If total_flips is odd, the node ends with value 1.
If it is even, the node ends with value 0.
6. Recursively visit children.
Since this is a heap-style tree:
- left child =
2 * x - right child =
2 * x + 1
Only recurse if the child label is <= n.
7. Pass total_flips downward.
Every descendant inherits all flips affecting the current node. 8. Count how many nodes finish with odd flip counts.
Return this count after DFS completes.
Why it works
A subtree flip affects exactly all descendants of the queried node, including itself. During DFS, every node accumulates all flips originating from its ancestors. Since a node belongs precisely to the subtrees rooted at its ancestors, the cumulative flip count exactly equals the number of times the node should be flipped. A node ends as 1 if and only if that count is odd.
Python Solution
from typing import List
class Solution:
def numberOfNodes(self, n: int, queries: List[int]) -> int:
flip_count = [0] * (n + 1)
for node in queries:
flip_count[node] += 1
result = 0
def dfs(node: int, inherited_flips: int) -> None:
nonlocal result
if node > n:
return
total_flips = inherited_flips + flip_count[node]
if total_flips % 2 == 1:
result += 1
dfs(node * 2, total_flips)
dfs(node * 2 + 1, total_flips)
dfs(1, 0)
return result
The implementation begins by counting how many flip operations originate at each node. Instead of simulating subtree updates immediately, the algorithm postpones all work until DFS traversal.
The recursive DFS function receives two parameters:
node, the current node labelinherited_flips, the number of flips passed down from ancestors
At every node, the algorithm adds locally initiated flips using:
total_flips = inherited_flips + flip_count[node]
If total_flips is odd, the node finishes with value 1, so the result counter increases.
The recursion then propagates the updated flip count to both children. Because every descendant inherits all ancestor flips, this naturally models subtree operations without explicitly modifying subtree nodes.
The recursion stops whenever a node label exceeds n, since such nodes do not exist in the tree.
Go Solution
func numberOfNodes(n int, queries []int) int {
flipCount := make([]int, n+1)
for _, node := range queries {
flipCount[node]++
}
result := 0
var dfs func(int, int)
dfs = func(node int, inheritedFlips int) {
if node > n {
return
}
totalFlips := inheritedFlips + flipCount[node]
if totalFlips%2 == 1 {
result++
}
dfs(node*2, totalFlips)
dfs(node*2+1, totalFlips)
}
dfs(1, 0)
return result
}
The Go implementation follows the same logic as the Python version.
Some Go-specific details:
- Slices are used instead of Python lists
- Recursive functions are declared using function variables
- Integer overflow is not a concern because values remain within standard integer limits
- Go does not have
nonlocal, soresultis captured naturally by the closure
Worked Examples
Example 1
n = 5
queries = [1, 2, 5]
Tree structure:
1
/ \
2 3
/ \
4 5
Step 1: Build flip count
| Node | flip_count |
|---|---|
| 1 | 1 |
| 2 | 1 |
| 3 | 0 |
| 4 | 0 |
| 5 | 1 |
Step 2: DFS Traversal
| Current Node | Inherited Flips | Local Flips | Total Flips | Final Value |
|---|---|---|---|---|
| 1 | 0 | 1 | 1 | 1 |
| 2 | 1 | 1 | 2 | 0 |
| 4 | 2 | 0 | 2 | 0 |
| 5 | 2 | 1 | 3 | 1 |
| 3 | 1 | 0 | 1 | 1 |
Nodes with value 1 are:
1, 3, 5
Final answer:
3
Example 2
n = 3
queries = [2, 3, 3]
Tree structure:
1
/ \
2 3
Step 1: Build flip count
| Node | flip_count |
|---|---|
| 1 | 0 |
| 2 | 1 |
| 3 | 2 |
Step 2: DFS Traversal
| Current Node | Inherited Flips | Local Flips | Total Flips | Final Value |
|---|---|---|---|---|
| 1 | 0 | 0 | 0 | 0 |
| 2 | 0 | 1 | 1 | 1 |
| 3 | 0 | 2 | 2 | 0 |
Only node 2 ends with value 1.
Final answer:
1
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n + q) | One pass over queries and one DFS traversal over all nodes |
| Space | O(n) | Flip count array plus recursion stack |
The algorithm processes every query exactly once and visits every node exactly once during DFS. No subtree is traversed repeatedly, which eliminates the inefficiency of the brute force approach.
The recursion depth depends on tree height, which is O(log n) for this heap-structured tree, but the flip count array requires O(n) memory.
Test Cases
from typing import List
class Solution:
def numberOfNodes(self, n: int, queries: List[int]) -> int:
flip_count = [0] * (n + 1)
for node in queries:
flip_count[node] += 1
result = 0
def dfs(node: int, inherited_flips: int) -> None:
nonlocal result
if node > n:
return
total_flips = inherited_flips + flip_count[node]
if total_flips % 2 == 1:
result += 1
dfs(node * 2, total_flips)
dfs(node * 2 + 1, total_flips)
dfs(1, 0)
return result
sol = Solution()
assert sol.numberOfNodes(5, [1, 2, 5]) == 3 # provided example 1
assert sol.numberOfNodes(3, [2, 3, 3]) == 1 # provided example 2
assert sol.numberOfNodes(1, [1]) == 1 # single node flipped once
assert sol.numberOfNodes(1, [1, 1]) == 0 # single node flipped twice
assert sol.numberOfNodes(7, [1]) == 7 # entire tree flipped once
assert sol.numberOfNodes(7, [1, 1]) == 0 # entire tree flipped twice
assert sol.numberOfNodes(7, [4]) == 1 # leaf node only
assert sol.numberOfNodes(7, [2]) == 3 # subtree rooted at node 2
assert sol.numberOfNodes(15, [2, 2, 2]) == 7 # odd repeated flips
assert sol.numberOfNodes(15, [2, 2]) == 0 # even repeated flips
assert sol.numberOfNodes(10, [10]) == 1 # deepest node flip
assert sol.numberOfNodes(10, [5, 10]) == 1 # overlapping subtree effects
| Test | Why |
|---|---|
n=5, [1,2,5] |
Validates example 1 |
n=3, [2,3,3] |
Validates example 2 |
n=1, [1] |
Smallest possible tree |
n=1, [1,1] |
Double flip cancellation |
n=7, [1] |
Entire tree flip |
n=7, [1,1] |
Entire tree restored |
n=7, [4] |
Leaf-only subtree |
n=7, [2] |
Internal subtree flip |
n=15, [2,2,2] |
Odd repeated subtree flips |
n=15, [2,2] |
Even repeated subtree flips |
n=10, [10] |
Deep incomplete-tree node |
n=10, [5,10] |
Overlapping subtree behavior |
Edge Cases
One important edge case occurs when the root node is queried. Since the root's subtree contains the entire tree, every node must flip. A naive implementation might repeatedly traverse the whole tree for such queries, causing severe performance problems. The optimized solution handles this naturally because the root's flip count is simply propagated to all descendants during DFS.
Another important case involves repeated queries on the same node. Since flipping twice cancels out, implementations that directly toggle values may become error-prone or inefficient. The algorithm avoids this issue entirely by tracking flip counts mathematically and checking parity using total_flips % 2.
A third edge case appears when n does not form a perfect binary tree. For example, n = 10 means some potential children do not exist. Recursive traversal must carefully stop whenever a child label exceeds n. The implementation explicitly checks this condition before processing nodes, preventing invalid accesses.
A final subtle case involves leaf nodes. A leaf subtree contains only the node itself. Some implementations incorrectly assume every node has children and may attempt invalid recursion. The boundary condition if node > n: return ensures that nonexistent children are safely ignored.