# 3144. Minimum Substring Partition of Equal Character Frequency

## Description

Given a string s, you need to partition it into one or more balanced substrings. For example, if s == "ababcc" then ("abab", "c", "c"), ("ab", "abc", "c"), and ("ababcc") are all valid partitions, but ("a", "bab", "cc"), ("aba", "bc", "c"), and ("ab", "abcc") are not. The unbalanced substrings are bolded.

Return the minimum number of substrings that you can partition s into.

Note: A balanced string is a string where each character in the string occurs the same number of times.

Example 1:

Input: s = "fabccddg"

Output: 3

Explanation:

We can partition the string s into 3 substrings in one of the following ways: ("fab, "ccdd", "g"), or ("fabc", "cd", "dg").

Example 2:

Input: s = "abababaccddb"

Output: 2

Explanation:

We can partition the string s into 2 substrings like so: ("abab", "abaccddb").

Constraints:

• 1 <= s.length <= 1000
• s consists only of English lowercase letters.

## Solutions

### Solution 1: Memoization Search + Hash Table

We design a function $\text{dfs}(i)$, which represents the minimum number of substrings starting from the string $s[i]$. So the answer is $\text{dfs}(0)$.

The calculation process of the function $\text{dfs}(i)$ is as follows:

If $i \geq n$, it means that all characters have been processed, return $0$.

Otherwise, we maintain a hash table $\text{cnt}$, which represents the number of occurrences of each character in the current substring. In addition, we also maintain a hash table $\text{freq}$, which represents the frequency of the number of occurrences of each character.

Then we enumerate $j$ from $i$ to $n-1$, which represents the end position of the current substring. For each $j$, we update $\text{cnt}$ and $\text{freq}$, then check whether the size of $\text{freq}$ is $1$. If so, we can start splitting from $j+1$, at this time the answer is $1 + \text{dfs}(j+1)$. We take the minimum value of the answer among all $j$ as the return value of the function.

To avoid repeated calculations, we use memoization search.

The time complexity is $O(n^2)$, and the space complexity is $O(n)$. Where $n$ is the length of the string $s$.

• class Solution {
private int n;
private char[] s;
private Integer[] f;

public int minimumSubstringsInPartition(String s) {
n = s.length();
f = new Integer[n];
this.s = s.toCharArray();
return dfs(0);
}

private int dfs(int i) {
if (i >= n) {
return 0;
}
if (f[i] != null) {
return f[i];
}
int[] cnt = new int[26];
Map<Integer, Integer> freq = new HashMap<>(26);
int ans = n - i;
for (int j = i; j < n; ++j) {
int k = s[j] - 'a';
if (cnt[k] > 0) {
if (freq.merge(cnt[k], -1, Integer::sum) == 0) {
freq.remove(cnt[k]);
}
}
++cnt[k];
freq.merge(cnt[k], 1, Integer::sum);
if (freq.size() == 1) {
ans = Math.min(ans, 1 + dfs(j + 1));
}
}
return f[i] = ans;
}
}

• class Solution {
public:
int minimumSubstringsInPartition(string s) {
int n = s.size();
int f[n];
memset(f, -1, sizeof(f));
function<int(int)> dfs = [&](int i) {
if (i >= n) {
return 0;
}
if (f[i] != -1) {
return f[i];
}
f[i] = n - i;
int cnt[26]{};
unordered_map<int, int> freq;
for (int j = i; j < n; ++j) {
int k = s[j] - 'a';
if (cnt[k]) {
freq[cnt[k]]--;
if (freq[cnt[k]] == 0) {
freq.erase(cnt[k]);
}
}
++cnt[k];
++freq[cnt[k]];
if (freq.size() == 1) {
f[i] = min(f[i], 1 + dfs(j + 1));
}
}
return f[i];
};
return dfs(0);
}
};

• class Solution:
def minimumSubstringsInPartition(self, s: str) -> int:
@cache
def dfs(i: int) -> int:
if i >= n:
return 0
cnt = defaultdict(int)
freq = defaultdict(int)
ans = n - i
for j in range(i, n):
if cnt[s[j]]:
freq[cnt[s[j]]] -= 1
if not freq[cnt[s[j]]]:
freq.pop(cnt[s[j]])
cnt[s[j]] += 1
freq[cnt[s[j]]] += 1
if len(freq) == 1 and (t := 1 + dfs(j + 1)) < ans:
ans = t
return ans

n = len(s)
return dfs(0)


• func minimumSubstringsInPartition(s string) int {
n := len(s)
f := make([]int, n)
for i := range f {
f[i] = -1
}
var dfs func(int) int
dfs = func(i int) int {
if i >= n {
return 0
}
if f[i] != -1 {
return f[i]
}
cnt := [26]int{}
freq := map[int]int{}
f[i] = n - i
for j := i; j < n; j++ {
k := int(s[j] - 'a')
if cnt[k] > 0 {
freq[cnt[k]]--
if freq[cnt[k]] == 0 {
delete(freq, cnt[k])
}
}
cnt[k]++
freq[cnt[k]]++
if len(freq) == 1 {
f[i] = min(f[i], 1+dfs(j+1))
}
}
return f[i]
}
return dfs(0)
}

• function minimumSubstringsInPartition(s: string): number {
const n = s.length;
const f: number[] = Array(n).fill(-1);
const dfs = (i: number): number => {
if (i >= n) {
return 0;
}
if (f[i] !== -1) {
return f[i];
}
const cnt: Map<number, number> = new Map();
const freq: Map<number, number> = new Map();
f[i] = n - i;
for (let j = i; j < n; ++j) {
const k = s.charCodeAt(j) - 97;
if (freq.has(cnt.get(k)!)) {
freq.set(cnt.get(k)!, freq.get(cnt.get(k)!)! - 1);
if (freq.get(cnt.get(k)!) === 0) {
freq.delete(cnt.get(k)!);
}
}
cnt.set(k, (cnt.get(k) || 0) + 1);
freq.set(cnt.get(k)!, (freq.get(cnt.get(k)!) || 0) + 1);
if (freq.size === 1) {
f[i] = Math.min(f[i], 1 + dfs(j + 1));
}
}
return f[i];
};
return dfs(0);
}