Question: Longest substring without three repeats in a row
Companies: Microsoft
Difficulty: Medium
Problem statement
Given a string s, return the length of the longest substring of s in which no character appears three or more times in a row. Call such a substring semi-alternating: runs of one or two equal characters are fine, but a run of three is not.
Implement:
def longest_semi_alternating(s: str) -> int
A substring is a contiguous slice of s. Return 0 for the empty string.
Constraints
0 <= len(s) <= 2 * 10^5-
smay contain any characters (letters, digits, symbols), not only two letters. - Aim for O(n) time and O(1) extra space.
Example 1
s = "kkkhhkkkhh"
output: 6
The triple kkk appears twice. The longest stretch that avoids both is "kkhhkk" (indices 1 to 6).
Example 2
s = "ppqqpp"
output: 6
Every run has length at most two, so the whole string counts.
Interviewers often follow up by asking for the substring itself, a general limit of k repeats, or the number of valid substrings.
Approach
A substring is valid exactly when no run of equal characters inside it is
longer than two. So the only thing that can break a window is a third copy
of the same character in a row.
Brute force. Check every substring and scan it for xxx. That is
O(n^3), or O(n^2) if you extend each start until the first bad triple. It
is a fine opening line, but say right away that the bad spots are local, so
one pass is enough.
One pass. Walk the string while tracking run, the length of the
current block of equal characters ending at i, and start, the left end
of the best valid window that ends at i.
- If
s[i] == s[i-1],run += 1; otherwiserun = 1. - When
runreaches 3, the window may not contains[i-2..i]all
together. The longest valid window ending atitherefore starts at
i - 1(it keeps the last two equal characters). Setstart = i - 1
andrun = 2. - After each step the answer is
max(best, i - start + 1).
Why it is correct. Invariant: s[start..i] is the longest valid
substring that ends at i. Moving start further right than needed would
only shorten the window, and any start left of i - 1 after a triple would
include s[i-2..i]. Every valid substring ends somewhere, so taking the max
over all i covers them all.
The same loop works for any alphabet: nothing depends on the characters
being a/b. It also generalizes to "no run longer than k" by swapping
the constant 3 for k + 1.
Code
from typing import Optional
def longest_semi_alternating(s: str, max_run: int = 2) -> int:
"""Length of the longest substring with no run of equal chars > max_run."""
if max_run < 1:
raise ValueError("max_run must be at least 1")
n = len(s)
if n <= max_run:
return n
best = 1
start = 0 # left end of the longest valid window ending at i
run = 1 # length of the equal-character run ending at i
for i in range(1, n):
run = run + 1 if s[i] == s[i - 1] else 1
if run > max_run:
# Keep only the last max_run copies of s[i].
start = i - max_run + 1
run = max_run
best = max(best, i - start + 1)
return best
def longest_semi_alternating_span(s: str, max_run: int = 2) -> Optional[tuple[int, int]]:
"""Follow-up: return (left, right) inclusive bounds of the first longest window."""
if not s:
return None
best_len, best_span = 1, (0, 0)
start, run = 0, 1
for i in range(1, len(s)):
run = run + 1 if s[i] == s[i - 1] else 1
if run > max_run:
start, run = i - max_run + 1, max_run
if i - start + 1 > best_len:
best_len, best_span = i - start + 1, (start, i)
return best_span
if __name__ == "__main__":
assert longest_semi_alternating("") == 0
assert longest_semi_alternating("z") == 1
assert longest_semi_alternating("zz") == 2
assert longest_semi_alternating("zzz") == 2
assert longest_semi_alternating("ccccc") == 2
assert longest_semi_alternating("xyzxyz") == 6
assert longest_semi_alternating("ppqqpp") == 6
assert longest_semi_alternating("mmmnnm") == 5 # "mmnnm"
assert longest_semi_alternating("ttxttt") == 5 # "ttxtt"
assert longest_semi_alternating("kkkhhkkkhh") == 6 # "kkhhkk"
assert longest_semi_alternating("abbbcddde") == 5 # "bbcdd"
assert longest_semi_alternating("aaaa", max_run=1) == 1
assert longest_semi_alternating("aabbaab", max_run=3) == 7
assert longest_semi_alternating_span("mmmnnm") == (1, 5)
assert longest_semi_alternating_span("") is None
# Cross-check against brute force.
import itertools
import random
def brute(t: str) -> int:
best = 0
for i in range(len(t)):
for j in range(i, len(t)):
sub = t[i:j + 1]
if all(len(list(g)) <= 2 for _, g in itertools.groupby(sub)):
best = max(best, j - i + 1)
return best
rng = random.Random(7)
for _ in range(2000):
t = "".join(rng.choice("ab") for _ in range(rng.randint(0, 12)))
assert longest_semi_alternating(t) == brute(t), t
print("ok")
Complexity
- Time: O(n), one pass with constant work per character.
- Space: O(1) extra (the span variant also stores only two indices).
- Brute force for comparison: O(n^2) with early stop, O(n^3) naive.
Test cases
-
""->0(empty input) -
"z"->1;"zz"->2(shorter than the limit, whole string) -
"zzz"->2;"ccccc"->2(one long run) -
"xyzxyz"->6(no repeats at all) -
"ppqqpp"->6(pairs only, whole string is valid) -
"mmmnnm"->5("mmnnm", triple at the start) -
"ttxttt"->5("ttxtt", triple at the end) -
"kkkhhkkkhh"->6("kkhhkk", two triples split the string) -
"abbbcddde"->5("bbcdd", window keeps two copies on each side) -
max_run=1,"aaaa"->1;max_run=3,"aabbaab"->7 - Large input: 10^5 copies of
"a"->2, runs in linear time
Follow-ups
-
Return the substring, not the length. Track the
(start, i)pair
whenbestimproves (shown inlongest_semi_alternating_span). Ask
whether ties go to the first or last occurrence. -
No run longer than
k. Replace 2 withk: when the run exceeds
k, setstart = i - k + 1. Same O(n) time and O(1) space. -
Count all valid substrings. The number of valid substrings ending
atiisi - start + 1, so sum it instead of taking the max. Use a
64-bit or Python int; the total is O(n^2). -
Allow up to
m"bad" triples (or delete at mostmcharacters).
Use a general sliding window that counts violations inside it and
advances the left pointer while the count exceedsm. Still O(n). -
Streaming input. The state is just
(prev_char, run, current length, best), so it works on an unbounded stream with O(1) memory.
A related version asks for the minimum deletions to make the whole
string valid: deleterun - 2from each run, a single pass too.
Interviewer expectations
Mid-level (L4)
- States the rule as "no run of three equal characters" and handles
n < 3and the empty string. - Gets to a correct O(n) pass, possibly after a brute force, with clean
index handling. - Tests a triple at the start, middle, and end.
Senior (L5)
- Goes straight to the one-pass window and explains the invariant (the
window is the longest valid one ending ati) and why the new start is
i - 1. - Generalizes to
kand to counting substrings without rewriting. - Validates against a brute force on small random strings.
Staff (L6+)
- Frames it as a sliding window with a local constraint and explains when
the window can jump instead of shrinking one step at a time. - Handles the "allow
mviolations" and streaming variants and discusses
Unicode or grapheme boundaries if the input is user text. - Keeps the code short, and talks through test strategy and edge cases
without prompting.