Valid Arrangement of Pairs
HardPrompt
You are given a 0-indexed 2D integer array pairs where pairs[i] = [starti, endi]. An arrangement of pairs is valid if for every index i where 1 <= i < pairs.length, we have endi-1 == starti.
Return any valid arrangement of pairs.
Note: The inputs will be generated such that there exists a valid arrangement of pairs.
Example 1:
Input: pairs = [[5,1],[4,5],[11,9],[9,4]]
Output: [[11,9],[9,4],[4,5],[5,1]]
Explanation:
This is a valid arrangement since endi-1 always equals starti.
end0 = 9 == 9 = start1
end1 = 4 == 4 = start2
end2 = 5 == 5 = start3Example 2:
Input: pairs = [[1,3],[3,2],[2,1]]
Output: [[1,3],[3,2],[2,1]]
Explanation:
This is a valid arrangement since endi-1 always equals starti.
end0 = 3 == 3 = start1
end1 = 2 == 2 = start2
The arrangements [[2,1],[1,3],[3,2]] and [[3,2],[2,1],[1,3]] are also valid.Example 3:
Input: pairs = [[1,2],[1,3],[2,1]]
Output: [[1,2],[2,1],[1,3]]
Explanation:
This is a valid arrangement since endi-1 always equals starti.
end0 = 2 == 2 = start1
end1 = 1 == 1 = start2
Constraints:
1 <= pairs.length <= 105pairs[i].length == 20 <= starti, endi <= 109starti != endi- No two pairs are exactly the same.
- There exists a valid arrangement of
pairs.
Approaches
3 approaches with complexity analysis and trade-offs.
This approach involves generating every possible ordering (permutation) of the input pairs and checking each one to see if it forms a valid arrangement. A valid arrangement requires that for any two consecutive pairs [start1, end1] and [start2, end2], end1 must equal start2.
Algorithm
-
- Define a recursive helper function
permute(list, start_index)to generate all permutations of the inputpairs.
- Define a recursive helper function
-
- The base case for the recursion is when
start_indexreaches the end of the list. At this point, a complete permutation has been generated.
- The base case for the recursion is when
-
- For each generated permutation, check if it's a valid arrangement by iterating from the second pair and ensuring
pair[i-1][1] == pair[i][0].
- For each generated permutation, check if it's a valid arrangement by iterating from the second pair and ensuring
-
- Since a valid arrangement is guaranteed to exist, the first one found is the answer. Store it and terminate the search.
-
- The recursive step involves swapping the element at
start_indexwith all subsequent elements to explore different orderings, and backtracking after the recursive call.
- The recursive step involves swapping the element at
Walkthrough
The core idea is to explore all N! permutations of the pairs array, where N is the number of pairs. We can implement a recursive function that generates these permutations. The function, say generatePermutations(index, currentPermutation), would place each available pair at the current index and recurse for the next index. Once a full permutation of size N is formed, we validate it. Validation involves iterating from the second pair to the end and checking the condition pairs[i-1][1] == pairs[i][0]. Since the problem guarantees that a valid arrangement exists, the first one we find is a valid answer. However, this approach is computationally infeasible for the given constraints.
// Conceptual structure for brute-force. This will time out.class Solution { int[][] result; public int[][] validArrangement(int[][] pairs) { List<int[]> pairList = new ArrayList<>(); for (int[] p : pairs) { pairList.add(p); } permute(pairList, 0); return result; } private void permute(List<int[]> arr, int k) { if (result != null) return; // Already found a solution if (k == arr.size()) { if (isValid(arr)) { result = new int[arr.size()][2]; for (int i = 0; i < arr.size(); i++) { result[i] = arr.get(i); } } return; } for (int i = k; i < arr.size(); i++) { Collections.swap(arr, i, k); permute(arr, k + 1); Collections.swap(arr, k, i); // backtrack } } private boolean isValid(List<int[]> arrangement) { for (int i = 1; i < arrangement.size(); i++) { if (arrangement.get(i - 1)[1] != arrangement.get(i)[0]) { return false; } } return true; }}Complexity
Time
O(N! * N). There are `N!` permutations, and validating each takes `O(N)` time.
Space
O(N) for the recursion stack depth.
Trade-offs
Pros
Conceptually simple to understand.
Cons
Extremely inefficient with factorial time complexity.
Guaranteed to cause a 'Time Limit Exceeded' (TLE) error for the given constraints.
This is an improvement over the brute-force approach. Instead of generating full permutations and then checking, we build the arrangement one pair at a time. We prune search branches that cannot lead to a valid solution.
Algorithm
-
- Pre-process the pairs to build a map where keys are starting numbers and values are lists of pairs that begin with that number. This allows for efficient lookups of next possible pairs.
-
- Use a boolean array
usedto track which pairs (by their original index) have been included in the current path.
- Use a boolean array
-
- Define a recursive function
backtrack(lastPair, currentPath).
- Define a recursive function
-
- The base case: if
currentPath.size()equals the total number of pairs, a valid arrangement has been found. Store it and returntrue.
- The base case: if
-
- In the recursive step, get the
endvalue of thelastPair. Find all unused pairs that start with thisendvalue.
- In the recursive step, get the
-
- For each such candidate pair, add it to the path, mark it as used, and make a recursive call.
-
- If the recursive call returns
true, it means a solution was found down that branch, so propagatetrueupwards.
- If the recursive call returns
-
- If the call returns
false, backtrack by removing the pair from the path and unmarking it as used.
- If the call returns
-
- To start the process, iterate through all pairs, trying each one as the first pair in the arrangement.
Walkthrough
We can model this as finding a path of length N in a state-space graph. We start by trying each pair as the first pair in the arrangement. Then, we recursively search for the next pair. If the current path ends with pair p1, the next pair p2 must have p2.start == p1.end. We use a boolean array or a bitmask to keep track of which pairs have already been included in the current path to avoid reusing pairs. The search backtracks if it hits a dead end (i.e., no unused pair can be appended). The search terminates successfully when a path of length N (using all pairs) is formed.
// Conceptual structure for backtracking. This is also likely to time out.class Solution { int[][] result; int n; int[][] pairs; public int[][] validArrangement(int[][] pairs) { this.n = pairs.length; this.pairs = pairs; boolean[] used = new boolean[n]; List<int[]> path = new ArrayList<>(); // Try starting with each pair for (int i = 0; i < n; i++) { path.add(pairs[i]); used[i] = true; if (backtrack(path, used)) { return result; } used[i] = false; path.remove(path.size() - 1); } return null; // Should not be reached } private boolean backtrack(List<int[]> path, boolean[] used) { if (path.size() == n) { result = path.toArray(new int[n][]); return true; } int[] lastPair = path.get(path.size() - 1); int endNode = lastPair[1]; for (int i = 0; i < n; i++) { if (!used[i] && pairs[i][0] == endNode) { used[i] = true; path.add(pairs[i]); if (backtrack(path, used)) { return true; } path.remove(path.size() - 1); used[i] = false; // backtrack } } return false; }}Complexity
Time
O(N * k!) or similar exponential complexity, where `k` is the maximum out-degree of any node. This is better than `O(N!)` but still too slow.
Space
O(N) for the recursion stack and storing the path.
Trade-offs
Pros
More efficient than blind permutation generation due to pruning.
Cons
Still has an exponential time complexity in the worst case.
Will likely time out for the given constraints.
Implementation can be complex, especially handling cases with duplicate numbers in pairs.
The problem can be modeled as finding an Eulerian path in a directed graph. The numbers are vertices, and each pair [start, end] is a directed edge from start to end. An Eulerian path visits every edge exactly once. Since the problem guarantees a solution exists, we just need to find this path using a standard algorithm like Hierholzer's.
Algorithm
-
- Build Graph and Degrees:
- Create an adjacency list
adj(e.g.,Map<Integer, Deque<Integer>>) to store the graph. - Create a degree map
degree(e.g.,Map<Integer, Integer>) to storeout-degree - in-degreefor each node. - Iterate through each pair
[u, v]: add an edgeu -> vtoadj, incrementdegree[u], and decrementdegree[v].
-
- Find Start Node:
- The start node of the Eulerian path is the unique node with
degree == 1. - Iterate through the
degreemap to find this node. - If no such node exists, the graph has an Eulerian circuit. Any node with outgoing edges can be the start node (e.g.,
pairs[0][0])
-
- Hierholzer's Algorithm (Iterative DFS):
- Use a
stackfor the traversal and apathlist to store the result. - Push the
startNodeonto thestack. - While the
stackis not empty:- Peek at the current node
uon top of the stack. - If
uhas any unvisited outgoing edges (i.e.,adj.get(u)is not empty), pop an edgeu -> v, and pushvonto the stack. - If
uhas no more outgoing edges, popufrom the stack and add it to thepathlist.
- Peek at the current node
-
- Construct Final Result:
- The
pathlist is generated in reverse post-order. Reverse it to get the correct vertex sequence. - Create the final
int[][]arrangement by taking consecutive nodes from thepathlist:[path.get(i), path.get(i+1)].
Walkthrough
1. Graph Representation: We first build a graph. An adjacency list, represented by a Map<Integer, Deque<Integer>>, is suitable. The keys are the source vertices, and the values are deques of destination vertices. We also compute the in-degrees and out-degrees of all vertices to find the starting point of the path. A Map<Integer, Integer> degree can store out-degree - in-degree.
2. Finding the Start Node: An Eulerian path starts at a vertex v where out-degree(v) - in-degree(v) = 1. If no such vertex exists, the graph has an Eulerian circuit, and any vertex can be a starting point. We iterate through our degree map to find this special start node. If not found, we can default to pairs[0][0].
3. Path Finding (Hierholzer's Algorithm): We use a post-order DFS traversal. An iterative approach with a stack is common.
- Push the start node onto a stack.
- While the stack is not empty, look at the top vertex
u. - If
uhas unvisited outgoing edges, pick one (e.g.,u -> v), pushvonto the stack, and remove the edgeu -> vfrom the graph to mark it as visited. - If
uhas no more outgoing edges, pop it from the stack and add it to a result listpath.
4. Final Arrangement: The path list we constructed contains the sequence of vertices in reverse order. We reverse it to get the correct path. Then, we construct the final int[][] result by creating pairs [path[i], path[i+1]] for i from 0 to path.length - 2.
import java.util.*; class Solution { public int[][] validArrangement(int[][] pairs) { int n = pairs.length; Map<Integer, Deque<Integer>> adj = new HashMap<>(); Map<Integer, Integer> degree = new HashMap<>(); for (int[] pair : pairs) { int u = pair[0]; int v = pair[1]; adj.computeIfAbsent(u, k -> new ArrayDeque<>()).add(v); degree.put(u, degree.getOrDefault(u, 0) + 1); degree.put(v, degree.getOrDefault(v, 0) - 1); } int startNode = -1; for (Map.Entry<Integer, Integer> entry : degree.entrySet()) { if (entry.getValue() == 1) { startNode = entry.getKey(); break; } } if (startNode == -1) { // It's an Eulerian circuit, start from any node with an edge startNode = pairs[0][0]; } List<Integer> path = new ArrayList<>(); Deque<Integer> stack = new ArrayDeque<>(); stack.push(startNode); while (!stack.isEmpty()) { int u = stack.peek(); if (adj.containsKey(u) && !adj.get(u).isEmpty()) { int v = adj.get(u).pop(); stack.push(v); } else { path.add(stack.pop()); } } Collections.reverse(path); int[][] result = new int[n][2]; for (int i = 0; i < n; i++) { result[i][0] = path.get(i); result[i][1] = path.get(i + 1); } return result; }}Complexity
Time
O(N), where `N` is the number of pairs. Each step (building graph, finding start, traversing) takes time proportional to the number of edges (`N`) and vertices (`U`).
Space
O(N). The adjacency list, degree map, and path/stack all require space proportional to the number of pairs `N` and unique nodes `U` (where `U <= 2N`).
Trade-offs
Pros
Highly efficient with linear time complexity.
Guaranteed to find the solution correctly and quickly.
It's the standard, optimal algorithm for this type of problem.
Cons
Requires knowledge of graph theory (Eulerian paths).
Implementation is more involved than naive approaches.
Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.