Longest Substring with At Most K Distinct Characters

Problem

Given a string s and an integer k, return the length of the longest substring of s that contains at most k distinct characters.

  • 0 ≤ k ≤ 10⁵
  • 0 ≤ s.length ≤ 10⁵
  • s consists of ASCII characters

Example

Input: s = "eceba", k = 2
Output: 3

The longest substring with at most 2 distinct characters is "ece", which has length 3. The algorithm uses a sliding window to track character counts. Starting with the first character 'e', it expands the window to include 'c' and 'e' again, maintaining at most 2 distinct characters. When adding 'b' would exceed the limit, it shrinks the window from the left until the distinct count is back to 2. The maximum window length recorded during this process is 3.

Approach

Straightforward Solution

A brute-force approach would check every substring and count distinct characters, resulting in O(n^2) time complexity, which is inefficient for large inputs.

Core Observation

The problem requires finding the longest contiguous substring with a constraint on the number of distinct characters. This naturally suggests a sliding window approach that dynamically adjusts its boundaries to maintain the constraint.

Path to Optimal

The key insight is to use two pointers to represent a window and a hash map (or dictionary) to count characters within the window. Expand the right pointer to include new characters and update counts. When the number of distinct characters exceeds k, move the left pointer to shrink the window until the constraint is satisfied again. This approach ensures each character is visited at most twice, yielding O(n) time complexity.

Optimal Approach

Use a sliding window with two pointers and a dictionary to track character counts. Expand the right pointer to add characters, and shrink from the left when distinct characters exceed k. Track the maximum window size during this process. This guarantees O(n) time and O(k) space complexity.

Time

O(n)

Each character is visited at most twice: once when the right pointer expands the window and once when the left pointer shrinks it, resulting in linear time.

Space

O(k)

The dictionary stores counts of at most k distinct characters at any time, so space scales with k, which is at most the size of the input alphabet or k.

Pattern Spotlight

Sliding Window (Variable Size with Frequency Map)

Maintain a window that satisfies the problem's constraint by expanding the right pointer and contracting the left pointer only when the constraint is violated, using a frequency map to efficiently track the window's state.

Solution

1class Solution:
2 def lengthOfLongestSubstringKDistinct(self, s: str, k: int) -> int:
3 if k <= 0 or not s:
4 return 0
5
6 counts = {}
7 left = 0
8 res = 0
9
10 for right, ch in enumerate(s):
11 counts[ch] = counts.get(ch, 0) + 1
12
13 while len(counts) > k:
14 left_ch = s[left]
15 counts[left_ch] -= 1
16 if counts[left_ch] == 0:
17 del counts[left_ch]
18 left += 1
19
20 res = max(res, right - left + 1)
21
22 return res

Step-by-Step Solution

1

Validate Input and Handle Edge Cases

3if k <= 0 or not s:
4 return 0

Objective

To immediately return 0 if k is zero or the input string is empty, as no valid substring can exist.

Key Insight

Checking for trivial or invalid inputs upfront prevents unnecessary computation and handles edge cases cleanly. If k is zero, no characters are allowed, so the longest substring length is zero. Similarly, an empty string has no substrings.

Interview Quick-Check

State & Boundaries

Return 0 immediately if k <= 0 or s is empty, as no valid substring can exist.

Common Pitfalls & Bugs

Failing to handle k=0 or empty string inputs can cause incorrect results or runtime errors.

2

Initialize Sliding Window State

6counts = {}
7left = 0
8res = 0

Objective

To initialize the frequency map, left pointer, and result accumulator before scanning.

Key Insight

The frequency map tracks the window contents, left defines the window start, and res stores the best valid window length seen so far.

Interview Quick-Check

Core Logic

Initialize counts to track character frequencies in the window.

State & Boundaries

Initialize left and res to represent an empty window and no best answer yet.

3

Expand Sliding Window and Track Character Frequencies

10for right, ch in enumerate(s):
11 counts[ch] = counts.get(ch, 0) + 1

Objective

To iterate through the string with a right pointer, updating character counts and expanding the window.

Key Insight

By incrementing the count of the current character, the algorithm maintains an accurate frequency map of the window's contents. This allows quick checks on the number of distinct characters and supports efficient window adjustments.

Interview Quick-Check

Core Logic

Increment the count of the character at the right pointer to update the window's frequency map.

Complexity

Each character is processed once when expanding the window, contributing to O(n) time complexity.

4

Shrink Window to Maintain At Most K Distinct Characters

13while len(counts) > k:
14 left_ch = s[left]
15 counts[left_ch] -= 1
16 if counts[left_ch] == 0:
17 del counts[left_ch]
18 left += 1

Objective

To move the left pointer forward while the window contains more than k distinct characters, updating counts accordingly.

Key Insight

Shrinking the window from the left removes characters and their counts, reducing the number of distinct characters. Deleting characters with zero count ensures the frequency map accurately reflects the window's state. This dynamic adjustment maintains the problem's constraint efficiently.

Interview Quick-Check

Core Logic

While the number of distinct characters exceeds k, decrement counts of the leftmost character and remove it if its count reaches zero, then move the left pointer forward.

State & Boundaries

The loop condition ensures the window always satisfies the constraint after shrinking.

Common Pitfalls & Bugs

Forgetting to remove characters with zero count from the map can cause incorrect distinct character counts.

5

Update Maximum Length of Valid Substring

20res = max(res, right - left + 1)

Objective

To record the maximum window size seen so far that satisfies the constraint.

Key Insight

After adjusting the window to satisfy the constraint, the current window length is a candidate for the longest valid substring. Tracking the maximum ensures the final answer reflects the largest such window encountered during the iteration.

Interview Quick-Check

Core Logic

Update the result with the maximum of the current result and the current window size (right - left + 1).

Complexity

This constant-time update inside the loop maintains the global maximum efficiently.

6

Return the Length of the Longest Valid Substring

22return res

Objective

To return the maximum length found after processing the entire string.

Key Insight

Returning the accumulated maximum length completes the solution, providing the length of the longest substring with at most k distinct characters.

Interview Quick-Check

State & Boundaries

Return the final result after the loop completes, representing the longest valid substring length.

Line Analysis

This solution has 3 Critical lines interviewers watch for.

Line 13 Critical
while len(counts) > k:

While the window has more than k distinct characters, shrink it from the left.

This loop restores the invariant that the window contains at most k distinct characters by shrinking from the left until len(counts) <= k.

Line 16 Critical
if counts[left_ch] == 0:

Check whether the leftmost character has fully left the window.

A character stops contributing to the distinct count only when its frequency becomes zero, so this check is required for correctness.

Line 17 Critical
del counts[left_ch]

Remove the character from the map when its count reaches zero.

Deleting the zero-count key ensures len(counts) equals the true number of distinct characters, allowing the while condition to terminate correctly.

Line 3 Core
if k <= 0 or not s:

Check if k is zero or the string is empty.

This early check handles edge cases where no valid substring can exist, preventing unnecessary computation and ensuring correctness.

Line 4 Core
return 0

Return 0 immediately for invalid or trivial inputs.

Returning zero here correctly signals that no substring meets the criteria when k is zero or the string is empty.

Line 6 Core
counts = {}

Initialize a dictionary to count characters in the current window.

This dictionary enables O(1) average-time updates and lookups of character frequencies, which is essential for efficient window management.

Line 11 Core
counts[ch] = counts.get(ch, 0) + 1

Add the current character to the window state by incrementing its count.

Incrementing the frequency map maintains an accurate count of characters in the current window, enabling correct distinct-count checks.

Line 15 Core
counts[left_ch] -= 1

Decrement the count of the leftmost character.

Decrementing reflects removing one occurrence of the leftmost character; this may eliminate a distinct character if the count hits zero.

Line 18 Core
left += 1

Move the left pointer forward to complete one shrink step.

Advancing the left pointer actually shrinks the window boundary, pairing with the frequency updates to restore the constraint.

Line 20 Core
res = max(res, right - left + 1)

Update the maximum valid window length found so far.

After the window is valid, its length is a candidate for the longest valid substring ending at right; taking the max preserves the best seen.

Line 7 Core
left = 0

Initialize the left boundary of the sliding window.

The left pointer marks the start of the current window and moves forward to shrink the window when the distinct-count constraint is violated.

Line 10 Core
for right, ch in enumerate(s):

Iterate over the string with the right pointer and current character.

The right pointer expands the window by scanning the string once, ensuring every character is considered as a window endpoint.

Line 14 Core
left_ch = s[left]

Read the leftmost character that will be removed from the window.

Identifying the character leaving the window is necessary to correctly update its frequency as the left boundary advances.

Line 8 Standard
res = 0

Initialize the variable that tracks the maximum substring length found.

This accumulator stores the maximum valid window length encountered, ensuring the final return value is correct.

Line 22 Standard
return res

Return the length of the longest substring with at most k distinct characters.

Returning the tracked maximum completes the algorithm and provides the required length of the longest valid substring.

Test Your Understanding

Why does shrinking the window from the left guarantee that the substring will again satisfy the 'at most k distinct characters' constraint?

Because removing characters from the left reduces the count of distinct characters, and once the count is at most k, the window satisfies the constraint again. Shrinking continues until this condition is met.

Related Problems

Sliding Window pattern

Don't just read it. Drill it.

Reconstruct Longest Substring with At Most K Distinct Characters from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.

Unlock the Longest Substring with At Most K Distinct Characters drill

or drill a free problem