2516. Take K of Each Character From Left and Right
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.
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:
- We first count the total occurrences of each character in the string
- We use a sliding window to represent the middle substring we're leaving untouched
- As we expand the window, we're "removing" characters from our available pool
- We need to ensure that after removing the characters in the window, we still have at least
k
of each character type available - 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):
-
Expand the window: For each character
s[i]
, we decrement its count incnt
by 1. This represents "removing" this character from our available pool (since it's now inside the window we're leaving untouched). -
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 positionj
to our available pool - Move
j
forward by 1 - Continue until
cnt[c] >= k
again
- Increment
-
Update maximum: After ensuring all character counts are valid, we update
mx
with the current window sizei - 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 EvaluatorExample 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:
i | char | Window | Outside Window | cnt after removing | Valid? | Action |
---|---|---|---|---|---|---|
0 | 'a' | [0,0]: "a" | "abaaaacaabc" | {'a': 6, 'b': 2, 'c': 2} | Yes | mx = 1 |
1 | 'a' | [0,1]: "aa" | "baaaacaabc" | {'a': 5, 'b': 2, 'c': 2} | Yes | mx = 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 backs[1]='a'
: cnt = {'a': 7, 'b': 1, 'c': 2}, j=2 - Still
cnt['b'] < k
, add backs[2]='b'
: cnt = {'a': 7, 'b': 2, 'c': 2}, j=3 - Now valid! Window is empty [3,2], mx stays 2
i | char | Window | Outside Window | cnt after removing | Valid? | Action |
---|---|---|---|---|---|---|
3 | 'a' | [3,3]: "a" | "aab" + "aaacaabc" | {'a': 6, 'b': 2, 'c': 2} | Yes | mx = 2 |
4 | 'a' | [3,4]: "aa" | "aab" + "aacaabc" | {'a': 5, 'b': 2, 'c': 2} | Yes | mx = 2 |
5 | 'a' | [3,5]: "aaa" | "aab" + "acaabc" | {'a': 4, 'b': 2, 'c': 2} | Yes | mx = 3 |
6 | 'a' | [3,6]: "aaaa" | "aab" + "caabc" | {'a': 3, 'b': 2, 'c': 2} | Yes | mx = 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
usingCounter(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 mostn
, never backwards - This creates a two-pointer sliding window pattern where both pointers traverse the string at most once
- Inside the loop, the while loop might seem concerning, but each character can only be visited by pointer
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
objectcnt
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
.
How does merge sort divide the problem into subproblems?
Recommended Readings
https assets algo monster cover_photos stack svg Sliding Window Maximum Monotonic Stack We have an array and a sliding window defined by a start index and an end index The sliding window moves from left of the array to right There are always k elements in the window The window
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!