1. Problem Statement
You are given a 0-indexed 2D integer array pairs where pairs[i] = [start_i, end_i]. An arrangement of pairs is valid if for every index i where 1 <= i < pairs.length, we have end_{i-1} == start_i.
Return any valid arrangement of pairs. You may assume a valid arrangement exists.
Input: pairs = [[5,1],[4,5],[11,9],[9,4]]
Output: [[11,9],[9,4],[4,5],[5,1]]
2. Approach: Eulerian Path (Hierholzer's Algorithm)
Connecting dominoes like end == start is equivalent to finding a path in a graph that visits every edge exactly once. This is known as an Eulerian Path.
- Build Graph & Degrees:
- Treat each pair
[u, v]as a directed edge fromutov. - Track the
outDegreeandinDegreeof every node.
- Treat each pair
- Find Start Node:
- In an Eulerian Path, the start node has
outDegree - inDegree == 1. - If all nodes have
outDegree == inDegree, it's an Eulerian Circuit, and any node can be the start.
- In an Eulerian Path, the start node has
- Hierholzer's Algorithm (DFS):
- Start DFS from the start node.
- Follow an outgoing edge, remove it from the graph, and recurse.
- When a node has no more outgoing edges, push it to a result list.
- Reverse: The result list will have the path in reverse order. Reverse it to get the final valid arrangement.
3. Java Implementation
public int[][] validArrangement(int[][] pairs) {
Map<Integer, Stack<Integer>> adj = new HashMap<>();
Map<Integer, Integer> outDegree = new HashMap<>();
Map<Integer, Integer> inDegree = new HashMap<>();
// 1. Build Graph
for (int[] pair : pairs) {
int u = pair[0], v = pair[1];
adj.computeIfAbsent(u, k -> new Stack<>()).push(v);
outDegree.put(u, outDegree.getOrDefault(u, 0) + 1);
inDegree.put(v, inDegree.getOrDefault(v, 0) + 1);
}
// 2. Find Start Node
int startNode = pairs[0][0]; // Default start
for (int node : outDegree.keySet()) {
if (outDegree.get(node) - inDegree.getOrDefault(node, 0) == 1) {
startNode = node;
break;
}
}
// 3. Hierholzer's DFS
List<Integer> path = new ArrayList<>();
dfs(adj, startNode, path);
// 4. Construct Result (Reverse)
int[][] res = new int[pairs.length][2];
Collections.reverse(path);
for (int i = 0; i < path.size() - 1; i++) {
res[i][0] = path.get(i);
res[i][1] = path.get(i + 1);
}
return res;
}
private void dfs(Map<Integer, Stack<Integer>> adj, int curr, List<Integer> path) {
Stack<Integer> neighbors = adj.getOrDefault(curr, new Stack<>());
while (!neighbors.isEmpty()) {
int next = neighbors.pop();
dfs(adj, next, path);
}
path.add(curr);
}
4. 5-Minute "Video-Style" Walkthrough
- The "Aha!" Moment: It looks like Topological Sort, but it's not. Topo Sort visits every node once. We need to use every pair (edge) exactly once. This is the definition of an Eulerian Path.
- The Start Node Logic: If a path goes straight through a node, it enters once and leaves once (
in == out). The only exception is the very start of the entire chain (it leaves one more time than it enters:out - in = 1) and the very end (in - out = 1). - The Post-Order Trap: Why do we add to
pathafter thewhileloop? Because we might take a sub-cycle that returns to the current node. By adding nodes after exploring all edges, we guarantee that dead ends are appended first (which become the end of the path when reversed).
5. Interview Discussion
- Interviewer: "What is the time complexity?"
- You: "O(V + E), where V is the number of unique numbers and E is the number of pairs. We visit each edge exactly once during the DFS."
- Interviewer: "Why use a Stack in the adjacency map?"
- You: "A Stack or Queue allows us to retrieve and remove an edge in O(1) time. This prevents us from traversing the same pair twice."