1743. Restore the Array From Adjacent Pairs
Problem Description
In this problem, you have forgotten an integer array nums
that consists of n
unique elements. Despite not remembering the array itself, you do recall every pair of adjacent elements in nums
. You are given a 2D integer array adjacentPairs
with a size of n - 1
. Each element adjacentPairs[i] = [u_i, v_i]
indicates that the elements u_i
and v_i
are adjacent in the array nums
.
It is guaranteed that every adjacent pair of elements nums[i]
and nums[i+1]
will be represented in adjacentPairs
, either as [nums[i], nums[i+1]]
or [nums[i+1], nums[i]]
. These pairs can be listed in any order. Your task is to reconstruct and return the original array nums
. If there are several possible arrays that fit the adjacent pairs, returning any one of them is acceptable.
Intuition
To restore the array, we need to recognize that the description constructs a path (like a one-dimensional graph) where each number except for the two endpoints has exactly two neighbors (think of nodes in a graph). The graph formed by adjacentPairs
will have nodes with the following properties: the starting and ending elements (the ones found at the beginning and end of the array nums
) will have only one adjacent element in adjacentPairs
(they have a degree of one), while all other elements will have two adjacent elements (they have a degree of two). Identifying the unique starting or ending point allows us to sequentially rebuild the array from one end to the other.
The solution approach begins by creating a graph representation where each element of adjacentPairs
is a node and the adjacent elements are the connected edges. Once the graph is built, we look for a node with a degree of one, which we know will be one of the endpoints in our original array. After identifying an endpoint, we can begin traversing the graph to 'visit' each node exactly once. The traversal is done by always moving to the 'unvisited' neighbor of the current node, adding each visited node to our answer array, ans
. This process continues until the entire array is reconstructed, ensuring that all pairs are honored as they were in the original array.
Solution Approach
The Solution
class in the provided Python code implements a function called restoreArray
to solve the problem by reconstructing the original array from the given pairs of adjacent integers.
Here's a step-by-step walkthrough of the solution approach:
-
Create a Graph Representation: To keep track of the adjacent nodes (the integers that can come before or after a given integer in the array), we use a defaultdict of lists, named
g
. A defaultdict is used here because it conveniently allows appending to lists for keys that have not been explicitly set. For eacha, b
pair inadjacentPairs
, we addb
as an adjacent node toa
and vice versa.g = defaultdict(list) for a, b in adjacentPairs: g[a].append(b) g[b].append(a)
-
Identify an Endpoint: Since we know the original array has unique elements with exactly two endpoints, we start by finding an integer that appears only once in the adjacency list (this has a graph degree of one) – meaning it's an endpoint.
for i, v in g.items(): if len(v) == 1: ans[0] = i ans[1] = v[0] break
-
Reconstruct the Array: Once we have one endpoint, we initialize our answer array
ans
with the starting endpoint as the first element. We also know what follows it immediately given our graph.From there, we traverse the graph to rebuild the original array. We iterate from the second to the last element of the
ans
array and at each step, pull the adjacent vertices from our graph. Since each element has two neighboring elements (except the endpoints), we can easily determine the next element in the array by checking which of the two vertices is not the one we've just visited (i.e., not the second-to-last element inans
).n = len(adjacentPairs) + 1 ans = [0] * n # ... previous endpoint identification code ... for i in range(2, n): v = g[ans[i - 1]] ans[i] = v[0] if v[1] == ans[i - 2] else v[1]
-
Return the Result: After the loop completes,
ans
contains the reconstructed array following the original order.
The nature of the problem aligns well with graph traversal algorithms – specifically, using a simple walk technique, as we are guaranteed a valid path that visits each node exactly once. The time complexity is O(N) because we visit each vertex exactly once, where N is the number of elements in the original array.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
To illustrate the solution approach, let's take a small example. Suppose we have the following pairs of adjacent elements from the problem statement:
adjacentPairs = [[4,3], [1,2], [3,1]]
From these pairs, we need to reconstruct the original integer array nums
. Here's how we would apply the solution approach:
-
Create a Graph Representation: First, we create a graph by adding each pair to our adjacency list, representing each number and its corresponding adjacent numbers.
We get the following graph:
{ 4: [3], 3: [4, 1], 1: [2, 3], 2: [1] }
-
Identify an Endpoint: We look for an integer that appears only once in the adjacency list, as this will be an endpoint. In our graph, both
4
and2
appear only once, so either can be chosen as the starting endpoint.Assume we pick
4
as the starting point. Our answer arrayans
tentatively starts with:ans = [4, _]
Since
4
is connected to3
, the next element is3
:ans = [4, 3, _]
-
Reconstruct the Array: From
3
, we look at its neighbors, which are4
(which we've just visited) and1
. So the next element is1
, and our array now looks like:ans = [4, 3, 1, _]
Continuing this, the next number we add is the neighbor of
1
which isn't3
(since we've already been there). That's2
, resulting in:ans = [4, 3, 1, 2]
The array is now fully reconstructed.
-
Return the Result: Finally, the reconstructed array
ans = [4, 3, 1, 2]
is the original array, considering the givenadjacentPairs
. Therefore, the result of therestoreArray
function with the given input would be[4, 3, 1, 2]
.
In this example, the algorithm successfully reconstructs the original array from the pairs of adjacent elements. The use of graph representation simplifies the problem, making it more approachable and easier to solve efficiently.
Solution Implementation
1from collections import defaultdict
2
3class Solution:
4 def restoreArray(self, adjacentPairs):
5 # Create a graph using a dictionary where each key has an associated list of its adjacent nodes
6 graph = defaultdict(list)
7 for pair in adjacentPairs:
8 # Since it's an undirected graph, add each node to the list of its neighboring node
9 graph[pair[0]].append(pair[1])
10 graph[pair[1]].append(pair[0])
11
12 # The original array would have a length of one more than the number of adjacent pairs
13 array_length = len(adjacentPairs) + 1
14 restored_array = [0] * array_length
15
16 # Find the first and the second elements of the array, which are the endpoints of the path
17 # (having only one adjacent node)
18 for node, neighbors in graph.items():
19 if len(neighbors) == 1: # Endpoints will only have one neighbor
20 restored_array[0] = node
21 restored_array[1] = neighbors[0]
22 break
23
24 # Construct the rest of the array
25 for i in range(2, array_length):
26 # Find the next adjacent node that is not the previous element in the array
27 current_neighbors = graph[restored_array[i - 1]]
28 # If the current node has only one neighbor, it must be the next element; otherwise
29 # it is the one that is not the previously used element
30 restored_array[i] = current_neighbors[0] if current_neighbors[1] == restored_array[i - 2] else current_neighbors[1]
31
32 # Return the reconstructed array
33 return restored_array
34
1// Solution class to restore the array from its adjacent pairs
2class Solution {
3 // Method to restore array using adjacent pairs
4 public int[] restoreArray(int[][] adjacentPairs) {
5 // Calculate the total number of unique elements
6 int n = adjacentPairs.length + 1;
7 // Create a graph using a Map to store element and its adjacent elements
8 Map<Integer, List<Integer>> graph = new HashMap<>();
9
10 // Iterate over each adjacent pair
11 for (int[] edge : adjacentPairs) {
12 // Unpack the pair
13 int a = edge[0], b = edge[1];
14 // Build the adjacency list for each element in the pair
15 graph.computeIfAbsent(a, k -> new ArrayList<>()).add(b);
16 graph.computeIfAbsent(b, k -> new ArrayList<>()).add(a);
17 }
18
19 // Initialize the array to hold the restored sequence
20 int[] restoredArray = new int[n];
21
22 // Find the first element (head of array) which will be an element with only one neighbor
23 for (Map.Entry<Integer, List<Integer>> entry : graph.entrySet()) {
24 if (entry.getValue().size() == 1) {
25 // Found the first element, which is the head of our restored array
26 restoredArray[0] = entry.getKey();
27 // The only neighbor is the second element
28 restoredArray[1] = entry.getValue().get(0);
29 break;
30 }
31 }
32
33 // Reconstruct the array starting from index 2
34 for (int i = 2; i < n; ++i) {
35 // Get the neighbors of the last added element in restoredArray
36 List<Integer> neighbors = graph.get(restoredArray[i - 1]);
37 // Check the two neighbors to determine which is the next element in sequence
38 // If one neighbor is the same as the previous element in restoredArray, choose the other one
39 restoredArray[i] = neighbors.get(0) == restoredArray[i - 2] ? neighbors.get(1) : neighbors.get(0);
40 }
41
42 // Return the restored array
43 return restoredArray;
44 }
45}
46
1#include <vector>
2#include <unordered_map>
3using namespace std;
4
5class Solution {
6public:
7 vector<int> restoreArray(vector<vector<int>>& adjacent_pairs) {
8 // Calculate the number of elements in the original array.
9 int num_elements = adjacent_pairs.size() + 1;
10
11 // Create a graph represented as an adjacency list.
12 unordered_map<int, vector<int>> graph;
13 for (auto& edge : adjacent_pairs) {
14 int a = edge[0], b = edge[1];
15 graph[a].push_back(b);
16 graph[b].push_back(a);
17 }
18
19 // Initialize the answer array with the given size.
20 vector<int> answer(num_elements);
21
22 // Find the starting element which is the one with only one neighbor.
23 for (auto& pair : graph) {
24 if (pair.second.size() == 1) {
25 answer[0] = pair.first;
26 answer[1] = pair.second[0];
27 break;
28 }
29 }
30
31 // Iterate over each element in the answer to find the next element.
32 for (int i = 2; i < num_elements; ++i) {
33 // Access the neighbors of the last added element.
34 auto neighbors = graph[answer[i - 1]];
35
36 // The next element is the one which is not the previously added element.
37 answer[i] = neighbors[0] == answer[i - 2] ? neighbors[1] : neighbors[0];
38 }
39
40 // Return the restored original array.
41 return answer;
42 }
43};
44
1type AdjacencyList = Map<number, number[]>;
2
3// Restore the array from the given adjacency pairs
4function restoreArray(adjacentPairs: number[][]): number[] {
5 // Calculate the number of elements in the original array
6 const numElements: number = adjacentPairs.length + 1;
7
8 // Create a graph represented as an adjacency list
9 const graph: AdjacencyList = new Map<number, number[]>();
10 for (const [a, b] of adjacentPairs) {
11 if (!graph.has(a)) graph.set(a, []);
12 if (!graph.has(b)) graph.set(b, []);
13 graph.get(a)!.push(b);
14 graph.get(b)!.push(a);
15 }
16
17 // Initialize the answer array with the given size
18 const answer: number[] = new Array(numElements);
19
20 // Find the starting element which is the one with only one neighbor
21 for (const [key, neighbors] of graph) {
22 if (neighbors.length === 1) {
23 answer[0] = key;
24 answer[1] = neighbors[0];
25 break;
26 }
27 }
28
29 // Iterate over each element in the answer to find the next element
30 for (let i = 2; i < numElements; ++i) {
31 // Access the neighbors of the last added element
32 const neighbors = graph.get(answer[i - 1])!;
33
34 // The next element is the one which is not the previously added element
35 answer[i] = neighbors[0] === answer[i - 2] ? neighbors[1] : neighbors[0];
36 }
37
38 // Return the restored original array
39 return answer;
40}
41
42// Example usage:
43const adjacentPairs = [[2, 1], [3, 4], [3, 2]];
44const restoredArray = restoreArray(adjacentPairs);
45console.log(restoredArray); // Output will be the restored array based on the given adjacent pairs
46
Time and Space Complexity
Time Complexity
The time complexity of the given code can be analyzed as follows:
-
Building the graph
g
has a complexity ofO(m)
, wherem
is the number of adjacent pairs. Every adjacent pair requires two insert operations in the dictionary. -
Finding the starting node (where the degree is 1) has a maximum complexity of
O(n)
wheren
is the number of unique nodes. This is because in the worst case, we have to check every entry in the dictionary. -
Reconstructing the array
ans
requires us to iteraten - 2
times (since the first two elements are already filled), and in each iteration, we find the next node inO(1)
time (since we're dealing with at most two elements in a list, and we know one of them is the previous node). So, the complexity for this part isO(n)
.
Overall, the time complexity is O(m + n + n) => O(m + 2n)
. Since m = n - 1
, we simplify the time complexity to O(n)
.
Space Complexity
The space complexity of the given code can be analyzed as follows:
-
The graph
g
will contain each unique node and its adjacent nodes. Since each edge contributes to two nodes' adjacency lists, the space needed forg
isO(2m)
. -
The
ans
array will containn
elements, so it requiresO(n)
space.
Together, the space complexity for storing the graph and the array amounts to O(2m + n)
. Since m = n - 1
, we can simplify the space complexity to O(n)
.
Hence, both time and space complexities of the given code are O(n)
.
Learn more about how to find time and space complexity quickly using problem constraints.
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
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
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https algomonster s3 us east 2 amazonaws com recursion jpg You first
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!