Problem Overview: Replace At Most K Characters
LeetCode 424 Longest Repeating Character Replacement gives you a string s consisting of uppercase English letters and an integer k. Your task is to find the length of the longest substring you can obtain by replacing at most k characters in s so that all characters in the substring are the same.
The replacement operation lets you change any character in the substring to any other uppercase letter. You want to maximize the length of the resulting all-same-character substring. For example, with s = "AABABBA" and k = 1, the answer is 4 because you can replace one character in "AABA" or "ABAB" to get "AAAA" or "AAAB" — wait, specifically replacing the 'B' in "AABA" gives "AAAA", length 4.
This is a medium-difficulty problem that sits at the intersection of sliding window technique and frequency counting. The key insight is that you never need to know which characters to replace — you only need to know how many replacements are required, which is window length minus the count of the most frequent character in the window.
- s consists of uppercase English letters A through Z only
- 1 <= s.length <= 10^5
- 0 <= k <= s.length
- Replace at most k characters (to any letter) to make all chars identical
- Find the length of the longest such achievable substring
- You can replace characters to ANY letter, not just existing ones in the window
Window Validity Condition: The Core Formula
A window is valid when the number of characters you need to replace is at most k. The number of replacements needed equals the window size minus the count of the most frequent character in the window. In other words: replacements needed = (right - left + 1) - maxFreqInWindow. If this value is <= k, the window is valid.
Why does this formula work? Whatever the most frequent character is, keep all of those and replace everything else. The cost (number of replacements) is exactly window_size - max_frequency. If that cost fits within your budget k, the window can be made into an all-same-character string. If it exceeds k, the window is too large for the budget and must shrink.
This formula elegantly avoids needing to know which character is most frequent or which characters to replace. It reduces the problem to two values at every step: window size and max frequency in window. Both can be maintained incrementally as the window slides, making each step O(1).
The Key Formula
Replacements needed = window length - max frequency of any single char in window. The window is valid when this value is <= k. Keep the most frequent character and replace all others. You never need to track which character is most frequent — just how large its count is.
Variable Sliding Window: Expand Right, Shrink Left
The sliding window approach uses two pointers left and right, both starting at index 0. Expand right by one at each step, adding the new character to the frequency array. After expanding, check if the window is still valid using the formula. If it is invalid, move left forward by one, removing the leftmost character from the frequency array.
Unlike fixed-size windows, this window can grow and shrink. But crucially, the window never needs to shrink by more than one at a time. When the window becomes invalid (replacements > k), shrinking by one restores validity — or at worst keeps the window the same size. This means the window size is monotonically non-decreasing, which is the key to why the algorithm finds the maximum in O(n).
Track character frequencies with an integer array of size 26 indexed by character minus the character code of 'A'. Increment the slot for the new right character at each step, decrement the slot for the removed left character when shrinking. The maxFreq variable requires separate attention, as discussed next.
- 1Initialize left = 0, maxFreq = 0, freq[26] = all zeros, result = 0
- 2Expand: increment freq[s[right] - A] for each new right character
- 3Update maxFreq = max(maxFreq, freq[s[right] - A])
- 4Check validity: if (right - left + 1) - maxFreq > k, shrink left by 1
- 5When shrinking: decrement freq[s[left] - A], increment left
- 6Update result = max(result, right - left + 1) after each step
The maxFreq Optimization: Never Decrease
The most surprising aspect of this algorithm is that maxFreq never needs to be decreased, even when the window shrinks and the most frequent character may have been partially removed. This seems wrong at first — if the most frequent character count drops, shouldn't maxFreq reflect that? The answer is no, because of a subtle but powerful invariant.
The invariant is this: we only care about finding a window LONGER than the best we have seen so far. A window of the current size is valid if and only if maxFreq is at the current level. If shrinking causes the true max frequency to drop below the stored maxFreq, the window validity check (window_size - maxFreq > k) will still cause us to shrink — keeping the window the same size, not growing it. We never miss a longer valid window.
In practice this means maxFreq is a "lazy" maximum that only moves up, never down. The window grows only when a new character reaches a count exceeding the previous maxFreq, which is exactly when a longer valid window is possible. Any other expansion just slides at constant size — valid only by accident — but those don't contribute to the answer.
Why Not Decreasing maxFreq Is Safe
We only care about finding LONGER windows. A stale (too-high) maxFreq makes the validity check optimistic: it thinks the window needs fewer replacements than it actually does. This causes the window to either stay valid (great — we found a longer window) or fail the check anyway and shrink. We never miss a better answer because a stale maxFreq can only keep the window the same size or larger — never smaller than the true optimum.
Why Decreasing maxFreq Would Be Wasteful
To correctly decrease maxFreq after shrinking, you would need to scan all 26 frequency slots to find the new maximum — O(26) per step. Over n steps, this is O(26n), which while technically O(n) is unnecessary overhead. The optimization avoids this entirely: simply never decrease maxFreq, and the algorithm remains correct.
The window grows only in two scenarios: either the window is already valid with the current maxFreq (so result updates), or a character's frequency exceeds maxFreq (so maxFreq increases and the window can stay valid at a larger size). In all other cases the window slides at constant size, which never worsens the answer.
This is an example of an amortized O(n) algorithm where the window pointer left never moves backward and right only moves forward — total O(n) pointer movements across all iterations. Combined with the O(1) per-step work (no 26-slot scan), the overall time complexity is O(n) with O(1) space for the 26-slot frequency array.
- 1Without the optimization: scan freq[26] each shrink step to recompute maxFreq — O(26n) total
- 2With the optimization: maxFreq only increases, never decreases — O(n) total
- 3Window grows only when freq[new_char] > maxFreq: a genuinely longer valid window is possible
- 4When window_size - maxFreq > k: shrink left by 1, keep window same size or decrease by 1
- 5result never decreases: once a longer window is seen, it is recorded
- 6Total: left moves right at most n times, right moves right n times — O(n) total movement
Code Walkthrough: Python and Java
The Python solution is around 10 lines. Initialize freq = [0] * 26, left = 0, maxFreq = 0. Loop right from 0 to len(s) - 1: increment freq[ord(s[right]) - ord('A')], update maxFreq = max(maxFreq, freq[ord(s[right]) - ord('A')]). If (right - left + 1) - maxFreq > k: decrement freq[ord(s[left]) - ord('A')], increment left. Update result = max(result, right - left + 1). Return result. Total: O(n) time, O(1) space.
In Java, use int[] freq = new int[26]. The loop structure is identical. freq[s.charAt(right) - 'A']++ adds the right char; freq[s.charAt(left) - 'A']-- removes the left char when shrinking. The condition is (right - left + 1) - maxFreq > k. Java solution is equally concise — about 12 lines. Both languages benefit from the same invariant: maxFreq never decreases.
A common mistake is computing the answer only when the window is explicitly valid. This is unnecessary — you can just track result = max(result, right - left + 1) at every step, because the window size never shrinks below the previous maximum answer. The window is monotonically non-decreasing in size, so the last window size is always the maximum window size seen.
if vs while When Shrinking
Some solutions use "if (window - maxFreq > k)" to shrink by 1 each step (O(n) total). Others use "while (window - maxFreq > k)" to shrink fully until valid. The if-version is correct and O(n) — it never shrinks more than needed because the window only grew by 1. The while-version also works but the inner loop is unnecessary; the condition can only be violated by at most 1 after a single right-expand.