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 ofk
consecutive identical characters, we need to place flips strategically to split it into segments of at mostm
characters. The optimal way is to place flips everym+1
positions 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 implements a binary search combined with a validation function to find the minimum achievable maximum length of consecutive identical characters.
Binary Search Setup:
The main function uses bisect_left
to perform binary search on the range [1, n]
where n
is the length of the string. We're searching for the smallest value m
where we can successfully limit all consecutive identical substrings to at most length m
.
The Check Function:
The check(m)
function determines whether it's possible to achieve a maximum consecutive length of m
with at most numOps
operations.
Case 1: When m = 1
if m == 1:
t = "01"
cnt = sum(c == t[i & 1] for i, c in enumerate(s))
cnt = min(cnt, n - cnt)
- We need the string to alternate completely (no two adjacent characters can be the same)
- We check two patterns: "010101..." and "101010..."
t[i & 1]
generates the alternating pattern: wheni
is even,i & 1 = 0
gives '0'; wheni
is odd,i & 1 = 1
gives '1'- Count mismatches with the "01" pattern starting with '0'
- Since we can also start with '1', the alternative pattern would require
n - cnt
flips - Take the minimum of both options
Case 2: When m > 1
else:
k = 0
for i, c in enumerate(s):
k += 1
if i == len(s) - 1 or c != s[i + 1]:
cnt += k // (m + 1)
k = 0
- Iterate through the string to identify consecutive groups of identical characters
k
tracks the current group size- When we reach the end of a group (character changes or string ends):
- Calculate flips needed:
k // (m + 1)
- This formula works because placing a flip every
m+1
positions optimally breaks a group of sizek
into segments of at mostm
- Reset
k
for the next group
- Calculate flips needed:
Final Check:
return cnt <= numOps
Returns True
if the required number of operations doesn't exceed numOps
.
Binary Search Execution:
bisect_left(range(n), True, lo=1, key=check)
finds the leftmost (smallest) value in the range [1, n]
where check(m)
returns True
. This gives us the minimum achievable maximum length of consecutive identical characters.
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 """
4 Find the minimum possible length of the longest consecutive identical characters
5 after performing at most numOps operations.
6
7 Args:
8 s: Input string containing binary characters ('0' or '1')
9 numOps: Maximum number of operations allowed
10
11 Returns:
12 Minimum possible length of longest consecutive identical characters
13 """
14
15 def can_achieve_max_length(max_length: int) -> bool:
16 """
17 Check if we can achieve a maximum consecutive length of max_length
18 with at most numOps operations.
19
20 Args:
21 max_length: Target maximum consecutive length to check
22
23 Returns:
24 True if achievable with numOps operations, False otherwise
25 """
26 operations_needed = 0
27
28 # Special case: if target max length is 1, we need alternating pattern
29 if max_length == 1:
30 # Try pattern starting with '0' at index 0
31 pattern = "01"
32 # Count mismatches with alternating pattern "010101..."
33 operations_for_pattern_01 = sum(
34 char == pattern[index & 1]
35 for index, char in enumerate(s)
36 )
37 # Also try pattern starting with '1' at index 0 (complement)
38 # This would need (n - operations_for_pattern_01) operations
39 operations_needed = min(operations_for_pattern_01, string_length - operations_for_pattern_01)
40 else:
41 # For max_length > 1: count operations needed to break long consecutive sequences
42 consecutive_count = 0
43
44 for index, char in enumerate(s):
45 consecutive_count += 1
46
47 # Check if this is the last character or if next character is different
48 if index == len(s) - 1 or char != s[index + 1]:
49 # Calculate operations needed to break this consecutive sequence
50 # To ensure no sequence exceeds max_length, we need to insert
51 # different characters at intervals
52 operations_needed += consecutive_count // (max_length + 1)
53 consecutive_count = 0
54
55 return operations_needed <= numOps
56
57 # Store string length for efficiency
58 string_length = len(s)
59
60 # Binary search for the minimum achievable maximum consecutive length
61 # Using bisect_left to find the leftmost position where check returns True
62 from bisect import bisect_left
63 return bisect_left(
64 range(string_length), # Search space: possible lengths from 0 to n-1
65 True, # We're looking for the first True value
66 lo=1, # Start search from 1 (minimum possible length)
67 key=can_achieve_max_length # Function to evaluate each candidate
68 )
69
1class Solution {
2 private char[] charArray;
3 private int maxOperations;
4
5 /**
6 * Finds the minimum possible length of the longest substring with same characters
7 * after performing at most numOps operations (flipping characters).
8 * Uses binary search to find the optimal length.
9 *
10 * @param s The input string containing '0's and '1's
11 * @param numOps Maximum number of character flips allowed
12 * @return The minimum possible length of the longest consecutive substring
13 */
14 public int minLength(String s, int numOps) {
15 this.maxOperations = numOps;
16 this.charArray = s.toCharArray();
17
18 // Binary search on the answer: minimum possible max length
19 int left = 1;
20 int right = s.length();
21
22 while (left < right) {
23 int mid = (left + right) >> 1; // Equivalent to (left + right) / 2
24
25 if (canAchieveMaxLength(mid)) {
26 // If we can achieve this max length, try smaller
27 right = mid;
28 } else {
29 // If we cannot achieve this max length, we need larger
30 left = mid + 1;
31 }
32 }
33
34 return left;
35 }
36
37 /**
38 * Checks if it's possible to make all consecutive substrings have length <= targetMaxLength
39 * using at most maxOperations flips.
40 *
41 * @param targetMaxLength The target maximum length for consecutive substrings
42 * @return true if achievable with available operations, false otherwise
43 */
44 private boolean canAchieveMaxLength(int targetMaxLength) {
45 int requiredOperations = 0;
46
47 if (targetMaxLength == 1) {
48 // Special case: when target max length is 1, we need alternating pattern
49 // Try both patterns: "0101..." and "1010..."
50 char[] alternatingPattern = {'0', '1'};
51
52 // Count operations needed to match pattern starting with '0'
53 for (int i = 0; i < charArray.length; ++i) {
54 if (charArray[i] == alternatingPattern[i & 1]) { // i & 1 gives 0 for even, 1 for odd
55 ++requiredOperations;
56 }
57 }
58
59 // Choose the pattern that requires fewer operations
60 // (either starting with '0' or starting with '1')
61 requiredOperations = Math.min(requiredOperations, charArray.length - requiredOperations);
62 } else {
63 // General case: count operations needed to break long consecutive sequences
64 int consecutiveCount = 0;
65
66 for (int i = 0; i < charArray.length; ++i) {
67 ++consecutiveCount;
68
69 // Check if we've reached the end or found a different character
70 if (i == charArray.length - 1 || charArray[i] != charArray[i + 1]) {
71 // Calculate operations needed to break this sequence
72 // We need to flip every (targetMaxLength + 1)-th character
73 requiredOperations += consecutiveCount / (targetMaxLength + 1);
74 consecutiveCount = 0; // Reset counter for next sequence
75 }
76 }
77 }
78
79 return requiredOperations <= maxOperations;
80 }
81}
82
1class Solution {
2public:
3 int minLength(string s, int numOps) {
4 int n = s.size();
5
6 // Lambda function to check if we can achieve max consecutive length of targetLength
7 // with at most numOps operations
8 auto canAchieveLength = [&](int targetLength) {
9 int operationsNeeded = 0;
10
11 if (targetLength == 1) {
12 // Special case: alternating pattern (no two consecutive same characters)
13 // Try pattern "010101..." and count mismatches
14 string alternatingPattern = "01";
15 for (int i = 0; i < n; ++i) {
16 if (s[i] == alternatingPattern[i & 1]) {
17 ++operationsNeeded;
18 }
19 }
20 // Choose minimum between current pattern and its inverse "101010..."
21 operationsNeeded = min(operationsNeeded, n - operationsNeeded);
22 } else {
23 // General case: count operations needed to break segments longer than targetLength
24 int currentSegmentLength = 0;
25 for (int i = 0; i < n; ++i) {
26 ++currentSegmentLength;
27
28 // Check if we reached end of a segment (last char or different from next)
29 if (i == n - 1 || s[i] != s[i + 1]) {
30 // Calculate operations needed to break this segment
31 operationsNeeded += currentSegmentLength / (targetLength + 1);
32 currentSegmentLength = 0;
33 }
34 }
35 }
36
37 return operationsNeeded <= numOps;
38 };
39
40 // Binary search for the minimum achievable length
41 int left = 1, right = n;
42 while (left < right) {
43 int mid = (left + right) >> 1;
44 if (canAchieveLength(mid)) {
45 right = mid; // Can achieve this length, try smaller
46 } else {
47 left = mid + 1; // Cannot achieve this length, need larger
48 }
49 }
50
51 return left;
52 }
53};
54
1/**
2 * Finds the minimum possible length of string after performing at most numOps operations
3 * @param s - The input binary string
4 * @param numOps - Maximum number of operations allowed
5 * @returns The minimum possible length of the string
6 */
7function minLength(s: string, numOps: number): number {
8 const stringLength: number = s.length;
9
10 /**
11 * Checks if it's possible to achieve a target length with given operations
12 * @param targetLength - The target length to check
13 * @returns true if achievable with numOps operations, false otherwise
14 */
15 const canAchieveLength = (targetLength: number): boolean => {
16 let operationsNeeded: number = 0;
17
18 if (targetLength === 1) {
19 // Special case: when target length is 1, we need to make alternating pattern
20 const alternatingPattern: string = '01';
21
22 // Count mismatches with alternating pattern starting with '0'
23 for (let i = 0; i < stringLength; i++) {
24 if (s[i] === alternatingPattern[i & 1]) {
25 operationsNeeded++;
26 }
27 }
28
29 // Choose minimum between starting with '0' or '1'
30 operationsNeeded = Math.min(operationsNeeded, stringLength - operationsNeeded);
31 } else {
32 // For target length > 1, count operations needed to break consecutive blocks
33 let consecutiveCount: number = 0;
34
35 for (let i = 0; i < stringLength; i++) {
36 consecutiveCount++;
37
38 // When we reach end of string or find a different character
39 if (i === stringLength - 1 || s[i] !== s[i + 1]) {
40 // Calculate operations needed for this block
41 operationsNeeded += Math.floor(consecutiveCount / (targetLength + 1));
42 consecutiveCount = 0;
43 }
44 }
45 }
46
47 return operationsNeeded <= numOps;
48 };
49
50 // Binary search for the minimum achievable length
51 let left: number = 1;
52 let right: number = stringLength;
53
54 while (left < right) {
55 const mid: number = (left + right) >> 1;
56
57 if (canAchieveLength(mid)) {
58 // If we can achieve this length, try smaller
59 right = mid;
60 } else {
61 // If we cannot achieve this length, we need larger
62 left = mid + 1;
63 }
64 }
65
66 return left;
67}
68
Time 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
check
function uses variablescnt
,t
(whenm == 1
), andk
which 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. Incorrect Formula for Breaking Consecutive Sequences
The Pitfall:
When calculating how many operations are needed to break a consecutive sequence of length k
into segments of at most length m
, developers often use incorrect formulas like:
(k - 1) // m
- This underestimates the operations neededk // m
- This doesn't account for the spacing correctly(k + m - 1) // m
- This is for counting segments, not operations
Why It Happens:
The confusion arises from not clearly understanding the relationship between segment placement and the number of flips needed. If you have a string of k
consecutive identical characters and want no segment longer than m
, you need to place flips strategically.
The Correct Formula:
k // (m + 1)
is correct because:
- To break a sequence into segments of at most
m
, you place a flip everym+1
positions - For example, with "111111" (k=6) and m=2:
- Place flips at positions 2 and 5: "11011011"
- This requires 2 flips = 6 // (2+1) = 6 // 3 = 2
Solution:
Always use k // (m + 1)
for calculating the minimum operations needed to break a consecutive sequence.
2. Mishandling the Alternating Pattern Case (m = 1)
The Pitfall:
When m = 1
, developers might:
- Only check one alternating pattern ("010101...") without considering the complement
- Use incorrect indexing like
i % 2
instead ofi & 1
(though both work, bitwise is more efficient) - Count the matches instead of mismatches
Example of Wrong Implementation:
if m == 1:
cnt = sum(1 for i, c in enumerate(s) if c != str(i % 2))
# Missing: doesn't consider the "101010..." pattern
The Correct Approach:
if m == 1:
pattern = "01"
# Count mismatches with "010101..." pattern
cnt = sum(c == pattern[i & 1] for i, c in enumerate(s))
# Consider both patterns and take minimum
cnt = min(cnt, n - cnt)
3. Off-by-One Errors in Binary Search Range
The Pitfall: Using incorrect bounds for binary search:
- Starting from 0 instead of 1 (a consecutive sequence must have at least length 1)
- Using
n+1
as the upper bound whenn
is sufficient
Why It Matters:
- Starting from 0 would be meaningless (no consecutive sequence has length 0)
- The maximum possible consecutive length is
n
(the entire string), so searching beyondn
is unnecessary
Solution:
Always use lo=1
in the binary search since 1 is the minimum meaningful length for consecutive sequences.
4. Confusing the Binary Search Target
The Pitfall:
Misunderstanding what the binary search is looking for. The code uses bisect_left
to find where the check function first returns True
, but developers might:
- Think we're looking for
False
values - Use
bisect_right
instead, which would give a different result - Reverse the logic in the check function
The Correct Understanding:
- We want the minimum achievable maximum length
- The check function returns
True
if we can achieve that maximum length with available operations bisect_left
finds the smallest value where this is possible
Solution:
Ensure the check function returns True
when the target is achievable, and use bisect_left
to find the minimum such value.
How does quick sort divide the problem into subproblems?
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!