3081. Replace Question Marks in String to Minimize Its Value
Problem Description
You are given a string s
where each character is either a lowercase English letter or a question mark '?'
.
For any string t
containing only lowercase English letters, we define a cost function for each position. The cost(i)
at position i
equals the number of times the character t[i]
appears before position i
(in the range [0, i-1]
). The total value of string t
is the sum of all cost(i)
values.
For example, with string t = "aab"
:
cost(0) = 0
(no characters before index 0)cost(1) = 1
(one 'a' appears before index 1)cost(2) = 0
(no 'b' appears before index 2)- Total value =
0 + 1 + 0 = 1
Your task is to replace all question marks '?'
in string s
with lowercase English letters to minimize the total value of the resulting string. If multiple solutions yield the same minimum value, return the lexicographically smallest one.
The key insight is that when a character appears multiple times, it contributes an increasing cost. If a character c
appears v
times total, its contribution to the overall value is 0 + 1 + 2 + ... + (v-1) = v × (v-1) / 2
. To minimize this, we should distribute characters as evenly as possible among the question mark positions.
The solution uses a greedy approach with a min-heap to track character frequencies. For each question mark, we select the character with the lowest current frequency, ensuring balanced distribution. After determining which characters to use, we sort them alphabetically and assign them to question marks in order, guaranteeing the lexicographically smallest result among all minimum-value solutions.
Intuition
The key observation is understanding how the cost accumulates. When a character appears multiple times in a string, each subsequent occurrence adds to the total cost based on how many times it has appeared before. This creates a quadratic growth pattern - the more times a character appears, the more expensive it becomes.
Think of it like stacking identical blocks. The first block costs nothing (cost = 0), the second block costs 1 (one block beneath it), the third costs 2, and so on. If we stack 5 'a' blocks, the total cost is 0 + 1 + 2 + 3 + 4 = 10
. But if we split them into 3 'a' blocks and 2 'b' blocks, the cost becomes (0 + 1 + 2) + (0 + 1) = 4
, which is much lower.
This leads us to the greedy insight: to minimize the total cost, we should keep the frequency of all characters as balanced as possible. It's better to have multiple characters appear twice than to have one character appear four times.
When we encounter question marks, we should replace them with the characters that currently have the lowest frequency. This prevents any single character from becoming too frequent and expensive. A min-heap (priority queue) is perfect for this - it always gives us the character with the smallest count.
However, there's a catch with the lexicographical requirement. If multiple replacements yield the same minimum cost, we need the alphabetically smallest result. We can't just greedily assign 'a' to the first question mark if we encounter it. Instead, we first determine which characters to use (maintaining minimum cost), collect them, sort them alphabetically, and then assign them to the question marks in order. This two-phase approach ensures both optimal cost and lexicographical ordering.
Learn more about Greedy, Sorting and Heap (Priority Queue) patterns.
Solution Approach
The implementation follows a greedy strategy using a min-heap to track character frequencies:
Step 1: Count existing characters
First, we count the frequency of all characters in the string s
using a Counter
. This gives us the initial distribution of characters, including any existing lowercase letters.
Step 2: Initialize the priority queue
We create a min-heap with tuples (count, character)
for all 26 lowercase letters. The heap is ordered by count first, then by character (for tie-breaking). This ensures we always get the character with the minimum frequency, and among those with equal frequency, the lexicographically smallest one.
pq = [(cnt[c], c) for c in ascii_lowercase] heapify(pq)
Step 3: Determine which characters to use For each question mark in the string, we:
- Extract the character with minimum frequency from the heap
- Add this character to our replacement list
t
- Increment its count and put it back in the heap using
heapreplace
for _ in range(s.count("?")):
v, c = pq[0] # Get min frequency character
t.append(c) # Record this character
heapreplace(pq, (v + 1, c)) # Update count and reinsert
Step 4: Sort for lexicographical order
After determining all replacement characters, we sort the list t
. This crucial step ensures that when we assign characters to question marks from left to right, we get the lexicographically smallest result among all minimum-cost solutions.
t.sort()
Step 5: Replace question marks
Finally, we traverse the original string and replace each question mark with characters from our sorted list t
in order:
cs = list(s)
j = 0
for i, c in enumerate(s):
if c == "?":
cs[i] = t[j]
j += 1
return "".join(cs)
The time complexity is O(n log 26 + n log n)
where n
is the number of question marks, simplifying to O(n log n)
. The space complexity is O(n)
for storing the replacement characters.
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 the solution with s = "a?b??"
:
Initial Setup:
- Count existing characters:
{'a': 1, 'b': 1}
, all others have count 0 - We have 3 question marks to replace
Step 1: Initialize the min-heap Create heap with all 26 letters and their counts:
[(0, 'c'), (0, 'd'), ..., (0, 'z'), (1, 'a'), (1, 'b')]
After heapification, smallest counts are at the top.
Step 2: Determine replacement characters For each of the 3 question marks:
First question mark:
- Extract min:
(0, 'c')
- 'c' has count 0 - Add 'c' to replacement list:
t = ['c']
- Update heap with
(1, 'c')
Second question mark:
- Extract min:
(0, 'd')
- 'd' has count 0 - Add 'd' to replacement list:
t = ['c', 'd']
- Update heap with
(1, 'd')
Third question mark:
- Extract min:
(0, 'e')
- 'e' has count 0 - Add 'e' to replacement list:
t = ['c', 'd', 'e']
- Update heap with
(1, 'e')
Step 3: Sort for lexicographical order
t = ['c', 'd', 'e']
(already sorted in this case)
Step 4: Replace question marks left to right
- Position 1:
?
→ 'c' - Position 3:
?
→ 'd' - Position 4:
?
→ 'e'
Final Result: "acbde"
Cost Verification:
- 'a' at position 0: cost = 0
- 'c' at position 1: cost = 0 (no 'c' before)
- 'b' at position 2: cost = 0 (no 'b' before)
- 'd' at position 3: cost = 0 (no 'd' before)
- 'e' at position 4: cost = 0 (no 'e' before)
- Total cost = 0
By using different characters for each question mark, we achieved the minimum possible cost while maintaining lexicographical order.
Solution Implementation
1from collections import Counter
2from heapq import heapify, heapreplace
3from string import ascii_lowercase
4
5class Solution:
6 def minimizeStringValue(self, s: str) -> str:
7 # Count frequency of each character in the original string
8 char_count = Counter(s)
9
10 # Create a min heap with (count, character) for all lowercase letters
11 # This ensures we always pick the character with minimum frequency
12 min_heap = [(char_count[char], char) for char in ascii_lowercase]
13 heapify(min_heap)
14
15 # Collect characters to replace '?' with, maintaining minimum frequency
16 replacement_chars = []
17 num_questions = s.count("?")
18
19 for _ in range(num_questions):
20 # Get character with minimum frequency
21 freq, char = min_heap[0]
22 replacement_chars.append(char)
23 # Update heap with incremented frequency for the used character
24 heapreplace(min_heap, (freq + 1, char))
25
26 # Sort replacement characters lexicographically
27 # This ensures we fill '?' positions optimally for minimum string value
28 replacement_chars.sort()
29
30 # Replace '?' characters with the selected characters
31 result_chars = list(s)
32 replacement_index = 0
33
34 for i, char in enumerate(s):
35 if char == "?":
36 result_chars[i] = replacement_chars[replacement_index]
37 replacement_index += 1
38
39 return "".join(result_chars)
40
1class Solution {
2 public String minimizeStringValue(String s) {
3 // Count frequency of each character (excluding '?')
4 int[] characterCount = new int[26];
5 int questionMarkCount = 0;
6 char[] charArray = s.toCharArray();
7
8 // Count existing characters and question marks
9 for (char ch : charArray) {
10 if (ch == '?') {
11 questionMarkCount++;
12 } else {
13 characterCount[ch - 'a']++;
14 }
15 }
16
17 // Priority queue to track characters with minimum frequency
18 // Sorted by: 1) frequency (ascending), 2) character value (ascending)
19 PriorityQueue<int[]> minHeap = new PriorityQueue<>((a, b) -> {
20 if (a[0] == b[0]) {
21 return a[1] - b[1]; // If frequency is same, sort by character
22 }
23 return a[0] - b[0]; // Sort by frequency
24 });
25
26 // Add all characters with their frequencies to the priority queue
27 for (int i = 0; i < 26; i++) {
28 minHeap.offer(new int[] {characterCount[i], i});
29 }
30
31 // Greedily select characters to replace question marks
32 int[] replacementCharacters = new int[questionMarkCount];
33 for (int i = 0; i < questionMarkCount; i++) {
34 // Get character with minimum frequency
35 int[] minFrequencyChar = minHeap.poll();
36 replacementCharacters[i] = minFrequencyChar[1]; // Store character index
37
38 // Update frequency and add back to queue
39 minHeap.offer(new int[] {minFrequencyChar[0] + 1, minFrequencyChar[1]});
40 }
41
42 // Sort replacement characters to ensure lexicographically smallest result
43 Arrays.sort(replacementCharacters);
44
45 // Replace question marks with selected characters
46 int replacementIndex = 0;
47 for (int i = 0; i < charArray.length; i++) {
48 if (charArray[i] == '?') {
49 charArray[i] = (char) (replacementCharacters[replacementIndex++] + 'a');
50 }
51 }
52
53 return new String(charArray);
54 }
55}
56
1class Solution {
2public:
3 string minimizeStringValue(string s) {
4 // Count frequency of each character (a-z) in the string
5 int charFrequency[26]{};
6 int questionMarkCount = 0;
7
8 // First pass: count existing characters and question marks
9 for (char& c : s) {
10 if (c == '?') {
11 questionMarkCount++;
12 } else {
13 charFrequency[c - 'a']++;
14 }
15 }
16
17 // Min heap to store (frequency, character_index) pairs
18 // This helps us always pick the character with minimum frequency
19 priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> minHeap;
20
21 // Initialize heap with all 26 characters and their frequencies
22 for (int i = 0; i < 26; ++i) {
23 minHeap.push({charFrequency[i], i});
24 }
25
26 // Greedily select characters to replace question marks
27 // Always pick the character with minimum frequency to balance the string
28 vector<int> replacementChars(questionMarkCount);
29 for (int i = 0; i < questionMarkCount; ++i) {
30 // Get the character with minimum frequency
31 auto [frequency, charIndex] = minHeap.top();
32 minHeap.pop();
33
34 // Store this character for replacement
35 replacementChars[i] = charIndex;
36
37 // Update frequency and push back to heap
38 minHeap.push({frequency + 1, charIndex});
39 }
40
41 // Sort replacement characters to ensure lexicographically smallest result
42 // when multiple question marks exist
43 sort(replacementChars.begin(), replacementChars.end());
44
45 // Second pass: replace question marks with selected characters
46 int replacementIndex = 0;
47 for (char& c : s) {
48 if (c == '?') {
49 c = replacementChars[replacementIndex++] + 'a';
50 }
51 }
52
53 return s;
54 }
55};
56
1function minimizeStringValue(s: string): string {
2 // Count frequency of each character (a-z) in the string
3 const charFrequency: number[] = new Array(26).fill(0);
4 let questionMarkCount = 0;
5
6 // Convert string to array for mutation
7 const sArray: string[] = s.split('');
8
9 // First pass: count existing characters and question marks
10 for (const char of sArray) {
11 if (char === '?') {
12 questionMarkCount++;
13 } else {
14 charFrequency[char.charCodeAt(0) - 'a'.charCodeAt(0)]++;
15 }
16 }
17
18 // Min heap to store [frequency, characterIndex] pairs
19 // This helps us always pick the character with minimum frequency
20 const minHeap: [number, number][] = [];
21
22 // Initialize heap with all 26 characters and their frequencies
23 for (let i = 0; i < 26; i++) {
24 minHeap.push([charFrequency[i], i]);
25 }
26
27 // Sort to create initial min heap structure
28 minHeap.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
29
30 // Greedily select characters to replace question marks
31 // Always pick the character with minimum frequency to balance the string
32 const replacementChars: number[] = new Array(questionMarkCount);
33
34 for (let i = 0; i < questionMarkCount; i++) {
35 // Get the character with minimum frequency
36 const [frequency, charIndex] = minHeap[0];
37
38 // Store this character for replacement
39 replacementChars[i] = charIndex;
40
41 // Update frequency and reinsert to maintain heap property
42 minHeap[0] = [frequency + 1, charIndex];
43
44 // Re-sort to maintain min heap property after update
45 minHeap.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
46 }
47
48 // Sort replacement characters to ensure lexicographically smallest result
49 // when multiple question marks exist
50 replacementChars.sort((a, b) => a - b);
51
52 // Second pass: replace question marks with selected characters
53 let replacementIndex = 0;
54 for (let i = 0; i < sArray.length; i++) {
55 if (sArray[i] === '?') {
56 sArray[i] = String.fromCharCode(replacementChars[replacementIndex++] + 'a'.charCodeAt(0));
57 }
58 }
59
60 return sArray.join('');
61}
62
Time and Space Complexity
Time Complexity: O(n × log 26)
which simplifies to O(n)
The analysis breaks down as follows:
Counter(s)
:O(n)
wheren
is the length of strings
- Creating the priority queue with 26 lowercase letters:
O(26)
=O(1)
heapify(pq)
:O(26)
=O(1)
since the heap has constant size 26s.count("?")
:O(n)
- The loop that processes question marks: runs
k
times wherek
is the number of "?" ins
(at mostn
). Each iteration performsheapreplace
which isO(log 26)
=O(1)
. Total:O(k)
=O(n)
t.sort()
:O(k × log k)
wherek
is the number of "?". In worst case:O(n × log n)
- Converting to list and back to string:
O(n)
The dominant operation is sorting t
, giving us O(n × log n)
overall time complexity.
Space Complexity: O(n)
The space usage includes:
cnt
Counter:O(26)
=O(1)
for storing counts of 26 letterspq
heap:O(26)
=O(1)
t
list:O(k)
wherek
is the number of "?" marks, at mostO(n)
cs
list:O(n)
for storing the character list- Final string join:
O(n)
The dominant space usage is from the list cs
and the final string, resulting in O(n)
space complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Not Sorting the Replacement Characters
A critical mistake is forgetting to sort the replacement characters before assigning them to question marks. Without sorting, you might achieve the minimum total value but not the lexicographically smallest result.
Incorrect approach:
# Wrong: Using characters directly without sorting
replacement_chars = []
for _ in range(num_questions):
freq, char = min_heap[0]
replacement_chars.append(char)
heapreplace(min_heap, (freq + 1, char))
# Directly replacing without sort
for i, char in enumerate(s):
if char == "?":
result_chars[i] = replacement_chars[replacement_index]
replacement_index += 1
Why it fails: If the string is "a?b?"
and the heap gives us ['c', 'a']
, we'd get "acba"
instead of the lexicographically smaller "aabc"
.
Solution: Always sort the replacement characters before assignment:
replacement_chars.sort() # Critical step!
Pitfall 2: Incorrectly Calculating Initial Character Frequencies
Another common error is only counting lowercase letters and ignoring question marks when initializing the frequency counter.
Incorrect approach:
# Wrong: Only counting lowercase letters char_count = Counter() for c in s: if c != '?': char_count[c] += 1
Why it seems wrong but isn't: Actually, this works fine because Counter
in Python returns 0 for missing keys. However, explicitly handling this can lead to confusion.
Better approach: Use Counter directly on the entire string:
char_count = Counter(s) # Question marks are counted but won't affect lowercase letter frequencies
Pitfall 3: Using Wrong Heap Comparison
When multiple characters have the same frequency, failing to break ties lexicographically in the heap can lead to incorrect character selection.
Incorrect approach:
# Wrong: Only considering frequency min_heap = [(char_count[char],) for char in ascii_lowercase] # This doesn't guarantee lexicographical order for ties
Solution: Include the character itself in the tuple for proper tie-breaking:
min_heap = [(char_count[char], char) for char in ascii_lowercase] # Python's heap will use character for tie-breaking automatically
Pitfall 4: Modifying String In-Place
Attempting to modify the string directly instead of converting to a list first.
Incorrect approach:
# Wrong: Strings are immutable in Python
for i, char in enumerate(s):
if char == "?":
s[i] = replacement_chars[replacement_index] # Error!
Solution: Convert to list, modify, then join:
result_chars = list(s)
for i, char in enumerate(s):
if char == "?":
result_chars[i] = replacement_chars[replacement_index]
replacement_index += 1
return "".join(result_chars)
Which of the following shows the order of node visit in a Breadth-first Search?
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!