1719. Number Of Ways To Reconstruct A Tree
Problem Description
You are given an array called pairs
where each element in this array is a pair of integers [xi, yi]
, indicating a relationship between two elements with no duplicates and where xi
is always less than yi
. The goal is to determine the number of unique rooted trees that can be constructed where:
- The trees are formed by nodes whose values come from the pairs.
- For each pair
[xi, yi]
, eitherxi
is an ancestor ofyi
, oryi
is an ancestor ofxi
and this relationship is captured if and only if the pair exists inpairs
. - The tree is a rooted tree, meaning it has a single specified root node, and edges are directed away from this root.
- A rooted tree does not necessarily have to be a binary tree.
The term ancestor refers to any node along the path from the root to that node, excluding the node itself, and the root node is the only node without ancestors.
The problem asks to determine the number of possible unique rooted trees, returning:
0
if no such trees can be constructed,1
if exactly one unique tree can be constructed,2
if more than one unique tree can be constructed.
Intuition
To approach this problem, we focus on the properties of a rooted tree, particularly, the fact that ancestors have a specific relationship in the tree and that the root node would typically be the node with the highest number of connections, reflecting its position at the top of the tree with no ancestors.
We can start by considering the following points:
- Searching for the root node of these trees: The node with the most connections is likely the root, as in a tree each child has exactly one parent, and the root will be the only node without a parent.
- Once we find a potential root, we can check if the relationships described by
pairs
adhere to the constraints of being ancestor-descendant relationships. - If there are multiple possible roots, or if at any point the ancestor-descendant relationship does not hold for any pair, we can be sure that no such tree exists.
- If we can construct exactly one tree, our answer is
1
; if we find that there's more than one way to arrange nodes to create valid trees, the answer is2
.
From this intuition, the solution could be approached as follows:
- We'll want to efficiently check the relationships between pairs of nodes. A graph data structure can help us keep track of all pairings.
- We sort the nodes based on the number of connections they have, in descending order, to quickly identify potential root nodes (nodes with the most connections).
- We iterate over this sorted list, verifying connections, and ensuring that our ancestor-descendant relationships hold.
- We need to watch for special cases, such as when two nodes have the same number of connections -- this indicates that we may have multiple valid solutions.
- If we find multiple root nodes or violations of the ancestor-descendant rule, we immediately know there are zero ways to construct the tree.
- If we can construct the tree without finding such conflicting situations, we then check whether we've encountered a situation indicating multiple solutions.
The solution code implements these ideas by using a graph represented by a matrix (g
) to track the pairings and a dictionary (v
) to quickly list the connections each node has. It then works through the logical process described above to reach the final answer.
Solution Approach
The solution provided uses graph theory principles to create a graph to represent the possible rooted trees. The following steps are executed in the solution:
Step 1: Data Structures
- A 2D list (
g[]
) of size510x510
is used to represent the adjacency matrix of the graph, where the indices are node values and aTrue
value atg[x][y]
represents an edge between nodesx
andy
. Initially, it's filled withFalse
, and the indices are up to510
as an upper limit on the number of different nodes. - A dictionary (
v
) where the key is a node and the value is a list of other nodes connected to it.
Step 2: Building the Graph
- Iteration over pairs
[[xi, yi]]
. For each pair, bothg[xi][yi]
andg[yi][xi]
are set toTrue
, as the graph is undirected. Also,xi
andyi
are appended to each other's connection list inv
.
Step 3: Sorting Nodes and Marking Self Connections
- A list of nodes
nodes
is created from keys inv
(which guarantees no duplicates asv
is a dictionary). - Each node is marked connected to itself:
g[i][i]
is set toTrue
. nodes
is sorted based on the length of their connections list (v[x]
) to prioritize nodes with more connections, as they are more likely to be ancestors.
Step 4: Finding a Root and Checking Ancestor-Descendant Relations
- The solution starts to iterate over
nodes
, for eachx
innodes
, it searches for ay
that has a direct connection tox
(g[x][y]
isTrue
). - If a node with an equal number of connections is found (
len(v[x]) == len(v[y])
),equal
is set toTrue
, indicating a potential for multiple solutions. - For every node
z
connected tox
, it checks ifz
is also connected toy
(g[y][z]
). If not, it returns0
, becausey
must be the ancestor of allx
's connections. - If no connected
y
is found after the iteration,root
is incremented, identifying a potential root.
Step 5: Determining the Final Output
- If more than one root is found (
root > 1
), the function returns0
as it is not possible to have more than one root in a rooted tree. - If the process completes without inconsistencies or extra roots, but
equal
was flagged asTrue
at any point, then there are multiple ways to arrange the tree, and2
is returned. - Otherwise, if exactly one valid tree configuration is found,
1
is returned.
The algorithm constructs potential trees by establishing the root nodes based on their connection count while also ensuring that each node's ancestor-descendant constraints are respected. It uses a graph represented by an adjacency matrix to check the connections between nodes and employs a sorting strategy to quickly identify potential roots and validate the trees' structures.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's consider a small example to illustrate the solution approach. Suppose we have the following pairs of relationships represented as an array pairs
: [[1,2], [1,3], [3,4]]
.
Step 1: Data Structures
We initialize an adjacency matrix g[][]
with a size of 510x510
. All values are initially set to False
. Also, we prepare a dictionary v
to maintain a list of connections for each node.
Step 2: Building the Graph
For each pair:
[[1,2]]
: We setg[1][2]
andg[2][1]
toTrue
and put2
in the list ofv[1]
and1
in the list ofv[2]
.[[1,3]]
: We setg[1][3]
andg[3][1]
toTrue
and append3
to the list ofv[1]
and1
to the list ofv[3]
.[[3,4]]
: We setg[3][4]
andg[4][3]
toTrue
and append4
to the list ofv[3]
and3
to the list ofv[4]
.
Following these steps, we have connections represented as:
v[1]
contains[2, 3]
v[2]
contains[1]
v[3]
contains[1, 4]
v[4]
contains[3]
Step 3: Sorting Nodes and Marking Self Connections
We create a list nodes
with unique values of nodes [1, 2, 3, 4]
. Self-connections are marked by setting g[i][i]
to True
for all i
in nodes
.
The list nodes
is then sorted by the length of their connection list in v
, resulting in [1, 3, 2, 4]
.
Step 4: Finding a Root and Checking Ancestor-Descendant Relations
We start iterating over the sorted nodes:
- Looking at
node 1
, it is connected to nodes2
and3
. There is no other node with the same length of connectivity so we proceed. - Next, we consider
node 3
, which has two connections, to nodes1
and4
. We see thatnode 3
is a descendant ofnode 1
, so the relationship holds. - We continue for the rest, but since the length of connectivity is decreasing, no further checks for the root are needed.
No multiple roots or violations of ancestor-descendant relationships are found.
Step 5: Determining the Final Output
Since we found exactly one root (node 1
) and there is no indication of potential multiple solutions (no equal
flagged), the algorithm returns 1
- meaning exactly one unique tree can be constructed with node 1
as its root, node 2
and node 3
as its children, and node 4
as a child of node 3
.
This walkthrough demonstrates how the algorithm efficiently constructs a graph, identifies a potential root, and validates the constraints to determine the number of unique rooted trees that can be constructed from the given pairs.
Solution Implementation
1from collections import defaultdict
2
3class Solution:
4 def checkWays(self, pairs: List[List[int]]) -> int:
5 # Create a graph as a 2D adjacency matrix initialized with False
6 graph = [[False] * 510 for _ in range(510)]
7
8 # Vertex map to keep track of adjacent vertices
9 adjacency_map = defaultdict(list)
10
11 # Fill the graph and the adjacency map based on the input pairs
12 for node1, node2 in pairs:
13 graph[node1][node2] = graph[node2][node1] = True
14 adjacency_map[node1].append(node2)
15 adjacency_map[node2].append(node1)
16
17 # Collect valid nodes and sort based on the number of adjacent vertices
18 nodes = [node for node in range(510) if adjacency_map[node]]
19 nodes.sort(key=lambda node: len(adjacency_map[node]))
20
21 # Flag to check if there's a pair of nodes with equal number of adjacent nodes
22 has_equal_adjacency_nodes = False
23
24 # Counter for root nodes
25 root_counter = 0
26
27 # Iterate through the nodes to validate the tree structure
28 for i, node in enumerate(nodes):
29 next_node_index = i + 1
30 # Find the next node which shares an edge with the current node
31 while next_node_index < len(nodes) and not graph[node][nodes[next_node_index]]:
32 next_node_index += 1
33
34 if next_node_index < len(nodes):
35 potential_parent = nodes[next_node_index]
36 # Check if the current node has the same number of adjacent nodes as a possible parent node
37 if len(adjacency_map[node]) == len(adjacency_map[potential_parent]):
38 has_equal_adjacency_nodes = True
39 # Check if the possible parent node is connected to all the adjacent nodes of the current node
40 for adjacent in adjacency_map[node]:
41 if not graph[potential_parent][adjacent]:
42 # The structure cannot form a tree if the connection is missing
43 return 0
44 else:
45 # Increment root counter if no parent node is found
46 root_counter += 1
47
48 # If there are more than one root, the structure cannot form a tree
49 if root_counter > 1:
50 return 0
51
52 # Return 2 if there's at least one pair of nodes with the same number of adjacent nodes, else return 1
53 return 2 if has_equal_adjacency_nodes else 1
54
1class Solution {
2 public int checkWays(int[][] pairs) {
3 // Adjacency matrix to represent the graph's connections
4 boolean[][] adjacencyMatrix = new boolean[510][510];
5
6 // Adjacency lists to track connections for each node
7 List<Integer>[] adjacencyLists = new List[510];
8 Arrays.setAll(adjacencyLists, k -> new ArrayList<>());
9
10 // Fill the adjacency matrix and lists based on given pairs
11 for (int[] pair : pairs) {
12 int node1 = pair[0], node2 = pair[1];
13 adjacencyMatrix[node1][node2] = true;
14 adjacencyMatrix[node2][node1] = true;
15 adjacencyLists[node1].add(node2);
16 adjacencyLists[node2].add(node1);
17 }
18
19 // Collect all nodes that are part of the graph
20 List<Integer> nodes = new ArrayList<>();
21 for (int i = 0; i < 510; ++i) {
22 if (!adjacencyLists[i].isEmpty()) {
23 nodes.add(i);
24 adjacencyMatrix[i][i] = true;
25 }
26 }
27
28 // Sort nodes based on the number of connections they have
29 nodes.sort(Comparator.comparingInt(node -> adjacencyLists[node].size()));
30 boolean isParentChildCountEqual = false;
31 int rootCount = 0;
32
33 // Loop to validate if the graph can represent a BST
34 for (int i = 0; i < nodes.size(); ++i) {
35 int currentNode = nodes.get(i);
36 int j = i + 1;
37
38 // Find a node which is connected to the current node
39 while (j < nodes.size() && !adjacencyMatrix[currentNode][nodes.get(j)]) {
40 ++j;
41 }
42
43 if (j < nodes.size()) {
44 int parentNode = nodes.get(j);
45 // Check if the parent node and the current node have the same number of children
46 if (adjacencyLists[currentNode].size() == adjacencyLists[parentNode].size()) {
47 isParentChildCountEqual = true;
48 }
49 // Validate that all connections of the current node are also connections of the parent node.
50 for (int childNode : adjacencyLists[currentNode]) {
51 if (!adjacencyMatrix[parentNode][childNode]) {
52 return 0; // Return 0 if the tree structure cannot be formed
53 }
54 }
55 } else {
56 ++rootCount; // Count potential root nodes
57 }
58 }
59
60 // There can only be one root in a BST
61 if (rootCount > 1) {
62 return 0;
63 }
64 // Return 2 if there's a possibility of multiple BSTs being formed, otherwise 1
65 return isParentChildCountEqual ? 2 : 1;
66 }
67}
68
1class Solution {
2public:
3 int checkWays(vector<vector<int>>& pairs) {
4 // Define a matrix to represent a graph and an adjacency list.
5 vector<vector<bool>> graph(510, vector<bool>(510));
6 vector<vector<int>> adjacencyList(510);
7
8 // Initialize the graph with the given pairs.
9 for (auto& pair : pairs) {
10 int node1 = pair[0], node2 = pair[1];
11 graph[node1][node2] = graph[node2][node1] = true;
12 adjacencyList[node1].push_back(node2);
13 adjacencyList[node2].push_back(node1);
14 }
15
16 // Create and populate a list of nodes present in the graph.
17 vector<int> nodesList;
18 for (int node = 1; node <= 500; ++node) {
19 if (adjacencyList[node].size()) {
20 nodesList.push_back(node);
21 graph[node][node] = true; // A node is always connected to itself.
22 }
23 }
24
25 // Sort the nodes based on the degree (number of neighbors), in ascending order.
26 sort(nodesList.begin(), nodesList.end(), [&](int nodeX, int nodeY) -> bool {
27 return adjacencyList[nodeX].size() < adjacencyList[nodeY].size();
28 });
29
30 // Will be used to detect if there are two nodes with the same degree.
31 bool hasEqualDegreeNodes = false;
32 // Counter to check how many possible root nodes we have.
33 int rootCount = 0;
34
35 // Iterate through each node to check if the graph can form a tree.
36 for (int i = 0; i < nodesList.size(); ++i) {
37 int currentNode = nodesList[i];
38 int nextNodeIndex = i + 1;
39
40 // Find the next node in the sorted list that is connected to the current node.
41 while (nextNodeIndex < nodesList.size() && !graph[currentNode][nodesList[nextNodeIndex]]) {
42 ++nextNodeIndex;
43 }
44
45 // If a connected node is found, validate further.
46 if (nextNodeIndex < nodesList.size()) {
47 int nextNode = nodesList[nextNodeIndex];
48 // Check if the current and next nodes have the same degree.
49 if (adjacencyList[currentNode].size() == adjacencyList[nextNode].size()) {
50 hasEqualDegreeNodes = true;
51 }
52
53 // Make sure all neighbors of the currentNode are connected to nextNode.
54 for (int neighbor : adjacencyList[currentNode])
55 if (!graph[nextNode][neighbor])
56 return 0; // Return 0 if the neighbor is not connected to the next node.
57
58 } else {
59 // If no connected node is found, increment root count as it might be a root.
60 ++rootCount;
61 }
62 }
63
64 // If there is more than one root, it can’t form a tree.
65 if (rootCount > 1) return 0;
66
67 // If there are nodes with equal degrees, there are two ways to arrange the tree.
68 if (hasEqualDegreeNodes) return 2;
69
70 // If the execution reaches here, there is exactly one way to arrange the tree.
71 return 1;
72 }
73};
74
1// Define a matrix to represent a graph and an adjacency list.
2const graph: boolean[][] = Array.from({ length: 510 }, () => Array(510).fill(false));
3const adjacencyList: number[][] = Array.from({ length: 510 }, () => []);
4
5interface NodePair {
6 node1: number;
7 node2: number;
8}
9
10// Initialize the graph with the given pairs.
11function initializeGraph(pairs: NodePair[]): void {
12 pairs.forEach(pair => {
13 const { node1, node2 } = pair;
14 graph[node1][node2] = graph[node2][node1] = true;
15 adjacencyList[node1].push(node2);
16 adjacencyList[node2].push(node1);
17 });
18}
19
20// Creates and populates a list of nodes present in the graph.
21function populateNodesList(): number[] {
22 const nodesList: number[] = [];
23 for (let node = 1; node <= 500; ++node) {
24 if (adjacencyList[node].length) {
25 nodesList.push(node);
26 graph[node][node] = true; // A node is always connected to itself.
27 }
28 }
29 return nodesList;
30}
31
32// Sort the nodes based on the degree (number of neighbors), in ascending order.
33function sortNodesList(nodesList: number[]): void {
34 nodesList.sort((nodeX, nodeY) => adjacencyList[nodeX].length - adjacencyList[nodeY].length);
35}
36
37// CheckWays function determines the number of valid BSTs that can be formed from the given graph.
38function checkWays(pairs: NodePair[]): number {
39 initializeGraph(pairs);
40 const nodesList = populateNodesList();
41 sortNodesList(nodesList);
42
43 let hasEqualDegreeNodes = false; // Used to detect if there are two nodes with the same degree.
44 let rootCount = 0; // Counter to check how many possible root nodes we have.
45
46 // Iterate through each node to check if the graph can form a tree.
47 for (let i = 0; i < nodesList.length; ++i) {
48 const currentNode = nodesList[i];
49 let nextNodeIndex = i + 1;
50
51 // Find the next node in the sorted list that is connected to the current node.
52 while (nextNodeIndex < nodesList.length && !graph[currentNode][nodesList[nextNodeIndex]]) {
53 ++nextNodeIndex;
54 }
55
56 if (nextNodeIndex < nodesList.length) {
57 const nextNode = nodesList[nextNodeIndex];
58 // Check if the current and next nodes have the same degree.
59 if (adjacencyList[currentNode].length === adjacencyList[nextNode].length) {
60 hasEqualDegreeNodes = true;
61 }
62
63 // Make sure all neighbors of the currentNode are connected to nextNode.
64 for (const neighbor of adjacencyList[currentNode]) {
65 if (!graph[nextNode][neighbor])
66 return 0; // Return 0 if the neighbor is not connected to the next node.
67 }
68
69 } else {
70 // If no connected node is found, increment root count as it might be a root.
71 rootCount++;
72 }
73 }
74
75 // If there is more than one root, it can’t form a tree.
76 if (rootCount > 1) return 0;
77
78 // If there are nodes with equal degrees, there are two ways to arrange the tree.
79 if (hasEqualDegreeNodes) return 2;
80
81 // If the execution reaches here, there is exactly one way to arrange the tree.
82 return 1;
83}
84
Time and Space Complexity
Time Complexity
The primary operations in this code are as follows:
-
Looping over the pairs to fill the adjacency matrix
g
and create the adjacency listv
. This has a time complexity ofO(P)
, whereP
is the number of pairs. -
Looping over the range
510
to find and sort the nodes. The finding of nodes has a time complexity ofO(N)
, whereN
is the total number of different nodes which could be up to510
in the worst case. -
The sorting of nodes has a time complexity of
O(N log N)
due to the sort operation. -
The nested loops where it compares every
x
withy
and iterates through allz
inv[x]
. In the worst case, this results in looping through all edges for each node, giving it a time complexity ofO(N * P)
.
Given that the largest number of nodes is capped at 510
, the overall time complexity is:
O(P + N log N + N * P) = O(P + N * P) = O(N * P)
, because N
is fixed and small, we can consider it a constant and simplify to O(P)
.
Space Complexity
The space complexity consists of the storage for:
-
The adjacency matrix
g
, which is a fixed size of510x510
. This constitutes a space complexity ofO(1)
as it does not grow with the input size. -
The adjacency list
v
, which can potentially have all pairs stored, resulting in a space complexity ofO(P)
. -
The
nodes
list containing at mostN
elements, addingO(N)
to the space complexity.
As a result, the overall space complexity is O(P + N) = O(P)
because N
is a fixed constant.
Learn more about how to find time and space complexity quickly using problem constraints.
Which of the following is a good use case for backtracking?
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https algomonster s3 us east 2 amazonaws com cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could
LeetCode Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way we
Want a Structured Path to Master System Design Too? Don’t Miss This!