1640. Check Array Formation Through Concatenation
Problem Description
You are given two arrays:
arr
: an array of distinct integerspieces
: an array containing multiple integer arrays, where all integers across all pieces are distinct
Your task is to determine if you can reconstruct the array arr
by concatenating the arrays in pieces
in some order.
Key constraints:
- You can use the arrays in
pieces
in any order you want - You cannot rearrange the elements within each individual piece - they must stay in their original order
- Each piece can only be used once (or not at all)
- All integers in the problem are distinct
Example scenarios:
- If
arr = [1,3,5,7]
andpieces = [[5,7],[1,3]]
, you can formarr
by concatenating[1,3]
first, then[5,7]
, so returntrue
- If
arr = [1,2,3]
andpieces = [[1],[3,2]]
, you cannot formarr
because you'd need to rearrange[3,2]
to[2,3]
, which is not allowed, so returnfalse
The solution works by iterating through arr
and for each position, finding a piece that starts with the current element. If found, it verifies that the entire piece matches the subsequent elements in arr
in the exact order. If at any point a matching piece cannot be found or a piece doesn't match completely, it returns false
. If the entire arr
is successfully matched, it returns true
.
Intuition
The key insight is that since all integers are distinct and we cannot reorder elements within each piece, each element in arr
uniquely determines which piece (if any) must be used at that position.
When we encounter an element in arr
, we need to find the piece that starts with that element. Since elements are distinct, at most one piece can start with any given element. Once we find such a piece, we must use the entire piece in its original order - we can't skip elements or rearrange them.
This leads to a greedy approach:
- Start from the beginning of
arr
- For the current element, find which piece starts with it
- If no piece starts with it, we can't form
arr
(returnfalse
) - If we find a matching piece, verify that the entire piece matches the next consecutive elements in
arr
- Move forward in
arr
by the length of the matched piece - Repeat until we've processed all of
arr
The reason this greedy approach works is because of the distinct elements constraint. Each element in arr
has only one possible piece that can provide it (the piece containing that element). And since we can't reorder within pieces, once we commit to using a piece at a certain position, we must use all of its elements consecutively. There's no benefit to delaying the use of a piece - if it fits now, use it now.
The algorithm essentially treats each piece as an indivisible block that must be placed whole, and the distinct elements constraint means there's only one valid way to match pieces to positions in arr
.
Solution Approach
The solution uses a two-pointer traversal approach with nested loops to match pieces to segments of the array.
Implementation walkthrough:
-
Main traversal pointer
i
: We maintain a pointeri
that tracks our current position inarr
. This pointer advances as we successfully match pieces. -
Finding matching pieces: For each position
i
inarr
, we search through all pieces to find one that starts witharr[i]
:k = 0 while k < len(pieces) and pieces[k][0] != arr[i]: k += 1
This linear search checks the first element of each piece until we find a match.
-
No match found: If
k == len(pieces)
, it means we've searched all pieces and none starts witharr[i]
. This makes it impossible to formarr
, so we returnfalse
. -
Verifying the entire piece: Once we find a matching piece at index
k
, we need to verify that the entire piece matches consecutive elements inarr
:j = 0 while j < len(pieces[k]) and arr[i] == pieces[k][j]: i, j = i + 1, j + 1
This simultaneous traversal compares each element of
pieces[k]
with corresponding elements inarr
, advancing both pointers together. -
Automatic validation: The inner while loop exits when either:
- We've matched the entire piece (
j == len(pieces[k])
) - success, continue with next segment - We found a mismatch (
arr[i] != pieces[k][j]
) - the loop exits buti
has already advanced partially, and the next iteration of the outer loop will fail to find a piece starting with this mismatched element
- We've matched the entire piece (
-
Final check: If we successfully process all elements of
arr
(the outer while loop completes withi == len(arr)
), we returntrue
.
Time Complexity: O(n × m)
where n
is the length of arr
and m
is the number of pieces, as we might search through all pieces for each element.
Space Complexity: O(1)
as we only use a constant amount of extra space for pointers.
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 arr = [91, 4, 64, 78]
and pieces = [[78], [4, 64], [91]]
.
Step 1: Initialize pointer i = 0
- Current element:
arr[0] = 91
- Search through pieces for one starting with 91:
pieces[0][0] = 78
❌pieces[1][0] = 4
❌pieces[2][0] = 91
✓
- Found matching piece
[91]
at indexk = 2
Step 2: Verify and consume the piece
- Compare
pieces[2]
witharr
starting at position 0:pieces[2][0] = 91
vsarr[0] = 91
✓
- Entire piece matched! Advance
i
by length of piece:i = 0 + 1 = 1
Step 3: Continue with i = 1
- Current element:
arr[1] = 4
- Search through pieces for one starting with 4:
pieces[0][0] = 78
❌pieces[1][0] = 4
✓
- Found matching piece
[4, 64]
at indexk = 1
Step 4: Verify and consume the piece
- Compare
pieces[1]
witharr
starting at position 1:pieces[1][0] = 4
vsarr[1] = 4
✓pieces[1][1] = 64
vsarr[2] = 64
✓
- Entire piece matched! Advance
i
by length of piece:i = 1 + 2 = 3
Step 5: Continue with i = 3
- Current element:
arr[3] = 78
- Search through pieces for one starting with 78:
pieces[0][0] = 78
✓
- Found matching piece
[78]
at indexk = 0
Step 6: Verify and consume the piece
- Compare
pieces[0]
witharr
starting at position 3:pieces[0][0] = 78
vsarr[3] = 78
✓
- Entire piece matched! Advance
i
by length of piece:i = 3 + 1 = 4
Step 7: Check completion
i = 4
equalslen(arr) = 4
- We've successfully matched all elements!
- Return
true
The array can be formed by concatenating pieces in the order: [91]
+ [4, 64]
+ [78]
= [91, 4, 64, 78]
Solution Implementation
1class Solution:
2 def canFormArray(self, arr: List[int], pieces: List[List[int]]) -> bool:
3 """
4 Check if array 'arr' can be formed by concatenating arrays in 'pieces'.
5
6 Args:
7 arr: Target array to form
8 pieces: List of arrays that can be used to form arr
9
10 Returns:
11 True if arr can be formed from pieces, False otherwise
12 """
13 # Current index in the target array
14 arr_index = 0
15
16 # Iterate through the target array
17 while arr_index < len(arr):
18 # Find a piece that starts with current element in arr
19 piece_index = 0
20 while piece_index < len(pieces) and pieces[piece_index][0] != arr[arr_index]:
21 piece_index += 1
22
23 # If no matching piece found, cannot form the array
24 if piece_index == len(pieces):
25 return False
26
27 # Verify that the found piece matches consecutive elements in arr
28 element_index = 0
29 while (element_index < len(pieces[piece_index]) and
30 arr_index < len(arr) and
31 arr[arr_index] == pieces[piece_index][element_index]):
32 arr_index += 1
33 element_index += 1
34
35 # If piece wasn't fully matched, array cannot be formed
36 if element_index != len(pieces[piece_index]):
37 return False
38
39 # All elements in arr were successfully matched
40 return True
41
1class Solution {
2 public boolean canFormArray(int[] arr, int[][] pieces) {
3 // Iterate through each element in the target array
4 for (int arrIndex = 0; arrIndex < arr.length;) {
5 // Find the piece that starts with the current element in arr
6 int pieceIndex = 0;
7 while (pieceIndex < pieces.length && pieces[pieceIndex][0] != arr[arrIndex]) {
8 pieceIndex++;
9 }
10
11 // If no matching piece found, array cannot be formed
12 if (pieceIndex == pieces.length) {
13 return false;
14 }
15
16 // Verify that the found piece matches consecutive elements in arr
17 int elementIndex = 0;
18 while (elementIndex < pieces[pieceIndex].length &&
19 arrIndex < arr.length &&
20 arr[arrIndex] == pieces[pieceIndex][elementIndex]) {
21 arrIndex++;
22 elementIndex++;
23 }
24
25 // If the piece wasn't fully matched, array cannot be formed
26 if (elementIndex != pieces[pieceIndex].length) {
27 return false;
28 }
29 }
30
31 // All elements in arr were successfully matched with pieces
32 return true;
33 }
34}
35
1class Solution {
2public:
3 bool canFormArray(vector<int>& arr, vector<vector<int>>& pieces) {
4 int arrIndex = 0;
5
6 // Iterate through the main array
7 while (arrIndex < arr.size()) {
8 // Find a piece that starts with the current element in arr
9 int pieceIndex = 0;
10 while (pieceIndex < pieces.size() && pieces[pieceIndex][0] != arr[arrIndex]) {
11 pieceIndex++;
12 }
13
14 // If no matching piece found, array cannot be formed
15 if (pieceIndex == pieces.size()) {
16 return false;
17 }
18
19 // Verify that the found piece matches consecutive elements in arr
20 int elementIndex = 0;
21 while (elementIndex < pieces[pieceIndex].size() &&
22 arrIndex < arr.size() &&
23 arr[arrIndex] == pieces[pieceIndex][elementIndex]) {
24 arrIndex++;
25 elementIndex++;
26 }
27
28 // If piece was not fully matched, array cannot be formed
29 if (elementIndex != pieces[pieceIndex].size()) {
30 return false;
31 }
32 }
33
34 return true;
35 }
36};
37
1/**
2 * Checks if the array 'arr' can be formed by concatenating the subarrays in 'pieces'
3 * @param arr - The target array to be formed
4 * @param pieces - Array of subarrays that can be used to form the target array
5 * @returns true if arr can be formed from pieces, false otherwise
6 */
7function canFormArray(arr: number[], pieces: number[][]): boolean {
8 const arrayLength: number = arr.length;
9 let currentIndex: number = 0;
10
11 // Iterate through the target array
12 while (currentIndex < arrayLength) {
13 // Get the current element we need to match
14 const currentElement: number = arr[currentIndex];
15
16 // Find a piece that starts with the current element
17 const matchingPiece: number[] | undefined = pieces.find(
18 (piece: number[]) => piece[0] === currentElement
19 );
20
21 // If no piece starts with the current element, we cannot form the array
22 if (matchingPiece === undefined) {
23 return false;
24 }
25
26 // Verify that the entire piece matches the corresponding elements in arr
27 for (const pieceElement of matchingPiece) {
28 // Check if the current piece element matches the array element
29 if (pieceElement !== arr[currentIndex]) {
30 return false;
31 }
32 currentIndex++;
33 }
34 }
35
36 // All elements matched successfully
37 return true;
38}
39
Time and Space Complexity
Time Complexity: O(n * m)
where n
is the length of arr
and m
is the number of pieces in pieces
.
The outer while loop iterates through each element of arr
once, giving us O(n)
iterations. For each element in arr
, we search through the pieces
list to find a matching piece that starts with the current element. This search takes O(m)
time in the worst case. Once a matching piece is found, we iterate through that piece to verify it matches the corresponding elements in arr
, but this verification is bounded by the total number of elements across all pieces, which equals n
(since all pieces together must form the array). Therefore, the overall time complexity is O(n * m)
.
Space Complexity: O(1)
The algorithm uses only a constant amount of extra space. The variables i
, j
, and k
are the only additional memory used, regardless of the input size. The algorithm operates directly on the input arrays without creating any additional data structures.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Inefficient Piece Lookup Leading to Poor Performance
The current solution performs a linear search through all pieces for every position in arr
, resulting in O(n × m) time complexity. When dealing with large inputs, this becomes a significant bottleneck.
Why it's a problem:
- For each element in
arr
, we potentially scan through all pieces - The same pieces are searched multiple times
- This approach doesn't leverage the fact that all integers are distinct
Better Solution: Use a hashmap to map the first element of each piece to the piece itself, reducing lookup time from O(m) to O(1).
class Solution:
def canFormArray(self, arr: List[int], pieces: List[List[int]]) -> bool:
# Create a mapping from first element to piece
piece_map = {piece[0]: piece for piece in pieces}
arr_index = 0
while arr_index < len(arr):
# O(1) lookup instead of O(m) search
if arr[arr_index] not in piece_map:
return False
# Get the matching piece
current_piece = piece_map[arr[arr_index]]
# Verify the entire piece matches
for element in current_piece:
if arr_index >= len(arr) or arr[arr_index] != element:
return False
arr_index += 1
return True
2. Not Handling Edge Cases with Boundary Checks
A subtle pitfall occurs when verifying if a piece matches: the original code increments arr_index
inside the verification loop without explicitly checking if we've exceeded array bounds.
Why it's problematic: While the current implementation handles this implicitly through the loop condition, it can lead to confusion and potential bugs if the logic is modified.
Clearer Solution:
Explicitly check if the remaining elements in arr
are sufficient to match the piece:
# Before matching a piece, verify there are enough elements left
if arr_index + len(current_piece) > len(arr):
return False
# Then perform the matching
for element in current_piece:
if arr[arr_index] != element:
return False
arr_index += 1
This makes the boundary checking explicit and the code more maintainable.
In a binary min heap, the minimum element can be found in:
Recommended Readings
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
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 assets algo monster recursion jpg You first call Ben and ask
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!