Mutating counts without cleanup
Wrong move: Zero-count keys stay in map and break distinct/count constraints.
Usually fails on: Window/map size checks are consistently off by one.
Fix: Delete keys when count reaches zero.
Break down a hard problem into reliable checkpoints, edge-case handling, and complexity trade-offs.
You are given a string s.
A string t is called good if all characters of t occur the same number of times.
You can perform the following operations any number of times:
s.s.s to its next letter in the alphabet.Note that you cannot change 'z' to 'a' using the third operation.
Return the minimum number of operations required to make s good.
Example 1:
Input: s = "acab"
Output: 1
Explanation:
We can make s good by deleting one occurrence of character 'a'.
Example 2:
Input: s = "wddw"
Output: 0
Explanation:
We do not need to perform any operations since s is initially good.
Example 3:
Input: s = "aaabc"
Output: 2
Explanation:
We can make s good by applying these operations:
'a' to 'b''c' into sConstraints:
3 <= s.length <= 2 * 104s contains only lowercase English letters.Problem summary: You are given a string s. A string t is called good if all characters of t occur the same number of times. You can perform the following operations any number of times: Delete a character from s. Insert a character in s. Change a character in s to its next letter in the alphabet. Note that you cannot change 'z' to 'a' using the third operation. Return the minimum number of operations required to make s good.
Start with the most direct exhaustive search. That gives a correctness anchor before optimizing.
Pattern signal: Hash Map · Dynamic Programming
"acab"
"wddw"
"aaabbc"
minimum-number-of-steps-to-make-two-strings-anagram)Source-backed implementations are provided below for direct study and interview prep.
// Accepted solution for LeetCode #3389: Minimum Operations to Make Character Frequencies Equal
class Solution {
public int makeStringGood(String s) {
int ans = s.length();
int[] count = new int[26];
for (final char c : s.toCharArray())
++count[c - 'a'];
final int maxCount = Arrays.stream(count).max().getAsInt();
for (int target = 1; target <= maxCount; ++target)
ans = Math.min(ans, getMinOperations(count, target));
return ans;
}
private int getMinOperations(int[] count, int target) {
// dp[i] represents the minimum number of operations to make the frequency of
// (i..25)-th (0-indexed) letters equal to `target`.
int[] dp = new int[27];
for (int i = 25; i >= 0; --i) {
// 1. Delete all the i-th letters.
int deleteAllToZero = count[i];
// 2. Insert/delete the i-th letters to have `target` number of letters.
int deleteOrInsertToTarget = Math.abs(target - count[i]);
dp[i] = Math.min(deleteAllToZero, deleteOrInsertToTarget) + dp[i + 1];
if (i + 1 < 26 && count[i + 1] < target) {
final int nextDeficit = target - count[i + 1];
// Make the frequency of the i-th letter equal to the `target` or 0.
final int needToChange = count[i] <= target ? count[i] : count[i] - target;
final int changeToTarget = (nextDeficit > needToChange)
// 3. Change all the i-th letters to the next letter and then
// insert the remaining deficit for the next letter.
? needToChange + (nextDeficit - needToChange)
// 4. Change `nextDeficit` i-th letters to the next letter
// and then delete the remaining i-th letters.
: nextDeficit + (needToChange - nextDeficit);
dp[i] = Math.min(dp[i], changeToTarget + dp[i + 2]);
}
}
return dp[0];
}
}
// Accepted solution for LeetCode #3389: Minimum Operations to Make Character Frequencies Equal
package main
import "slices"
// https://space.bilibili.com/206214
func makeStringGood(s string) int {
cnt := [26]int{}
for _, b := range s {
cnt[b-'a']++
}
targets := map[int]struct{}{}
targets[cnt[0]] = struct{}{}
for i := 1; i < len(cnt); i++ {
x, y := cnt[i-1], cnt[i]
targets[y] = struct{}{}
targets[x+y] = struct{}{}
targets[(x+y)/2] = struct{}{}
targets[(x+y+1)/2] = struct{}{}
}
ans := len(s) // target = 0 时的答案
f := [27]int{}
for target := range targets {
f[25] = min(cnt[25], abs(cnt[25]-target))
for i := 24; i >= 0; i-- {
x := cnt[i]
if x == 0 {
f[i] = f[i+1]
continue
}
// 单独操作 x(变成 target 或 0)
f[i] = f[i+1] + min(x, abs(x-target))
// x 变成 target 或 0,y 变成 target
y := cnt[i+1]
if 0 < y && y < target { // 只有当 y 需要变大时,才去执行第三种操作
if x > target { // x 变成 target
f[i] = min(f[i], f[i+2]+max(x-target, target-y))
} else { // x 变成 0
f[i] = min(f[i], f[i+2]+max(x, target-y))
}
}
}
ans = min(ans, f[0])
}
return ans
}
func abs(x int) int { if x < 0 { return -x }; return x }
func makeStringGoodAC(s string) int {
cnt := [26]int{}
for _, b := range s {
cnt[b-'a']++
}
m := slices.Max(cnt[:])
ans := len(s) // target = 0 时的答案
f := [27]int{}
for target := 1; target <= m; target++ {
f[25] = min(cnt[25], abs(cnt[25]-target))
for i := 24; i >= 0; i-- {
x := cnt[i]
if x == 0 {
f[i] = f[i+1]
continue
}
// 单独操作 x(变成 target 或 0)
f[i] = f[i+1] + min(x, abs(x-target))
// x 变成 target 或 0,y 变成 target
y := cnt[i+1]
if 0 < y && y < target { // 只有当 y 需要变大时,才去执行第三种操作
if x > target { // x 变成 target
f[i] = min(f[i], f[i+2]+max(x-target, target-y))
} else { // x 变成 0
f[i] = min(f[i], f[i+2]+max(x, target-y))
}
}
}
ans = min(ans, f[0])
}
return ans
}
func makeStringGood2(s string) int {
n := len(s)
cnt := [26]int{}
for _, b := range s {
cnt[b-'a']++
}
mx := slices.Max(cnt[:])
ans := n
for target := 0; target <= mx; target++ {
calcOp := func(v int) int { return min(v, abs(target-v)) }
memo := [26]int{}
for i := range memo {
memo[i] = -1
}
var dfs func(int) int
dfs = func(i int) (res int) {
if i >= 26 {
return
}
p := &memo[i]
if *p != -1 {
return *p
}
defer func() { *p = res }()
x := cnt[i]
if x == 0 {
return dfs(i + 1)
}
if i == 25 {
return dfs(i+1) + calcOp(x)
}
y := cnt[i+1]
mn := calcOp(x) + calcOp(y)
// 只有当 y 需要变大时,才去执行第三种操作
if y < target {
if x > target { // x 夹带着第三种操作,变成 target
mn = min(mn, max(x-target, target-y))
} else { // x 夹带着第三种操作,变成 0
mn = min(mn, max(x, target-y))
}
}
return min(dfs(i+1)+calcOp(x), dfs(i+2)+mn)
}
ans = min(ans, dfs(0))
}
return ans
}
# Accepted solution for LeetCode #3389: Minimum Operations to Make Character Frequencies Equal
class Solution:
def makeStringGood(self, s: str) -> int:
count = [0] * 26
for c in s:
count[ord(c) - ord('a')] += 1
return min(self._getMinOperations(count, target)
for target in range(1, max(count) + 1))
def _getMinOperations(self, count: list[int], target: int) -> int:
# dp[i] represents the minimum number of operations to make the frequency of
# (i..25)-th (0-indexed) letters equal to `target`.
dp = [0] * 27
for i in range(25, -1, -1):
# 1. Delete all the i-th letters.
deleteAllToZero = count[i]
# 2. Insert/delete the i-th letters to have `target` number of letters.
deleteOrInsertToTarget = abs(target - count[i])
dp[i] = min(deleteAllToZero, deleteOrInsertToTarget) + dp[i + 1]
if i + 1 < 26 and count[i + 1] < target:
nextDeficit = target - count[i + 1]
# Make the frequency of the i-th letter equal to the `target` or 0.
needToChange = count[i] if count[i] <= target else count[i] - target
changeToTarget = (
# 3. Change all the i-th letters to the next letter and then
# insert the remaining deficit for the next letter.
needToChange + (nextDeficit - needToChange) if nextDeficit > needToChange
# 4. Change `nextDeficit` i-th letters to the next letter and
# then delete the remaining i-th letters.
else nextDeficit + (needToChange - nextDeficit)
)
dp[i] = min(dp[i], changeToTarget + dp[i + 2])
return dp[0]
// Accepted solution for LeetCode #3389: Minimum Operations to Make Character Frequencies Equal
// Rust example auto-generated from java reference.
// Replace the signature and local types with the exact LeetCode harness for this problem.
impl Solution {
pub fn rust_example() {
// Port the logic from the reference block below.
}
}
// Reference (java):
// // Accepted solution for LeetCode #3389: Minimum Operations to Make Character Frequencies Equal
// class Solution {
// public int makeStringGood(String s) {
// int ans = s.length();
// int[] count = new int[26];
//
// for (final char c : s.toCharArray())
// ++count[c - 'a'];
//
// final int maxCount = Arrays.stream(count).max().getAsInt();
// for (int target = 1; target <= maxCount; ++target)
// ans = Math.min(ans, getMinOperations(count, target));
//
// return ans;
// }
//
// private int getMinOperations(int[] count, int target) {
// // dp[i] represents the minimum number of operations to make the frequency of
// // (i..25)-th (0-indexed) letters equal to `target`.
// int[] dp = new int[27];
//
// for (int i = 25; i >= 0; --i) {
// // 1. Delete all the i-th letters.
// int deleteAllToZero = count[i];
// // 2. Insert/delete the i-th letters to have `target` number of letters.
// int deleteOrInsertToTarget = Math.abs(target - count[i]);
// dp[i] = Math.min(deleteAllToZero, deleteOrInsertToTarget) + dp[i + 1];
// if (i + 1 < 26 && count[i + 1] < target) {
// final int nextDeficit = target - count[i + 1];
// // Make the frequency of the i-th letter equal to the `target` or 0.
// final int needToChange = count[i] <= target ? count[i] : count[i] - target;
// final int changeToTarget = (nextDeficit > needToChange)
// // 3. Change all the i-th letters to the next letter and then
// // insert the remaining deficit for the next letter.
// ? needToChange + (nextDeficit - needToChange)
// // 4. Change `nextDeficit` i-th letters to the next letter
// // and then delete the remaining i-th letters.
// : nextDeficit + (needToChange - nextDeficit);
// dp[i] = Math.min(dp[i], changeToTarget + dp[i + 2]);
// }
// }
//
// return dp[0];
// }
// }
// Accepted solution for LeetCode #3389: Minimum Operations to Make Character Frequencies Equal
// Auto-generated TypeScript example from java.
function exampleSolution(): void {
}
// Reference (java):
// // Accepted solution for LeetCode #3389: Minimum Operations to Make Character Frequencies Equal
// class Solution {
// public int makeStringGood(String s) {
// int ans = s.length();
// int[] count = new int[26];
//
// for (final char c : s.toCharArray())
// ++count[c - 'a'];
//
// final int maxCount = Arrays.stream(count).max().getAsInt();
// for (int target = 1; target <= maxCount; ++target)
// ans = Math.min(ans, getMinOperations(count, target));
//
// return ans;
// }
//
// private int getMinOperations(int[] count, int target) {
// // dp[i] represents the minimum number of operations to make the frequency of
// // (i..25)-th (0-indexed) letters equal to `target`.
// int[] dp = new int[27];
//
// for (int i = 25; i >= 0; --i) {
// // 1. Delete all the i-th letters.
// int deleteAllToZero = count[i];
// // 2. Insert/delete the i-th letters to have `target` number of letters.
// int deleteOrInsertToTarget = Math.abs(target - count[i]);
// dp[i] = Math.min(deleteAllToZero, deleteOrInsertToTarget) + dp[i + 1];
// if (i + 1 < 26 && count[i + 1] < target) {
// final int nextDeficit = target - count[i + 1];
// // Make the frequency of the i-th letter equal to the `target` or 0.
// final int needToChange = count[i] <= target ? count[i] : count[i] - target;
// final int changeToTarget = (nextDeficit > needToChange)
// // 3. Change all the i-th letters to the next letter and then
// // insert the remaining deficit for the next letter.
// ? needToChange + (nextDeficit - needToChange)
// // 4. Change `nextDeficit` i-th letters to the next letter
// // and then delete the remaining i-th letters.
// : nextDeficit + (needToChange - nextDeficit);
// dp[i] = Math.min(dp[i], changeToTarget + dp[i + 2]);
// }
// }
//
// return dp[0];
// }
// }
Use this to step through a reusable interview workflow for this problem.
Pure recursion explores every possible choice at each step. With two choices per state (take or skip), the decision tree has 2ⁿ leaves. The recursion stack uses O(n) space. Many subproblems are recomputed exponentially many times.
Each cell in the DP table is computed exactly once from previously solved subproblems. The table dimensions determine both time and space. Look for the state variables — each unique combination of state values is one cell. Often a rolling array can reduce space by one dimension.
Review these before coding to avoid predictable interview regressions.
Wrong move: Zero-count keys stay in map and break distinct/count constraints.
Usually fails on: Window/map size checks are consistently off by one.
Fix: Delete keys when count reaches zero.
Wrong move: An incomplete state merges distinct subproblems and caches incorrect answers.
Usually fails on: Correctness breaks on cases that differ only in hidden state.
Fix: Define state so each unique subproblem maps to one DP cell.