Minimum Additions to Make Valid String
MedPrompt
Given a string word to which you can insert letters "a", "b" or "c" anywhere and any number of times, return the minimum number of letters that must be inserted so that word becomes valid.
A string is called valid if it can be formed by concatenating the string "abc" several times.
Example 1:
Input: word = "b"
Output: 2
Explanation: Insert the letter "a" right before "b", and the letter "c" right next to "b" to obtain the valid string "abc".Example 2:
Input: word = "aaa"
Output: 6
Explanation: Insert letters "b" and "c" next to each "a" to obtain the valid string "abcabcabc".Example 3:
Input: word = "abc"
Output: 0
Explanation: word is already valid. No modifications are needed.
Constraints:
1 <= word.length <= 50wordconsists of letters "a", "b" and "c" only.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach models the problem using a recursive structure. We define a function that calculates the minimum insertions for the remainder of the string, given the current state (which character is expected next). To optimize this and avoid re-computing results for the same subproblems, we use a memoization table (a 2D array) to store the outcomes. This transforms the plain recursion into a more efficient dynamic programming solution.
Algorithm
- The problem can be solved using recursion with memoization, which is a top-down dynamic programming approach.
- We define a function
solve(index, state)which computes the minimum insertions needed for the suffix of the word starting atindex, given that the next expected character in the valid sequence corresponds tostate(wherestate0='a', 1='b', 2='c'). - Base Case: If
indexreaches the end of the string, we've processed all characters. We might need to add characters to complete the last"abc"block. The number of insertions needed is(3 - state) % 3. - Recursive Step: For the character
word.charAt(index), we calculate how many characters we need to insert to make ourstatematch this character. This is the cyclic distance betweenstateandword.charAt(index). After matching, the new state becomes the one followingword.charAt(index), and we recurse forindex + 1. - Memoization: We use a 2D array
memo[n][3]to store the results ofsolve(index, state)to avoid redundant calculations. The initial call issolve(0, 0).
Walkthrough
The core idea is to define a state by (index, expected_char_state) and find the minimum insertions from that state. The state expected_char_state can be an integer from 0 to 2, representing 'a', 'b', and 'c' respectively.
The function solve(index, state) will work as follows:
- If we have processed the entire string (
index == word.length()), we calculate the final insertions needed to complete the last"abc"sequence. If we expect 'a' (state=0), 0 insertions are needed. If we expect 'b' (state=1), 2 insertions ('b', 'c') are needed. If we expect 'c' (state=2), 1 insertion ('c') is needed. This can be generalized to(3 - state) % 3. - If the result for
(index, state)is already in our memoization table, we return it directly. - Otherwise, we take the current character
char c = word.charAt(index). We calculate the number of insertions required to satisfy the expectation. For example, if we expect 'a' (state=0) and see 'c' (c_val=2), we must insert 'a' and 'b'. The number of insertions is(c_val - state + 3) % 3. - After this, we have effectively matched
c. The next expected character state will be(c_val + 1) % 3. - The total insertions from the current state is the sum of insertions at this step and the result of the recursive call for the next index and new state:
insertions_at_this_step + solve(index + 1, new_state). - We store this result in the memoization table and return it.
class Solution { private int[][] memo; private String word; public int addMinimum(String word) { this.word = word; this.memo = new int[word.length()][3]; for (int[] row : memo) { java.util.Arrays.fill(row, -1); } return solve(0, 0); } private int solve(int index, int state) { // Base case: reached the end of the word if (index == word.length()) { // Insertions needed to complete the last "abc" sequence return (3 - state) % 3; } // Return memoized result if available if (memo[index][state] != -1) { return memo[index][state]; } int charVal = word.charAt(index) - 'a'; // Calculate insertions needed at this step int insertions = (charVal - state + 3) % 3; // The new state after matching the current character int newState = (charVal + 1) % 3; // Total insertions = insertions now + insertions for the rest of the string int result = insertions + solve(index + 1, newState); // Memoize and return the result memo[index][state] = result; return result; }}Complexity
Time
O(N), where N is the length of `word`. Each state `(index, state)` is computed only once, and there are N * 3 possible states.
Space
O(N), where N is the length of `word`. This is for the memoization table of size N x 3 and the depth of the recursion stack.
Trade-offs
Pros
The recursive structure with memoization is a standard DP pattern, making it a clear and logical way to solve the problem.
It correctly breaks down the problem into smaller, overlapping subproblems.
Cons
Uses O(N) extra space for the memoization table and recursion stack, which is less optimal than the iterative approach.
For very long strings, recursion could lead to a stack overflow error, although not an issue with the given constraints.
A more optimal approach is to iterate through the string once, maintaining a single state variable that represents the character we currently expect in the valid "abc" sequence. This is essentially a state machine. At each character in the input word, we greedily calculate the minimum number of insertions needed to match it and then update our state. This avoids the overhead of recursion and extra space for a memoization table.
Algorithm
- This approach uses a greedy strategy with a state machine.
- Initialize
insertions = 0and astatevariable to 0 (representing the next expected character 'a'). - Iterate through each character
cof the inputword. - For each
c, determine its integer valuec_val(0 for 'a', 1 for 'b', 2 for 'c'). - Calculate the number of insertions needed to bridge the gap between the
stateandc_val. This is(c_val - state + 3) % 3. - Add this number to the total
insertions. - After matching
c, update thestateto be the next character in the sequence:state = (c_val + 1) % 3. - After the loop finishes, the
stateindicates if the last"abc"sequence is complete. Ifstateis not 0, add the remaining(3 - state) % 3insertions. - Return the total
insertions.
Walkthrough
We can think of building the valid string by consuming characters from the input word. We keep track of which character we expect next in the "abc" cycle ('a', then 'b', then 'c', then 'a' again). Let's use an integer state (0 for 'a', 1 for 'b', 2 for 'c') for this.
We start by expecting 'a' (state = 0). We iterate through word. When we encounter a character c, we see how many characters we need to insert to make our expectation match c. For instance, if we expect 'a' (state=0) and see 'b' (c_val=1), we must insert one character ('a'). The number of insertions is simply c_val - state. If we expect 'c' (state=2) and see 'a' (c_val=0), we must wrap around. This means inserting 'c' to finish the old block, and then we are ready for the new 'a'. The number of insertions can be calculated with modular arithmetic: (c_val - state + 3) % 3.
After accounting for the insertions and matching c, our next expected character will be the one that follows c in the cycle, so we update state to (c_val + 1) % 3.
Finally, after the loop, we might have an incomplete sequence. We add the final insertions needed to complete it.
class Solution { public int addMinimum(String word) { int insertions = 0; // State represents the next character we need: 0 for 'a', 1 for 'b', 2 for 'c'. int state = 0; for (char c : word.toCharArray()) { int charVal = c - 'a'; // Calculate how many characters we need to insert before the current character. // This is the cyclic distance from the expected state to the current character's value. insertions += (charVal - state + 3) % 3; // After matching the current character, the state advances to the next one in the cycle. state = (charVal + 1) % 3; } // After the loop, if the last sequence is not complete (state != 0), // add the remaining characters needed. if (state != 0) { insertions += (3 - state); } return insertions; }}Complexity
Time
O(N), where N is the length of `word`. We perform a single pass through the string.
Space
O(1), as we only use a few variables to keep track of the state and the total insertions, regardless of the input string's size.
Trade-offs
Pros
Extremely efficient, with optimal O(1) space complexity.
The single-pass iterative solution is fast and avoids recursion overhead.
The code is concise and simple to implement.
Cons
The greedy logic, while simple, might be slightly less intuitive to prove correct compared to a formal DP setup for those unfamiliar with state machine patterns.
Solutions
Solution
class Solution {public int addMinimum(String word) { String s = "abc"; int ans = 0, n = word.length(); for (int i = 0, j = 0; j < n; i = (i + 1) % 3) { if (word.charAt(j) != s.charAt(i)) { ++ans; } else { ++j; } } if (word.charAt(n - 1) != 'c') { ans += word.charAt(n - 1) == 'b' ? 1 : 2; } return ans; }}class Solution {public: int addMinimum(string word) { string s = "abc"; int ans = 0, n = word.size(); for (int i = 0, j = 0; j < n; i = (i + 1) % 3) { if (word[j] != s[i]) { ++ans; } else { ++j; } } if (word[n - 1] != 'c') { ans += word[n - 1] == 'b' ? 1 : 2; } return ans; }};class Solution: def addMinimum(self, word: str) -> int: s = 'abc' ans, n = 0, len(word) i = j = 0 while j < n: if word[j] != s[i]: ans += 1 else: j += 1 i = (i + 1) % 3 if word[- 1] != 'c': ans += 1 if word[- 1] == 'b' else 2 return ansVideo 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.