3398. Smallest Substring With Identical Characters I
Problem Description
You have a binary string s
consisting of only '0's and '1's, with length n
. You're allowed to perform up to numOps
operations on this string.
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 "111001", the longest substring with identical characters is "111" with length 3
- After some flips, you might reduce this maximum length
The task is to find the minimum possible length of such longest identical substring after performing at most numOps
operations optimally.
The solution uses binary search to find this minimum length. For each candidate length m
, it checks whether it's possible to achieve that maximum length using at most numOps
flips:
- When
m = 1
, the string should alternate like "010101..." or "101010...", and we count how many flips are needed - For
m > 1
, we count how many flips are needed to break up each consecutive group of identical characters into segments of at most lengthm
The binary search finds the smallest m
where the required number of flips doesn't exceed numOps
.
Intuition
The key insight is that we're looking for the minimum possible value of the maximum consecutive identical characters. This "minimize the maximum" pattern naturally suggests binary search on the answer.
Think about it this way: if we can achieve a maximum length of m
, then we can definitely achieve any length greater than m
(we just need fewer or the same number of flips). Conversely, if we cannot achieve length m
with our available operations, we cannot achieve any length smaller than m
either. This monotonic property makes binary search applicable.
For any target maximum length m
, we need to figure out the minimum number of flips required to ensure no consecutive identical substring exceeds length m
:
-
Special case when
m = 1
: This means we want no two adjacent characters to be the same - the string must alternate perfectly like "010101..." or "101010...". We can calculate flips needed for both patterns and take the minimum. -
General case when
m > 1
: For each group of consecutive identical characters of lengthk
:- If
k <= m
, no flips needed for this group - If
k > m
, we need to break it up by flipping some characters within it - To break a group of length
k
into segments of at mostm
, we needk // (m + 1)
flips strategically placed
- If
The formula k // (m + 1)
comes from the fact that each flip creates a breakpoint, and to divide a segment of length k
into parts of at most length m
, we need to place flips at intervals. For example, to break "11111" (length 5) into segments of at most 2, we need 5 // 3 = 1
flip in the middle.
By binary searching on the possible values of m
from 1 to n
, we find the smallest m
where the required flips don't exceed numOps
.
Learn more about Binary Search patterns.
Solution Approach
The solution implements a binary search combined with a validation function to find the minimum possible maximum length of consecutive identical characters.
Binary Search Setup:
- We use
bisect_left
to search in the range[1, n]
wheren
is the length of the string - The search finds the smallest value
m
wherecheck(m)
returnsTrue
The check
Function:
This function determines if we can achieve a maximum consecutive length of m
using at most numOps
flips.
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 (no two adjacent characters can be the same)
- Two possible patterns: "010101..." or "101010..."
- For pattern "010101...", count mismatches where even indices should be '0' and odd indices should be '1'
i & 1
gives 0 for even indices and 1 for odd indicesn - cnt
gives the count for the opposite pattern "101010..."- Take the minimum of both patterns
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 and identify groups of consecutive identical characters
- Variable
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 gives the minimum flips to break a group of size
k
into segments of at mostm
- Reset
k
to 0 for the next group
- Calculate flips needed:
Final Check:
- Return
True
ifcnt <= numOps
(we can achieve this maximum length with available operations) - The binary search finds the minimum
m
where this condition holds
The time complexity is O(n log n)
where each binary search iteration takes O(n)
to validate, and we have O(log n)
iterations.
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 a concrete example:
- String
s = "111001"
numOps = 2
(we can perform up to 2 flips)
Initial Analysis:
- Current consecutive groups: "111" (length 3), "00" (length 2), "1" (length 1)
- Maximum consecutive length = 3
Binary Search Process:
We'll binary search on the answer range [1, 6] (string length is 6).
Iteration 1: Check if m = 3 is achievable
- For each group:
- "111" (length 3): needs
3 // (3+1) = 0
flips - "00" (length 2): needs
2 // (3+1) = 0
flips - "1" (length 1): needs
1 // (3+1) = 0
flips
- "111" (length 3): needs
- Total flips needed: 0 ≤ 2 ✓
- Since we can achieve m=3, try smaller values
Iteration 2: Check if m = 2 is achievable
- For each group:
- "111" (length 3): needs
3 // (2+1) = 1
flip - "00" (length 2): needs
2 // (2+1) = 0
flips - "1" (length 1): needs
1 // (2+1) = 0
flips
- "111" (length 3): needs
- Total flips needed: 1 ≤ 2 ✓
- Since we can achieve m=2, try smaller values
Iteration 3: Check if m = 1 is achievable
- This requires alternating pattern
- Pattern "010101": compare with "111001"
- Positions needing flips: indices 0, 1, 3 → 3 flips
- Pattern "101010": compare with "111001"
- Positions needing flips: indices 2, 4, 5 → 3 flips
- Minimum flips needed: 3 > 2 ✗
- Cannot achieve m=1
Result: The minimum achievable maximum consecutive length is 2.
To verify: With 1 flip at index 1, we get "101001" with maximum consecutive length of 2 ("00").
Solution Implementation
1class Solution:
2 def minLength(self, s: str, numOps: int) -> int:
3 """
4 Find the minimum possible length of consecutive identical characters
5 after performing at most numOps operations.
6
7 Args:
8 s: Input binary string containing '0's and '1's
9 numOps: Maximum number of operations allowed
10
11 Returns:
12 Minimum possible length of consecutive identical characters
13 """
14
15 def can_achieve_max_length(max_consecutive_length: int) -> bool:
16 """
17 Check if we can achieve a maximum consecutive length with given operations.
18
19 Args:
20 max_consecutive_length: Target maximum length of consecutive identical chars
21
22 Returns:
23 True if achievable with numOps operations, False otherwise
24 """
25 operations_needed = 0
26
27 # Special case: if target length is 1, we need alternating pattern
28 if max_consecutive_length == 1:
29 # Try pattern starting with '0' at even indices
30 pattern = "01"
31 changes_for_pattern = sum(
32 current_char == pattern[index & 1]
33 for index, current_char in enumerate(s)
34 )
35 # We can start with either '0' or '1', so take minimum
36 operations_needed = min(changes_for_pattern, string_length - changes_for_pattern)
37 else:
38 # Count operations needed to break long consecutive sequences
39 consecutive_count = 0
40 for index, current_char in enumerate(s):
41 consecutive_count += 1
42
43 # Check if we're at the end or next character is different
44 if index == len(s) - 1 or current_char != s[index + 1]:
45 # Calculate operations needed to break this sequence
46 # We need to change every (max_consecutive_length + 1)-th character
47 operations_needed += consecutive_count // (max_consecutive_length + 1)
48 consecutive_count = 0
49
50 return operations_needed <= numOps
51
52 # Store string length for efficiency
53 string_length = len(s)
54
55 # Binary search for minimum achievable maximum consecutive length
56 # Using bisect_left to find the leftmost position where condition is True
57 from bisect import bisect_left
58 return bisect_left(
59 range(string_length), # Search space: [0, n-1]
60 True, # We're looking for the first True value
61 lo=1, # Start from 1 (minimum possible length)
62 key=can_achieve_max_length # Function to evaluate each candidate
63 )
64
1class Solution {
2 private char[] charArray;
3 private int maxOperations;
4
5 /**
6 * Finds the minimum possible length of the longest consecutive sequence of identical characters
7 * after performing at most numOps operations on the string.
8 *
9 * @param s The input string containing '0' and '1' characters
10 * @param numOps Maximum number of operations allowed
11 * @return The minimum achievable length of the longest consecutive sequence
12 */
13 public int minLength(String s, int numOps) {
14 this.maxOperations = numOps;
15 this.charArray = s.toCharArray();
16
17 // Binary search on the answer: minimum length ranges from 1 to string length
18 int left = 1;
19 int right = s.length();
20
21 while (left < right) {
22 int mid = (left + right) >> 1; // Equivalent to (left + right) / 2
23
24 if (check(mid)) {
25 // If we can achieve length 'mid', try for a smaller length
26 right = mid;
27 } else {
28 // If we cannot achieve length 'mid', we need a larger length
29 left = mid + 1;
30 }
31 }
32
33 return left;
34 }
35
36 /**
37 * Checks if it's possible to make all consecutive sequences have length at most maxLength
38 * using at most maxOperations operations.
39 *
40 * @param maxLength The target maximum length for consecutive sequences
41 * @return true if achievable with available operations, false otherwise
42 */
43 private boolean check(int maxLength) {
44 int operationsNeeded = 0;
45
46 if (maxLength == 1) {
47 // Special case: when maxLength is 1, we want an alternating pattern
48 // Count operations needed to convert to either "010101..." or "101010..."
49 char[] alternatingPattern = {'0', '1'};
50
51 // Count mismatches with pattern starting with '0'
52 for (int i = 0; i < charArray.length; ++i) {
53 if (charArray[i] == alternatingPattern[i & 1]) { // i & 1 gives 0 for even, 1 for odd
54 ++operationsNeeded;
55 }
56 }
57
58 // Choose the pattern that requires fewer operations
59 operationsNeeded = Math.min(operationsNeeded, charArray.length - operationsNeeded);
60 } else {
61 // General case: count operations needed to break sequences longer than maxLength
62 int currentSequenceLength = 0;
63
64 for (int i = 0; i < charArray.length; ++i) {
65 ++currentSequenceLength;
66
67 // Check if current sequence ends (at string end or character changes)
68 if (i == charArray.length - 1 || charArray[i] != charArray[i + 1]) {
69 // Calculate operations needed to break this sequence into pieces of maxLength
70 // We need floor(currentSequenceLength / (maxLength + 1)) operations
71 operationsNeeded += currentSequenceLength / (maxLength + 1);
72 currentSequenceLength = 0; // Reset for next sequence
73 }
74 }
75 }
76
77 // Check if required operations are within the limit
78 return operationsNeeded <= maxOperations;
79 }
80}
81
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 consecutive same characters)
13 // We need to transform string to either "010101..." or "101010..."
14 string alternatingPattern = "01";
15 int changesForPattern1 = 0;
16
17 // Count changes needed to match "010101..." pattern
18 for (int i = 0; i < n; ++i) {
19 if (s[i] == alternatingPattern[i & 1]) {
20 ++changesForPattern1;
21 }
22 }
23
24 // Choose the pattern that requires fewer changes
25 // changesForPattern2 = n - changesForPattern1 (for "101010..." pattern)
26 operationsNeeded = min(changesForPattern1, n - changesForPattern1);
27 } else {
28 // General case: allow consecutive segments of length up to targetLength
29 int currentSegmentLength = 0;
30
31 for (int i = 0; i < n; ++i) {
32 ++currentSegmentLength;
33
34 // Check if we're at the end of a segment (last char or different from next)
35 if (i == n - 1 || s[i] != s[i + 1]) {
36 // Calculate operations needed to break this segment into pieces
37 // of size at most targetLength
38 operationsNeeded += currentSegmentLength / (targetLength + 1);
39 currentSegmentLength = 0;
40 }
41 }
42 }
43
44 return operationsNeeded <= numOps;
45 };
46
47 // Binary search for the minimum achievable length
48 int left = 1, right = n;
49
50 while (left < right) {
51 int mid = (left + right) >> 1;
52
53 if (canAchieveLength(mid)) {
54 // If we can achieve mid, try smaller values
55 right = mid;
56 } else {
57 // If we cannot achieve mid, we need larger values
58 left = mid + 1;
59 }
60 }
61
62 return left;
63 }
64};
65
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 we can achieve a target length with at most numOps operations
12 * @param targetLength - The target length to check
13 * @returns True if achievable with numOps or fewer 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 the string alternating
20 // Count operations needed to match pattern "010101..." or "101010..."
21 const alternatingPattern: string = '01';
22
23 for (let i = 0; i < stringLength; ++i) {
24 // Check if current character matches the expected alternating pattern
25 if (s[i] === alternatingPattern[i & 1]) {
26 ++operationsNeeded;
27 }
28 }
29
30 // Choose the minimum between making it "0101..." or "1010..."
31 operationsNeeded = Math.min(operationsNeeded, stringLength - operationsNeeded);
32 } else {
33 // For target length > 1, count consecutive blocks and calculate operations needed
34 let consecutiveCount: number = 0;
35
36 for (let i = 0; i < stringLength; ++i) {
37 ++consecutiveCount;
38
39 // Check if we've reached the end of a consecutive block
40 if (i === stringLength - 1 || s[i] !== s[i + 1]) {
41 // Calculate operations needed for this block
42 // We need to break blocks larger than targetLength
43 operationsNeeded += Math.floor(consecutiveCount / (targetLength + 1));
44 consecutiveCount = 0;
45 }
46 }
47 }
48
49 return operationsNeeded <= numOps;
50 };
51
52 // Binary search for the minimum achievable length
53 let left: number = 1;
54 let right: number = stringLength;
55
56 while (left < right) {
57 const mid: number = (left + right) >> 1; // Equivalent to Math.floor((left + right) / 2)
58
59 if (canAchieveLength(mid)) {
60 // If we can achieve mid length, try smaller values
61 right = mid;
62 } else {
63 // If we cannot achieve mid length, we need larger values
64 left = mid + 1;
65 }
66 }
67
68 return left;
69}
70
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 binary search, the check
function is called:
- When
m == 1
: The function iterates through the string once to count mismatches with alternating patterns, takingO(n)
time. - When
m > 1
: The function iterates through the string once to count consecutive segments and calculate required operations, takingO(n)
time.
Therefore, 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
,k
, andt
(whenm == 1
) - The string
t = "01"
whenm == 1
is constant size - The binary search itself only requires a few variables for bounds and mid-point calculations
The space used does not scale with the input size, resulting in O(1)
space complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Formula for Breaking Consecutive Groups
Pitfall: A common mistake is using the wrong formula to calculate the number of flips needed to break a consecutive group into segments of at most length m
. Developers might incorrectly use:
cnt += (k - 1) // m
cnt += k // m
cnt += (k + m - 1) // m
Why it's wrong: These formulas don't correctly calculate the minimum number of flips needed. For example, if we have a group of 7 consecutive '1's and want max length of 2:
- String: "1111111" (length 7)
- To achieve max length 2, we need to flip at positions to get: "1101101"
- We need 2 flips (positions 2 and 5)
- The correct formula
7 // (2 + 1) = 7 // 3 = 2
gives us 2 flips - But
(7 - 1) // 2 = 3
would give wrong answer
Solution: Use the correct formula k // (m + 1)
where:
k
is the length of the consecutive groupm
is the target maximum length- This formula works because we place flips every
(m + 1)
positions to break the group optimally
2. Off-by-One Error in Binary Search Range
Pitfall: Setting the binary search range incorrectly, such as:
# Wrong: searching from 0
bisect_left(range(string_length + 1), True, lo=0, key=can_achieve_max_length)
Why it's wrong:
- The minimum possible maximum consecutive length is 1, not 0
- A consecutive length of 0 doesn't make sense (empty substring)
- Starting from 0 would waste one iteration and could cause logical errors
Solution: Always start the search from lo=1
:
bisect_left(range(string_length), True, lo=1, key=can_achieve_max_length)
3. Misunderstanding the Alternating Pattern Check
Pitfall: When checking for m = 1
(alternating pattern), incorrectly calculating the flips needed:
# Wrong approach 1: Only checking one pattern
cnt = sum(c != str(i % 2) for i, c in enumerate(s))
# Wrong approach 2: Not considering both starting patterns
cnt = sum(c == t[i % 2] for i, c in enumerate(s)) # Without taking min
Why it's wrong:
- For alternating patterns, we have two choices: start with '0' or start with '1'
- Pattern "010101..." and pattern "101010..." might require different numbers of flips
- We should choose the pattern that requires fewer flips
Solution: Check both patterns and take the minimum:
pattern = "01"
cnt = sum(c == pattern[i & 1] for i, c in enumerate(s))
cnt = min(cnt, n - cnt) # n - cnt gives flips for opposite pattern
4. Not Resetting the Consecutive Counter
Pitfall: Forgetting to reset the consecutive counter after processing each group:
# Wrong: counter not reset
k = 0
for i, c in enumerate(s):
k += 1
if i == len(s) - 1 or c != s[i + 1]:
cnt += k // (m + 1)
# Missing: k = 0
Why it's wrong: Without resetting k
, the counter continues to accumulate across different groups, leading to incorrect calculations for subsequent groups.
Solution: Always reset the counter after processing each group:
if i == len(s) - 1 or c != s[i + 1]:
cnt += k // (m + 1)
k = 0 # Reset for next group
5. Edge Case: Single Character Groups
Pitfall: Not handling strings with no consecutive identical characters properly (e.g., "0101010"):
- When all groups have length 1, the formula
1 // (m + 1)
always gives 0 form ≥ 1
- This might lead to confusion about whether the algorithm is working correctly
Solution: The algorithm handles this correctly by design. When groups are already small enough (length ≤ m), no flips are needed, which is the expected behavior. Ensure test cases cover such scenarios to validate correctness.
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!