444. Sequence Reconstruction π
Problem Description
You are given an integer array nums
of length n
that is a permutation of integers from 1 to n. You also have a 2D array sequences
where each sequences[i]
is a subsequence of nums
.
Your task is to determine if nums
is both:
- The shortest possible supersequence that contains all given subsequences
- The only unique shortest supersequence
A supersequence is a sequence that contains all the given subsequences as its subsequences. A subsequence is derived from a sequence by deleting some or no elements without changing the order of remaining elements.
For example:
- Given
sequences = [[1,2],[1,3]]
, there are two possible shortest supersequences:[1,2,3]
and[1,3,2]
. Since there are multiple valid shortest supersequences, the answer would befalse
. - Given
sequences = [[1,2],[1,3],[1,2,3]]
, the only shortest supersequence is[1,2,3]
. While[1,2,3,4]
is also a valid supersequence, it's not the shortest. Since there's only one shortest supersequence, the answer would betrue
.
The solution uses topological sorting to solve this problem. For each consecutive pair of elements (a, b)
in every subsequence, we create a directed edge a β b
in a graph. We then perform topological sorting using a queue-based approach:
- Build a directed graph from the subsequences and calculate in-degrees for each node
- Add all nodes with in-degree 0 to a queue
- Process nodes one at a time when the queue has exactly one element (ensuring unique ordering)
- For each processed node, reduce the in-degree of its neighbors and add any that reach 0 to the queue
- If at any point the queue has more than one element, multiple orderings are possible
- Return
true
if the queue remains empty after processing (unique shortest supersequence exists),false
otherwise
Intuition
The key insight is recognizing that this problem is about finding a unique topological ordering. Each subsequence gives us ordering constraints - if element a
appears before element b
in a subsequence, then a
must appear before b
in any valid supersequence.
Think of it this way: each subsequence tells us "these elements must appear in this relative order." If we combine all these constraints, we're essentially building a directed graph where an edge a β b
means "a must come before b."
The critical observation is that for nums
to be the only shortest supersequence, there must be exactly one valid way to order all elements while respecting these constraints. This happens when at each step of building the sequence, we have exactly one choice of which element to place next.
In topological sorting terms, this means:
- When we start, there should be exactly one element with no prerequisites (in-degree 0)
- After placing that element, there should be exactly one new element that becomes available
- This pattern continues until all elements are placed
If at any point we have multiple elements with in-degree 0 simultaneously, it means we could choose any of them next, leading to multiple valid orderings. For example, if both elements 2 and 3 have no prerequisites after placing element 1, we could create either [1,2,3,...]
or [1,3,2,...]
as valid supersequences.
The shortest supersequence will have length n
(using each number exactly once). By performing topological sorting and checking that we always have exactly one choice at each step, we can verify both that nums
is the shortest possible supersequence and that it's the only one.
Solution Approach
The implementation uses topological sorting with a queue to verify if there's a unique ordering:
1. Graph Construction:
- Create an adjacency list
g
withn
empty lists (one for each number 1 to n) - Create an
indeg
array to track the in-degree of each node - For each subsequence, iterate through consecutive pairs using
pairwise(seq)
- For each pair
(a, b)
, add a directed edge froma-1
tob-1
(converting to 0-indexed) - Increment the in-degree of
b-1
2. Initialize Queue:
- Use a
deque
to store all nodes with in-degree 0 - These are the nodes that can be placed first (no prerequisites)
3. Process Nodes:
- While the queue has exactly one element:
- Remove the single element
i
from the queue - For each neighbor
j
of nodei
:- Decrement
indeg[j]
by 1 (removing the edgei β j
) - If
indeg[j]
becomes 0, addj
to the queue
- Decrement
- Remove the single element
- The loop continues only when there's exactly one choice at each step
4. Verify Result:
- After the loop ends, check if the queue is empty
- If
len(q) == 0
: All nodes were processed with a unique ordering β returntrue
- If
len(q) > 0
: Multiple nodes have in-degree 0 simultaneously, meaning multiple valid orderings exist β returnfalse
The algorithm works because:
- If the queue ever has more than 1 element, the loop breaks, indicating multiple possible orderings
- If the queue becomes empty before all nodes are processed, there's a cycle (impossible since we have a valid permutation)
- Only when exactly
n
nodes are processed one-by-one with no ambiguity do we have a unique shortest supersequence
Time Complexity: O(m)
where m
is the total number of elements across all subsequences
Space Complexity: O(n)
for the graph and in-degree arrays
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with nums = [1,2,3]
and sequences = [[1,2], [1,3], [2,3]]
.
Step 1: Build the Graph
- From subsequence
[1,2]
: Add edge 1β2 - From subsequence
[1,3]
: Add edge 1β3 - From subsequence
[2,3]
: Add edge 2β3
Converting to 0-indexed:
- Node 0 (represents 1): edges to [1, 2]
- Node 1 (represents 2): edges to [2]
- Node 2 (represents 3): no outgoing edges
In-degrees after building:
- Node 0: in-degree = 0 (no incoming edges)
- Node 1: in-degree = 1 (edge from node 0)
- Node 2: in-degree = 2 (edges from nodes 0 and 1)
Step 2: Initialize Queue
- Only node 0 has in-degree 0
- Queue = [0]
Step 3: Process Nodes
Iteration 1:
- Queue has exactly 1 element (node 0), so continue
- Remove node 0, process its neighbors:
- Neighbor 1: decrement in-degree from 1 to 0, add to queue
- Neighbor 2: decrement in-degree from 2 to 1
- Queue = [1]
Iteration 2:
- Queue has exactly 1 element (node 1), so continue
- Remove node 1, process its neighbor:
- Neighbor 2: decrement in-degree from 1 to 0, add to queue
- Queue = [2]
Iteration 3:
- Queue has exactly 1 element (node 2), so continue
- Remove node 2 (no neighbors to process)
- Queue = []
Step 4: Verify Result
- Queue is empty (length 0)
- All nodes were processed with exactly one choice at each step
- Return
true
- there is a unique shortest supersequence [1,2,3]
Counter-example: If we had sequences = [[1,2], [1,3]]
instead:
- After processing node 0, both nodes 1 and 2 would have in-degree 0
- Queue = [1, 2] (length > 1)
- The loop would break immediately
- Return
false
- multiple valid orderings exist ([1,2,3] or [1,3,2])
Solution Implementation
1from collections import deque
2from itertools import pairwise
3from typing import List
4
5
6class Solution:
7 def sequenceReconstruction(
8 self, nums: List[int], sequences: List[List[int]]
9 ) -> bool:
10 """
11 Determines if the original sequence 'nums' can be uniquely reconstructed
12 from the given subsequences.
13
14 Args:
15 nums: The original sequence to be reconstructed (1-indexed values)
16 sequences: List of subsequences that should reconstruct nums
17
18 Returns:
19 True if nums is the only sequence that can be reconstructed from sequences
20 """
21 n = len(nums)
22
23 # Build adjacency list for directed graph (0-indexed)
24 adjacency_list = [[] for _ in range(n)]
25
26 # Track in-degree for each node (0-indexed)
27 in_degrees = [0] * n
28
29 # Build the graph from all subsequences
30 for sequence in sequences:
31 # Process each consecutive pair in the sequence
32 for first_val, second_val in pairwise(sequence):
33 # Convert from 1-indexed to 0-indexed
34 from_node = first_val - 1
35 to_node = second_val - 1
36
37 # Add edge from_node -> to_node
38 adjacency_list[from_node].append(to_node)
39 in_degrees[to_node] += 1
40
41 # Initialize queue with all nodes having in-degree of 0
42 queue = deque(
43 node for node, in_degree in enumerate(in_degrees) if in_degree == 0
44 )
45
46 # Process nodes in topological order
47 # The reconstruction is unique only if at each step there's exactly one choice
48 while len(queue) == 1:
49 current_node = queue.popleft()
50
51 # Process all neighbors of current node
52 for neighbor in adjacency_list[current_node]:
53 in_degrees[neighbor] -= 1
54
55 # If neighbor has no more dependencies, add to queue
56 if in_degrees[neighbor] == 0:
57 queue.append(neighbor)
58
59 # Reconstruction is valid and unique if queue is empty
60 # (all nodes processed with exactly one choice at each step)
61 return len(queue) == 0
62
1class Solution {
2 public boolean sequenceReconstruction(int[] nums, List<List<Integer>> sequences) {
3 int n = nums.length;
4
5 // Array to store the in-degree of each node (number of incoming edges)
6 int[] inDegree = new int[n];
7
8 // Adjacency list representation of the graph
9 // Each index represents a node, and the list contains its outgoing edges
10 List<Integer>[] adjacencyList = new List[n];
11
12 // Initialize each adjacency list
13 Arrays.setAll(adjacencyList, index -> new ArrayList<>());
14
15 // Build the directed graph from the sequences
16 for (List<Integer> sequence : sequences) {
17 for (int i = 1; i < sequence.size(); i++) {
18 // Convert to 0-indexed (subtract 1 from each value)
19 int fromNode = sequence.get(i - 1) - 1;
20 int toNode = sequence.get(i) - 1;
21
22 // Add directed edge from fromNode to toNode
23 adjacencyList[fromNode].add(toNode);
24
25 // Increment in-degree of the destination node
26 inDegree[toNode]++;
27 }
28 }
29
30 // Queue for topological sort using Kahn's algorithm
31 Deque<Integer> queue = new ArrayDeque<>();
32
33 // Add all nodes with in-degree 0 to the queue (starting nodes)
34 for (int i = 0; i < n; i++) {
35 if (inDegree[i] == 0) {
36 queue.offer(i);
37 }
38 }
39
40 // Process nodes while there is exactly one node in the queue
41 // This ensures a unique topological ordering
42 while (queue.size() == 1) {
43 // Remove the current node from the queue
44 int currentNode = queue.poll();
45
46 // Process all neighbors of the current node
47 for (int neighbor : adjacencyList[currentNode]) {
48 // Decrement in-degree of the neighbor
49 inDegree[neighbor]--;
50
51 // If in-degree becomes 0, add to queue
52 if (inDegree[neighbor] == 0) {
53 queue.offer(neighbor);
54 }
55 }
56 }
57
58 // Return true if queue is empty (unique reconstruction exists)
59 // Return false if queue has multiple elements (multiple valid orderings)
60 return queue.isEmpty();
61 }
62}
63
1class Solution {
2public:
3 bool sequenceReconstruction(vector<int>& nums, vector<vector<int>>& sequences) {
4 int n = nums.size();
5
6 // Initialize in-degree array for each node (0-indexed)
7 vector<int> inDegree(n, 0);
8
9 // Build adjacency list representation of the graph
10 vector<vector<int>> graph(n);
11
12 // Construct the directed graph from sequences
13 for (const auto& sequence : sequences) {
14 for (int i = 1; i < sequence.size(); ++i) {
15 // Convert to 0-indexed (assuming sequences contain 1-indexed values)
16 int from = sequence[i - 1] - 1;
17 int to = sequence[i] - 1;
18
19 // Add edge from 'from' to 'to'
20 graph[from].push_back(to);
21 // Increment in-degree of 'to' node
22 ++inDegree[to];
23 }
24 }
25
26 // Initialize queue for topological sort with all nodes having in-degree 0
27 queue<int> topoQueue;
28 for (int node = 0; node < n; ++node) {
29 if (inDegree[node] == 0) {
30 topoQueue.push(node);
31 }
32 }
33
34 // Perform topological sort
35 // The reconstruction is unique only if at each step we have exactly one choice
36 while (topoQueue.size() == 1) {
37 // Get the only node available for processing
38 int currentNode = topoQueue.front();
39 topoQueue.pop();
40
41 // Process all neighbors of current node
42 for (int neighbor : graph[currentNode]) {
43 // Decrement in-degree and add to queue if it becomes 0
44 if (--inDegree[neighbor] == 0) {
45 topoQueue.push(neighbor);
46 }
47 }
48 }
49
50 // If queue is empty, all nodes were processed with unique ordering
51 // If queue has more than 1 element, multiple valid orderings exist
52 return topoQueue.empty();
53 }
54};
55
1/**
2 * Determines if the given sequences can uniquely reconstruct the nums array
3 * using topological sorting to verify if there's exactly one valid ordering
4 *
5 * @param nums - The target sequence to be reconstructed
6 * @param sequences - Array of subsequences used for reconstruction
7 * @returns true if nums is the only sequence that can be reconstructed from sequences
8 */
9function sequenceReconstruction(nums: number[], sequences: number[][]): boolean {
10 const sequenceLength: number = nums.length;
11
12 // Build adjacency list for the directed graph
13 // Each index represents a node (0-indexed), containing its outgoing edges
14 const adjacencyList: number[][] = Array.from({ length: sequenceLength }, () => []);
15
16 // Track the in-degree (number of incoming edges) for each node
17 const inDegree: number[] = Array(sequenceLength).fill(0);
18
19 // Construct the graph from the given sequences
20 for (const sequence of sequences) {
21 for (let i = 1; i < sequence.length; i++) {
22 // Convert to 0-indexed and create edge from previous to current element
23 const fromNode: number = sequence[i - 1] - 1;
24 const toNode: number = sequence[i] - 1;
25
26 adjacencyList[fromNode].push(toNode);
27 inDegree[toNode]++;
28 }
29 }
30
31 // Initialize queue with all nodes having zero in-degree (starting nodes)
32 const queue: number[] = inDegree
33 .map((degree, index) => (degree === 0 ? index : -1))
34 .filter(node => node !== -1);
35
36 // Process nodes using topological sort
37 // The reconstruction is unique only if we have exactly one node to process at each step
38 while (queue.length === 1) {
39 const currentNode: number = queue.pop()!;
40
41 // Update in-degrees of neighboring nodes
42 for (const neighbor of adjacencyList[currentNode]) {
43 inDegree[neighbor]--;
44
45 // Add to queue if all dependencies are resolved
46 if (inDegree[neighbor] === 0) {
47 queue.push(neighbor);
48 }
49 }
50 }
51
52 // Reconstruction is valid and unique if all nodes were processed
53 // (queue should be empty after processing all nodes)
54 return queue.length === 0;
55}
56
Time and Space Complexity
Time Complexity: O(n + m)
The algorithm performs the following operations:
- Building the graph: Iterating through all sequences and all adjacent pairs takes
O(m)
time, wherem
is the total number of edges (sum of lengths of all sequences minus the number of sequences) - Initializing the indegree array:
O(n)
time - Finding initial nodes with zero indegree:
O(n)
time - BFS/Topological sort: Each node is processed once (
O(n)
), and each edge is traversed once when updating indegrees (O(m)
)
Total time complexity: O(n + m)
Space Complexity: O(n + m)
The algorithm uses:
- Graph adjacency list
g
: Stores all edges, requiringO(m)
space - Indegree array
indeg
: Stores indegree for each node, requiringO(n)
space - Queue
q
: In the worst case contains all nodes, requiringO(n)
space
Total space complexity: O(n + m)
Where n
is the number of nodes (length of nums
) and m
is the total number of edges across all sequences.
Common Pitfalls
1. Not Handling Duplicate Edges in the Graph
One critical pitfall is that the current implementation doesn't handle duplicate edges properly. When building the graph from subsequences, if the same edge appears multiple times (e.g., [1,2]
appears in multiple subsequences), the in-degree gets incremented multiple times for the same relationship.
Problem Example:
nums = [1, 2, 3] sequences = [[1, 2], [1, 2], [2, 3]] # [1,2] appears twice
The current code would set in_degrees[1]
to 2 instead of 1, leading to incorrect results.
Solution: Use a set to track edges and only increment in-degree once per unique edge:
def sequenceReconstruction(self, nums: List[int], sequences: List[List[int]]) -> bool:
n = len(nums)
adjacency_list = [[] for _ in range(n)]
in_degrees = [0] * n
# Track edges to avoid duplicates
edges_seen = set()
for sequence in sequences:
for first_val, second_val in pairwise(sequence):
from_node = first_val - 1
to_node = second_val - 1
edge = (from_node, to_node)
# Only process each unique edge once
if edge not in edges_seen:
edges_seen.add(edge)
adjacency_list[from_node].append(to_node)
in_degrees[to_node] += 1
# Rest of the code remains the same...
2. Not Validating Input Values Are Within Range
The code assumes all values in subsequences are valid (between 1 and n). Invalid values would cause index out of bounds errors.
Problem Example:
nums = [1, 2, 3] sequences = [[1, 4]] # 4 is out of range
Solution: Add validation before processing:
for sequence in sequences: for val in sequence: if val < 1 or val > n: return False # Invalid value in subsequence # Continue with pairwise processing...
3. Not Checking if All Numbers Are Covered
The algorithm might return true even if not all numbers from nums
appear in the subsequences, as it only checks the topological ordering of edges that exist.
Problem Example:
nums = [1, 2, 3] sequences = [[1, 2]] # 3 is never mentioned
Solution: Track which numbers appear in subsequences and verify all are present:
seen_numbers = set()
for sequence in sequences:
seen_numbers.update(sequence)
if seen_numbers != set(range(1, n + 1)):
return False # Not all numbers are covered
4. Empty Subsequences Handling
The code doesn't explicitly handle empty subsequences, though pairwise()
naturally handles them by producing no pairs.
Better Practice: Filter out empty subsequences explicitly for clarity:
sequences = [seq for seq in sequences if len(seq) > 0]
Which of the following is a good use case for backtracking?
Recommended Readings
https assets algo monster 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 be disconnected A tree
Topological Sort Topological Order Prereq Breadth First Search Review problems graph_bfs Directed Graph Before we get started on topological order let's talk about directed graphs first A graph is directed when its edges have directions these edges are also called arcs Suppose that v w are vertices in a directed
Coding Interview 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
Want a Structured Path to Master System Design Too? Donβt Miss This!