3399. Smallest Substring With Identical Characters II
Problem Description
You have a binary string s consisting of only '0's and '1's with length n, and you're allowed to perform up to numOps operations on it.
Each operation allows you to:
- Choose any index
i(where0 <= i < n) - Flip the character at that position (change '0' to '1' or '1' to '0')
Your goal is to minimize the length of the longest substring where all characters are identical (all '0's or all '1's).
For example:
- In string "111000", the longest substring of identical characters has length 3 (either "111" or "000")
- After flipping the character at index 2 to get "110000", the longest substring now has length 4 ("0000")
- After flipping the character at index 3 to get "110100", the longest substring now has length 2 (either "11" or "00")
The function should return the minimum possible length of such a longest identical substring after performing at most numOps flips optimally.
The solution uses binary search to find the minimum achievable length. For each candidate length m, it checks whether it's possible to ensure no identical substring exceeds length m using at most numOps operations. The check function handles two cases:
- When
m = 1: The string needs to alternate between '0' and '1', so it counts how many positions need flipping to achieve either "010101..." or "101010..." pattern - When
m > 1: It counts how many flips are needed within each group of consecutive identical characters to break them into segments of length at mostm
Intuition
The key insight is that we want to find the minimum possible value for the maximum length of consecutive identical characters. This is a classic optimization problem where we're minimizing a maximum value - a perfect candidate for binary search.
Why binary search? The answer lies in the monotonic property: if we can achieve a maximum length of m with some number of operations, then we can definitely achieve any length greater than m with the same or fewer operations. Conversely, if we cannot achieve length m, we cannot achieve any length smaller than m either.
For any target maximum length m, we need to figure out the minimum number of flips required to ensure no consecutive identical substring exceeds this length. This breaks down into two distinct strategies:
-
When
m = 1: We need a completely alternating pattern like "010101..." or "101010...". We can count how many positions differ from each pattern and choose the one requiring fewer flips. This is optimal because any substring of length 2 or more would contain different characters. -
When
m > 1: We need to break up long runs of identical characters. For a run ofkconsecutive identical characters, we need to place flips strategically to split it into segments of at mostmcharacters. The optimal way is to place flips everym+1positions within the run, requiringk // (m+1)flips.
The binary search tries different values of m from 1 to n, checking for each whether it's achievable with at most numOps operations. The smallest achievable m is our answer.
This approach is efficient because instead of trying all possible combinations of flips (which would be exponential), we're leveraging the problem's structure to directly compute the minimum flips needed for any given target length.
Learn more about Binary Search patterns.
Solution Approach
The solution uses the boundary-finding binary search template to find the minimum achievable maximum length of consecutive identical characters.
Binary Search Template Structure:
We search for the minimum m where feasible(m) returns True. The search space is [1, n].
left, right = 1, n first_true_index = -1 while left <= right: mid = (left + right) // 2 if feasible(mid): first_true_index = mid right = mid - 1 # Look for smaller m else: left = mid + 1 return first_true_index
The Feasible Function:
feasible(m) determines if we can achieve a maximum consecutive length of m using at most numOps flips.
Case 1: When m = 1
- We need the string to alternate completely
- Count operations for pattern "010101..." using
sum(c == pattern[i & 1]) - Take minimum of this count and
n - count(for pattern "101010...")
Case 2: When m > 1
- For each group of
kconsecutive identical characters:- Calculate flips needed:
k // (m + 1) - This formula optimally breaks a segment into parts of at most
m
- Calculate flips needed:
Why This Template Works:
The feasible function creates a monotonic pattern: False, False, ..., True, True, True. If we can achieve max length m, we can certainly achieve any length greater than m. We find the first True (minimum achievable m).
Time Complexity: O(n log n) where binary search runs O(log n) iterations, each check takes O(n).
Space Complexity: O(1) - only using a few variables.
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 s = "111000" and numOps = 2.
Initial Analysis:
- String: "111000" (length n = 6)
- Current longest consecutive identical substring: 3 (both "111" and "000")
- We have 2 operations available
Binary Search Process:
We'll binary search on the range [1, 6] to find the minimum achievable maximum length.
Try m = 3 (midpoint of [1, 6]):
- Check if we can limit all consecutive substrings to length 3
- Current groups: "111" (length 3), "000" (length 3)
- For group "111": 3 // (3+1) = 3 // 4 = 0 flips needed
- For group "000": 3 // (3+1) = 3 // 4 = 0 flips needed
- Total flips needed: 0 ≤ 2 ✓
- Since this works, try smaller values
Try m = 1:
- Need alternating pattern: either "010101" or "101010"
- Compare with "010101": positions 0,1,2 need flipping → 3 flips
- Compare with "101010": positions 3,4,5 need flipping → 3 flips
- Minimum flips needed: 3 > 2 ✗
- Cannot achieve m = 1
Try m = 2:
- Need to break groups longer than 2
- Group "111" (length 3): 3 // (2+1) = 3 // 3 = 1 flip needed
- One flip at position 1 or 2 would give "101000" or "110000"
- Group "000" (length 3): 3 // (2+1) = 3 // 3 = 1 flip needed
- One flip at position 3 or 4 would give "110100" or "111010"
- Total flips needed: 2 ≤ 2 ✓
- This works!
Binary Search Conclusion:
- m = 1 doesn't work (needs 3 flips)
- m = 2 works (needs 2 flips)
- Therefore, the minimum achievable maximum length is 2
Verification: After flipping positions 1 and 4: "111000" → "101010"
- All consecutive identical substrings have length ≤ 1 Actually, we can also flip positions 2 and 3: "111000" → "110100"
- Consecutive identical substrings: "11", "01", "00" (max length = 2)
The answer is 2.
Solution Implementation
1class Solution:
2 def minLength(self, s: str, numOps: int) -> int:
3 n = len(s)
4
5 def feasible(m: int) -> bool:
6 """Check if max consecutive length of m is achievable."""
7 ops_needed = 0
8
9 if m == 1:
10 # Need alternating pattern
11 pattern = "01"
12 ops_needed = sum(c == pattern[i & 1] for i, c in enumerate(s))
13 ops_needed = min(ops_needed, n - ops_needed)
14 else:
15 # Count ops to break long consecutive groups
16 k = 0
17 for i, c in enumerate(s):
18 k += 1
19 if i == n - 1 or c != s[i + 1]:
20 ops_needed += k // (m + 1)
21 k = 0
22
23 return ops_needed <= numOps
24
25 # Binary search template: find first m where feasible(m) is True
26 left, right = 1, n
27 first_true_index = -1
28
29 while left <= right:
30 mid = (left + right) // 2
31 if feasible(mid):
32 first_true_index = mid
33 right = mid - 1 # Look for smaller m
34 else:
35 left = mid + 1
36
37 return first_true_index
381class Solution {
2 private char[] chars;
3 private int numOps;
4 private int n;
5
6 public int minLength(String s, int numOps) {
7 this.chars = s.toCharArray();
8 this.numOps = numOps;
9 this.n = s.length();
10
11 // Binary search template: find first m where feasible(m) is True
12 int left = 1;
13 int right = n;
14 int firstTrueIndex = -1;
15
16 while (left <= right) {
17 int mid = left + (right - left) / 2;
18 if (feasible(mid)) {
19 firstTrueIndex = mid;
20 right = mid - 1; // Look for smaller m
21 } else {
22 left = mid + 1;
23 }
24 }
25
26 return firstTrueIndex;
27 }
28
29 private boolean feasible(int m) {
30 int opsNeeded = 0;
31
32 if (m == 1) {
33 char[] pattern = {'0', '1'};
34 for (int i = 0; i < n; i++) {
35 if (chars[i] == pattern[i & 1]) {
36 opsNeeded++;
37 }
38 }
39 opsNeeded = Math.min(opsNeeded, n - opsNeeded);
40 } else {
41 int k = 0;
42 for (int i = 0; i < n; i++) {
43 k++;
44 if (i == n - 1 || chars[i] != chars[i + 1]) {
45 opsNeeded += k / (m + 1);
46 k = 0;
47 }
48 }
49 }
50
51 return opsNeeded <= numOps;
52 }
53}
541class Solution {
2public:
3 int minLength(string s, int numOps) {
4 int n = s.size();
5
6 auto feasible = [&](int m) -> bool {
7 int opsNeeded = 0;
8
9 if (m == 1) {
10 string pattern = "01";
11 for (int i = 0; i < n; ++i) {
12 if (s[i] == pattern[i & 1]) {
13 ++opsNeeded;
14 }
15 }
16 opsNeeded = min(opsNeeded, n - opsNeeded);
17 } else {
18 int k = 0;
19 for (int i = 0; i < n; ++i) {
20 ++k;
21 if (i == n - 1 || s[i] != s[i + 1]) {
22 opsNeeded += k / (m + 1);
23 k = 0;
24 }
25 }
26 }
27
28 return opsNeeded <= numOps;
29 };
30
31 // Binary search template: find first m where feasible(m) is True
32 int left = 1;
33 int right = n;
34 int firstTrueIndex = -1;
35
36 while (left <= right) {
37 int mid = left + (right - left) / 2;
38 if (feasible(mid)) {
39 firstTrueIndex = mid;
40 right = mid - 1; // Look for smaller m
41 } else {
42 left = mid + 1;
43 }
44 }
45
46 return firstTrueIndex;
47 }
48};
491function minLength(s: string, numOps: number): number {
2 const n: number = s.length;
3
4 const feasible = (m: number): boolean => {
5 let opsNeeded: number = 0;
6
7 if (m === 1) {
8 const pattern: string = '01';
9 for (let i = 0; i < n; i++) {
10 if (s[i] === pattern[i & 1]) {
11 opsNeeded++;
12 }
13 }
14 opsNeeded = Math.min(opsNeeded, n - opsNeeded);
15 } else {
16 let k: number = 0;
17 for (let i = 0; i < n; i++) {
18 k++;
19 if (i === n - 1 || s[i] !== s[i + 1]) {
20 opsNeeded += Math.floor(k / (m + 1));
21 k = 0;
22 }
23 }
24 }
25
26 return opsNeeded <= numOps;
27 };
28
29 // Binary search template: find first m where feasible(m) is True
30 let left: number = 1;
31 let right: number = n;
32 let firstTrueIndex: number = -1;
33
34 while (left <= right) {
35 const mid: number = Math.floor((left + right) / 2);
36 if (feasible(mid)) {
37 firstTrueIndex = mid;
38 right = mid - 1; // Look for smaller m
39 } else {
40 left = mid + 1;
41 }
42 }
43
44 return firstTrueIndex;
45}
46Time and Space Complexity
Time Complexity: O(n * log n)
The algorithm uses binary search on the range [1, n] where n is the length of string s. The binary search performs O(log n) iterations.
For each iteration of the binary search, the check function is called:
- When
m == 1: The function iterates through the string once to count mismatches with alternating patterns "01" or "10", takingO(n)time. - When
m > 1: The function iterates through the string once to count consecutive segments and calculate required operations, takingO(n)time.
Since each check call takes O(n) time and binary search makes O(log n) calls, the overall time complexity is O(n * log n).
Space Complexity: O(1)
The algorithm uses only a constant amount of extra space:
- The
checkfunction uses variablescnt,t(whenm == 1), andkwhich requireO(1)space. - The binary search itself only uses a few variables for bounds and mid-point calculations.
- No additional data structures proportional to input size are created.
Therefore, the space complexity is O(1).
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Using Wrong Binary Search Template Variant
A critical mistake is using the while left < right with right = mid pattern instead of the correct template.
Incorrect approach:
left, right = 1, n while left < right: mid = (left + right) // 2 if feasible(mid): right = mid else: left = mid + 1 return left
Correct approach (template-compliant):
left, right = 1, n first_true_index = -1 while left <= right: mid = (left + right) // 2 if feasible(mid): first_true_index = mid right = mid - 1 else: left = mid + 1 return first_true_index
The template uses while left <= right with right = mid - 1 and tracks the answer in first_true_index.
2. Incorrect Formula for Breaking Consecutive Groups
Using the wrong formula to calculate flips needed:
k // m- doesn't account for spacing correctly(k - 1) // m- underestimates
Correct formula: k // (m + 1) - places flips every (m + 1) positions optimally.
3. Misunderstanding the Alternating Pattern Check
When checking for m = 1, forgetting to consider both patterns ("010101..." and "101010..."):
# Wrong: Only checking one pattern
cnt = sum(c == pattern[i & 1] for i, c in enumerate(s))
# Correct: Take minimum of both patterns
cnt = min(cnt, n - cnt)
4. Not Resetting the Consecutive Counter
Forgetting to reset k = 0 after processing each group leads to incorrect calculations for subsequent groups.
Which of the following array represent a max heap?
Recommended Readings
https assets algo monster cover_photos Binary_Search svg Binary Search Intuition Binary search is an efficient array search algorithm It works by narrowing down the search range by half each time If you have looked up a word in a physical dictionary you've already used binary search in real life Let's
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 If you prefer videos here's a video that explains recursion in a fun and easy way 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 him
Want a Structured Path to Master System Design Too? Don’t Miss This!