A uniform substring is one in which all characters are identical. Given a string, determine the length of the longest uniform substring that can be formed by replacing up to k characters.
Input: s = 'aabcdcca', k = 2
Output: 5
Explanation: if we can only replace 2 characters, the longest uniform substring we can achieve is "ccccc", obtained by replacing 'b' and 'd' with 'c'.
New to this one? Start here. Below is the core idea in everyday language, plus a real-world analogy—then the sections that follow build on it with the formal reasoning, code, and step-by-step walkthroughs.
You have a row of coloured planks and k cans of paint. You want the longest stretch of fence that is a single colour. Within any stretch, keep the colour that already appears most, and repaint the rest. The stretch works as long as the planks you must repaint (its length minus the most common colour's count) is ≤ k.
Determining if a substring is uniform
Before we try finding the longest uniform substring, let’s first determine the most efficient way to make a string uniform with the fewest character replacements. Consider the example below:
Which characters should we replace to ensure the minimum number of replacements are performed to make the string uniform? There are three main choices: make the string all ‘a’s, or all ‘b’s, or all ‘c’s. The most efficient choice, requiring the fewest replacements, is to make all characters ‘a’, which involves just three replacements:
The key observation is that the minimum number of replacements needed to achieve uniformity is obtained by replacing all characters except the most frequent one.
This suggests that if we know the highest frequency of a character in a substring, we can determine if our value of k is sufficient to make that substring uniform. The number of characters that need to be replaced (num_chars_to_replace) can be found by subtracting this highest frequency from the total number of characters in the substring:
Once we’ve calculated num_chars_to_replace for a given substring, we can assess if the substring can be made uniform:
num_chars_to_replace ≤ k, the substring can be made uniform.num_chars_to_replace > k, the substring cannot be made uniform.To calculate num_chars_to_replace, we need to know the value of highest_freq. This requires tracking the frequency of each character, which can be efficiently managed using a hash map (freqs). This hash map allows us to update highest_freq whenever we encounter a character with a higher frequency. Below is an illustration of how freqs is updated:
Now that we have the tools to determine if a substring can be made uniform, the next step is to figure out how to identify the longest uniform substring. Let’s explore a technique that lets us do this.
Dynamic sliding window
We know sliding windows can be useful for solving problems involving substrings. This problem requires that we find the longest substring that satisfies a specific condition:
num_chars_to_replace <= k
So, a dynamic sliding window might be appropriate, as discussed in the chapter introduction.
We can use the above condition to determine how to expand or shrink the window:
If the condition is met (i.e., the window is valid), we expand the window to find a longer window that still meets this condition.
If the condition is violated (i.e., the window is invalid), we shrink the window until it meets the condition again.
Let’s see how this works over the example below:
Start by defining the left and right boundaries of the window at index 0. Continue expanding the window for as long as it satisfies our condition (nums_char_to_replace <= k):
Once the window expands to the fifth character (‘d’), it will contain 3 characters that must be replaced to make the window uniform. Since we can only replace up to k = 2 characters, the window is invalid. So, we shrink the window:
Notice that after shrinking the window, the value of highest_freq is still 2, which is no longer correct. Recall that our current method for updating highest_freq only increases it when encountering a character with a higher frequency, meaning highest_freq can only remain the same or increase, but it can never decrease.
One way to work around this is to develop a new method for updating highest_freq that accurately decreases it when the highest frequency in a window decreases. However, our goal is to find the longest substring that meets the condition, so shrinking the window might not even be necessary. The crucial point here is that when we find a valid window of a certain length, no shorter window will provide a longer uniform substring.
This means we can just slide the window instead of shrinking it whenever we encounter an invalid window, effectively maintaining the length of the current window.
With this observation, we should correct our previous logic:
Let’s correct the action taken in the first invalid window above by sliding instead of shrinking. Then, we can continue processing the rest of the string.
The above window is the final window because we cannot expand or slide it any further. The longest valid window encountered during this process is a window of length 5.
def longest_uniform_substring_after_replacements(s: str, k: int) -> int:
freqs = {}
highest_freq = max_len = 0
left = right = 0
while right < len(s):
# Update the frequency of the character at the right pointer and the highest
# frequency for the current window.
freqs[s[right]] = freqs.get(s[right], 0) + 1
highest_freq = max(highest_freq, freqs[s[right]])
# Calculate replacements needed for the current window.
num_chars_to_replace = (right - left + 1) - highest_freq
# Slide the window if the number of replacements needed exceeds 'k'.
# The right pointer always gets advanced, so we just need to advance 'left'.
if num_chars_to_replace > k:
# Remove the character at the left pointer from the hash map before
# advancing the left pointer.
freqs[s[left]] -= 1
left += 1
# Since the length of the current window increases or stays the same, assign
# the length of the current window to 'max_len'.
max_len = right - left + 1
# Expand the window.
right += 1
return max_len#include <string>
#include <unordered_map>
#include <algorithm>
using namespace std;
int longestUniformSubstringAfterReplacements(string s, int k) {
unordered_map<char, int> count; // char -> frequency in the window
int left = 0, maxFreq = 0, best = 0;
for (int right = 0; right < (int)s.size(); right++) {
char c = s[right];
count[c]++;
maxFreq = max(maxFreq, count[c]);
// if too many chars would need replacing, slide left by one
if ((right - left + 1) - maxFreq > k) {
count[s[left]]--;
left++;
}
best = max(best, right - left + 1);
}
return best;
}import java.util.*;
public int longestUniformSubstringAfterReplacements(String s, int k) {
Map<Character, Integer> count = new HashMap<>(); // char -> freq in window
int left = 0, maxFreq = 0, best = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
count.put(c, count.getOrDefault(c, 0) + 1);
maxFreq = Math.max(maxFreq, count.get(c));
// if too many chars would need replacing, slide left by one
if ((right - left + 1) - maxFreq > k) {
char leftChar = s.charAt(left);
count.put(leftChar, count.get(leftChar) - 1);
left++;
}
best = Math.max(best, right - left + 1);
}
return best;
}Time complexity: The time complexity of longest_uniform_substring_after_replacements is , where is the length of the input string. This is because we traverse the string linearly with two pointers.
Space complexity: The space complexity is , where is the number of unique characters in the string stored in the hash map freqs.
Two step-by-step walkthroughs of the algorithm below: a valid case (the expected happy path) and an invalid / edge case that exercises the tricky parts. Each step shows the program state as the code runs—the same steps apply to Python, C++ and Java.
majority char (keep) to replace (≤ k)
| right | char | maxFreq | length | need | action | best |
|---|---|---|---|---|---|---|
| 0 | A | 1 | 1 | 0 | ok | 1 |
| 1 | A | 2 | 2 | 0 | ok | 2 |
| 2 | B | 2 | 3 | 1 | ok | 3 |
| 3 | A | 3 | 4 | 1 | ok | 4 |
| 4 | B | 3 | 5 | 2 | slide → left=1 | 4 |
| 5 | B | 3 | 5→4 | 2 | slide → left=2 | 4 |
| 6 | A | 3 | 5→4 | 2 | slide → left=3 | 4 |
Final answer: best = 4 ("AABA" repainted to "AAAA").
single repeated char (valid at k = 0) current window
| right | char | maxFreq | length | need | action | best |
|---|---|---|---|---|---|---|
| 0 | A | 1 | 1 | 0 | ok | 1 |
| 1 | A | 2 | 2 | 0 | ok | 2 |
| 2 | B | 2 | 3→2 | 1 | slide → left=1 | 2 |
| 3 | A | 2 | 3→2 | 1 | slide → left=2 | 2 |
| 4 | B | 2 | 3→2 | 1 | slide → left=3 | 2 |
| 5 | B | 2 | 3→2 | 1 | slide → left=4 | 2 |
| 6 | A | 2 | 3→2 | 1 | slide → left=5 | 2 |
Final answer best = 2. Contrast with Dry run 1: with k = 1 we could repaint one character, so a length-4 window like “AABA” was valid. With k = 0 nothing may change, so a window is only valid while it holds a single repeated letter — the longest such run here is just “AA” (length 2). From right = 2 on, every step needs one replacement it can't afford, so the window slides forward without ever growing.
Both runs step exactly as the tables above. Python slides once whenever (right - left + 1) - max_freq > k; the window never shrinks, only slides.
Same steps. C++: the slide condition is (right - left + 1) - maxFreq > k.
Same steps. Java: same condition, with Math.max for maxFreq and best = Math.max(best, right - left + 1).