358. Rearrange String k Distance Apart 🔒
Problem Description
You are given a string s
and an integer k
. Your task is to rearrange the characters in the string such that any two identical characters are separated by at least k
distance from each other.
For example, if you have the character 'a' at position i
, then the next 'a' cannot appear until position i + k
or later.
If it's impossible to create such a rearrangement where all identical characters maintain this minimum distance requirement, you should return an empty string ""
.
The key challenge is to strategically place characters to ensure the distance constraint is satisfied for all characters. Characters that appear more frequently in the original string will be harder to place since they need more positions while maintaining the required spacing.
Example scenarios:
- If
s = "aabbcc"
andk = 3
, one valid rearrangement could be"abcabc"
where each pair of identical characters is exactly 3 positions apart - If
s = "aaabc"
andk = 3
, it would be impossible to arrange since we have three 'a's that each need to be at least 3 positions apart, requiring more total positions than available - If
k = 0
ork = 1
, any arrangement works since there's no effective distance requirement
Intuition
The key insight is that we need to greedily place the most frequent characters first to avoid getting stuck later. Think about it this way: if a character appears many times, it needs many "slots" with proper spacing. If we delay placing these high-frequency characters, we might run out of valid positions.
Consider a greedy strategy: always pick the character with the highest remaining count that is currently available (not in the recent k-1
positions). This ensures we're always making progress on the characters that are hardest to place.
To implement this strategy, we need two main components:
- A max heap to always know which character has the highest remaining count among those currently available to place
- A cooling queue to temporarily hold characters that were recently used and cannot be placed yet
The process works like this:
- Pick the character with the highest count from the heap
- Place it in the result
- Decrease its count and put it in a "cooling" queue
- After exactly
k
positions, the character becomes available again and returns to the heap
The cooling mechanism is crucial: when we place a character at position i
, it cannot be used again until position i+k
. By using a queue of size k
, we naturally enforce this constraint - a character enters the queue after being used and only exits (becoming available again) after k
other characters have been placed.
If at any point we cannot place a character (the heap is empty but we still have characters to place), it means the arrangement is impossible. This happens when the cooling queue contains characters that still need to be placed, but there are no available characters to fill the gap.
The beauty of this approach is that it automatically handles the distance constraint through the queue mechanism while ensuring we prioritize characters that are most likely to cause problems if delayed.
Solution Approach
Let's walk through the implementation step by step:
1. Character Frequency Count and Max Heap Setup
h = [(-v, c) for c, v in Counter(s).items()] heapify(h)
- First, we count the frequency of each character using
Counter(s)
- We create a max heap by negating the frequencies (Python's heapq is a min heap by default)
- Each heap element is a tuple
(-frequency, character)
2. Initialize Data Structures
q = deque() ans = []
q
is our cooling queue that holds characters temporarily unavailableans
accumulates our result string
3. Main Processing Loop
while h: v, c = heappop(h) v *= -1 ans.append(c) q.append((v - 1, c))
- While we have available characters in the heap:
- Pop the character with highest frequency
- Convert back to positive frequency (
v *= -1
) - Add this character to our result
- Decrease its count and add to cooling queue
4. Cooling Queue Management
if len(q) >= k:
w, c = q.popleft()
if w:
heappush(h, (-w, c))
- Once the cooling queue reaches size
k
, the oldest character has "cooled down" - We remove it from the queue
- If it still has remaining count (
w > 0
), push it back to the heap with negated frequency
5. Validation and Return
return "" if len(ans) != len(s) else "".join(ans)
- If we successfully placed all characters,
len(ans)
equalslen(s)
- Otherwise, some characters got stuck in the cooling queue, making arrangement impossible
Why This Works:
The algorithm ensures that after placing a character, it won't be available for selection until exactly k
other characters have been placed. The max heap guarantees we always choose the most frequent available character, preventing scenarios where high-frequency characters get blocked later.
Time Complexity: O(n log m)
where n
is the length of string and m
is the number of unique characters
Space Complexity: O(m)
for the heap and queue
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through the algorithm with s = "aabbcc"
and k = 3
.
Initial Setup:
- Character frequencies:
{'a': 2, 'b': 2, 'c': 2}
- Max heap (negated):
[(-2, 'a'), (-2, 'b'), (-2, 'c')]
- Cooling queue:
[]
- Result:
[]
Iteration 1:
- Pop from heap:
(-2, 'c')
→ frequency = 2 - Add 'c' to result:
['c']
- Add to cooling queue with decreased count:
[(1, 'c')]
- Queue size (1) < k (3), so nothing returns to heap
- Heap:
[(-2, 'a'), (-2, 'b')]
Iteration 2:
- Pop from heap:
(-2, 'b')
→ frequency = 2 - Add 'b' to result:
['c', 'b']
- Add to cooling queue:
[(1, 'c'), (1, 'b')]
- Queue size (2) < k (3), so nothing returns to heap
- Heap:
[(-2, 'a')]
Iteration 3:
- Pop from heap:
(-2, 'a')
→ frequency = 2 - Add 'a' to result:
['c', 'b', 'a']
- Add to cooling queue:
[(1, 'c'), (1, 'b'), (1, 'a')]
- Queue size (3) = k (3), so first element
(1, 'c')
exits queue - Since count = 1 > 0, push 'c' back to heap with
(-1, 'c')
- Heap:
[(-1, 'c')]
Iteration 4:
- Pop from heap:
(-1, 'c')
→ frequency = 1 - Add 'c' to result:
['c', 'b', 'a', 'c']
- Add to cooling queue:
[(1, 'b'), (1, 'a'), (0, 'c')]
- Queue size (3) = k (3), so
(1, 'b')
exits queue - Push 'b' back to heap with
(-1, 'b')
- Heap:
[(-1, 'b')]
Iteration 5:
- Pop from heap:
(-1, 'b')
→ frequency = 1 - Add 'b' to result:
['c', 'b', 'a', 'c', 'b']
- Add to cooling queue:
[(1, 'a'), (0, 'c'), (0, 'b')]
- Queue size (3) = k (3), so
(1, 'a')
exits queue - Push 'a' back to heap with
(-1, 'a')
- Heap:
[(-1, 'a')]
Iteration 6:
- Pop from heap:
(-1, 'a')
→ frequency = 1 - Add 'a' to result:
['c', 'b', 'a', 'c', 'b', 'a']
- Add to cooling queue:
[(0, 'c'), (0, 'b'), (0, 'a')]
- Queue size (3) = k (3), so
(0, 'c')
exits queue - Count = 0, so don't push back to heap
- Heap:
[]
(empty)
Final Check:
- Heap is empty, loop ends
- Result length (6) = original string length (6) ✓
- Return:
"cbacba"
Each identical character is exactly 3 positions apart: c's at positions 0 and 3, b's at positions 1 and 4, a's at positions 2 and 5.
Solution Implementation
1from collections import Counter, deque
2from heapq import heapify, heappop, heappush
3
4class Solution:
5 def rearrangeString(self, s: str, k: int) -> str:
6 # Edge case: if k is 0 or 1, any arrangement works
7 if k <= 1:
8 return s
9
10 # Count frequency of each character and create max heap
11 # Using negative values to simulate max heap (since Python has min heap by default)
12 char_freq = Counter(s)
13 max_heap = [(-freq, char) for char, freq in char_freq.items()]
14 heapify(max_heap)
15
16 # Queue to hold characters that are in cooldown period
17 cooldown_queue = deque()
18
19 # Result list to build the rearranged string
20 result = []
21
22 # Process characters from heap
23 while max_heap:
24 # Get the character with highest frequency
25 neg_freq, char = heappop(max_heap)
26 freq = -neg_freq
27
28 # Add character to result
29 result.append(char)
30
31 # Add character to cooldown queue with decremented frequency
32 cooldown_queue.append((freq - 1, char))
33
34 # If cooldown period is over (queue has k elements),
35 # the first element can be added back to heap
36 if len(cooldown_queue) >= k:
37 remaining_freq, cooldown_char = cooldown_queue.popleft()
38 if remaining_freq > 0:
39 heappush(max_heap, (-remaining_freq, cooldown_char))
40
41 # If we couldn't place all characters, return empty string
42 # This happens when the cooldown constraint cannot be satisfied
43 if len(result) != len(s):
44 return ""
45
46 return "".join(result)
47
1class Solution {
2 public String rearrangeString(String s, int k) {
3 int length = s.length();
4
5 // Count frequency of each character
6 int[] charFrequency = new int[26];
7 for (char ch : s.toCharArray()) {
8 charFrequency[ch - 'a']++;
9 }
10
11 // Create max heap based on character frequency
12 // Each element is [frequency, character_index]
13 PriorityQueue<int[]> maxHeap = new PriorityQueue<>((a, b) -> b[0] - a[0]);
14 for (int i = 0; i < 26; i++) {
15 if (charFrequency[i] > 0) {
16 maxHeap.offer(new int[] {charFrequency[i], i});
17 }
18 }
19
20 // Queue to hold characters that are in cooldown period
21 Deque<int[]> cooldownQueue = new ArrayDeque<>();
22 StringBuilder result = new StringBuilder();
23
24 // Process characters greedily
25 while (!maxHeap.isEmpty()) {
26 // Get character with highest frequency
27 int[] current = maxHeap.poll();
28 int frequency = current[0];
29 int charIndex = current[1];
30
31 // Append character to result
32 result.append((char) ('a' + charIndex));
33
34 // Add character to cooldown queue with decremented frequency
35 cooldownQueue.offer(new int[] {frequency - 1, charIndex});
36
37 // If cooldown period is over (queue size >= k),
38 // release the first character from cooldown
39 if (cooldownQueue.size() >= k) {
40 int[] released = cooldownQueue.pollFirst();
41 // If character still has remaining frequency, add back to heap
42 if (released[0] > 0) {
43 maxHeap.offer(released);
44 }
45 }
46 }
47
48 // Check if all characters were successfully placed
49 return result.length() == length ? result.toString() : "";
50 }
51}
52
1class Solution {
2public:
3 string rearrangeString(string s, int k) {
4 // Count frequency of each character
5 unordered_map<char, int> charFrequency;
6 for (char c : s) {
7 ++charFrequency[c];
8 }
9
10 // Create max heap with pairs of (frequency, character)
11 // Heap will prioritize characters with higher frequency
12 priority_queue<pair<int, char>> maxHeap;
13 for (auto& [character, frequency] : charFrequency) {
14 maxHeap.push({frequency, character});
15 }
16
17 // Queue to hold characters that are in cooldown period
18 // Each character must wait k positions before being used again
19 queue<pair<int, char>> cooldownQueue;
20 string result;
21
22 // Greedily build the result string
23 while (!maxHeap.empty()) {
24 // Get the character with highest frequency
25 auto [frequency, character] = maxHeap.top();
26 maxHeap.pop();
27
28 // Add character to result
29 result += character;
30
31 // Put character in cooldown with decremented frequency
32 cooldownQueue.push({frequency - 1, character});
33
34 // If cooldown period is over (queue has k elements),
35 // the front element can be reused
36 if (cooldownQueue.size() >= k) {
37 auto cooledPair = cooldownQueue.front();
38 cooldownQueue.pop();
39
40 // Only add back to heap if character still has remaining frequency
41 if (cooledPair.first > 0) {
42 maxHeap.push(cooledPair);
43 }
44 }
45 }
46
47 // Check if we successfully rearranged all characters
48 // If not, it means rearrangement with distance k is impossible
49 return result.size() == s.size() ? result : "";
50 }
51};
52
1function rearrangeString(s: string, k: number): string {
2 // Count frequency of each character
3 const charFrequency = new Map<string, number>();
4 for (const char of s) {
5 charFrequency.set(char, (charFrequency.get(char) || 0) + 1);
6 }
7
8 // Create max heap with pairs of [frequency, character]
9 // Heap will prioritize characters with higher frequency
10 const maxHeap: Array<[number, string]> = [];
11 for (const [character, frequency] of charFrequency.entries()) {
12 maxHeap.push([frequency, character]);
13 }
14 // Sort in descending order by frequency, then by character for consistency
15 maxHeap.sort((a, b) => b[0] - a[0] || a[1].localeCompare(b[1]));
16
17 // Queue to hold characters that are in cooldown period
18 // Each character must wait k positions before being used again
19 const cooldownQueue: Array<[number, string]> = [];
20 let result = '';
21
22 // Greedily build the result string
23 while (maxHeap.length > 0) {
24 // Get the character with highest frequency
25 const [frequency, character] = maxHeap.shift()!;
26
27 // Add character to result
28 result += character;
29
30 // Put character in cooldown with decremented frequency
31 cooldownQueue.push([frequency - 1, character]);
32
33 // If cooldown period is over (queue has k elements),
34 // the front element can be reused
35 if (cooldownQueue.length >= k) {
36 const cooledPair = cooldownQueue.shift()!;
37
38 // Only add back to heap if character still has remaining frequency
39 if (cooledPair[0] > 0) {
40 maxHeap.push(cooledPair);
41 // Re-sort to maintain max heap property
42 maxHeap.sort((a, b) => b[0] - a[0] || a[1].localeCompare(b[1]));
43 }
44 }
45 }
46
47 // Check if we successfully rearranged all characters
48 // If not, it means rearrangement with distance k is impossible
49 return result.length === s.length ? result : '';
50}
51
Time and Space Complexity
Time Complexity: O(n log m)
where n
is the length of the input string and m
is the number of unique characters (at most 26 for lowercase English letters).
- Creating the frequency counter takes
O(n)
time - Building the initial heap from the counter takes
O(m)
time to create the list andO(m)
time for heapify operation - The main while loop runs exactly
n
iterations (once for each character in the string) - In each iteration:
heappop()
operation:O(log m)
- Append to answer list:
O(1)
- Append to queue:
O(1)
- Conditionally
heappush()
operation:O(log m)
- Total:
O(n)
+O(m)
+O(n × log m)
=O(n log m)
Since m ≤ 26
for English letters, this can also be considered O(n)
in practice.
Space Complexity: O(m)
or O(1)
if we consider the alphabet size as constant
- Counter storage:
O(m)
for storing frequency of unique characters - Heap storage:
O(m)
for at mostm
unique characters - Queue storage:
O(k)
but sincek ≤ m
, this is bounded byO(m)
- Answer list:
O(n)
but this is typically not counted as it's the output - Total auxiliary space:
O(m)
Since the number of unique characters is bounded by 26 for lowercase English letters, the space complexity can be considered O(1)
constant space.
Common Pitfalls
1. Forgetting the Edge Case When k = 0
The Pitfall:
Many implementations forget that when k = 0
, there's no distance requirement between identical characters. The main algorithm with the cooldown queue becomes unnecessary and can even produce incorrect results due to the queue management logic.
Why It Happens:
# Without handling k = 0, this condition fails:
if len(cooldown_queue) >= k: # When k = 0, this is always True
# Characters immediately get popped from queue
When k = 0
, characters would immediately cycle back from the cooldown queue to the heap, but the overhead and logic complexity can introduce bugs.
The Solution:
# Add this check at the beginning if k <= 1: return s # Any arrangement is valid
2. Incorrect Cooldown Queue Size Check
The Pitfall:
Using >
instead of >=
when checking if the cooldown period has elapsed:
# WRONG - This makes characters wait one extra position
if len(cooldown_queue) > k:
remaining_freq, cooldown_char = cooldown_queue.popleft()
# CORRECT - Character becomes available exactly after k positions
if len(cooldown_queue) >= k:
remaining_freq, cooldown_char = cooldown_queue.popleft()
Why It Matters:
- With
>
, a character at positioni
wouldn't be available until positioni + k + 1
- This unnecessarily restricts placement options and can cause valid arrangements to fail
3. Not Checking for Remaining Characters in Cooldown Queue
The Pitfall: Assuming that if the main loop completes, the arrangement is valid:
# WRONG - Missing validation while max_heap: # ... process characters return "".join(result) # Might return partial result!
Why It Fails: When impossible arrangements occur, some characters remain stuck in the cooldown queue. Without checking, you might return a string shorter than the input.
The Solution:
# Always validate that all characters were placed
if len(result) != len(s):
return ""
return "".join(result)
4. Heap Value Sign Confusion
The Pitfall: Forgetting to negate values when working with the max heap simulation:
# WRONG - Forgets to negate when pushing back if remaining_freq > 0: heappush(max_heap, (remaining_freq, cooldown_char)) # Should be negative! # WRONG - Forgets to convert back to positive neg_freq, char = heappop(max_heap) cooldown_queue.append((neg_freq - 1, char)) # Should use positive freq
The Solution: Always maintain consistency:
- Negate when pushing to heap:
heappush(max_heap, (-remaining_freq, cooldown_char))
- Convert to positive after popping:
freq = -neg_freq
5. Inefficient String Concatenation
The Pitfall: Using string concatenation instead of a list:
# INEFFICIENT - O(n²) due to string immutability result = "" while max_heap: result += char # Creates new string each time
The Solution: Use a list and join at the end:
result = [] while max_heap: result.append(char) return "".join(result) # O(n) operation
A person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".
What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
https assets algo monster cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue is a data structure
Want a Structured Path to Master System Design Too? Don’t Miss This!