Facebook Pixel

1562. Find Latest Group of Size M

MediumArrayHash TableBinary SearchSimulation
Leetcode Link

Problem Description

You start with a binary string of length n where all bits are initially set to 0. You're given an array arr which is a permutation of numbers from 1 to n (meaning it contains each number from 1 to n exactly once).

At each step i (from 1 to n), you set the bit at position arr[i] to 1 in the binary string. Both the array and binary string use 1-based indexing.

A group of ones is defined as a contiguous substring of 1's that cannot be extended in either direction (meaning it's either at the boundary of the string or surrounded by 0's).

Given an integer m, you need to find the latest step at which there exists a group of ones with length exactly equal to m.

For example, if you have a binary string "01110100", there are two groups of ones:

  • One group of length 3 at positions 2-4
  • One group of length 1 at position 6

The task is to return the latest step number where a group of exactly length m exists. If no such group ever exists during the entire process, return -1.

The solution uses Union-Find (Disjoint Set Union) data structure to efficiently track and merge groups of consecutive 1's as they form. When a new bit is set to 1, it checks if it can be merged with adjacent groups, and before merging, it checks if any of those groups have exactly size m (since after merging, a group of size m would no longer exist as a separate group).

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is to think about this problem in reverse. Instead of tracking when groups of size m are created, we need to track when they disappear or get merged into larger groups.

When we set a bit to 1, it can do one of three things:

  1. Create a new isolated group of size 1
  2. Extend an existing group (merging with one adjacent group)
  3. Bridge two existing groups (merging with groups on both sides)

The critical observation is that once a group grows larger than size m, it can never become size m again (we only add 1's, never remove them). So if a group of size m exists at some step, we want to record that step because it might be the last time we see a group of exactly that size.

This leads us to the Union-Find approach:

  • We maintain connected components of consecutive 1's
  • Before merging components, we check if any component about to be merged has size m
  • If it does, this current step becomes a candidate for our answer (since after merging, that group of size m will no longer exist as a separate entity)

Why do we check the size before merging? Because after the merge operation, the group of size m becomes part of a larger group. So the last moment it exists as a group of exactly size m is right before the merge.

The special case if m == n: return n handles when we're looking for a group that spans the entire array - this can only happen at the very last step when all bits are set to 1.

By processing the array in order and keeping track of the latest step where we're about to "lose" a group of size m through merging, we find the answer to the problem.

Learn more about Binary Search patterns.

Solution Approach

The solution uses the Union-Find (Disjoint Set Union) data structure with size tracking to efficiently manage groups of consecutive 1's.

Data Structures Used:

  • vis[i]: Boolean array tracking whether position i has been set to 1
  • p[i]: Parent array for Union-Find, where p[i] points to the parent of element i
  • size[i]: Array tracking the size of the component where i is the root

Implementation Details:

  1. Initialization:

    • Create arrays for Union-Find: p = list(range(n)) (each element is its own parent initially)
    • Initialize size = [1] * n (each component has size 1)
    • vis = [False] * n to track which positions have been set to 1
  2. Special Case Check:

    if m == n:
        return n

    If we're looking for a group of size n, it can only exist when all bits are 1, which happens at step n.

  3. Main Processing Loop: For each step i with value v in arr:

    • Convert to 0-indexed: v -= 1
    • Check left neighbor (v-1):
      • If v > 0 and vis[v-1] is True (left neighbor is 1)
      • Check if the component containing v-1 has size m: if size[find(v-1)] == m
      • If yes, update ans = i (current step)
      • Merge current position with left neighbor: union(v, v-1)
    • Check right neighbor (v+1):
      • If v < n-1 and vis[v+1] is True (right neighbor is 1)
      • Check if the component containing v+1 has size m: if size[find(v+1)] == m
      • If yes, update ans = i (current step)
      • Merge current position with right neighbor: union(v, v+1)
    • Mark current position as visited: vis[v] = True
  4. Union-Find Operations:

    • find(x): Uses path compression to find the root of component containing x
    • union(a, b): Merges two components by making one root point to the other, and updates the size of the new root

Why This Works:

  • We record ans = i when we're about to merge a group of size m because after merging, that group no longer exists as a separate entity of size m
  • Since we process steps in order, the last time we update ans is the latest step where a group of size m exists
  • If no group of size m ever exists, ans remains -1

The time complexity is O(n × α(n)) where α is the inverse Ackermann function (practically constant), and space complexity is O(n).

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 small example to illustrate the solution approach.

Input: arr = [3, 5, 1, 2, 4], n = 5, m = 2

We want to find the latest step where a group of exactly 2 consecutive 1's exists.

Initial Setup:

  • Binary string: 00000 (all positions start as 0)
  • vis = [False, False, False, False, False] (0-indexed)
  • p = [0, 1, 2, 3, 4] (each element is its own parent)
  • size = [1, 1, 1, 1, 1] (each component has size 1)
  • ans = -1

Step 1: Set position 3 (arr[0] = 3)

  • Convert to 0-indexed: position 2
  • Binary string becomes: 00100
  • Check left neighbor (position 1): vis[1] = False, no merge needed
  • Check right neighbor (position 3): vis[3] = False, no merge needed
  • Mark vis[2] = True
  • Current groups: one group of size 1 at position 3

Step 2: Set position 5 (arr[1] = 5)

  • Convert to 0-indexed: position 4
  • Binary string becomes: 00101
  • Check left neighbor (position 3): vis[3] = False, no merge needed
  • Check right neighbor: out of bounds
  • Mark vis[4] = True
  • Current groups: two separate groups of size 1

Step 3: Set position 1 (arr[2] = 1)

  • Convert to 0-indexed: position 0
  • Binary string becomes: 10101
  • Check left neighbor: out of bounds
  • Check right neighbor (position 1): vis[1] = False, no merge needed
  • Mark vis[0] = True
  • Current groups: three separate groups of size 1

Step 4: Set position 2 (arr[3] = 2)

  • Convert to 0-indexed: position 1
  • Binary string becomes: 11101
  • Check left neighbor (position 0): vis[0] = True
    • Find size of group containing position 0: size[find(0)] = 1
    • Size is not 2, so don't update ans
    • Merge position 1 with position 0 (creating group of size 2)
  • Check right neighbor (position 2): vis[2] = True
    • Find size of group containing position 2: size[find(2)] = 1
    • Size is not 2, so don't update ans
    • Merge the group with position 2 (now all three positions 0,1,2 form one group of size 3)
  • Mark vis[1] = True
  • Current groups: one group of size 3 (positions 1-3), one group of size 1 (position 5)

Step 5: Set position 4 (arr[4] = 4)

  • Convert to 0-indexed: position 3
  • Binary string becomes: 11111
  • Check left neighbor (position 2): vis[2] = True
    • Find size of group containing position 2: size[find(2)] = 3
    • Size is not 2, so don't update ans
    • Merge position 3 with the group containing position 2 (creating group of size 4)
  • Check right neighbor (position 4): vis[4] = True
    • Find size of group containing position 4: size[find(4)] = 1
    • Size is not 2, so don't update ans
    • Merge with position 4 (now all positions form one group of size 5)
  • Mark vis[3] = True
  • Current groups: one group of size 5 (all positions)

Result: ans = -1

No group of exactly size 2 ever existed in this example. Let's modify the example slightly to see when we would update ans:

Alternative Example: arr = [3, 1, 5, 4, 2], n = 5, m = 2

Following similar steps:

  • Step 1: Set position 3 → 00100
  • Step 2: Set position 1 → 10100
  • Step 3: Set position 5 → 10101
  • Step 4: Set position 4 → 10111
    • When merging position 4 with position 5, we check: size[find(4)] = 1
    • After merge, positions 4-5 form a group of size 2
  • Step 5: Set position 2 → 11111
    • Check left neighbor (position 1): size is 1, not 2
    • Check right neighbor (position 3): Before merging, check the group containing position 3
    • The group (positions 3-5) has size 3? No, let me recalculate...
    • Actually positions 4-5 form a group of size 2
    • Before merging position 2 with position 3, we check if the group at position 3 (which connects to 4-5) has size 2
    • If the group 4-5 has exactly size 2, we update ans = 5
    • Then we merge, creating a larger group

This demonstrates how we capture the last moment a group of size m exists before it gets absorbed into a larger group.

Solution Implementation

1class Solution:
2    def findLatestStep(self, arr: List[int], m: int) -> int:
3        """
4        Find the latest step where there exists a group of exactly m consecutive 1s.
5      
6        Args:
7            arr: Array where arr[i] represents the position (1-indexed) to flip at step i+1
8            m: Target group size to find
9          
10        Returns:
11            The latest step number where a group of size m exists, or -1 if never exists
12        """
13      
14        def find_root(node: int) -> int:
15            """
16            Find the root of the set containing the given node.
17            Uses path compression for optimization.
18          
19            Args:
20                node: The node to find the root for
21              
22            Returns:
23                The root node of the set
24            """
25            if parent[node] != node:
26                # Path compression: make every node point directly to root
27                parent[node] = find_root(parent[node])
28            return parent[node]
29      
30        def union_sets(node_a: int, node_b: int) -> None:
31            """
32            Union two sets by connecting their roots.
33            Updates the size of the resulting set.
34          
35            Args:
36                node_a: First node to union
37                node_b: Second node to union
38            """
39            root_a = find_root(node_a)
40            root_b = find_root(node_b)
41          
42            # Already in the same set
43            if root_a == root_b:
44                return
45          
46            # Connect root_a to root_b and update size
47            parent[root_a] = root_b
48            component_size[root_b] += component_size[root_a]
49      
50        n = len(arr)
51      
52        # Special case: if m equals n, the last step creates the only group of size n
53        if m == n:
54            return n
55      
56        # Initialize data structures
57        is_flipped = [False] * n  # Track which positions have been flipped to 1
58        parent = list(range(n))   # Parent array for Union-Find (each node is its own parent initially)
59        component_size = [1] * n   # Size of component rooted at each node
60        latest_step = -1          # Answer: latest step where size m group exists
61      
62        # Process each step
63        for step_index, position in enumerate(arr):
64            # Convert to 0-indexed position
65            current_pos = position - 1
66          
67            # Check left neighbor
68            if current_pos > 0 and is_flipped[current_pos - 1]:
69                # Before merging, check if left component has size m
70                if component_size[find_root(current_pos - 1)] == m:
71                    latest_step = step_index
72                # Merge with left neighbor
73                union_sets(current_pos, current_pos - 1)
74          
75            # Check right neighbor
76            if current_pos < n - 1 and is_flipped[current_pos + 1]:
77                # Before merging, check if right component has size m
78                if component_size[find_root(current_pos + 1)] == m:
79                    latest_step = step_index
80                # Merge with right neighbor
81                union_sets(current_pos, current_pos + 1)
82          
83            # Mark current position as flipped
84            is_flipped[current_pos] = True
85      
86        return latest_step
87
1class Solution {
2    // Parent array for Union-Find data structure
3    private int[] parent;
4    // Size array to track the size of each connected component
5    private int[] componentSize;
6
7    /**
8     * Finds the latest step at which there exists a group of exactly m consecutive 1s.
9     * @param arr Array where arr[i] indicates the position set to 1 at step i+1
10     * @param m Target size of consecutive 1s group
11     * @return The latest step at which a group of size m exists, or -1 if never exists
12     */
13    public int findLatestStep(int[] arr, int m) {
14        int n = arr.length;
15      
16        // Special case: if m equals n, the answer is the last step
17        if (m == n) {
18            return n;
19        }
20      
21        // Track which positions have been set to 1
22        boolean[] isSet = new boolean[n];
23      
24        // Initialize Union-Find data structure
25        parent = new int[n];
26        componentSize = new int[n];
27        for (int i = 0; i < n; ++i) {
28            parent[i] = i;
29            componentSize[i] = 1;
30        }
31      
32        int latestStep = -1;
33      
34        // Process each step
35        for (int step = 0; step < n; ++step) {
36            // Convert 1-indexed position to 0-indexed
37            int position = arr[step] - 1;
38          
39            // Check and merge with left neighbor if it exists and is set
40            if (position > 0 && isSet[position - 1]) {
41                // Check if the left component has size m before merging
42                if (componentSize[find(position - 1)] == m) {
43                    latestStep = step;
44                }
45                union(position, position - 1);
46            }
47          
48            // Check and merge with right neighbor if it exists and is set
49            if (position < n - 1 && isSet[position + 1]) {
50                // Check if the right component has size m before merging
51                if (componentSize[find(position + 1)] == m) {
52                    latestStep = step;
53                }
54                union(position, position + 1);
55            }
56          
57            // Mark current position as set
58            isSet[position] = true;
59        }
60      
61        return latestStep;
62    }
63
64    /**
65     * Finds the root parent of element x with path compression.
66     * @param x The element to find the root for
67     * @return The root parent of x
68     */
69    private int find(int x) {
70        if (parent[x] != x) {
71            // Path compression: make every node point directly to root
72            parent[x] = find(parent[x]);
73        }
74        return parent[x];
75    }
76
77    /**
78     * Unions two elements by connecting their root parents.
79     * @param a First element
80     * @param b Second element
81     */
82    private void union(int a, int b) {
83        int rootA = find(a);
84        int rootB = find(b);
85      
86        // Already in the same component
87        if (rootA == rootB) {
88            return;
89        }
90      
91        // Merge component A into component B
92        parent[rootA] = rootB;
93        componentSize[rootB] += componentSize[rootA];
94    }
95}
96
1class Solution {
2public:
3    vector<int> parent;
4    vector<int> componentSize;
5
6    int findLatestStep(vector<int>& arr, int m) {
7        int n = arr.size();
8      
9        // Special case: if m equals n, the entire array forms one group at the last step
10        if (m == n) return n;
11      
12        // Initialize Union-Find data structures
13        parent.resize(n);
14        componentSize.assign(n, 1);  // Each position initially has size 1
15        for (int i = 0; i < n; ++i) {
16            parent[i] = i;  // Each element is its own parent initially
17        }
18      
19        int latestStep = -1;
20        vector<int> isActive(n, 0);  // Track which positions have been set to 1
21      
22        // Process each step
23        for (int step = 0; step < n; ++step) {
24            int currentPos = arr[step] - 1;  // Convert to 0-indexed
25          
26            // Check and merge with left neighbor if it exists and is active
27            if (currentPos > 0 && isActive[currentPos - 1]) {
28                int leftRoot = find(currentPos - 1);
29                // Before merging, check if the left component has size m
30                if (componentSize[leftRoot] == m) {
31                    latestStep = step;
32                }
33                unite(currentPos, currentPos - 1);
34            }
35          
36            // Check and merge with right neighbor if it exists and is active
37            if (currentPos < n - 1 && isActive[currentPos + 1]) {
38                int rightRoot = find(currentPos + 1);
39                // Before merging, check if the right component has size m
40                if (componentSize[rightRoot] == m) {
41                    latestStep = step;
42                }
43                unite(currentPos, currentPos + 1);
44            }
45          
46            // Mark current position as active
47            isActive[currentPos] = 1;
48        }
49      
50        return latestStep;
51    }
52
53private:
54    // Find operation with path compression
55    int find(int x) {
56        if (parent[x] != x) {
57            parent[x] = find(parent[x]);  // Path compression
58        }
59        return parent[x];
60    }
61
62    // Union operation - merge two components
63    void unite(int a, int b) {
64        int rootA = find(a);
65        int rootB = find(b);
66      
67        // Already in the same component
68        if (rootA == rootB) return;
69      
70        // Merge component A into component B
71        parent[rootA] = rootB;
72        componentSize[rootB] += componentSize[rootA];
73    }
74};
75
1/**
2 * Finds the latest step at which there exists a group of exactly m consecutive 1s
3 * @param arr - Array where arr[i] represents the position to flip from 0 to 1 at step i+1
4 * @param m - Target size of consecutive 1s group
5 * @returns The latest step at which a group of exactly m consecutive 1s exists, or -1 if never
6 */
7const findLatestStep = function(arr: number[], m: number): number {
8    /**
9     * Find operation with path compression for Union-Find
10     * @param x - Element to find root for
11     * @returns Root of the set containing x
12     */
13    function find(x: number): number {
14        if (parent[x] !== x) {
15            parent[x] = find(parent[x]); // Path compression
16        }
17        return parent[x];
18    }
19
20    /**
21     * Union operation to merge two sets
22     * @param a - First element
23     * @param b - Second element
24     */
25    function union(a: number, b: number): void {
26        const rootA: number = find(a);
27        const rootB: number = find(b);
28      
29        // Already in the same set
30        if (rootA === rootB) {
31            return;
32        }
33      
34        // Merge set A into set B
35        parent[rootA] = rootB;
36        groupSize[rootB] += groupSize[rootA];
37    }
38
39    const n: number = arr.length;
40  
41    // Special case: if m equals n, all positions form one group at the last step
42    if (m === n) {
43        return n;
44    }
45  
46    // Track which positions have been flipped to 1
47    const visited: boolean[] = Array(n).fill(false);
48  
49    // Parent array for Union-Find (initially each element is its own parent)
50    const parent: number[] = Array.from({ length: n }, (_, i) => i);
51  
52    // Size of the group for each root element
53    const groupSize: number[] = Array(n).fill(1);
54  
55    let latestStep: number = -1;
56  
57    // Process each step
58    for (let i = 0; i < n; ++i) {
59        // Convert to 0-indexed position
60        const currentPos: number = arr[i] - 1;
61      
62        // Check and merge with left neighbor if it exists and is already flipped
63        if (currentPos > 0 && visited[currentPos - 1]) {
64            // Check if the left group has exactly m elements before merging
65            if (groupSize[find(currentPos - 1)] === m) {
66                latestStep = i;
67            }
68            union(currentPos, currentPos - 1);
69        }
70      
71        // Check and merge with right neighbor if it exists and is already flipped
72        if (currentPos < n - 1 && visited[currentPos + 1]) {
73            // Check if the right group has exactly m elements before merging
74            if (groupSize[find(currentPos + 1)] === m) {
75                latestStep = i;
76            }
77            union(currentPos, currentPos + 1);
78        }
79      
80        // Mark current position as flipped
81        visited[currentPos] = true;
82    }
83  
84    return latestStep;
85};
86

Time and Space Complexity

Time Complexity: O(n × α(n)) where n is the length of the array and α(n) is the inverse Ackermann function.

  • The main loop iterates through all n elements in the array: O(n)
  • For each element, we perform:
    • At most 2 find operations to check neighboring groups: O(α(n)) each
    • At most 2 union operations to merge groups: O(α(n)) each
    • Constant time operations for array access and comparisons: O(1)
  • The find operation with path compression has an amortized time complexity of O(α(n))
  • The union operation calls find twice and performs constant work: O(α(n))
  • Overall: O(n) × O(α(n)) = O(n × α(n))

Since α(n) grows extremely slowly (practically constant for all reasonable values of n), the time complexity is effectively O(n).

Space Complexity: O(n)

  • vis array: O(n) to track visited positions
  • p array: O(n) for parent pointers in Union-Find
  • size array: O(n) to track the size of each group
  • Recursion stack for find operation: O(log n) in the worst case with path compression
  • Total space: O(n) + O(n) + O(n) + O(log n) = O(n)

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Checking Component Size After Merging Instead of Before

One of the most critical mistakes is checking if a component has size m after merging it with adjacent groups. This is wrong because once merged, the group of size m no longer exists as a separate entity.

Incorrect Approach:

# WRONG: Checking size after union
union_sets(current_pos, current_pos - 1)
if component_size[find_root(current_pos)] == m:  # Too late!
    latest_step = step_index + 1

Correct Approach:

# CORRECT: Check size before union
if component_size[find_root(current_pos - 1)] == m:
    latest_step = step_index + 1
union_sets(current_pos, current_pos - 1)

2. Forgetting to Check if Current Single Element Forms a Group of Size m=1

When m=1, a single newly flipped bit forms a valid group. The code might miss this case if it only checks adjacent groups before merging.

Solution: After checking and merging with neighbors, check if the current component has size m:

# After processing neighbors
is_flipped[current_pos] = True

# Check if current component (after any merges) has size m
if component_size[find_root(current_pos)] == m:
    latest_step = step_index + 1

3. Using 0-Based vs 1-Based Indexing Incorrectly

The problem states that both the array values and steps use 1-based indexing, but internally we work with 0-based arrays. Mixing these up leads to off-by-one errors.

Common Mistakes:

  • Forgetting to convert arr[i] - 1 for array access
  • Returning step_index instead of step_index + 1 (steps are 1-based)

Correct Handling:

for step_index, position in enumerate(arr):
    current_pos = position - 1  # Convert to 0-based
    # ... process ...
    if component_size[...] == m:
        latest_step = step_index + 1  # Store 1-based step number

4. Not Handling Path Compression Properly in find_root

Without path compression, the find operation can degrade to O(n) time, making the overall solution O(n²).

Incorrect (No Path Compression):

def find_root(node):
    while parent[node] != node:
        node = parent[node]
    return node

Correct (With Path Compression):

def find_root(node):
    if parent[node] != node:
        parent[node] = find_root(parent[node])  # Compress path
    return parent[node]

5. Updating Component Size Incorrectly During Union

When merging two components, the size must be updated for the new root, not both roots.

Incorrect:

def union_sets(node_a, node_b):
    root_a = find_root(node_a)
    root_b = find_root(node_b)
    parent[root_a] = root_b
    component_size[root_a] += component_size[root_b]  # WRONG!

Correct:

def union_sets(node_a, node_b):
    root_a = find_root(node_a)
    root_b = find_root(node_b)
    parent[root_a] = root_b
    component_size[root_b] += component_size[root_a]  # Update new root
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More