Facebook Pixel

2516. Take K of Each Character From Left and Right

MediumHash TableStringSliding Window
Leetcode Link

Problem Description

You have a string s that contains only three types of characters: 'a', 'b', and 'c'. You also have a non-negative integer k.

In each minute, you can take (remove) either the leftmost character or the rightmost character from the string s. Your goal is to collect at least k characters of each type ('a', 'b', and 'c').

The task is to find the minimum number of minutes (operations) needed to collect at least k characters of each type. If it's impossible to collect k of each character type, return -1.

For example, if s = "aabaaaacaabc" and k = 2, you need to collect at least 2 'a's, 2 'b's, and 2 'c's by taking characters from either end of the string. The function should return the minimum number of operations required to achieve this goal.

The key insight is that you can only take characters from the ends (left or right) of the string, not from the middle. This constraint means you'll end up taking a prefix and/or suffix of the original string, leaving some middle portion untouched.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

Since we can only take characters from the left or right ends of the string, the characters we take will form a prefix and/or a suffix of the original string. This means the characters we don't take will form a contiguous substring in the middle.

Let's think about this differently: instead of focusing on what we take, let's focus on what we leave behind. If we take some characters from the left and some from the right, we're essentially leaving a substring in the middle untouched.

The key realization is that we want to maximize the size of this middle substring (the part we don't take) while still ensuring that the remaining characters on both sides contain at least k of each character type.

Why maximize the middle substring? Because if we leave more characters in the middle, we take fewer characters from the ends, which minimizes our operation count.

So the problem transforms into: find the longest possible substring we can leave in the middle such that the characters outside this substring (on the left and right combined) still contain at least k of each character 'a', 'b', and 'c'.

This naturally leads to a sliding window approach where:

  1. We first count the total occurrences of each character in the string
  2. We use a sliding window to represent the middle substring we're leaving untouched
  3. As we expand the window, we're "removing" characters from our available pool
  4. We need to ensure that after removing the characters in the window, we still have at least k of each character type available
  5. The answer is the total length minus the maximum window size we can achieve

Learn more about Sliding Window patterns.

Solution Approach

The implementation uses a sliding window technique with two pointers to find the maximum substring we can leave untouched.

First, we count the total occurrences of each character using Counter(s). We check if any character appears less than k times - if so, it's impossible to collect k of that character, so we return -1.

Next, we initialize two variables:

  • mx: tracks the maximum window size (substring we can leave in the middle)
  • j: left pointer of the sliding window (starts at 0)

We iterate through the string with pointer i (right boundary of the window):

  1. Expand the window: For each character s[i], we decrement its count in cnt by 1. This represents "removing" this character from our available pool (since it's now inside the window we're leaving untouched).

  2. Shrink if necessary: If cnt[c] < k for the current character, it means we've removed too many of this character type. We need to shrink the window from the left:

    • Increment cnt[s[j]] to add back the character at position j to our available pool
    • Move j forward by 1
    • Continue until cnt[c] >= k again
  3. Update maximum: After ensuring all character counts are valid, we update mx with the current window size i - j + 1.

The sliding window maintains the invariant that the characters outside the window (in cnt) always have at least k of each type. By finding the maximum window size, we minimize the number of characters we need to take from the ends.

Finally, we return len(s) - mx, which represents the minimum number of characters we need to take from the ends to satisfy the requirement.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example with s = "aabaaaacaabc" and k = 2.

Step 1: Initial Setup

  • Count total characters: {'a': 7, 'b': 2, 'c': 2}
  • Check feasibility: All counts ≥ 2, so it's possible
  • Initialize: mx = 0, j = 0

Step 2: Sliding Window Process

We'll track the window boundaries and what's left outside:

icharWindowOutside Windowcnt after removingValid?Action
0'a'[0,0]: "a""abaaaacaabc"{'a': 6, 'b': 2, 'c': 2}Yesmx = 1
1'a'[0,1]: "aa""baaaacaabc"{'a': 5, 'b': 2, 'c': 2}Yesmx = 2
2'b'[0,2]: "aab""aaaacaabc"{'a': 5, 'b': 1, 'c': 2}No!Shrink

When i=2, we have cnt['b'] = 1 < k, so we shrink:

  • Add back s[0]='a': cnt = {'a': 6, 'b': 1, 'c': 2}, j=1
  • Still cnt['b'] < k, add back s[1]='a': cnt = {'a': 7, 'b': 1, 'c': 2}, j=2
  • Still cnt['b'] < k, add back s[2]='b': cnt = {'a': 7, 'b': 2, 'c': 2}, j=3
  • Now valid! Window is empty [3,2], mx stays 2
icharWindowOutside Windowcnt after removingValid?Action
3'a'[3,3]: "a""aab" + "aaacaabc"{'a': 6, 'b': 2, 'c': 2}Yesmx = 2
4'a'[3,4]: "aa""aab" + "aacaabc"{'a': 5, 'b': 2, 'c': 2}Yesmx = 2
5'a'[3,5]: "aaa""aab" + "acaabc"{'a': 4, 'b': 2, 'c': 2}Yesmx = 3
6'a'[3,6]: "aaaa""aab" + "caabc"{'a': 3, 'b': 2, 'c': 2}Yesmx = 4
7'c'[3,7]: "aaaac""aab" + "aabc"{'a': 3, 'b': 2, 'c': 1}No!Shrink

When i=7, we have cnt['c'] = 1 < k, so we shrink from j=3 until valid again.

Continuing this process, we find that the maximum window size is 7 (positions [3,9]: "aaaacaa").

When the window is "aaaacaa", we have:

  • Left of window: "aab"
  • Right of window: "bc"
  • Combined outside: 2 'a's, 2 'b's, 2 'c's ✓

Step 3: Calculate Result

  • Maximum window size: 7
  • Minimum operations: 12 - 7 = 5

We need to take 5 characters from the ends to collect at least 2 of each type. For instance, taking "aab" from the left and "bc" from the right gives us exactly 2 of each character.

Solution Implementation

1from collections import Counter
2
3class Solution:
4    def takeCharacters(self, s: str, k: int) -> int:
5        # Count frequency of each character in the string
6        char_count = Counter(s)
7      
8        # Check if we have at least k of each character 'a', 'b', 'c'
9        # If not, it's impossible to take k of each
10        if any(char_count[char] < k for char in "abc"):
11            return -1
12      
13        # Use sliding window to find the maximum substring we can leave in the middle
14        # while still having k of each character available from the ends
15        max_window_size = 0
16        left_pointer = 0
17      
18        # Expand window by moving right pointer
19        for right_pointer, char in enumerate(s):
20            # Remove current character from available count (it's in our window)
21            char_count[char] -= 1
22          
23            # If we don't have enough of this character outside the window,
24            # shrink window from left until we have k of each character available
25            while char_count[char] < k:
26                char_count[s[left_pointer]] += 1
27                left_pointer += 1
28          
29            # Update maximum window size we can leave in the middle
30            max_window_size = max(max_window_size, right_pointer - left_pointer + 1)
31      
32        # Minimum characters to take from ends = total length - max middle substring
33        return len(s) - max_window_size
34
1class Solution {
2    public int takeCharacters(String s, int k) {
3        // Count frequency of each character 'a', 'b', 'c' in the string
4        int[] charCount = new int[3];
5        int stringLength = s.length();
6      
7        // Count total occurrences of each character
8        for (int i = 0; i < stringLength; i++) {
9            charCount[s.charAt(i) - 'a']++;
10        }
11      
12        // Check if we have at least k of each character
13        // If not, it's impossible to take k of each
14        for (int count : charCount) {
15            if (count < k) {
16                return -1;
17            }
18        }
19      
20        // Find the maximum window that can be left in the middle
21        // while still having k characters of each type from both ends
22        int maxWindowSize = 0;
23        int left = 0;
24      
25        // Try to expand window from right
26        for (int right = 0; right < stringLength; right++) {
27            int currentChar = s.charAt(right) - 'a';
28            charCount[currentChar]--;
29          
30            // If removing this character causes count to drop below k,
31            // shrink window from left until we have at least k again
32            while (charCount[currentChar] < k) {
33                charCount[s.charAt(left) - 'a']++;
34                left++;
35            }
36          
37            // Update maximum window size that can be excluded
38            maxWindowSize = Math.max(maxWindowSize, right - left + 1);
39        }
40      
41        // Return minimum characters to take from both ends
42        return stringLength - maxWindowSize;
43    }
44}
45
1class Solution {
2public:
3    int takeCharacters(string s, int k) {
4        // Array to count occurrences of 'a', 'b', 'c' in the entire string
5        int charCount[3] = {0};
6        int stringLength = s.length();
7      
8        // Count total occurrences of each character
9        for (int i = 0; i < stringLength; ++i) {
10            ++charCount[s[i] - 'a'];
11        }
12      
13        // Check if it's possible to take k of each character
14        for (int count : charCount) {
15            if (count < k) {
16                return -1;  // Not enough of some character
17            }
18        }
19      
20        // Find the maximum window that can be removed from the middle
21        // while still leaving at least k of each character
22        int maxWindowSize = 0;
23        int left = 0;
24      
25        // Sliding window approach: try to find the largest valid middle substring
26        for (int right = 0; right < stringLength; ++right) {
27            int charIndex = s[right] - 'a';
28            --charCount[charIndex];  // Remove current character from available count
29          
30            // If we don't have enough of this character outside the window,
31            // shrink the window from the left
32            while (charCount[charIndex] < k) {
33                ++charCount[s[left] - 'a'];  // Add back the left character
34                ++left;
35            }
36          
37            // Update maximum window size that can be removed
38            maxWindowSize = max(maxWindowSize, right - left + 1);
39        }
40      
41        // Return minimum characters to take (total - maximum removable)
42        return stringLength - maxWindowSize;
43    }
44};
45
1/**
2 * Finds the minimum number of characters to take from both ends of string
3 * to ensure at least k occurrences of each character 'a', 'b', and 'c'
4 * @param s - Input string containing only 'a', 'b', and 'c'
5 * @param k - Minimum required occurrences of each character
6 * @returns Minimum characters to take, or -1 if impossible
7 */
8function takeCharacters(s: string, k: number): number {
9    // Helper function to convert character to index (a=0, b=1, c=2)
10    const charToIndex = (char: string): number => char.charCodeAt(0) - 97;
11  
12    // Count occurrences of each character in the entire string
13    const charCount: number[] = Array(3).fill(0);
14    for (const char of s) {
15        charCount[charToIndex(char)]++;
16    }
17  
18    // Check if it's possible to take k of each character
19    if (charCount.some(count => count < k)) {
20        return -1;
21    }
22  
23    const stringLength = s.length;
24    let maxMiddleWindow = 0;  // Maximum size of window that can be left in middle
25    let leftPointer = 0;
26  
27    // Use sliding window to find the maximum substring we can leave in the middle
28    // while still having at least k of each character from both ends
29    for (let rightPointer = 0; rightPointer < stringLength; rightPointer++) {
30        const currentCharIndex = charToIndex(s[rightPointer]);
31        charCount[currentCharIndex]--;
32      
33        // If we've taken too many of a character (leaving less than k for the ends),
34        // shrink the window from the left
35        while (charCount[currentCharIndex] < k) {
36            charCount[charToIndex(s[leftPointer])]++;
37            leftPointer++;
38        }
39      
40        // Update the maximum middle window size
41        maxMiddleWindow = Math.max(maxMiddleWindow, rightPointer - leftPointer + 1);
42    }
43  
44    // Minimum characters to take from ends = total length - max middle window
45    return stringLength - maxMiddleWindow;
46}
47

Time and Space Complexity

Time Complexity: O(n), where n is the length of string s.

The algorithm performs the following operations:

  • Initial counting of characters in s using Counter(s): O(n)
  • Checking if any character count is less than k: O(1) (only checks 3 characters)
  • The main loop iterates through each character in s once: O(n)
    • Inside the loop, the while loop might seem concerning, but each character can only be visited by pointer j at most once throughout the entire execution
    • The pointer j moves from 0 to at most n, never backwards
    • This creates a two-pointer sliding window pattern where both pointers traverse the string at most once

Therefore, despite the nested loop structure, each element is processed at most twice (once by i and once by j), resulting in O(n) time complexity.

Space Complexity: O(1)

The algorithm uses:

  • A Counter object cnt that stores at most 3 key-value pairs (for characters 'a', 'b', 'c'): O(1)
  • A few integer variables (mx, j, i): O(1)

Since the space usage doesn't scale with the input size and remains constant regardless of n, the space complexity is O(1).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Window Shrinking Logic

A common mistake is using an if statement instead of a while loop when shrinking the window:

Incorrect:

# Wrong approach - only shrinks once
if char_count[char] < k:
    char_count[s[left_pointer]] += 1
    left_pointer += 1

Why it fails: When we decrement char_count[char], it might drop significantly below k (e.g., from k to k-1). Simply moving the left pointer once might not restore enough characters. We need to keep shrinking until the condition is satisfied.

Correct:

# Keep shrinking until we have enough characters
while char_count[char] < k:
    char_count[s[left_pointer]] += 1
    left_pointer += 1

2. Forgetting to Check Character Availability

Another pitfall is not verifying that the string contains at least k of each character type before starting the sliding window:

Incorrect:

def takeCharacters(self, s: str, k: int) -> int:
    char_count = Counter(s)
    # Missing validation - jumps straight to sliding window
    max_window_size = 0
    left_pointer = 0
    # ... rest of the code

Why it fails: If the string doesn't have k occurrences of each character, the algorithm will still try to find a valid window, potentially returning an incorrect result instead of -1.

Correct:

char_count = Counter(s)
# Essential validation step
if any(char_count[char] < k for char in "abc"):
    return -1

3. Misunderstanding the Problem Goal

A conceptual pitfall is trying to minimize the window size instead of maximizing it:

Incorrect thinking: "Find the minimum substring that contains at least k of each character."

Correct thinking: "Find the maximum substring we can leave untouched in the middle while ensuring the remaining characters (from both ends) contain at least k of each type."

The key insight is that we want to maximize what we leave behind, not minimize what we take. This is why we track max_window_size and return len(s) - max_window_size.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How does merge sort divide the problem into subproblems?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More