Facebook Pixel

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.

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

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 Evaluator

Example 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) where n is the length of string s
  • Creating the priority queue with 26 lowercase letters: O(26) = O(1)
  • heapify(pq): O(26) = O(1) since the heap has constant size 26
  • s.count("?"): O(n)
  • The loop that processes question marks: runs k times where k is the number of "?" in s (at most n). Each iteration performs heapreplace which is O(log 26) = O(1). Total: O(k) = O(n)
  • t.sort(): O(k × log k) where k 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 letters
  • pq heap: O(26) = O(1)
  • t list: O(k) where k is the number of "?" marks, at most O(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)
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following shows the order of node visit in a Breadth-first Search?


Recommended Readings

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

Load More