1764. Form Array by Concatenating Subarrays of Another Array
Problem Description
You are given two arrays:
groups
: a 2D integer array containingn
subarraysnums
: a 1D integer array
Your task is to determine if you can find n
disjoint subarrays within nums
that match the following conditions:
- The
i-th
subarray found innums
must be exactly equal togroups[i]
(matching element by element) - The subarrays must appear in
nums
in the same order as they appear ingroups
- meaning if subarraygroups[i]
is found at positionp
innums
, then subarraygroups[i+1]
must be found at some position afterp + len(groups[i])
- The subarrays must be disjoint - no element in
nums
can belong to more than one matched subarray
A subarray is defined as a contiguous sequence of elements within an array.
Example visualization:
- If
groups = [[1,2], [3,4]]
andnums = [1,2,5,3,4]
- We can find
[1,2]
at indices 0-1 and[3,4]
at indices 3-4 - These subarrays are disjoint (don't overlap) and appear in order
- Therefore, return
true
The solution uses a greedy approach with two pointers:
- Pointer
i
tracks which group we're currently trying to match - Pointer
j
tracks the current position innums
- For each group, we check if it matches the subarray starting at position
j
innums
- If it matches, we move
j
forward by the length of the group and move to the next group - If it doesn't match, we just move
j
forward by 1 and try again - We return
true
if we successfully match all groups (i == n
)
Intuition
The key insight is that we need to find subarrays in a specific order without overlapping. This naturally suggests a greedy approach - once we find a match for the first group, we should take it immediately and move on to find the next group.
Why does greedy work here? Consider what happens if we skip a valid match for groups[i]
:
- We'd have to find another match for
groups[i]
later innums
- But this would only push our search position further right
- Since all subsequent groups must appear after
groups[i]
, delaying our match forgroups[i]
can never help us - it only reduces the remaining space to find later groups
The sequential nature of the problem (groups must appear in order) means we can process nums
from left to right in a single pass. At each position in nums
, we only need to consider:
- Can the current group start here?
- If yes, consume it and move to the next group
- If no, try the next position
This leads to the two-pointer approach:
- We maintain our position in
groups
(which group we're looking for) - We maintain our position in
nums
(where we're currently searching) - When we find a match, we "consume" the entire subarray by jumping past it
- The disjoint requirement is automatically satisfied because we jump past each matched subarray
The algorithm terminates successfully when we've matched all groups, or fails when we've exhausted nums
without finding all groups. The time complexity is O(n * m)
in the worst case, where we might need to check each position in nums
against each group.
Learn more about Greedy and Two Pointers patterns.
Solution Approach
The implementation uses a two-pointer greedy algorithm to efficiently find matching subarrays:
Variables:
i
: pointer tracking which group ingroups
we're currently trying to match (starts at 0)j
: pointer tracking our current position innums
(starts at 0)n
: total number of groups to matchm
: length of thenums
array
Main Algorithm:
while i < n and j < m:
We continue searching as long as we have groups to match (i < n
) and positions to check in nums
(j < m
).
Matching Logic:
g = groups[i]
if g == nums[j : j + len(g)]:
- Extract the current group we're looking for
- Use Python's slice notation to check if the subarray starting at position
j
matches the current group - The slice
nums[j : j + len(g)]
extracts exactlylen(g)
elements starting from indexj
Success Case:
j += len(g)
i += 1
When we find a match:
- Jump
j
forward by the length of the matched group (ensuring disjoint subarrays) - Move to the next group by incrementing
i
Failure Case:
else: j += 1
When the current position doesn't match:
- Simply move
j
forward by 1 to check the next possible starting position
Final Check:
return i == n
- If
i == n
, we successfully matched all groups - If
i < n
, we ran out of positions innums
before matching all groups
Example Walkthrough:
For groups = [[1,2], [3,4]]
and nums = [1,2,5,3,4]
:
- Start:
i=0, j=0
, check if[1,2]
matchesnums[0:2]
โ Yes! Movej
to 2,i
to 1 i=1, j=2
, check if[3,4]
matchesnums[2:4]
โ No ([5,3]
โ[3,4]
), movej
to 3i=1, j=3
, check if[3,4]
matchesnums[3:5]
โ Yes! Movej
to 5,i
to 2i=2
equalsn=2
, returntrue
The algorithm efficiently handles edge cases like empty groups or insufficient remaining elements in nums
through the slice comparison and loop conditions.
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 a detailed example to understand how the greedy two-pointer approach works.
Example: groups = [[1,2,3], [4,5]]
and nums = [1,2,7,1,2,3,4,5,6]
Initial Setup:
i = 0
(looking for first group[1,2,3]
)j = 0
(starting at beginning ofnums
)n = 2
(we have 2 groups to match)m = 9
(length ofnums
)
Step 1: i=0, j=0
- Current group:
[1,2,3]
- Check: Does
nums[0:3]
=[1,2,7]
match[1,2,3]
? - No! (third element differs: 7 โ 3)
- Action: Move
j
forward by 1 โj=1
Step 2: i=0, j=1
- Current group:
[1,2,3]
- Check: Does
nums[1:4]
=[2,7,1]
match[1,2,3]
? - No! (first element differs: 2 โ 1)
- Action: Move
j
forward by 1 โj=2
Step 3: i=0, j=2
- Current group:
[1,2,3]
- Check: Does
nums[2:5]
=[7,1,2]
match[1,2,3]
? - No! (first element differs: 7 โ 1)
- Action: Move
j
forward by 1 โj=3
Step 4: i=0, j=3
- Current group:
[1,2,3]
- Check: Does
nums[3:6]
=[1,2,3]
match[1,2,3]
? - Yes! Perfect match!
- Action:
- Jump
j
forward by length of group (3) โj=6
- Move to next group โ
i=1
- Jump
Step 5: i=1, j=6
- Current group:
[4,5]
- Check: Does
nums[6:8]
=[4,5]
match[4,5]
? - Yes! Perfect match!
- Action:
- Jump
j
forward by length of group (2) โj=8
- Move to next group โ
i=2
- Jump
Final Check:
i = 2
which equalsn = 2
- We've successfully matched all groups!
- Return
true
Visual representation of the matching process:
nums: [1, 2, 7, 1, 2, 3, 4, 5, 6] ^ ^--------^ ^-----^ | group[0] group[1] | matched matched | at j=3 at j=6 | skipped positions (j=0,1,2)
The key insights from this walkthrough:
- We skip past non-matching positions one by one until we find a match
- Once we find a match, we "consume" the entire matched subarray by jumping past it
- The disjoint property is automatically maintained because we never revisit consumed positions
- The order is preserved because we search for groups sequentially
Solution Implementation
1from typing import List
2
3
4class Solution:
5 def canChoose(self, groups: List[List[int]], nums: List[int]) -> bool:
6 """
7 Check if all groups can be found as disjoint subarrays in nums in order.
8
9 Args:
10 groups: List of integer arrays to find in nums
11 nums: Target array to search within
12
13 Returns:
14 True if all groups can be chosen from nums in order, False otherwise
15 """
16 num_groups = len(groups)
17 nums_length = len(nums)
18
19 # Pointer for current group we're trying to match
20 group_index = 0
21 # Pointer for current position in nums array
22 nums_index = 0
23
24 # Continue while we have groups to match and positions to check
25 while group_index < num_groups and nums_index < nums_length:
26 current_group = groups[group_index]
27 group_length = len(current_group)
28
29 # Check if current group matches the subarray starting at nums_index
30 if nums[nums_index:nums_index + group_length] == current_group:
31 # Match found: move past this group in nums
32 nums_index += group_length
33 # Move to next group
34 group_index += 1
35 else:
36 # No match: try next position in nums
37 nums_index += 1
38
39 # All groups matched if we've processed all of them
40 return group_index == num_groups
41
1class Solution {
2 /**
3 * Determines if we can choose all groups from the groups array as disjoint subarrays from nums array
4 * in the same order they appear in groups.
5 *
6 * @param groups 2D array where each element is a group to be found in nums
7 * @param nums The array in which we need to find all groups
8 * @return true if all groups can be found in order as disjoint subarrays, false otherwise
9 */
10 public boolean canChoose(int[][] groups, int[] nums) {
11 int groupCount = groups.length;
12 int numsLength = nums.length;
13
14 int currentGroupIndex = 0; // Track which group we're currently trying to match
15 int numsIndex = 0; // Current position in nums array
16
17 // Try to find each group in nums
18 while (currentGroupIndex < groupCount && numsIndex < numsLength) {
19 // Check if current group matches at current position in nums
20 if (isGroupMatchAtPosition(groups[currentGroupIndex], nums, numsIndex)) {
21 // Move past this group in nums array
22 numsIndex += groups[currentGroupIndex].length;
23 // Move to next group
24 currentGroupIndex++;
25 } else {
26 // Current position doesn't match, try next position in nums
27 numsIndex++;
28 }
29 }
30
31 // Return true if we successfully matched all groups
32 return currentGroupIndex == groupCount;
33 }
34
35 /**
36 * Checks if a group array matches the nums array starting at the given position.
37 *
38 * @param group The group array to match
39 * @param nums The array to match against
40 * @param startPosition Starting position in nums array
41 * @return true if the entire group matches starting at startPosition, false otherwise
42 */
43 private boolean isGroupMatchAtPosition(int[] group, int[] nums, int startPosition) {
44 int groupLength = group.length;
45 int numsLength = nums.length;
46
47 int groupIndex = 0;
48 int numsIndex = startPosition;
49
50 // Compare each element of group with corresponding position in nums
51 while (groupIndex < groupLength && numsIndex < numsLength) {
52 if (group[groupIndex] != nums[numsIndex]) {
53 return false; // Mismatch found
54 }
55 groupIndex++;
56 numsIndex++;
57 }
58
59 // Return true only if we matched the entire group
60 return groupIndex == groupLength;
61 }
62}
63
1class Solution {
2public:
3 bool canChoose(vector<vector<int>>& groups, vector<int>& nums) {
4 // Lambda function to check if a group matches at a specific position in nums
5 auto isGroupMatchAtPosition = [&](vector<int>& group, vector<int>& nums, int startPos) {
6 int groupSize = group.size();
7 int numsSize = nums.size();
8 int groupIdx = 0;
9
10 // Try to match each element of the group with nums starting from startPos
11 while (groupIdx < groupSize && startPos < numsSize) {
12 if (group[groupIdx] != nums[startPos]) {
13 return false; // Mismatch found
14 }
15 groupIdx++;
16 startPos++;
17 }
18
19 // Return true only if we matched the entire group
20 return groupIdx == groupSize;
21 };
22
23 int totalGroups = groups.size();
24 int numsSize = nums.size();
25 int currentGroupIdx = 0; // Index of the current group we're trying to match
26
27 // Iterate through nums array to find all groups in order
28 for (int numsIdx = 0; currentGroupIdx < totalGroups && numsIdx < numsSize;) {
29 // Check if current group matches at current position in nums
30 if (isGroupMatchAtPosition(groups[currentGroupIdx], nums, numsIdx)) {
31 // Match found, move past this group in nums array
32 numsIdx += groups[currentGroupIdx].size();
33 // Move to next group
34 currentGroupIdx++;
35 } else {
36 // No match, try next position in nums
37 numsIdx++;
38 }
39 }
40
41 // Return true if all groups were successfully matched
42 return currentGroupIdx == totalGroups;
43 }
44};
45
1function canChoose(groups: number[][], nums: number[]): boolean {
2 // Helper function to check if a group matches starting at a specific position in nums
3 const isGroupMatchAtPosition = (group: number[], nums: number[], startPos: number): boolean => {
4 const groupSize = group.length;
5 const numsSize = nums.length;
6 let groupIdx = 0;
7
8 // Try to match each element of the group with nums starting from startPos
9 while (groupIdx < groupSize && startPos < numsSize) {
10 if (group[groupIdx] !== nums[startPos]) {
11 return false; // Mismatch found
12 }
13 groupIdx++;
14 startPos++;
15 }
16
17 // Return true only if we matched the entire group
18 return groupIdx === groupSize;
19 };
20
21 const totalGroups = groups.length;
22 const numsSize = nums.length;
23 let currentGroupIdx = 0; // Index of the current group we're trying to match
24
25 // Iterate through nums array to find all groups in order
26 for (let numsIdx = 0; currentGroupIdx < totalGroups && numsIdx < numsSize;) {
27 // Check if current group matches at current position in nums
28 if (isGroupMatchAtPosition(groups[currentGroupIdx], nums, numsIdx)) {
29 // Match found, move past this group in nums array
30 numsIdx += groups[currentGroupIdx].length;
31 // Move to next group
32 currentGroupIdx++;
33 } else {
34 // No match, try next position in nums
35 numsIdx++;
36 }
37 }
38
39 // Return true if all groups were successfully matched
40 return currentGroupIdx === totalGroups;
41}
42
Time and Space Complexity
Time Complexity: O(n * m * k)
where n
is the number of groups, m
is the length of the nums array, and k
is the average length of each group.
The outer while loop can iterate at most m
times (through all elements in nums). For each iteration, when we perform the slice comparison g == nums[j : j + len(g)]
, this operation takes O(len(g))
time to compare the elements. In the worst case, we might need to check almost every position in nums before finding matches for all groups. Therefore, the overall time complexity is O(n * m * k)
.
Space Complexity: O(k)
where k
is the average length of each group.
The space complexity comes from the slice operation nums[j : j + len(g)]
which creates a new list of size len(g)
for comparison. No other auxiliary data structures are used, and the variables i
, j
, n
, m
, and g
only use constant space. Therefore, the space complexity is O(k)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Boundary Check When Slicing
A common mistake is not properly handling the case when the remaining elements in nums
are fewer than the current group's length. While Python's slice notation handles out-of-bounds indices gracefully (returning a shorter list), comparing a shorter slice with the group will correctly return False
. However, developers might try to "optimize" by adding an explicit length check that could introduce bugs.
Incorrect approach:
# Trying to add premature optimization
if nums_index + len(current_group) > nums_length:
break # This might exit too early!
Why it's wrong: This breaks out of the loop entirely, preventing the algorithm from checking if we've matched all groups. The correct behavior is to let the comparison fail naturally and continue incrementing nums_index
.
2. Using ==
vs Element-wise Comparison for Lists
Python's ==
operator for lists works correctly for our use case, but developers coming from other languages might overthink this and try to implement manual element-wise comparison, introducing unnecessary complexity and potential bugs.
Overcomplicated approach:
# Unnecessary manual comparison
def matches(group, nums_slice):
if len(group) != len(nums_slice):
return False
for i in range(len(group)):
if group[i] != nums_slice[i]:
return False
return True
3. Forgetting to Skip the Entire Matched Group
A critical mistake is incrementing nums_index
by only 1 when a match is found, instead of by the group's length. This would violate the disjoint requirement.
Incorrect approach:
if nums[nums_index:nums_index + group_length] == current_group: nums_index += 1 # Wrong! Should be += group_length group_index += 1
Solution: Always increment by group_length
to ensure the matched elements aren't reused for subsequent groups.
4. Not Handling Empty Groups Correctly
If a group is empty ([]
), the slice comparison nums[j:j+0]
returns an empty list, which correctly equals the empty group. However, developers might add special handling that breaks this natural behavior.
Incorrect special case handling:
if len(current_group) == 0:
continue # Wrong! This skips the group without proper index updates
Solution: Trust the slice comparison to handle empty groups naturally. The algorithm will correctly match an empty group and move forward.
5. Off-by-One Errors in Final Check
Some developers might check group_index >= num_groups
or group_index == num_groups - 1
in the return statement.
Solution: The correct check is group_index == num_groups
because group_index
represents the number of successfully matched groups, and we need to match exactly num_groups
groups.
Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
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!