Longest Repeating Character Replacement
1. Problem & Core Objective
The problem
You are given a string s and an integer k. You may choose any character of the string and change it to any other uppercase English character, and you may do this at most k times.
Return the length of the longest substring containing the same letter you can obtain.
Input: s = "ABAB", k = 2 Output: 4 (change both A's to B, or both B's to A)
Input: s = "AABABBA", k = 1 Output: 4 ("AABA" → change the B → "AAAA")Constraints:
1 <= s.length <= 10^5sconsists of only uppercase English letters0 <= k <= s.length
What the interviewer is actually testing
The template is the same longest-valid window as the previous question. The difficulty is entirely in deriving the validity condition — the problem doesn't hand it to you.
- Can you derive
windowLength − maxFreq <= k? That expression is the whole problem. Everything else is boilerplate. - Do you realize you never simulate the replacements? You only count how many would be needed.
- Can you defend the stale
maxFreq? The efficient version never decreasesmaxFreqwhen shrinking, which looks like a bug and isn't. Interviewers who know this question probe it specifically. - Do you see why the window is monotonic? Growing can only increase the replacements needed, which is what licenses the shrink loop.
2. First-Principles Thought Process
Step 1 — Constraints
n up to 10^5, so O(n) or O(26n) is expected and O(n²) is not.
The alphabet is uppercase English only — 26 characters. That's the signal to use int[26] rather than a HashMap (06 — Arrays & Hashing).
Step 2 — Restate the goal in terms of a window
We're looking for a contiguous substring that can be made uniform with at most k changes. So:
Find the longest window that can be made all-one-character using at most
kreplacements.
Contiguous + longest + a budget constraint → longest-valid sliding window.
Step 3 — Derive the validity condition
This is the step that matters. For a given window, what's the cheapest way to make it uniform?
Keep whichever character already appears most often, and replace everything else. Any other choice replaces more characters.
So for a window of length L where the most frequent character appears maxFreq times:
replacements needed = L − maxFreqAnd the window is valid exactly when:
windowLength − maxFreq <= k
Worked example. Window "AABAB", so L = 5. Counts: A appears 3 times, B twice, so maxFreq = 3. Replacements needed = 5 − 3 = 2. Valid if k >= 2.
Why you never simulate the replacements: you only need to know how many would be required, not which ones. The string is never modified.
Step 4 — Confirm monotonicity
Windowing is only sound if growing the window can't make an invalid window valid again.
Adding one character increases L by 1. It increases maxFreq by at most 1 — and only if the added character happens to be the current majority. So L − maxFreq either stays the same or increases. It never decreases.
That's the monotonicity that licenses shrinking from the left: once invalid, a window stays invalid until you shrink it.
Step 5 — Maintaining maxFreq
The obvious approach recomputes maxFreq by scanning all 26 counts after each change: O(26) per step, so O(26n) overall — which is O(n) with a constant of 26.
The refinement is to keep maxFreq as a running value and never decrease it when shrinking. That looks wrong, and defending it is the question's real test — see the counter-questions on Approach 3.
3. Solution Paths
Approach 1 — Brute force: every substring
public int characterReplacement(String s, int k) {
int best = 0;
for (int i = 0; i < s.length(); i++) {
int[] count = new int[26];
int maxFreq = 0;
for (int j = i; j < s.length(); j++) {
count[s.charAt(j) - 'A']++;
maxFreq = Math.max(maxFreq, count[s.charAt(j) - 'A']);
int len = j - i + 1;
if (len - maxFreq <= k) best = Math.max(best, len);
}
}
return best;
}Extends every start rightward, tracking counts incrementally within each start.
- Time:
O(n²). - Space:
O(1)— 26 ints.
Counter-questions on this approach
⭐ "When a window becomes invalid, why do you keep extending it?"
I shouldn't, and that's a real inefficiency even within the brute force — once
len - maxFreq > kfor this start, extending further only makes it worse, since the expression is monotonic. I couldbreak. But the deeper waste is restarting fromi + 1at all: most of the failing window is still usable if I shrink from the left instead of discarding it.
"Is maxFreq correct here, given you never reset it within a start?"
Yes, because within a single start the window only ever grows — counts never decrease, so the running maximum is genuinely the maximum. The subtlety only appears once shrinking enters the picture, in Approach 3.
Approach 2 — Sliding window, recomputing maxFreq
public int characterReplacement(String s, int k) {
int[] count = new int[26];
int left = 0, best = 0;
for (int right = 0; right < s.length(); right++) {
count[s.charAt(right) - 'A']++;
while (right - left + 1 - maxCount(count) > k) { // recompute each time
count[s.charAt(left) - 'A']--;
left++;
}
best = Math.max(best, right - left + 1);
}
return best;
}
private int maxCount(int[] count) {
int m = 0;
for (int c : count) m = Math.max(m, c);
return m;
}Trace on s = "AABABBA", k = 1:
right | char | Window | maxFreq | L − maxFreq | > k? | left | best |
|---|---|---|---|---|---|---|---|
| 0 | A | A | 1 | 0 | no | 0 | 1 |
| 1 | A | AA | 2 | 0 | no | 0 | 2 |
| 2 | B | AAB | 2 | 1 | no | 0 | 3 |
| 3 | A | AABA | 3 | 1 | no | 0 | 4 |
| 4 | B | AABAB | 3 | 2 | yes → shrink | 1 | 4 |
| 5 | B | ABABB → shrink | — | — | yes | 2 | 4 |
| 6 | A | BABA… | 2 | 2 | yes → shrink | 3 | 4 |
Answer: 4 ✓ (the substring "AABA", changing the B)
- Time:
O(26n)=O(n). - Space:
O(1).
Counter-questions on this approach
⭐ "You scan 26 counts on every shrink check. Is that really O(n)?"
Yes — 26 is a constant fixed by the problem's alphabet, not a function of
n. So it'sO(26n)which isO(n). It's a genuine constant factor though, and it can be removed entirely, which is the next approach.
⭐ "Why is right - left + 1 - maxFreq the right thing to compare against k?"
right - left + 1is the window length.maxFreqis how many characters I can keep — the majority character. Everything else must be replaced, so the replacements needed are the difference. The window is valid exactly while that's within budget.
"How do you know shrinking will ever restore validity?"
Because the expression is monotonic in window size: removing a character reduces
Lby 1 and reducesmaxFreqby at most 1, soL − maxFreqnever increases when shrinking. In the worst case the window shrinks to length 1, where the value is 0 and anyk >= 0is satisfied. The loop always terminates.
Approach 3 — Sliding window with a running maxFreq (optimal)
public int characterReplacement(String s, int k) {
int[] count = new int[26];
int left = 0, maxFreq = 0, best = 0;
for (int right = 0; right < s.length(); right++) {
count[s.charAt(right) - 'A']++;
maxFreq = Math.max(maxFreq, count[s.charAt(right) - 'A']);
while (right - left + 1 - maxFreq > k) {
count[s.charAt(left) - 'A']--; // note: maxFreq is NOT decreased
left++;
}
best = Math.max(best, right - left + 1);
}
return best;
}The only change from Approach 2: maxFreq is updated incrementally and never decreased when shrinking.
- Time:
O(n)— no inner 26-scan at all. - Space:
O(1).
Counter-questions on this approach
⭐ "You never decrease maxFreq when you shrink. Isn't it now wrong — a stale, too-large value?"
It can be stale, yes — and the answer is still correct. Here's why.
A too-large
maxFreqmakes the conditionL − maxFreq > kharder to trigger, so the window shrinks less than it strictly should. That means the window can temporarily hold a length that isn't genuinely achievable.But
bestis only updated withright - left + 1, and the window never grows beyond one character per iteration. Forbestto increase, the window must reach a length it has never reached before — and reaching a new maximum length requires a genuinely valid window with an up-to-datemaxFreq, becausemaxFreqis refreshed every time a character enters. So a stale value can delay shrinking, but it can never manufacture a new maximum.The invariant is:
bestnever exceeds the largest genuinely valid window seen.
⭐ "So would recomputing maxFreq on every shrink also be correct?"
Yes, and that's Approach 2 — correct and easier to defend, at
O(26n). I'd write that version first if I weren't confident in the staleness argument. The optimal version is only worth claiming if you can justify it, because "it passes the tests" is not an answer to this follow-up.
"Does the window ever actually shrink here, or does left just track right?"
It shrinks by at most one position per iteration, because the condition is checked after adding exactly one character. In practice the
whilebehaves like anif— worth noting, though writing it as awhilekeeps it consistent with the general template and costs nothing.
"Why s.charAt(right) - 'A' rather than - 'a'?"
The constraints say uppercase English letters, and
'A'is the base of that range. Using'a'would produce large negative indices and throw. It's a one-character difference from the previous question, and exactly the kind of thing to check against the constraints rather than habit.
Comparison
| Approach | Time | Space | Notes |
|---|---|---|---|
| Brute force | O(n²) | O(1) | Restarts each window |
Window, recompute maxFreq | O(26n) = O(n) | O(1) | Easiest to justify |
Window, running maxFreq | O(n) | O(1) | Optimal; needs the staleness argument |
4. Why the Optimal Wins
Against brute force. Same restart-versus-shrink argument as the previous question: when a window becomes invalid, the brute force abandons it and rebuilds from the next index, while the window removes just enough from the left and continues. O(n²) → O(n).
Against the recomputing version. Both are O(n) asymptotically, since 26 is a constant. The running version removes that factor of 26 entirely. It's a constant-factor improvement bought with a subtle correctness argument — which is exactly the trade to discuss rather than assume.
Be honest about this one:
"Both versions are
O(n). The running-maxFreqversion is faster by a constant factor, but it relies on a staleness argument that's easy to get wrong. If I weren't confident in that argument I'd ship the recomputing version — a correctO(26n)beats a subtly-wrongO(n)."
Why O(n) is the floor. Every character must be examined, since the optimal window could begin or end anywhere. So O(n) is optimal.
The transferable lesson is the derivation, not the code:
When a problem gives you a budget ("at most
kchanges"), the window's validity condition is usually "cost of this window ≤ budget" — and your job is to find a cheap way to compute that cost incrementally.
Here the cost was L − maxFreq. In Minimum Window Substring it's a have/need counter. Same shape.
5. Java Prerequisites
Fixed-alphabet counting
int[] count = new int[26];
count[s.charAt(i) - 'A']++; // UPPERCASE — base is 'A', not 'a''A' is 65 and 'Z' is 90, so c - 'A' maps to 0–25. Using 'a' (97) as the base on uppercase input gives negative indices and throws ArrayIndexOutOfBoundsException.
This is O(1) space, not O(k) — 26 is fixed by the problem, not a function of the input. See 03.
Running maximum
maxFreq = Math.max(maxFreq, count[idx]);Only the count that just changed can raise the maximum, so there's no need to rescan — that's what makes the O(26) factor removable.
Window length
right - left + 1Inclusive both ends. Check on one character: 0 - 0 + 1 = 1.
while vs if in the shrink step
while (right - left + 1 - maxFreq > k) { ... }Because exactly one character enters per iteration, the window becomes invalid by at most one, so this behaves as an if. Writing while costs nothing and keeps the code consistent with the general template — which matters when you adapt it to a problem where multiple shrinks are needed.
Operator precedence
right - left + 1 - maxFreq > kAll the arithmetic binds tighter than >, so this parses as (right - left + 1 - maxFreq) > k. Correct, but adding the parentheses makes the intent obvious to a reader.
6. Interview Communication Guide
Clarifying questions
- "Is the string uppercase only?" — yes, which is what makes
int[26]valid. Get this before writing- 'A'. - "Can I replace a character with any letter, or only ones already present?" — any uppercase letter. It doesn't change the algorithm, but it confirms the
L − maxFreqreasoning. - "Does the substring have to be contiguous?" — yes. A subsequence version would be a different problem.
- "Can
kbe 0, or larger than the string?" — both are allowed by the constraints.k = 0means "longest run of identical characters";k >= nmeans the whole string. - "Do I return the length or the substring?" — the length.
The pitch
"I want the longest contiguous window I can make uniform with at most
kchanges.The key question is: for a given window, how many changes does it need? The cheapest strategy is to keep whichever character already appears most often and replace the rest. So the cost is
windowLength − maxFrequency, and the window is valid while that's at mostk.That condition is monotonic — adding a character increases the length by 1 and the max frequency by at most 1, so the cost never decreases as the window grows. That's what makes a sliding window sound.
So: expand
rightalways, and while the cost exceedsk, shrink from the left. Record the length after validity is restored. Since the alphabet is 26 uppercase letters, I'll count with anint[26].The simple version rescans the 26 counts to find
maxFreqon each check — that'sO(26n), which isO(n). I can drop the 26 by trackingmaxFreqincrementally and never decreasing it when shrinking. That looks wrong, but it's safe: a stalemaxFreqonly delays shrinking, andbestcan only improve when the window reaches a genuinely new maximum length, which requires a freshmaxFreq."
Edge cases to raise proactively
| Input | k | Expected | Why |
|---|---|---|---|
"ABAB" | 2 | 4 | Whole string; replace two of one letter |
"AABABBA" | 1 | 4 | "AABA" |
"AAAA" | 0 | 4 | Already uniform, no changes needed |
"ABCD" | 0 | 1 | No replacements → longest identical run is 1 |
"ABCD" | 3 | 4 | Budget covers everything |
"A" | 0 | 1 | Single character |
"AAAB" | 0 | 3 | The run must be contiguous |
k = 0 is the case to volunteer — it reduces to "longest run of a single repeated character", and it's a good check that the cost formula degenerates correctly. k >= n is the other: the answer is the whole string, since everything can be replaced.
7. Follow-Up Questions — Modified Constraints
The interviewer changes a constraint of the original problem and asks you to solve it again. ⭐ marks the most likely.
⭐ "What if the string weren't restricted to uppercase — full Unicode?"
int[26]breaks. Switch to aHashMap<Character,Integer>for the counts.maxFreqcan still be tracked incrementally the same way, so the algorithm is unchanged — only the container. Space becomesO(k)in the distinct characters present rather thanO(1).
"What if you had to return the substring itself rather than its length?"
Record
leftalongsidebestwhenever the length improves, thensubstring(bestLeft, bestLeft + best). No complexity change.
"What if you could only replace characters with one specific target letter, chosen in advance?"
Much simpler. The cost of a window becomes
windowLength − count[target], with no maximum to track. Run the same window once and you're done —O(n). If the target isn't given, run it 26 times, once per candidate letter, and take the best:O(26n), stillO(n). That's also a clean way to sidestep the staleness argument entirely, which is worth offering if the interviewer pushes on it.
"What if the replacements had different costs per character?"
The "keep the majority character" reasoning breaks — the cheapest character to standardize on is no longer necessarily the most frequent one. You'd need, for each window, the minimum total cost over all 26 target letters, which is
O(26)per window and pushes you back toward the recomputing version.
"What if k could change between queries on the same string?"
Each query is an independent
O(n)run, soqqueries costO(qn). To do better you'd note that the answer is monotonic ink— a larger budget never gives a shorter answer — so you could precompute answers for allkfrom 0 toninO(n²)and then answer each query inO(1). Worth it only ifqis large.
"What about the longest substring with at most k distinct characters?" (LC 340)
A different invariant on the same template: track counts in a map and shrink while
map.size() > k.O(n)time,O(k)space. Same shape, and a good demonstration that the template transfers once you can state the validity condition.