LeetCode 1737 - Change Minimum Characters to Satisfy One of Three Conditions
The problem gives us two lowercase English strings, a and b. In a single operation, we may change any character in either string into any other lowercase English letter.
Difficulty: 🟡 Medium
Topics: Hash Table, String, Counting, Prefix Sum
Solution
Problem Understanding
The problem gives us two lowercase English strings, a and b. In a single operation, we may change any character in either string into any other lowercase English letter.
Our task is to determine the minimum number of character changes required so that at least one of the following conditions becomes true:
- Every character in
ais strictly smaller than every character inb. - Every character in
bis strictly smaller than every character ina. - Both strings consist of only one distinct character.
The phrase "strictly smaller" refers to alphabetical order. For example, 'a' < 'b', 'b' < 'c', and so on. If condition 1 is satisfied, then the largest character in a must still be smaller than the smallest character in b.
The input sizes can each be as large as 10^5, which immediately tells us that expensive approaches involving repeated rescanning or checking all transformations explicitly will not work. We need something close to linear time.
Another important observation is that the alphabet size is fixed at only 26 lowercase letters. This strongly suggests that frequency counting and prefix sums will be useful, because we can efficiently reason about all possible character boundaries.
Some important edge cases include:
- Strings that already satisfy one of the conditions.
- Strings containing only one repeated letter.
- Cases where both strings heavily overlap in character ranges.
- Very small strings, such as length 1.
- Cases where the optimal answer comes from condition 3 instead of conditions 1 or 2.
Because the alphabet size is constant, we can afford to evaluate all possible split points between letters efficiently.
Approaches
Brute Force Approach
A brute force solution would attempt every possible target transformation.
For conditions 1 and 2, we could try every possible boundary character. For example, suppose we choose 'd' as the dividing point for condition 1. Then every character in a must become one of 'a', 'b', or 'c', and every character in b must become 'd' or larger. We could count how many changes are needed for that boundary.
For condition 3, we could try every possible target letter from 'a' to 'z', converting all characters in both strings into that letter.
Even this simplified brute force already hints at a better direction. A truly naive implementation might repeatedly scan both strings for every boundary and every letter, leading to roughly:
- 26 possibilities for condition 3
- 25 split boundaries for condition 1
- 25 split boundaries for condition 2
- Full scans of both strings each time
This gives a complexity of approximately O(26 * (n + m)), which is actually acceptable because the alphabet size is constant.
However, repeatedly rescanning strings is unnecessary. We can optimize further using frequency arrays and prefix sums.
Key Insight
The critical observation is that there are only 26 lowercase letters.
Instead of repeatedly scanning the strings, we can:
- Count how many times each character appears in
aandb. - Build prefix sums to quickly determine:
- How many characters are less than a boundary
- How many characters are greater than or equal to a boundary
This allows us to evaluate each condition in constant time per boundary.
For condition 3, we simply choose a target character and keep all occurrences already matching that character.
For conditions 1 and 2, we test every split between letters:
-
If the split is after
'c', then: -
amust contain only'a'to'c' -
bmust contain only'd'to'z'
Using prefix sums makes these counts extremely efficient.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(26 × (n + m)) | O(1) | Repeatedly scans strings for every boundary |
| Optimal | O(n + m + 26) | O(26) | Uses frequency counting and prefix sums |
Algorithm Walkthrough
- Create two frequency arrays of size 26.
Each index corresponds to a lowercase letter:
- Index 0 represents
'a' - Index 1 represents
'b' - ...
- Index 25 represents
'z'
We count how many times each letter appears in a and b.
2. Compute the answer for condition 3.
We try making both strings consist of only one distinct character.
For every letter:
- Keep all occurrences already equal to that letter
- Change every other character
If:
countA[i]is the number of occurrences inacountB[i]is the number of occurrences inb
then the operations needed are:
len(a) + len(b) - countA[i] - countB[i]
We take the minimum over all 26 letters. 3. Build prefix sums for both frequency arrays.
The prefix sum at index i tells us how many characters are less than or equal to letter i.
This lets us quickly answer range count queries. 4. Evaluate condition 1.
We try every split between adjacent letters.
Suppose the split is after letter i.
Then:
amay only contain letters0..ibmay only contain lettersi+1..25
The required changes are:
- Characters in
agreater thani - Characters in
bless than or equal toi
- Evaluate condition 2.
This is symmetric to condition 1.
For the same split:
bmay only contain letters0..iamay only contain lettersi+1..25
- Return the minimum value among all possibilities.
Why it works
The algorithm works because every valid configuration for conditions 1 and 2 can be represented by a boundary between two adjacent letters. Since the alphabet contains only 26 letters, testing all 25 boundaries guarantees that we examine every possible ordering arrangement.
For condition 3, any valid final state must consist entirely of one repeated character, so checking all 26 target letters guarantees we find the optimal one.
Because the algorithm exhaustively evaluates all valid structural possibilities while using exact frequency counts, the minimum computed value is guaranteed to be optimal.
Python Solution
class Solution:
def minCharacters(self, a: str, b: str) -> int:
count_a = [0] * 26
count_b = [0] * 26
for ch in a:
count_a[ord(ch) - ord('a')] += 1
for ch in b:
count_b[ord(ch) - ord('a')] += 1
n = len(a)
m = len(b)
answer = n + m
# Condition 3:
# Both strings consist of only one distinct letter
for i in range(26):
changes = (n - count_a[i]) + (m - count_b[i])
answer = min(answer, changes)
prefix_a = count_a[:]
prefix_b = count_b[:]
for i in range(1, 26):
prefix_a[i] += prefix_a[i - 1]
prefix_b[i] += prefix_b[i - 1]
# Conditions 1 and 2
for i in range(25):
# Condition 1:
# all chars in a < all chars in b
changes_a = n - prefix_a[i]
changes_b = prefix_b[i]
answer = min(answer, changes_a + changes_b)
# Condition 2:
# all chars in b < all chars in a
changes_b = m - prefix_b[i]
changes_a = prefix_a[i]
answer = min(answer, changes_a + changes_b)
return answer
The implementation begins by building two frequency arrays, one for each string. This gives us constant time access to how many times each character appears.
Next, the code evaluates condition 3. For every possible target letter, we count how many characters already match that letter and how many must be changed.
After that, prefix sums are constructed. These prefix arrays allow us to quickly determine:
- How many characters are less than or equal to a given letter
- How many characters are greater than a given letter
The final loop iterates over all 25 possible boundaries between letters. For each boundary, the code computes the number of changes required for both ordering conditions and updates the global minimum.
The solution is efficient because every calculation after preprocessing is constant time.
Go Solution
func minCharacters(a string, b string) int {
countA := make([]int, 26)
countB := make([]int, 26)
for _, ch := range a {
countA[ch-'a']++
}
for _, ch := range b {
countB[ch-'a']++
}
n := len(a)
m := len(b)
answer := n + m
// Condition 3
for i := 0; i < 26; i++ {
changes := (n - countA[i]) + (m - countB[i])
if changes < answer {
answer = changes
}
}
prefixA := make([]int, 26)
prefixB := make([]int, 26)
copy(prefixA, countA)
copy(prefixB, countB)
for i := 1; i < 26; i++ {
prefixA[i] += prefixA[i-1]
prefixB[i] += prefixB[i-1]
}
// Conditions 1 and 2
for i := 0; i < 25; i++ {
// Condition 1
changesA := n - prefixA[i]
changesB := prefixB[i]
if changesA+changesB < answer {
answer = changesA + changesB
}
// Condition 2
changesB = m - prefixB[i]
changesA = prefixA[i]
if changesA+changesB < answer {
answer = changesA + changesB
}
}
return answer
}
The Go implementation follows exactly the same logic as the Python version. Since Go does not have built in dynamic lists like Python, fixed size slices are used for the frequency and prefix arrays.
Character indexing is performed using rune arithmetic:
countA[ch-'a']++
This converts each lowercase letter into an index from 0 to 25.
No special handling for empty strings is needed because the constraints guarantee both strings contain at least one character.
Worked Examples
Example 1
a = "aba"
b = "caa"
Step 1: Frequency Counts
| Letter | a Count | b Count |
|---|---|---|
| a | 2 | 2 |
| b | 1 | 0 |
| c | 0 | 1 |
All other letters have frequency 0.
Step 2: Condition 3
Try making both strings one letter.
| Target Letter | Changes Needed |
|---|---|
| a | 1 |
| b | 5 |
| c | 5 |
For target 'a':
aneeds 1 change (b -> a)bneeds 1 change (c -> a)
Total = 2
Current answer = 2.
Step 3: Prefix Sums
| Letter | Prefix A | Prefix B |
|---|---|---|
| a | 2 | 2 |
| b | 3 | 2 |
| c | 3 | 3 |
Step 4: Check Boundaries
Split after 'b':
Condition 1:
amust stay<= 'b'bmust become> 'b'
Changes:
a: 0b: 2
Total = 2
This matches the best answer.
Final answer = 2.
Example 2
a = "dabadd"
b = "cda"
Step 1: Frequency Counts
| Letter | a Count | b Count |
|---|---|---|
| a | 2 | 1 |
| b | 1 | 0 |
| c | 0 | 1 |
| d | 3 | 1 |
Step 2: Evaluate Boundary After 'd'
Condition 1:
amust be<= 'd'bmust be> 'd'
All characters in a already satisfy the condition.
Characters in b:
c,d,a
All must become larger than 'd'.
Required changes = 3.
This becomes the minimum answer.
Final answer = 3.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n + m + 26) | Counting characters and checking all boundaries |
| Space | O(26) | Fixed size frequency and prefix arrays |
The dominant cost comes from scanning the two input strings once. Every other operation works over the fixed alphabet size of 26, which is constant. Therefore the runtime is effectively linear in the input size.
The space usage is constant because the algorithm only stores several arrays of length 26 regardless of input size.
Test Cases
solution = Solution()
assert solution.minCharacters("aba", "caa") == 2 # provided example 1
assert solution.minCharacters("dabadd", "cda") == 3 # provided example 2
assert solution.minCharacters("a", "a") == 0 # already satisfies condition 3
assert solution.minCharacters("a", "z") == 0 # already satisfies condition 1
assert solution.minCharacters("z", "a") == 0 # already satisfies condition 2
assert solution.minCharacters("abc", "abc") == 3 # overlapping ranges
assert solution.minCharacters("aaa", "aaa") == 0 # already one distinct letter
assert solution.minCharacters("abc", "def") == 0 # already ordered
assert solution.minCharacters("zzz", "zzz") == 0 # same repeated character
assert solution.minCharacters("abcde", "fghij") == 0 # perfect separation
assert solution.minCharacters("bbbb", "aaaa") == 0 # already satisfies condition 2
assert solution.minCharacters("az", "za") == 2 # requires multiple changes
assert solution.minCharacters("leetcode", "codeleet") >= 0 # stress style mixed letters
assert solution.minCharacters("a" * 100000, "z" * 100000) == 0 # maximum size style case
Test Summary
| Test | Why |
|---|---|
"aba", "caa" |
Validates provided example |
"dabadd", "cda" |
Validates provided example |
"a", "a" |
Smallest possible equal strings |
"a", "z" |
Already satisfies condition 1 |
"z", "a" |
Already satisfies condition 2 |
"abc", "abc" |
Overlapping character ranges |
"aaa", "aaa" |
Already satisfies condition 3 |
"abc", "def" |
Perfect alphabetical separation |
"zzz", "zzz" |
Single repeated high letter |
"bbbb", "aaaa" |
Reverse ordering condition |
"az", "za" |
Requires careful boundary handling |
| Large repeated strings | Stress test for performance |
Edge Cases
One important edge case occurs when the strings already satisfy one of the conditions. For example, a = "abc" and b = "xyz" already satisfy condition 1 because every character in a is smaller than every character in b. A buggy implementation might still perform unnecessary changes. This implementation correctly returns 0 because the boundary checks naturally produce zero required modifications.
Another tricky edge case is when both strings contain only one repeated character, such as a = "aaaa" and b = "aaaa". In this situation, condition 3 is already satisfied. The implementation handles this efficiently because the frequency counting immediately shows that all characters already match the target letter.
A third important edge case involves boundaries near the ends of the alphabet. For example, if the split occurs after 'y', then all characters in one string must be 'z'. Implementations with incorrect prefix handling often make off by one mistakes here. This solution avoids that problem by carefully interpreting the split as:
- left side uses letters
0..i - right side uses letters
i+1..25
and iterating only through valid boundaries from 0 to 24.
Another subtle case is very small strings of length 1. Since a single character trivially forms a uniform string, condition 3 is always easy to satisfy. The implementation works correctly because the frequency arrays and prefix sums naturally support strings of any valid size.