Smallest Substring With Identical Characters I
HardPrompt
You are given a binary string s of length n and an integer numOps.
You are allowed to perform the following operation on s at most numOps times:
- Select any index
i(where0 <= i < n) and flips[i]. Ifs[i] == '1', changes[i]to'0'and vice versa.
You need to minimize the length of the longest substring of s such that all the characters in the substring are identical.
Return the minimum length after the operations.
Example 1:
Input: s = "000001", numOps = 1
Output: 2
Explanation:
By changing s[2] to '1', s becomes "001001". The longest substrings with identical characters are s[0..1] and s[3..4].
Example 2:
Input: s = "0000", numOps = 2
Output: 1
Explanation:
By changing s[0] and s[2] to '1', s becomes "1010".
Example 3:
Input: s = "0101", numOps = 0
Output: 1
Constraints:
1 <= n == s.length <= 1000sconsists only of'0'and'1'.0 <= numOps <= n
Approaches
2 approaches with complexity analysis and trade-offs.
This approach involves checking every possible answer for the minimum length, from 1 up to n. For each potential length k, we calculate the minimum number of flips required to ensure no substring of identical characters is longer than k. The first value of k that can be achieved with at most numOps flips is our answer.
Algorithm
- Loop through
kfrom1ton.krepresents the potential maximum length of an identical character substring. - Inside the loop, calculate the total flips needed,
requiredFlips, to satisfy the length constraintk. - To calculate
requiredFlips:- Initialize
requiredFlips = 0. - Iterate through the string
sto find all contiguous runs of '0's. For each run of lengthlen, iflen > k, addlen / (k + 1)torequiredFlips. - Iterate through the string
sagain to find all contiguous runs of '1's. For each run of lengthlen, iflen > k, addlen / (k + 1)torequiredFlips.
- Initialize
- If
requiredFlips <= numOps, it means we can achieve a maximum run length ofk. Since we are iteratingkfrom 1 upwards, this is the smallest possible length. Returnk.
Walkthrough
The core idea is to test each possible length k starting from 1. Since we are looking for the minimum length, the first k that satisfies the condition will be the answer.
To check if a length k is achievable, we need to calculate the minimum number of operations. This involves breaking up any run of identical characters (both '0's and '1's) that is longer than k.
For a run of identical characters of length len > k, we need to introduce flips to break it into segments of length at most k. The most efficient way to do this is to place a flip at every (k+1)-th position. The number of flips required for this single run is floor(len / (k + 1)).
We calculate the total flips needed by summing up the flips required for all runs of '0's longer than k and all runs of '1's longer than k.
We iterate k from 1 to n. For each k, we compute this total required flips. If it's less than or equal to numOps, we've found our minimal length, and we can return k.
class Solution { public int smallestSubstring(String s, int numOps) { int n = s.length(); for (int k = 1; k <= n; k++) { if (isPossible(s, numOps, k)) { return k; } } return n; // Should not be reached given the problem constraints } private int countFlipsForChar(String s, int k, char targetChar) { int flips = 0; int currentRun = 0; for (int i = 0; i < s.length(); i++) { if (s.charAt(i) == targetChar) { currentRun++; } else { if (currentRun > k) { flips += currentRun / (k + 1); } currentRun = 0; } } if (currentRun > k) { flips += currentRun / (k + 1); } return flips; } private boolean isPossible(String s, int numOps, int k) { int requiredFlips = countFlipsForChar(s, k, '0') + countFlipsForChar(s, k, '1'); return requiredFlips <= numOps; }}Complexity
Time
O(n^2). The outer loop runs up to `n` times. Inside the loop, the `isPossible` check function takes `O(n)` time to scan the string.
Space
O(1). We only use a few variables to keep track of counts and loop indices.
Trade-offs
Pros
Simple and straightforward to understand and implement.
Correctly solves the problem for smaller constraints.
Cons
Inefficient due to the nested loop structure, leading to a quadratic time complexity.
For large
n, this approach can be too slow and may result in a 'Time Limit Exceeded' error on some platforms.
This approach optimizes the search for the minimum length by using binary search. The key observation is that if we can achieve a maximum length of k, we can also achieve any length greater than k. This monotonic property allows us to efficiently search for the smallest k that satisfies the condition.
Algorithm
- Initialize the search range for the answer:
low = 1,high = n. - Initialize a variable
ans = nto store the best possible answer found so far. - While
low <= high:- Calculate
mid = low + (high - low) / 2. - Call a helper function
isPossible(mid)which determines if a max length ofmidis achievable withnumOpsflips. - The
isPossible(mid)function calculates total flips needed for runs longer thanmidand checks if it's<= numOps. - If
isPossible(mid)is true:midis a valid length. Store it as a potential answer:ans = mid.- Try for a smaller length by narrowing the search space:
high = mid - 1.
- If
isPossible(mid)is false:midis too small. We need a larger length:low = mid + 1.
- Calculate
- After the loop terminates,
answill hold the minimum possible length. Returnans.
Walkthrough
The problem asks for the minimum value of the maximum length. This structure is a strong indicator that binary search on the answer is a viable strategy.
We can binary search for the answer k in the range [1, n].
For a given k (our mid in the binary search), we need a function isPossible(k) that checks if it's possible to make the longest run of identical characters at most k long using at most numOps flips.
This isPossible(k) function is the same as the one in the linear scan approach. It calculates the total flips needed to break all runs of '0's and '1's longer than k. The number of flips for a run of length len is floor(len / (k + 1)).
- If
isPossible(k)is true, it meanskis a potential answer, and we can try to find an even smaller length. So, we search in the lower half[low, mid - 1]. - If
isPossible(k)is false,kis too small, and we need to allow for a longer maximum length. So, we search in the upper half[mid + 1, high].
We keep track of the smallest k for which isPossible(k) was true.
class Solution { public int smallestSubstring(String s, int numOps) { int n = s.length(); int low = 1, high = n; int ans = n; while (low <= high) { int mid = low + (high - low) / 2; if (isPossible(s, numOps, mid)) { ans = mid; high = mid - 1; } else { low = mid + 1; } } return ans; } private int countFlipsForChar(String s, int k, char targetChar) { int flips = 0; int currentRun = 0; for (int i = 0; i < s.length(); i++) { if (s.charAt(i) == targetChar) { currentRun++; } else { if (currentRun > k) { flips += currentRun / (k + 1); } currentRun = 0; } } if (currentRun > k) { flips += currentRun / (k + 1); } return flips; } private boolean isPossible(String s, int numOps, int k) { if (k == 0) return false; // Minimum length is 1 int requiredFlips = countFlipsForChar(s, k, '0') + countFlipsForChar(s, k, '1'); return requiredFlips <= numOps; }}Complexity
Time
O(n log n). The binary search runs `log n` times, and each check (`isPossible` function) takes `O(n)` time.
Space
O(1). We only use a few variables for the search and the check.
Trade-offs
Pros
Much more efficient than the linear scan approach, especially for large
n.Utilizes the monotonic property of the problem to reduce the search space logarithmically.
Optimal for the given constraints.
Cons
Slightly more complex to conceptualize than a simple linear scan, as it requires understanding the binary search on answer pattern.
Solutions
Solution
class Solution {private char[] s;private int numOps;public int minLength(String s, int numOps) { this.numOps = numOps; this.s = s.toCharArray(); int l = 1, r = s.length(); while (l < r) { int mid = (l + r) >> 1; if (check(mid)) { r = mid; } else { l = mid + 1; } } return l; }private boolean check(int m) { int cnt = 0; if (m == 1) { char[] t = {'0', '1'}; for (int i = 0; i < s.length; ++i) { if (s[i] == t[i & 1]) { ++cnt; } } cnt = Math.min(cnt, s.length - cnt); } else { int k = 0; for (int i = 0; i < s.length; ++i) { ++k; if (i == s.length - 1 || s[i] != s[i + 1]) { cnt += k / (m + 1); k = 0; } } } return cnt <= numOps; }}class Solution {public: int minLength(string s, int numOps) { int n = s.size(); auto check = [&](int m) { int cnt = 0; if (m == 1) { string t = "01"; for (int i = 0; i < n; ++i) { if (s[i] == t[i & 1]) { ++cnt; } } cnt = min(cnt, n - cnt); } else { int k = 0; for (int i = 0; i < n; ++i) { ++k; if (i == n - 1 || s[i] != s[i + 1]) { cnt += k / (m + 1); k = 0; } } } return cnt <= numOps; }; int l = 1, r = n; while (l < r) { int mid = (l + r) >> 1; if (check(mid)) { r = mid; } else { l = mid + 1; } } return l; }};class Solution: def minLength(self, s: str, numOps: int) -> int: def check(m: int) -> bool: cnt = 0 if m == 1: t = "01" cnt = sum(c == t[i & 1] for i, c in enumerate(s)) cnt = min(cnt, n - cnt) 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 return cnt <= numOps n = len(s) return bisect_left(range(n), True, lo=1, key=check)Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.