1562. Find Latest Group of Size M
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).
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:
- Create a new isolated group of size 1
- Extend an existing group (merging with one adjacent group)
- 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 positioni
has been set to 1p[i]
: Parent array for Union-Find, wherep[i]
points to the parent of elementi
size[i]
: Array tracking the size of the component wherei
is the root
Implementation Details:
-
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
- Create arrays for Union-Find:
-
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 stepn
. -
Main Processing Loop: For each step
i
with valuev
inarr
:- Convert to 0-indexed:
v -= 1
- Check left neighbor (
v-1
):- If
v > 0
andvis[v-1]
is True (left neighbor is 1) - Check if the component containing
v-1
has sizem
:if size[find(v-1)] == m
- If yes, update
ans = i
(current step) - Merge current position with left neighbor:
union(v, v-1)
- If
- Check right neighbor (
v+1
):- If
v < n-1
andvis[v+1]
is True (right neighbor is 1) - Check if the component containing
v+1
has sizem
:if size[find(v+1)] == m
- If yes, update
ans = i
(current step) - Merge current position with right neighbor:
union(v, v+1)
- If
- Mark current position as visited:
vis[v] = True
- Convert to 0-indexed:
-
Union-Find Operations:
find(x)
: Uses path compression to find the root of component containingx
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 sizem
because after merging, that group no longer exists as a separate entity of sizem
- Since we process steps in order, the last time we update
ans
is the latest step where a group of sizem
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 EvaluatorExample 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)
- Find size of group containing position 0:
- 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)
- Find size of group containing position 2:
- 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)
- Find size of group containing position 2:
- 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)
- Find size of group containing position 4:
- 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
- When merging position 4 with position 5, we check:
- 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)
- At most 2
- The
find
operation with path compression has an amortized time complexity ofO(α(n))
- The
union
operation callsfind
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 positionsp
array:O(n)
for parent pointers in Union-Findsize
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 ofstep_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
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
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
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
Want a Structured Path to Master System Design Too? Don’t Miss This!