LeetCode 3939 - Count Non Adjacent Subsets in a Rooted Tree
We are given a rooted tree with n nodes. The tree is represented by the parent array, where parent[i] tells us the parent of node i. Node 0 is always the root because parent[0] = -1. Each node also has a value stored in nums[i].
Difficulty: 🔴 Hard
Topics: —
Solution
LeetCode 3939 - Count Non Adjacent Subsets in a Rooted Tree
Problem Understanding
We are given a rooted tree with n nodes. The tree is represented by the parent array, where parent[i] tells us the parent of node i. Node 0 is always the root because parent[0] = -1.
Each node also has a value stored in nums[i].
We want to count how many non-empty subsets of nodes satisfy two conditions simultaneously:
- The sum of the selected node values is divisible by
k. - No two selected nodes are directly connected by an edge. In other words, a node and its parent cannot both belong to the subset.
The second condition means the chosen nodes must form an independent set in the tree.
The answer can be extremely large, so we return it modulo 10^9 + 7.
The constraints are important:
n ≤ 1000k ≤ 100nums[i]can be very large, up to10^9
The relatively small value of k is the key observation. Since divisibility only depends on the remainder modulo k, we can store information using only k possible residue classes.
Some important edge cases include:
- A tree containing only one node.
- Cases where no valid subset exists.
- Cases where the only divisible subset is the empty subset, which must not be counted.
- Long chains, where adjacency restrictions are strongest.
- Star-shaped trees, where many children can be chosen simultaneously.
- Large node values, requiring us to reduce values modulo
k.
Approaches
Brute Force
A brute force solution would examine every possible subset of nodes.
For each subset, we would:
- Check whether any selected node and its parent are both included.
- Compute the sum of selected values.
- Verify divisibility by
k.
Since there are 2^n subsets, this approach requires exponential time.
Even for n = 50, this would already be infeasible, and the actual constraint is n = 1000.
Therefore, brute force is impossible.
Key Insight
The problem combines two properties:
- The adjacency restriction is local to tree edges.
- Divisibility only depends on the sum modulo
k.
This naturally suggests a tree dynamic programming solution.
For each node, we want to know:
- How many valid configurations exist inside its subtree.
- What residue modulo
keach configuration produces. - Whether the current node itself is selected.
The selection state is necessary because selecting a node restricts what its children are allowed to do.
This leads to a classic tree DP for independent sets, augmented with residue tracking modulo k.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(2^n · n) | O(1) | Enumerates every subset |
| Optimal Tree DP | O(n · k²) | O(n · k) | Independent-set DP with modulo tracking |
Algorithm Walkthrough
DP Definition
For each node u, maintain two arrays of length k.
-
dp0[u][r]= number of valid configurations in the subtree ofuwhere: -
node
uis not selected -
total sum modulo
kequalsr -
dp1[u][r]= number of valid configurations in the subtree ofuwhere: -
node
uis selected -
total sum modulo
kequalsr
Initialization
For a node before processing any children:
If u is not selected:
- The subtree contributes an empty set.
- Sum modulo
kis0.
Therefore:
dp0[0] = 1
If u is selected:
- The subset contains only node
u. - Residue is
nums[u] % k.
Therefore:
dp1[nums[u] % k] = 1
Processing Children
We process children one at a time and merge their contributions.
Case 1: Current Node Not Selected
If u is not selected, a child may be:
- selected
- not selected
So every child contributes:
childWays = dp0Child + dp1Child
We combine residues using modular convolution.
If the current accumulated residue is a and the child's residue is b, the new residue becomes:
(a + b) % k
Case 2: Current Node Selected
If u is selected, no child may be selected.
Therefore only dp0Child is allowed.
Again we perform residue convolution.
Postorder Traversal
Each node depends on its children, so we must process the tree from leaves upward.
A DFS postorder traversal naturally computes children before parents.
Final Answer
At the root:
answer = dp0Root[0] + dp1Root[0]
This counts all independent subsets whose sum is divisible by k.
However, the empty subset is included in dp0Root[0].
Since the problem asks for non-empty subsets, subtract one:
answer -= 1
Take the result modulo 10^9 + 7.
Why it Works
The DP maintains a complete count of all valid independent subsets inside every subtree.
The state distinguishes whether the current node is selected, which is exactly the information needed to enforce the parent-child restriction.
Every subtree configuration is represented by its sum modulo k, and residues combine correctly through modular addition.
Because every child is merged independently and every valid combination is counted exactly once, the root DP contains all valid independent subsets of the entire tree. Subtracting the unique empty subset leaves precisely the required answer.
Python Solution
from typing import List
import sys
class Solution:
def countValidSubsets(self, parent: List[int], nums: List[int], k: int) -> int:
MOD = 10**9 + 7
n = len(parent)
children = [[] for _ in range(n)]
for i in range(1, n):
children[parent[i]].append(i)
sys.setrecursionlimit(10000)
def dfs(u: int):
dp0 = [0] * k
dp1 = [0] * k
dp0[0] = 1
dp1[nums[u] % k] = 1
for v in children[u]:
child0, child1 = dfs(v)
next0 = [0] * k
next1 = [0] * k
# u not selected
for r1 in range(k):
if dp0[r1] == 0:
continue
for r2 in range(k):
ways = (child0[r2] + child1[r2]) % MOD
if ways:
next0[(r1 + r2) % k] = (
next0[(r1 + r2) % k]
+ dp0[r1] * ways
) % MOD
# u selected
for r1 in range(k):
if dp1[r1] == 0:
continue
for r2 in range(k):
if child0[r2]:
next1[(r1 + r2) % k] = (
next1[(r1 + r2) % k]
+ dp1[r1] * child0[r2]
) % MOD
dp0 = next0
dp1 = next1
return dp0, dp1
root0, root1 = dfs(0)
answer = (root0[0] + root1[0] - 1) % MOD
return answer
Implementation Explanation
The tree is first converted from the parent array into a children adjacency list. This allows efficient traversal from parent to children.
The DFS returns two arrays:
dp0, representing configurations where the current node is excluded.dp1, representing configurations where the current node is included.
Each array stores counts indexed by residue modulo k.
The initialization corresponds to the two possible states of a single node:
- Excluded, producing residue
0. - Included, producing residue
nums[u] % k.
For every child, we create new DP arrays and merge residue distributions through modular convolution.
When the current node is excluded, the child may be either included or excluded. When the current node is included, only the child's excluded state is allowed.
After processing all children, the resulting arrays represent the entire subtree.
At the root, residue 0 corresponds to sums divisible by k. The empty subset contributes exactly one configuration, so we subtract it.
Go Solution
package main
func countValidSubsets(parent []int, nums []int, k int) int {
const MOD int = 1000000007
n := len(parent)
children := make([][]int, n)
for i := 1; i < n; i++ {
children[parent[i]] = append(children[parent[i]], i)
}
var dfs func(int) ([]int, []int)
dfs = func(u int) ([]int, []int) {
dp0 := make([]int, k)
dp1 := make([]int, k)
dp0[0] = 1
dp1[nums[u]%k] = 1
for _, v := range children[u] {
child0, child1 := dfs(v)
next0 := make([]int, k)
next1 := make([]int, k)
for r1 := 0; r1 < k; r1++ {
if dp0[r1] == 0 {
continue
}
for r2 := 0; r2 < k; r2++ {
ways := (child0[r2] + child1[r2]) % MOD
if ways == 0 {
continue
}
idx := (r1 + r2) % k
next0[idx] = (next0[idx] + int((int64(dp0[r1])*int64(ways))%MOD)) % MOD
}
}
for r1 := 0; r1 < k; r1++ {
if dp1[r1] == 0 {
continue
}
for r2 := 0; r2 < k; r2++ {
if child0[r2] == 0 {
continue
}
idx := (r1 + r2) % k
next1[idx] = (next1[idx] + int((int64(dp1[r1])*int64(child0[r2]))%MOD)) % MOD
}
}
dp0 = next0
dp1 = next1
}
return dp0, dp1
}
root0, root1 := dfs(0)
ans := (root0[0] + root1[0] - 1) % MOD
if ans < 0 {
ans += MOD
}
return ans
}
Go-Specific Notes
Go's int type is large enough to store values modulo 10^9 + 7, but intermediate multiplications can exceed that range. Therefore the implementation performs multiplications using int64 before taking the modulus.
Slices are used for the residue arrays, and each merge allocates fresh arrays of size k.
The recursion depth is at most 1000, which is safe for Go.
Worked Examples
Example 1
parent = [-1,0,1]
nums = [1,2,3]
k = 3
Tree:
0
|
1
|
2
Node 2:
| State | Residue Counts |
|---|---|
| dp0 | [1,0,0] |
| dp1 | [1,0,0] |
Since 3 % 3 = 0.
Node 1 initialization:
| State | Residue Counts |
|---|---|
| dp0 | [1,0,0] |
| dp1 | [0,0,1] |
Merge child 2:
| State | Residue Counts |
|---|---|
| dp0 | [2,0,0] |
| dp1 | [0,0,1] |
Node 0 initialization:
| State | Residue Counts |
|---|---|
| dp0 | [1,0,0] |
| dp1 | [0,1,0] |
Merge node 1:
| State | Residue Counts |
|---|---|
| dp0 | [2,0,1] |
| dp1 | [0,2,0] |
Final:
dp0[0] + dp1[0] = 2
Subtract empty subset:
2 - 1 = 1
Answer:
1
Example 2
parent = [-1,0,0,0]
nums = [2,1,2,1]
k = 3
Tree:
0
/ | \
1 2 3
Leaf DPs:
Node 1:
dp0 = [1,0,0]
dp1 = [0,1,0]
Node 2:
dp0 = [1,0,0]
dp1 = [0,0,1]
Node 3:
dp0 = [1,0,0]
dp1 = [0,1,0]
After merging all three children into the root, the residue count for remainder 0 equals 3.
One of those configurations is the empty subset.
Therefore:
3 - 1 = 2
Answer:
2
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n · k²) | Each tree edge causes one DP merge, each merge performs residue convolution over k × k states |
| Space | O(n · k) | DP information across recursion and subtree states |
The dominant operation is merging residue distributions. Each merge examines all pairs of residues, resulting in O(k²) work. Since the tree has n - 1 edges and k ≤ 100, the overall complexity is efficient for the given constraints.
Test Cases
sol = Solution()
assert sol.countValidSubsets([-1, 0, 1], [1, 2, 3], 3) == 1
# Example 1
assert sol.countValidSubsets([-1, 0, 0, 0], [2, 1, 2, 1], 3) == 2
# Example 2
assert sol.countValidSubsets([-1], [3], 3) == 1
# Single node, divisible
assert sol.countValidSubsets([-1], [2], 3) == 0
# Single node, not divisible
assert sol.countValidSubsets([-1, 0], [1, 2], 3) == 1
# Only child alone forms valid subset
assert sol.countValidSubsets([-1, 0], [3, 3], 3) == 2
# Root alone and child alone
assert sol.countValidSubsets([-1, 0, 0], [1, 1, 1], 2) == 1
# Only {1,2} has even sum
assert sol.countValidSubsets([-1, 0, 1, 2, 3], [1, 1, 1, 1, 1], 2) == 3
# Chain structure
assert sol.countValidSubsets([-1, 0, 0, 0, 0], [5, 5, 5, 5, 5], 5) == 16
# Star tree, every non-empty independent subset of leaves plus root alone
assert sol.countValidSubsets([-1, 0, 0], [10**9, 10**9, 10**9], 1) == 4
# Every non-empty independent subset is valid when k=1
Test Summary
| Test | Why |
|---|---|
| Example 1 | Validates sample output |
| Example 2 | Validates sample output |
| Single divisible node | Smallest valid tree |
| Single non-divisible node | No valid subset exists |
| Two-node chain | Parent-child exclusion |
| Both values divisible | Multiple singleton solutions |
| Small star tree | Sibling selections allowed |
| Long chain | Strong adjacency constraints |
| Large star | Many independent combinations |
| k = 1 | Every sum divisible |
Edge Cases
Single Node Tree
When n = 1, the only possible non-empty subset is the root itself. A buggy implementation might accidentally count the empty subset or mishandle the base case. The DP initialization naturally handles this situation because the root starts with both exclusion and inclusion states, and the final subtraction removes only the empty subset.
Long Chain
A chain represents the strongest possible adjacency restriction because every node except the ends has two neighbors. Greedy approaches often fail here because choosing one node immediately affects the next. The DP explicitly tracks whether a node is selected, ensuring that parent-child conflicts are never counted.
Empty Subset Producing Residue Zero
The empty subset always has sum 0, which is divisible by every k. Since the problem requires a non-empty subset, forgetting to remove this configuration would produce an answer that is too large by exactly one whenever residue 0 is considered. The implementation subtracts one from the root's residue-0 count, removing the empty subset and nothing else.
Very Large Node Values
Values may be as large as 10^9. Storing exact sums would be unnecessary and inefficient. The implementation immediately reduces each value using nums[u] % k, because divisibility depends only on the residue modulo k. This keeps the DP state bounded by k ≤ 100.