Facebook Pixel

1764. Form Array by Concatenating Subarrays of Another Array

Problem Description

You are given two arrays:

  • groups: a 2D integer array containing n subarrays
  • nums: a 1D integer array

Your task is to determine if you can find n disjoint subarrays within nums that match the following conditions:

  1. The i-th subarray found in nums must be exactly equal to groups[i] (matching element by element)
  2. The subarrays must appear in nums in the same order as they appear in groups - meaning if subarray groups[i] is found at position p in nums, then subarray groups[i+1] must be found at some position after p + len(groups[i])
  3. 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]] and nums = [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 in nums
  • For each group, we check if it matches the subarray starting at position j in nums
  • 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)
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 in nums
  • But this would only push our search position further right
  • Since all subsequent groups must appear after groups[i], delaying our match for groups[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:

  1. Can the current group start here?
  2. If yes, consume it and move to the next group
  3. 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 in groups we're currently trying to match (starts at 0)
  • j: pointer tracking our current position in nums (starts at 0)
  • n: total number of groups to match
  • m: length of the nums 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 exactly len(g) elements starting from index j

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 in nums before matching all groups

Example Walkthrough:

For groups = [[1,2], [3,4]] and nums = [1,2,5,3,4]:

  1. Start: i=0, j=0, check if [1,2] matches nums[0:2] โ†’ Yes! Move j to 2, i to 1
  2. i=1, j=2, check if [3,4] matches nums[2:4] โ†’ No ([5,3] โ‰  [3,4]), move j to 3
  3. i=1, j=3, check if [3,4] matches nums[3:5] โ†’ Yes! Move j to 5, i to 2
  4. i=2 equals n=2, return true

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 Evaluator

Example 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 of nums)
  • n = 2 (we have 2 groups to match)
  • m = 9 (length of nums)

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

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

Final Check:

  • i = 2 which equals n = 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:

  1. We skip past non-matching positions one by one until we find a match
  2. Once we find a match, we "consume" the entire matched subarray by jumping past it
  3. The disjoint property is automatically maintained because we never revisit consumed positions
  4. 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?


Recommended Readings

Want a Structured Path to Master System Design Too? Donโ€™t Miss This!

Load More