Replace Question Marks in String to Minimize Its Value
MedPrompt
You are given a string s. s[i] is either a lowercase English letter or '?'.
For a string t having length m containing only lowercase English letters, we define the function cost(i) for an index i as the number of characters equal to t[i] that appeared before it, i.e. in the range [0, i - 1].
The value of t is the sum of cost(i) for all indices i.
For example, for the string t = "aab":
cost(0) = 0cost(1) = 1cost(2) = 0- Hence, the value of
"aab"is0 + 1 + 0 = 1.
Your task is to replace all occurrences of '?' in s with any lowercase English letter so that the value of s is minimized.
Return a string denoting the modified string with replaced occurrences of '?'. If there are multiple strings resulting in the minimum value, return the lexicographically smallest one.
Example 1:
Input: s = "???"
Output: "abc"
Explanation: In this example, we can replace the occurrences of '?' to make s equal to "abc".
For "abc", cost(0) = 0, cost(1) = 0, and cost(2) = 0.
The value of "abc" is 0.
Some other modifications of s that have a value of 0 are "cba", "abz", and, "hey".
Among all of them, we choose the lexicographically smallest.
Example 2:
Input: s = "a?a?"
Output: "abac"
Explanation: In this example, the occurrences of '?' can be replaced to make s equal to "abac".
For "abac", cost(0) = 0, cost(1) = 0, cost(2) = 1, and cost(3) = 0.
The value of "abac" is 1.
Constraints:
1 <= s.length <= 105s[i]is either a lowercase English letter or'?'.
Approaches
2 approaches with complexity analysis and trade-offs.
The core idea behind solving this problem is to understand how the 'value' of a string is calculated. The value is the sum of k*(k-1)/2 for each character, where k is its total count. To minimize this sum, we need to keep the character counts as balanced as possible. This suggests a greedy strategy.
We have a number of '?'s to replace, say q. For each replacement, we should choose a character that results in the smallest possible increase in the total value. The marginal increase in value when adding an instance of a character c that already appears k times is k. Therefore, at each step, we should greedily choose the character with the current minimum frequency.
To handle the secondary requirement of returning the lexicographically smallest string, we apply two rules:
- When determining the set of
qcharacters to use for replacement, if there's a tie in minimum frequency (e.g., 'b' and 'c' both appear 0 times), we choose the lexicographically smaller character ('b'). This makes the multiset of replacement characters itself lexicographically smaller. - After determining this multiset, we sort these characters and place them into the '?' positions from left to right. This ensures the final string is as small as possible.
This approach implements the greedy strategy by repeatedly scanning through all 26 alphabet characters to find the one with the minimum current frequency.
Algorithm
- Count Frequencies and '?'s:
- Create an integer array
countsof size 26, initialized to zero, to store the frequencies of characters 'a' through 'z'. - Create a list
q_indicesto store the original indices of the '?' characters. - Iterate through the input string
sfrom left to right. For each character:- If it's a letter, increment the corresponding counter in the
countsarray. - If it's a '?', add its index to
q_indices.
- If it's a letter, increment the corresponding counter in the
- Create an integer array
- Determine Replacement Characters:
- Let
qbe the number of '?'s (the size ofq_indices). - Create a list of characters called
replacements. - Loop
qtimes to decide which character to use for each '?': a. Initializemin_countto a very large value andchar_to_addto a placeholder. b. Iterate through all characters from 'a' to 'z'. In this inner loop, find the character that has the minimum count in thecountsarray. If multiple characters share the same minimum count, the one that comes first alphabetically is chosen due to the loop's order. c. Add the chosen character (char_to_add) to thereplacementslist. d. Increment the count ofchar_to_addin thecountsarray to reflect its addition.
- Let
- Construct the Final String:
- Sort the
replacementslist in alphabetical order. - Convert the input string
sinto a character arrays_charsfor easy modification. - Iterate
qtimes. In each iterationi, take thei-th index fromq_indicesand thei-th character from the sortedreplacementslist, and place the character at that index ins_chars. - Convert
s_charsback to a string and return it.
- Sort the
Walkthrough
class Solution { public String minimizeStringValue(String s) { int[] counts = new int[26]; List<Integer> qIndices = new ArrayList<>(); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if (c == '?') { qIndices.add(i); } else { counts[c - 'a']++; } } int q = qIndices.size(); if (q == 0) { return s; } List<Character> replacements = new ArrayList<>(); for (int i = 0; i < q; i++) { int minCount = Integer.MAX_VALUE; char charToUse = ' '; // Find char with min count, breaking ties lexicographically for (char c = 'a'; c <= 'z'; c++) { if (counts[c - 'a'] < minCount) { minCount = counts[c - 'a']; charToUse = c; } } replacements.add(charToUse); counts[charToUse - 'a']++; } Collections.sort(replacements); char[] sChars = s.toCharArray(); for (int i = 0; i < q; i++) { sChars[qIndices.get(i)] = replacements.get(i); } return new String(sChars); }}Complexity
Time
O(N + q * A + q log q), where N is the length of the string, q is the number of '?', and A is the alphabet size (26). The initial scan of the string is O(N). Determining the `q` replacement characters takes `q` iterations, with each iteration scanning `A` characters, totaling O(q * A). Sorting the replacements takes O(q log q). Building the final string takes O(N). Since A is a constant, the complexity simplifies to O(N + q log q).
Space
O(N + A), where N is the length of the string and A is the alphabet size (26). This is because we store the indices of '?' (up to O(N)), the replacement characters (up to O(N)), and the character counts (O(A)). If we consider the output string as part of the complexity, it's O(N). The auxiliary space is O(q + A), where q is the number of question marks.
Trade-offs
Pros
The approach is relatively straightforward to understand and implement.
It correctly minimizes the string's value and produces the lexicographically smallest result among optimal solutions.
Cons
The process of finding the character with the minimum frequency involves a linear scan through all 26 characters for each of the
qquestion marks. This is less efficient than using a data structure optimized for finding minimums, such as a priority queue.
This approach is an optimization of the previous greedy method. The fundamental logic of minimizing value by balancing character counts and ensuring lexicographical order remains the same. The key difference lies in how we efficiently find the character with the minimum frequency at each step.
Instead of a linear scan through all 26 characters for each of the q replacements, we use a min-priority queue. The priority queue is a data structure specifically designed to provide quick access to the minimum (or maximum) element. By storing the character frequencies in a priority queue, we can retrieve the best character to add in O(log A) time, where A is the alphabet size.
We configure the priority queue to order elements first by frequency, and then by character value for tie-breaking. This ensures we always pick the character that minimizes the value cost, and among those, the one that leads to a lexicographically smaller set of replacement characters. This makes the process of determining the q replacement characters significantly faster, especially when q is large.
Algorithm
- Count Frequencies and '?'s:
- This step is identical to the previous approach. We calculate initial character
countsand find allq_indices.
- This step is identical to the previous approach. We calculate initial character
- Setup Priority Queue:
- Create a min-priority queue. This queue will store elements representing each character and its frequency.
- The elements can be pairs or a small array like
[frequency, character_index]. - The priority queue must be configured with a custom comparator to order elements first by
frequency(ascending), and then bycharacter_index(ascending) to break ties. - Populate the priority queue by adding an entry for each of the 26 characters with its initial count from the
countsarray.
- Determine Replacement Characters:
- Let
qbe the number of '?'s. - Create a list of characters called
replacements. - Loop
qtimes: a. Extract the element with the highest priority (lowest frequency, then lowest character value) from the priority queue. Let this be(freq, char_idx). b. Add the corresponding character to thereplacementslist. c. Insert a new element(freq + 1, char_idx)back into the priority queue to update the character's frequency for subsequent steps.
- Let
- Construct the Final String:
- This step is also identical to the previous approach. Sort the
replacementslist and fill in the '?' positions in the original string.
- This step is also identical to the previous approach. Sort the
Walkthrough
import java.util.ArrayList;import java.util.Collections;import java.util.List;import java.util.PriorityQueue; class Solution { public String minimizeStringValue(String s) { int[] counts = new int[26]; List<Integer> qIndices = new ArrayList<>(); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if (c == '?') { qIndices.add(i); } else { counts[c - 'a']++; } } int q = qIndices.size(); if (q == 0) { return s; } // PriorityQueue stores pairs of [count, character_index] // It's a min-heap, ordered by count, then by character_index PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> { if (a[0] != b[0]) { return a[0] - b[0]; } return a[1] - b[1]; }); for (int i = 0; i < 26; i++) { pq.offer(new int[]{counts[i], i}); } List<Character> replacements = new ArrayList<>(); for (int i = 0; i < q; i++) { int[] top = pq.poll(); char charToUse = (char) ('a' + top[1]); replacements.add(charToUse); top[0]++; pq.offer(top); } Collections.sort(replacements); char[] sChars = s.toCharArray(); for (int i = 0; i < q; i++) { sChars[qIndices.get(i)] = replacements.get(i); } return new String(sChars); }}Complexity
Time
O(N + A log A + q log A + q log q). The initial scan is O(N). Building the priority queue of size A takes O(A log A). The main loop runs `q` times, with each poll/offer operation taking O(log A), for a total of O(q log A). Sorting replacements is O(q log q). As A is a small constant (26), `log A` is also a small constant. The dominant terms are O(N + q log q).
Space
O(N + A), where N is the string length and A is the alphabet size. The auxiliary space complexity is O(q + A) for storing `q_indices`, `replacements`, `counts`, and the priority queue.
Trade-offs
Pros
Highly efficient, as it uses the optimal data structure (priority queue) for repeatedly finding the minimum element.
Provides the optimal solution in terms of both value and lexicographical order.
The time complexity is better than the linear scan approach for the character selection part.
Cons
The implementation is slightly more complex due to the use of a
PriorityQueuewith a custom comparator.
Solutions
Solution
class Solution {public String minimizeStringValue(String s) { int[] cnt = new int[26]; int n = s.length(); int k = 0; char[] cs = s.toCharArray(); for (char c : cs) { if (c == '?') { ++k; } else { ++cnt[c - 'a']; } } PriorityQueue<int[]> pq = new PriorityQueue<>((a, b)->a[0] == b[0] ? a[1] - b[1] : a[0] - b[0]); for (int i = 0; i < 26; ++i) { pq.offer(new int[]{cnt[i], i}); } int[] t = new int[k]; for (int j = 0; j < k; ++j) { int[] p = pq.poll(); t[j] = p[1]; pq.offer(new int[]{p[0] + 1, p[1]}); } Arrays.sort(t); for (int i = 0, j = 0; i < n; ++i) { if (cs[i] == '?') { cs[i] = (char)(t[j++] + 'a'); } } return new String(cs); }}class Solution {public: string minimizeStringValue(string s) { int cnt[26]{}; int k = 0; for (char &c : s) { if (c == '?') { ++k; } else { ++cnt[c - 'a']; } } priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq; for (int i = 0; i < 26; ++i) { pq.push({cnt[i], i}); } vector<int> t(k); for (int i = 0; i < k; ++i) { auto [v, c] = pq.top(); pq.pop(); t[i] = c; pq.push({v + 1, c}); } sort(t.begin(), t.end()); int j = 0; for (char &c : s) { if (c == '?') { c = t[j++] + 'a'; } } return s; }};class Solution: def minimizeStringValue(self, s: str) -> str: cnt = Counter(s) pq = [(cnt[c], c) for c in ascii_lowercase] heapify(pq) t = [] for _ in range(s . count("?")): v, c = pq[0] t . append(c) heapreplace(pq, (v + 1, c)) t . sort() cs = list(s) j = 0 for i, c in enumerate(s): if c == "?": cs[i] = t[j] j += 1 return "" . join(cs)Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.