Facebook Pixel

1719. Number Of Ways To Reconstruct A Tree

Problem Description

You're given an array pairs where each element pairs[i] = [xi, yi] represents a relationship between two nodes. The array has these properties:

  • No duplicate pairs exist
  • For each pair, xi < yi

Your task is to determine how many different rooted trees can be constructed that satisfy these specific conditions:

  1. Node composition: The tree must contain exactly the nodes that appear in the pairs array (no more, no less).

  2. Ancestor relationship rule: A pair [xi, yi] exists in the input if and only if one node is an ancestor of the other. This means:

    • If [xi, yi] is in pairs, then either xi must be an ancestor of yi, or yi must be an ancestor of xi
    • If two nodes have an ancestor-descendant relationship in the tree, their pair must exist in pairs
  3. Tree structure: The tree doesn't have to be binary - nodes can have any number of children.

Two tree configurations are considered different if at least one node has a different parent between the two trees.

Return values:

  • Return 0 if no valid trees can be constructed (ways == 0)
  • Return 1 if exactly one valid tree can be constructed (ways == 1)
  • Return 2 if more than one valid tree can be constructed (ways > 1)

Key definitions:

  • A rooted tree has a single root node with all edges directed away from the root
  • An ancestor of a node is any node on the path from the root to that node (not including the node itself)
  • The root node has no ancestors
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that if two nodes have an ancestor-descendant relationship in the tree, then one must be on the path from the root to the other. This means they must be connected through a chain of ancestor relationships with all intermediate nodes.

Think about what the pairs tell us: if [x, y] is a pair, then in any valid tree, one must be an ancestor of the other. Now, if x is an ancestor of y, and we have another node z where [x, z] is also a pair, then either:

  • z is between x and y on the path (making [y, z] also a required pair)
  • z is on a different branch from y (no pair between y and z)

This leads to a crucial observation: nodes with more connections (higher degree) are more likely to be ancestors of nodes with fewer connections. Why? Because an ancestor node must have pairs with all its descendants, plus potentially its own ancestors.

The degree of a node in the pair relationships gives us a hint about its position in the tree hierarchy. A node with degree n-1 (connected to all other nodes) must be the root or very close to it, as it has an ancestor-descendant relationship with every other node.

To verify if a tree structure is valid, we can use a greedy approach:

  1. Sort nodes by their degree (number of connections)
  2. For each node, find a potential parent among nodes with higher or equal degree
  3. When we assign a parent p to node x, all nodes connected to x must also be connected to p (transitivity of ancestor relationships)

The solution can have multiple valid trees when two nodes have the same degree - they could potentially swap positions in the hierarchy without violating the ancestor-descendant requirements. This is why we track if any two nodes have equal degrees.

If we can't find a valid parent for any node (except the root), or if we end up with multiple roots, then no valid tree exists.

Learn more about Tree and Graph patterns.

Solution Approach

The implementation uses a greedy algorithm with graph representation to validate and count possible tree structures.

Data Structure Setup:

  • Create a 2D boolean array g[510][510] to represent the adjacency relationships between nodes
  • Use a dictionary v to store the adjacency list for each node (neighbors of each node)
  • For each pair [x, y], mark g[x][y] = g[y][x] = True and add them to each other's adjacency lists

Node Collection and Preprocessing:

nodes = []
for i in range(510):
    if v[i]:  # If node i exists in pairs
        nodes.append(i)
        g[i][i] = True  # Mark self-connection for easier processing

Sorting by Degree: Sort all nodes by their degree (number of connections) in ascending order:

nodes.sort(key=lambda x: len(v[x]))

Greedy Parent Assignment: For each node x in sorted order:

  1. Look for a potential parent among nodes with higher or equal degree:

    j = i + 1
    while j < len(nodes) and not g[x][nodes[j]]:
        j += 1
  2. If a potential parent y is found:

    • Check if they have equal degrees: len(v[x]) == len(v[y])
      • If yes, set equal = True (multiple trees possible)
    • Verify transitivity: All neighbors of x must also be connected to y
      for z in v[x]:
          if not g[y][z]:
              return 0  # Invalid [tree](/problems/tree_intro) structure
  3. If no parent is found, increment the root counter:

    else:
        root += 1

Result Determination:

  • If root > 1: Multiple roots exist, so no valid tree is possible → return 0
  • If equal is True: At least two nodes had equal degrees, allowing position swaps → return 2
  • Otherwise: Exactly one valid tree structure exists → return 1

The algorithm efficiently validates the tree structure by ensuring:

  1. Each non-root node has exactly one valid parent
  2. The transitivity property holds (if x is ancestor of y and y is ancestor of z, then x must be ancestor of z)
  3. All required pairs are satisfied by the ancestor-descendant relationships

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example with pairs = [[1,2], [2,3], [1,3]].

Step 1: Build the graph representation

  • Create adjacency matrix g and adjacency list v
  • From pairs: g[1][2] = g[2][1] = True, g[2][3] = g[3][2] = True, g[1][3] = g[3][1] = True
  • Adjacency lists: v[1] = [2,3], v[2] = [1,3], v[3] = [1,2]

Step 2: Collect nodes and mark self-connections

  • Nodes in the tree: [1, 2, 3]
  • Set g[1][1] = g[2][2] = g[3][3] = True

Step 3: Sort nodes by degree

  • Node degrees: 1 has degree 2, 2 has degree 2, 3 has degree 2
  • After sorting: nodes = [1, 2, 3] (all have same degree)

Step 4: Greedy parent assignment

Processing node 1 (index 0):

  • Look for parent starting from index 1 (node 2)
  • Check if g[1][2] is True → Yes!
  • Node 2 has same degree as node 1 → Set equal = True
  • Verify transitivity: All neighbors of 1 (which are [2,3]) must connect to 2
    • g[2][2] = True ✓
    • g[2][3] = True ✓
  • Parent found successfully

Processing node 2 (index 1):

  • Look for parent starting from index 2 (node 3)
  • Check if g[2][3] is True → Yes!
  • Node 3 has same degree as node 2equal remains True
  • Verify transitivity: All neighbors of 2 (which are [1,3]) must connect to 3
    • g[3][1] = True ✓
    • g[3][3] = True ✓
  • Parent found successfully

Processing node 3 (index 2):

  • No more nodes to check for parent
  • Increment root = 1 (this is our root node)

Step 5: Determine result

  • root = 1 (exactly one root)
  • equal = True (nodes had equal degrees)
  • Return 2 (multiple valid trees possible)

Why multiple trees? Since all nodes have the same degree, we could have:

  • Tree 1: 3 as root → 2 as child → 1 as child (chain: 3→2→1)
  • Tree 2: 3 as root → 1 as child → 2 as child (chain: 3→1→2)
  • Tree 3: 2 as root → 3 as child → 1 as child (chain: 2→3→1)
  • And so on...

Each configuration maintains all required ancestor-descendant relationships from the pairs.

Solution Implementation

1class Solution:
2    def checkWays(self, pairs: List[List[int]]) -> int:
3        # Create adjacency matrix to track connections between nodes
4        adjacency_matrix = [[False] * 510 for _ in range(510)]
5      
6        # Create adjacency list to store neighbors for each node
7        adjacency_list = defaultdict(list)
8      
9        # Build the graph from the given pairs
10        for node1, node2 in pairs:
11            adjacency_matrix[node1][node2] = adjacency_matrix[node2][node1] = True
12            adjacency_list[node1].append(node2)
13            adjacency_list[node2].append(node1)
14      
15        # Collect all nodes that appear in the pairs
16        active_nodes = []
17        for node_id in range(510):
18            if adjacency_list[node_id]:
19                active_nodes.append(node_id)
20                # Mark self-connection as true for active nodes
21                adjacency_matrix[node_id][node_id] = True
22      
23        # Sort nodes by their degree (number of connections) in ascending order
24        active_nodes.sort(key=lambda node: len(adjacency_list[node]))
25      
26        # Track if there are multiple valid trees possible
27        multiple_solutions_exist = False
28      
29        # Count potential root nodes
30        root_count = 0
31      
32        # Check each node to find its potential parent in the tree
33        for current_index, current_node in enumerate(active_nodes):
34            # Look for the first node with higher or equal degree that is connected
35            parent_index = current_index + 1
36            while parent_index < len(active_nodes) and not adjacency_matrix[current_node][active_nodes[parent_index]]:
37                parent_index += 1
38          
39            if parent_index < len(active_nodes):
40                # Found a potential parent node
41                parent_node = active_nodes[parent_index]
42              
43                # Check if degrees are equal (multiple valid parent choices)
44                if len(adjacency_list[current_node]) == len(adjacency_list[parent_node]):
45                    multiple_solutions_exist = True
46              
47                # Verify that all neighbors of current node are also connected to parent
48                # This is necessary for a valid tree structure
49                for neighbor in adjacency_list[current_node]:
50                    if not adjacency_matrix[parent_node][neighbor]:
51                        return 0  # Invalid tree structure
52            else:
53                # No parent found, this node could be a root
54                root_count += 1
55      
56        # A valid tree must have exactly one root
57        if root_count > 1:
58            return 0  # Invalid: multiple disconnected components
59      
60        # Return 2 if multiple valid trees exist, 1 if unique tree exists
61        return 2 if multiple_solutions_exist else 1
62
1class Solution {
2    public int checkWays(int[][] pairs) {
3        // Create adjacency matrix to track connections between nodes
4        boolean[][] adjacencyMatrix = new boolean[510][510];
5      
6        // Create adjacency list for each node
7        List<Integer>[] adjacencyList = new List[510];
8        Arrays.setAll(adjacencyList, k -> new ArrayList<>());
9      
10        // Build the graph from input pairs
11        for (int[] pair : pairs) {
12            int nodeX = pair[0];
13            int nodeY = pair[1];
14          
15            // Mark bidirectional connection in adjacency matrix
16            adjacencyMatrix[nodeX][nodeY] = true;
17            adjacencyMatrix[nodeY][nodeX] = true;
18          
19            // Add to adjacency lists
20            adjacencyList[nodeX].add(nodeY);
21            adjacencyList[nodeY].add(nodeX);
22        }
23      
24        // Collect all nodes that appear in the pairs
25        List<Integer> activeNodes = new ArrayList<>();
26        for (int nodeId = 0; nodeId < 510; ++nodeId) {
27            if (!adjacencyList[nodeId].isEmpty()) {
28                activeNodes.add(nodeId);
29                // Mark self-connection for active nodes
30                adjacencyMatrix[nodeId][nodeId] = true;
31            }
32        }
33      
34        // Sort nodes by degree (number of connections) in ascending order
35        activeNodes.sort(Comparator.comparingInt(node -> adjacencyList[node].size()));
36      
37        boolean hasEqualDegreeNodes = false;
38        int rootCount = 0;
39      
40        // Process each node to validate tree structure
41        for (int i = 0; i < activeNodes.size(); ++i) {
42            int currentNode = activeNodes.get(i);
43          
44            // Find the first node after current that is connected to it
45            int nextConnectedIndex = i + 1;
46            while (nextConnectedIndex < activeNodes.size() && 
47                   !adjacencyMatrix[currentNode][activeNodes.get(nextConnectedIndex)]) {
48                nextConnectedIndex++;
49            }
50          
51            if (nextConnectedIndex < activeNodes.size()) {
52                // Found a potential parent node
53                int parentNode = activeNodes.get(nextConnectedIndex);
54              
55                // Check if current node and parent have same degree
56                if (adjacencyList[currentNode].size() == adjacencyList[parentNode].size()) {
57                    hasEqualDegreeNodes = true;
58                }
59              
60                // Verify that all neighbors of current node are also connected to parent
61                for (int neighbor : adjacencyList[currentNode]) {
62                    if (!adjacencyMatrix[parentNode][neighbor]) {
63                        // Invalid tree structure: parent should connect to all children's neighbors
64                        return 0;
65                    }
66                }
67            } else {
68                // No parent found, this is a root candidate
69                rootCount++;
70            }
71        }
72      
73        // A valid tree should have exactly one root
74        if (rootCount > 1) {
75            return 0;
76        }
77      
78        // Return 2 if multiple valid trees possible (due to equal degree nodes), otherwise 1
79        return hasEqualDegreeNodes ? 2 : 1;
80    }
81}
82
1class Solution {
2public:
3    int checkWays(vector<vector<int>>& pairs) {
4        // adjacency[i][j] indicates if node i and j have ancestor-descendant relationship
5        vector<vector<bool>> adjacency(510, vector<bool>(510));
6        // neighbors[i] stores all nodes that have ancestor-descendant relationship with node i
7        vector<vector<int>> neighbors(510);
8      
9        // Build adjacency matrix and neighbor lists from input pairs
10        for (auto& pair : pairs) {
11            int nodeA = pair[0];
12            int nodeB = pair[1];
13            adjacency[nodeA][nodeB] = adjacency[nodeB][nodeA] = true;
14            neighbors[nodeA].push_back(nodeB);
15            neighbors[nodeB].push_back(nodeA);
16        }
17      
18        // Collect all nodes that appear in the pairs
19        vector<int> activeNodes;
20        for (int nodeId = 1; nodeId <= 500; ++nodeId) {
21            if (!neighbors[nodeId].empty()) {
22                activeNodes.push_back(nodeId);
23                adjacency[nodeId][nodeId] = true; // Mark node as connected to itself
24            }
25        }
26      
27        // Sort nodes by degree (number of neighbors) in ascending order
28        sort(activeNodes.begin(), activeNodes.end(), 
29             [&](int a, int b) -> bool { 
30                 return neighbors[a].size() < neighbors[b].size(); 
31             });
32      
33        bool hasEqualDegreeParent = false;
34        int rootCount = 0;
35      
36        // Process each node to find its parent in the tree
37        for (int i = 0; i < activeNodes.size(); ++i) {
38            int currentNode = activeNodes[i];
39          
40            // Find the first node with higher or equal degree that is connected to current node
41            int parentIndex = i + 1;
42            while (parentIndex < activeNodes.size() && 
43                   !adjacency[currentNode][activeNodes[parentIndex]]) {
44                ++parentIndex;
45            }
46          
47            if (parentIndex < activeNodes.size()) {
48                // Found a potential parent node
49                int parentNode = activeNodes[parentIndex];
50              
51                // Check if parent has same degree (multiple valid trees possible)
52                if (neighbors[currentNode].size() == neighbors[parentNode].size()) {
53                    hasEqualDegreeParent = true;
54                }
55              
56                // Verify that all neighbors of current node are also connected to parent
57                // (required property for ancestor-descendant relationship)
58                for (int neighbor : neighbors[currentNode]) {
59                    if (!adjacency[parentNode][neighbor]) {
60                        return 0; // Invalid tree structure
61                    }
62                }
63            } else {
64                // No parent found, this is a root node
65                ++rootCount;
66            }
67        }
68      
69        // Check validity and uniqueness of the tree
70        if (rootCount > 1) {
71            return 0; // Multiple roots, invalid tree
72        }
73        if (hasEqualDegreeParent) {
74            return 2; // Multiple valid trees possible
75        }
76        return 1; // Exactly one valid tree
77    }
78};
79
1function checkWays(pairs: number[][]): number {
2    // adjacency[i][j] indicates if node i and j have ancestor-descendant relationship
3    const adjacency: boolean[][] = Array(510).fill(null).map(() => Array(510).fill(false));
4    // neighbors[i] stores all nodes that have ancestor-descendant relationship with node i
5    const neighbors: number[][] = Array(510).fill(null).map(() => []);
6  
7    // Build adjacency matrix and neighbor lists from input pairs
8    for (const pair of pairs) {
9        const nodeA = pair[0];
10        const nodeB = pair[1];
11        adjacency[nodeA][nodeB] = adjacency[nodeB][nodeA] = true;
12        neighbors[nodeA].push(nodeB);
13        neighbors[nodeB].push(nodeA);
14    }
15  
16    // Collect all nodes that appear in the pairs
17    const activeNodes: number[] = [];
18    for (let nodeId = 1; nodeId <= 500; nodeId++) {
19        if (neighbors[nodeId].length > 0) {
20            activeNodes.push(nodeId);
21            adjacency[nodeId][nodeId] = true; // Mark node as connected to itself
22        }
23    }
24  
25    // Sort nodes by degree (number of neighbors) in ascending order
26    activeNodes.sort((a, b) => neighbors[a].length - neighbors[b].length);
27  
28    let hasEqualDegreeParent = false;
29    let rootCount = 0;
30  
31    // Process each node to find its parent in the tree
32    for (let i = 0; i < activeNodes.length; i++) {
33        const currentNode = activeNodes[i];
34      
35        // Find the first node with higher or equal degree that is connected to current node
36        let parentIndex = i + 1;
37        while (parentIndex < activeNodes.length && 
38               !adjacency[currentNode][activeNodes[parentIndex]]) {
39            parentIndex++;
40        }
41      
42        if (parentIndex < activeNodes.length) {
43            // Found a potential parent node
44            const parentNode = activeNodes[parentIndex];
45          
46            // Check if parent has same degree (multiple valid trees possible)
47            if (neighbors[currentNode].length === neighbors[parentNode].length) {
48                hasEqualDegreeParent = true;
49            }
50          
51            // Verify that all neighbors of current node are also connected to parent
52            // (required property for ancestor-descendant relationship)
53            for (const neighbor of neighbors[currentNode]) {
54                if (!adjacency[parentNode][neighbor]) {
55                    return 0; // Invalid tree structure
56                }
57            }
58        } else {
59            // No parent found, this is a root node
60            rootCount++;
61        }
62    }
63  
64    // Check validity and uniqueness of the tree
65    if (rootCount > 1) {
66        return 0; // Multiple roots, invalid tree
67    }
68    if (hasEqualDegreeParent) {
69        return 2; // Multiple valid trees possible
70    }
71    return 1; // Exactly one valid tree
72}
73

Time and Space Complexity

Time Complexity: O(n² + m·n) where n is the number of unique nodes and m is the number of pairs.

  • Building the adjacency matrix g and adjacency list v from pairs takes O(m) time
  • Extracting unique nodes by iterating through 510 positions takes O(510) = O(1) time
  • Sorting nodes by degree takes O(n log n) time
  • The main loop iterates through each node (O(n)):
    • Finding the next connected node with higher degree: worst case O(n) iterations
    • Checking if all neighbors of x are connected to y: O(degree(x)) which can be O(n) in worst case
  • Overall: O(m) + O(n log n) + O(n²·n) = O(n³) in worst case, but more precisely O(n² + m·n) considering the actual graph structure

Space Complexity: O(510² + n + m) = O(1) since 510 is a constant

  • The adjacency matrix g uses 510 × 510 = O(1) space (fixed size)
  • The adjacency list v stores at most O(m) edges across all nodes
  • The nodes list stores at most O(n) unique nodes
  • Since the maximum node value is bounded by 510, the space is effectively O(1) constant space

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Misunderstanding the Ancestor-Descendant Requirement

Pitfall: A common mistake is thinking that the pairs represent direct parent-child relationships. In reality, pairs represent ANY ancestor-descendant relationship, not just immediate parent-child connections.

Example: If we have pairs [[1,2], [1,3], [2,3]], node 1 could be the root with node 2 as its child, and node 3 as the child of node 2. The pair [1,3] exists because 1 is an ancestor of 3 (grandparent relationship).

Solution: When verifying tree validity, ensure that ALL descendants of a node are connected to its ancestors in the adjacency matrix, not just direct children.

2. Incorrect Handling of Equal Degree Nodes

Pitfall: When two nodes have the same degree and are connected, the algorithm marks multiple_solutions_exist = True. However, developers often forget that this only applies when one could be the parent of the other, not when they're siblings.

Example: If nodes A and B both have degree 3 and are connected, they could potentially swap positions in the tree hierarchy, but only if one can be the ancestor of the other while maintaining all required connections.

Solution: The check for equal degrees should only trigger the multiple solutions flag when the node with equal degree is actually chosen as the parent:

if parent_index < len(active_nodes):
    parent_node = active_nodes[parent_index]
    # Only mark multiple solutions if this parent has equal degree
    if len(adjacency_list[current_node]) == len(adjacency_list[parent_node]):
        multiple_solutions_exist = True

3. Forgetting the Transitivity Check

Pitfall: Not verifying that all neighbors of a child node are also connected to its chosen parent. This transitivity property is crucial for maintaining the ancestor-descendant relationships.

Example: If node X has neighbors [Y, Z] and we assign P as X's parent, then P must also be connected to both Y and Z. Otherwise, the ancestor relationships would be violated.

Solution: Always validate transitivity when assigning a parent:

for neighbor in adjacency_list[current_node]:
    if not adjacency_matrix[parent_node][neighbor]:
        return 0  # Invalid tree structure

4. Inefficient Node Range Iteration

Pitfall: Using a fixed range of 510 nodes when the actual node values might be sparse or have a different range. This can lead to unnecessary memory usage and potential index out of bounds errors.

Solution: Dynamically determine the range of nodes or use a more flexible data structure:

# Better approach: determine max node value first
max_node = max(max(pair) for pair in pairs) if pairs else 0
adjacency_matrix = [[False] * (max_node + 1) for _ in range(max_node + 1)]

# Or use a dictionary-based approach for sparse graphs
adjacency_matrix = defaultdict(lambda: defaultdict(bool))

5. Not Handling Edge Cases

Pitfall: Failing to handle special cases like empty input, single pair, or when all nodes are fully connected.

Solution: Add explicit checks for edge cases:

def checkWays(self, pairs: List[List[int]]) -> int:
    if not pairs:
        return 1  # Empty tree is valid
  
    if len(pairs) == 1:
        return 1  # Single edge forms a valid tree
  
    # Continue with main algorithm...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What's the output of running the following function using input [30, 20, 10, 100, 33, 12]?

1def fun(arr: List[int]) -> List[int]:
2    import heapq
3    heapq.heapify(arr)
4    res = []
5    for i in range(3):
6        res.append(heapq.heappop(arr))
7    return res
8
1public static int[] fun(int[] arr) {
2    int[] res = new int[3];
3    PriorityQueue<Integer> heap = new PriorityQueue<>();
4    for (int i = 0; i < arr.length; i++) {
5        heap.add(arr[i]);
6    }
7    for (int i = 0; i < 3; i++) {
8        res[i] = heap.poll();
9    }
10    return res;
11}
12
1class HeapItem {
2    constructor(item, priority = item) {
3        this.item = item;
4        this.priority = priority;
5    }
6}
7
8class MinHeap {
9    constructor() {
10        this.heap = [];
11    }
12
13    push(node) {
14        // insert the new node at the end of the heap array
15        this.heap.push(node);
16        // find the correct position for the new node
17        this.bubble_up();
18    }
19
20    bubble_up() {
21        let index = this.heap.length - 1;
22
23        while (index > 0) {
24            const element = this.heap[index];
25            const parentIndex = Math.floor((index - 1) / 2);
26            const parent = this.heap[parentIndex];
27
28            if (parent.priority <= element.priority) break;
29            // if the parent is bigger than the child then swap the parent and child
30            this.heap[index] = parent;
31            this.heap[parentIndex] = element;
32            index = parentIndex;
33        }
34    }
35
36    pop() {
37        const min = this.heap[0];
38        this.heap[0] = this.heap[this.size() - 1];
39        this.heap.pop();
40        this.bubble_down();
41        return min;
42    }
43
44    bubble_down() {
45        let index = 0;
46        let min = index;
47        const n = this.heap.length;
48
49        while (index < n) {
50            const left = 2 * index + 1;
51            const right = left + 1;
52
53            if (left < n && this.heap[left].priority < this.heap[min].priority) {
54                min = left;
55            }
56            if (right < n && this.heap[right].priority < this.heap[min].priority) {
57                min = right;
58            }
59            if (min === index) break;
60            [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61            index = min;
62        }
63    }
64
65    peek() {
66        return this.heap[0];
67    }
68
69    size() {
70        return this.heap.length;
71    }
72}
73
74function fun(arr) {
75    const heap = new MinHeap();
76    for (const x of arr) {
77        heap.push(new HeapItem(x));
78    }
79    const res = [];
80    for (let i = 0; i < 3; i++) {
81        res.push(heap.pop().item);
82    }
83    return res;
84}
85

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More