LeetCode 2581 - Count Number of Possible Root Nodes

This problem gives us an undirected tree with n nodes and a list of directed parent-child guesses. Each guess claims that one node is the parent of another node when the tree is rooted in some way. The important detail is that the tree itself is undirected.

LeetCode Problem 2581

Difficulty: 🔴 Hard
Topics: Array, Hash Table, Dynamic Programming, Tree, Depth-First Search

Solution

Problem Understanding

This problem gives us an undirected tree with n nodes and a list of directed parent-child guesses. Each guess claims that one node is the parent of another node when the tree is rooted in some way.

The important detail is that the tree itself is undirected. Parent-child relationships only become defined after choosing a root. Once a root is selected, every edge gains a direction from parent to child according to the rooted tree structure.

For every possible node that could act as the root, we must determine how many guesses become correct. A guess [u, v] is correct if, after rooting the tree, u is indeed the parent of v.

Alice tells us that at least k guesses are correct. We must count how many nodes can serve as valid roots under this condition.

The input consists of:

  • edges, which defines the undirected tree
  • guesses, which contains directed parent-child assumptions
  • k, the minimum number of guesses that must be correct

The output is the number of nodes that can be chosen as roots such that at least k guesses become true.

The constraints are very important:

  • n can be as large as 10^5
  • guesses.length can also be as large as 10^5

This immediately tells us that any solution slower than roughly O(n log n) or O(n) will likely time out.

A naive solution that recomputes all parent relationships for every possible root would become far too expensive.

There are several edge cases worth noticing immediately.

When k = 0, every node is automatically valid because zero correct guesses is always satisfied.

The tree may be extremely unbalanced, such as a straight chain. Recursive depth can become large, so recursion depth handling matters in Python.

The guesses may heavily contradict each other. It is possible that no root satisfies at least k correct guesses.

The guesses are guaranteed to correspond to actual edges in the tree, which simplifies validation logic because we never need to check whether a guessed edge exists.

Approaches

Brute Force Approach

The most direct solution is to try every node as the root.

For each root:

  1. Run a DFS or BFS to determine the parent of every node.
  2. Iterate through all guesses.
  3. Count how many guesses match the computed parent relationships.
  4. If the count is at least k, increment the answer.

This approach is correct because it explicitly constructs the rooted tree for every possible root and checks every guess against the actual parent-child structure.

However, it is far too slow.

For every root:

  • DFS takes O(n)
  • Checking guesses takes O(g), where g = len(guesses)

Since we repeat this for all n roots, total complexity becomes:

O(n * (n + g))

With both n and g up to 10^5, this becomes approximately 10^10 operations in the worst case, which is impossible within time limits.

Key Insight

The important observation is that rerooting the tree only changes the direction of one edge at a time.

Suppose we already know the number of correct guesses when the tree is rooted at node u.

Now imagine rerooting the tree from u to one of its children v.

Only one edge changes direction:

u -> v

becomes

v -> u

Every other parent-child relationship in the tree remains unchanged.

This means we can update the count of correct guesses incrementally instead of recomputing everything from scratch.

Specifically:

  • If the guess (u, v) existed, it was previously correct and now becomes incorrect.
  • If the guess (v, u) existed, it was previously incorrect and now becomes correct.

So we can compute the answer for neighboring roots in constant time.

This transforms the problem into a rerooting DFS problem.

We proceed in two phases:

  1. Root the tree initially at node 0 and count how many guesses are correct.
  2. Perform another DFS to reroot the tree across every edge while updating the correct guess count dynamically.

This yields a linear time solution.

Approach Comparison

Approach Time Complexity Space Complexity Notes
Brute Force O(n × (n + g)) O(n) Recomputes parent relationships for every root
Optimal O(n + g) O(n + g) Uses rerooting DFS to update counts incrementally

Algorithm Walkthrough

  1. Build the adjacency list for the tree.

Since the tree is undirected, each edge is added in both directions. An adjacency list is ideal because the tree is sparse and DFS traversal becomes efficient. 2. Store all guesses in a hash set.

We need extremely fast lookup for checking whether a directed edge (u, v) exists as a guess. A hash set provides average O(1) lookup time. 3. Choose node 0 as the initial root.

The actual root is unknown, but we can start from any node. Rooting at 0 allows us to compute an initial count of correct guesses. 4. Run the first DFS to count correct guesses for root 0.

During DFS:

  • If (parent, child) exists in the guess set, increment the count.
  • Continue traversal recursively.

At the end of this DFS, we know exactly how many guesses are correct when the tree is rooted at 0. 5. Run a second DFS for rerooting.

Suppose we move the root from u to child v.

Initially we have some count current_correct.

Two possible adjustments occur:

  • If (u, v) exists in guesses, decrement the count because this relationship reverses.
  • If (v, u) exists in guesses, increment the count because this relationship now becomes correct.

The updated value becomes the correct guess count for root v. 6. Check whether each root satisfies the condition.

During the rerooting DFS:

  • If current_correct >= k, increment the answer.
  • Continue propagating updated counts to neighbors.
  1. Return the final answer.

Why it works

The algorithm works because rerooting across an edge only changes the direction of that single edge. All other parent-child relationships remain identical.

Therefore, when moving the root from u to v, the only guesses whose correctness can change are:

  • (u, v)
  • (v, u)

By updating the count using only these local changes, we correctly maintain the number of valid guesses for every possible root.

Since every node is visited exactly once during rerooting, all possible roots are evaluated correctly and efficiently.

Python Solution

from typing import List
from collections import defaultdict
import sys

sys.setrecursionlimit(1 << 20)

class Solution:
    def rootCount(self, edges: List[List[int]], guesses: List[List[int]], k: int) -> int:
        n = len(edges) + 1

        graph = defaultdict(list)

        for u, v in edges:
            graph[u].append(v)
            graph[v].append(u)

        guess_set = set()

        for u, v in guesses:
            guess_set.add((u, v))

        initial_correct = 0

        def count_correct(node: int, parent: int) -> None:
            nonlocal initial_correct

            for neighbor in graph[node]:
                if neighbor == parent:
                    continue

                if (node, neighbor) in guess_set:
                    initial_correct += 1

                count_correct(neighbor, node)

        count_correct(0, -1)

        answer = 0

        def reroot(node: int, parent: int, current_correct: int) -> None:
            nonlocal answer

            if current_correct >= k:
                answer += 1

            for neighbor in graph[node]:
                if neighbor == parent:
                    continue

                next_correct = current_correct

                if (node, neighbor) in guess_set:
                    next_correct -= 1

                if (neighbor, node) in guess_set:
                    next_correct += 1

                reroot(neighbor, node, next_correct)

        reroot(0, -1, initial_correct)

        return answer

The implementation begins by constructing an adjacency list representation of the tree. Since the input is undirected, each edge is inserted in both directions.

The guesses are stored in a set of tuples. This is crucial because we repeatedly check whether a particular directed edge exists. Using a list would make lookups too slow.

The first DFS computes the number of correct guesses assuming node 0 is the root. During traversal, every edge naturally receives a parent-child orientation from the DFS direction. Whenever (parent, child) exists in the guess set, the count increases.

The second DFS performs rerooting. Instead of recomputing the entire answer for each root, it adjusts the current count based only on the edge whose direction changes during rerooting.

If moving the root reverses a previously correct guess, the count decreases. If it creates a newly correct guess, the count increases.

Every node is visited once during rerooting, and the running count always reflects the correct number of valid guesses for the current root.

Go Solution

package main

func rootCount(edges [][]int, guesses [][]int, k int) int {
	n := len(edges) + 1

	graph := make([][]int, n)

	for _, edge := range edges {
		u, v := edge[0], edge[1]

		graph[u] = append(graph[u], v)
		graph[v] = append(graph[v], u)
	}

	guessSet := make(map[[2]int]bool)

	for _, guess := range guesses {
		guessSet[[2]int{guess[0], guess[1]}] = true
	}

	initialCorrect := 0

	var countCorrect func(node, parent int)

	countCorrect = func(node, parent int) {
		for _, neighbor := range graph[node] {
			if neighbor == parent {
				continue
			}

			if guessSet[[2]int{node, neighbor}] {
				initialCorrect++
			}

			countCorrect(neighbor, node)
		}
	}

	countCorrect(0, -1)

	answer := 0

	var reroot func(node, parent, currentCorrect int)

	reroot = func(node, parent, currentCorrect int) {
		if currentCorrect >= k {
			answer++
		}

		for _, neighbor := range graph[node] {
			if neighbor == parent {
				continue
			}

			nextCorrect := currentCorrect

			if guessSet[[2]int{node, neighbor}] {
				nextCorrect--
			}

			if guessSet[[2]int{neighbor, node}] {
				nextCorrect++
			}

			reroot(neighbor, node, nextCorrect)
		}
	}

	reroot(0, -1, initialCorrect)

	return answer
}

The Go implementation follows the same rerooting strategy as the Python version.

Instead of Python tuples, Go uses fixed-size arrays of length two as hash map keys:

map[[2]int]bool

Go slices are used for adjacency lists. Since Go does not have Python's recursion limit issue, no explicit recursion depth adjustment is needed, although extremely deep recursion could still theoretically overflow the stack in pathological cases.

All counts fit safely within standard int because the maximum number of guesses is only 10^5.

Worked Examples

Example 1

edges = [[0,1],[1,2],[1,3],[4,2]]
guesses = [[1,3],[0,1],[1,0],[2,4]]
k = 3

Step 1: Build the Tree

Adjacency list:

Node Neighbors
0 [1]
1 [0,2,3]
2 [1,4]
3 [1]
4 [2]

Guess set:

(1,3)
(0,1)
(1,0)
(2,4)

Step 2: Root at 0

Tree orientation:

0
|
1
/ \
2  3
|
4

Correct guesses:

Guess Correct?
(1,3) Yes
(0,1) Yes
(1,0) No
(2,4) Yes

Initial correct count:

3

Since 3 >= k, node 0 is valid.

Step 3: Reroot from 0 to 1

Edge direction changes:

0 -> 1

becomes

1 -> 0

Adjustments:

Guess Effect
(0,1) exists subtract 1
(1,0) exists add 1

New count:

3 - 1 + 1 = 3

Node 1 is valid.

Step 4: Reroot from 1 to 2

Changed edge:

1 -> 2

becomes

2 -> 1

No related guesses exist.

Count remains:

3

Node 2 is valid.

Step 5: Reroot from 1 to 3

Changed edge:

1 -> 3

becomes

3 -> 1

Adjustments:

Guess Effect
(1,3) exists subtract 1

New count:

2

Node 3 is not valid.

Step 6: Reroot from 2 to 4

Changed edge:

2 -> 4

becomes

4 -> 2

Adjustments:

Guess Effect
(2,4) exists subtract 1

New count:

2

Node 4 is not valid.

Final answer:

3

Example 2

edges = [[0,1],[1,2],[2,3],[3,4]]
guesses = [[1,0],[3,4],[2,1],[3,2]]
k = 1

Initial Root = 0

Tree:

0 -> 1 -> 2 -> 3 -> 4

Correct guesses:

Guess Correct?
(1,0) No
(3,4) Yes
(2,1) No
(3,2) No

Count:

1

Already valid.

Reroot Progression

Root Correct Count
0 1
1 2
2 3
3 4
4 3

Every root satisfies at least one correct guess.

Final answer:

5

Complexity Analysis

Measure Complexity Explanation
Time O(n + g) Each edge and guess is processed a constant number of times
Space O(n + g) Adjacency list and guess hash set dominate memory usage

The adjacency list stores 2 * (n - 1) entries because the tree is undirected.

The first DFS visits every node once. The rerooting DFS also visits every node once. Each reroot transition performs only constant-time updates.

The guess set contains g entries and supports average O(1) lookup time.

Overall, the solution is linear in the size of the input.

Test Cases

from typing import List

class Solution:
    def rootCount(self, edges: List[List[int]], guesses: List[List[int]], k: int) -> int:
        from collections import defaultdict

        graph = defaultdict(list)

        for u, v in edges:
            graph[u].append(v)
            graph[v].append(u)

        guess_set = set(map(tuple, guesses))

        initial_correct = 0

        def dfs(node, parent):
            nonlocal initial_correct

            for neighbor in graph[node]:
                if neighbor == parent:
                    continue

                if (node, neighbor) in guess_set:
                    initial_correct += 1

                dfs(neighbor, node)

        dfs(0, -1)

        answer = 0

        def reroot(node, parent, current):
            nonlocal answer

            if current >= k:
                answer += 1

            for neighbor in graph[node]:
                if neighbor == parent:
                    continue

                next_current = current

                if (node, neighbor) in guess_set:
                    next_current -= 1

                if (neighbor, node) in guess_set:
                    next_current += 1

                reroot(neighbor, node, next_current)

        reroot(0, -1, initial_correct)

        return answer

sol = Solution()

# Example 1 from problem statement
assert sol.rootCount(
    [[0,1],[1,2],[1,3],[4,2]],
    [[1,3],[0,1],[1,0],[2,4]],
    3
) == 3

# Example 2 from problem statement
assert sol.rootCount(
    [[0,1],[1,2],[2,3],[3,4]],
    [[1,0],[3,4],[2,1],[3,2]],
    1
) == 5

# Minimum tree size
assert sol.rootCount(
    [[0,1]],
    [[0,1]],
    1
) == 1

# k = 0 means every root works
assert sol.rootCount(
    [[0,1],[1,2]],
    [[0,1]],
    0
) == 3

# No root satisfies condition
assert sol.rootCount(
    [[0,1],[1,2]],
    [[0,1]],
    2
) == 0

# Chain tree with alternating guesses
assert sol.rootCount(
    [[0,1],[1,2],[2,3]],
    [[0,1],[2,1],[2,3]],
    2
) == 3

# Star-shaped tree
assert sol.rootCount(
    [[0,1],[0,2],[0,3],[0,4]],
    [[0,1],[0,2],[0,3]],
    2
) == 1

# All guesses reversed
assert sol.rootCount(
    [[0,1],[1,2],[2,3]],
    [[1,0],[2,1],[3,2]],
    3
) == 1

# Large valid configuration pattern
assert sol.rootCount(
    [[0,1],[1,2],[2,3],[3,4],[4,5]],
    [[0,1],[1,2],[2,3],[3,4],[4,5]],
    5
) == 1

Test Summary

Test Why
Example 1 Verifies reroot transitions and mixed correct guesses
Example 2 Confirms all roots can be valid
Minimum tree size Tests smallest valid input
k = 0 Ensures every root is accepted
No valid root Confirms algorithm returns zero correctly
Chain tree Tests deep reroot propagation
Star-shaped tree Tests highly centralized topology
All guesses reversed Ensures reverse-direction updates work
Large valid chain Verifies fully aligned parent-child guesses

Edge Cases

One important edge case occurs when k = 0. In this situation, every node must be considered valid because having at least zero correct guesses is always true. A buggy implementation might accidentally apply stricter logic or fail to count all nodes. This implementation handles it naturally because every root satisfies the condition current_correct >= 0.

Another important case is a highly skewed tree, such as a linked-list-shaped tree. Recursive DFS implementations can become dangerously deep. In Python, recursion depth would normally fail around depth 1000. The implementation explicitly increases the recursion limit using:

sys.setrecursionlimit(1 << 20)

which safely supports the maximum constraint size.

A third important edge case involves contradictory guesses. For example, the guess list may contain both (u, v) and (v, u). Only one of these can ever be correct for a given rooted tree. Incorrect reroot logic may accidentally double count or fail to update correctly when edge directions flip. The rerooting transition explicitly handles both directions independently, ensuring correctness.

Another subtle case occurs when no root satisfies the condition. Some implementations mistakenly assume at least one valid root exists. This solution checks every root independently and only increments the answer when the count reaches k, so returning 0 works correctly.