3713. Longest Balanced Substring I
Problem Description
You are given a string s consisting of lowercase English letters.
A substring of s is called balanced if every distinct character that appears in the substring occurs the same number of times.
Your task is to return the length of the longest balanced substring of s.
A few points to clarify the problem:
- A substring is a contiguous, non-empty sequence of characters within the string. For example, the substrings of
"abc"include"a","ab","bc", and"abc", but not"ac". - A substring is balanced when all of its distinct characters share an identical frequency. For example:
"aabb"is balanced because both'a'and'b'appear exactly2times."abcabc"is balanced because'a','b', and'c'each appear exactly2times."aab"is not balanced because'a'appears2times while'b'appears only1time.
- Any substring consisting of a single character (e.g.,
"a") or repeated copies of one character (e.g.,"aaa") is trivially balanced, since there is only one distinct character. This means the answer is always at least1for a non-empty string.
The goal is to examine all possible substrings of s and find the maximum length among those that satisfy the balanced condition.
A useful observation for checking balance: if a substring has v distinct characters and the maximum frequency of any character in it is mx, then the substring is balanced if and only if mx * v equals the substring's length j - i + 1. This works because the total length is the sum of all character frequencies; if every frequency were equal to the maximum mx, the total would be exactly mx * v — any character appearing fewer times would make the actual length smaller than mx * v.
How We Pick the Algorithm
Why Hash Table / Counting?
This problem maps to Hash Table / Counting through a short path in the full flowchart.
Checking the balanced condition mx * v == length requires maintaining character frequencies and maximum count incrementally across all substrings.
Open in FlowchartIntuition
The first thing to notice is that the "balanced" property depends on the exact frequency of every character in a substring, and this property does not behave nicely as a substring grows or shrinks — a balanced substring can become unbalanced by adding one character, and then become balanced again later. For example, "ab" is balanced, "aba" is not, but "abab" is balanced again. This non-monotonic behavior means classic techniques like a standard sliding window are hard to apply directly, because there is no simple condition telling us when to shrink the window.
Given that, the most natural strategy is brute-force enumeration of all substrings: fix a starting index i, then extend the ending index j one character at a time, and check whether s[i..j] is balanced at each step.
The key question becomes: how do we check the balanced condition efficiently as we extend j? Recounting all frequencies for every substring from scratch would cost an extra factor of time. Instead, we maintain the state incrementally:
- A counter
cnttracking the frequency of each character in the current windows[i..j]. Adding one character only requires a single update. v, the number of distinct characters — it increases by1exactly when a character's count goes from0to1.mx, the maximum frequency among all characters — it can only stay the same or increase as we extend the window, so updating it is a singlemaxoperation.
Now comes the elegant observation that makes the check O(1). The length of the substring is the sum of all character frequencies. If there are v distinct characters and the largest frequency is mx, then:
- The total length is at most
mx * v(each of thevcharacters appears at mostmxtimes). - The total length equals
mx * vexactly when every distinct character appears preciselymxtimes — which is precisely the definition of balanced.
So instead of comparing all frequencies against each other, we simply test:
mx * v == j - i + 1
If this equation holds, the substring s[i..j] is balanced, and we update the answer with its length. This turns an expensive "are all counts equal?" check into a single multiplication and comparison, giving an overall O(n^2) solution with O(|Σ|) extra space, where |Σ| = 26 is the alphabet size.
Solution Approach
Enumeration + Counting
Following the intuition, the implementation enumerates every possible starting position i of a substring in the range [0, ..., n-1]. For each fixed i, it extends the ending position j through the range [i, ..., n-1], maintaining the window's state incrementally as each new character s[j] joins the substring.
Data structures and variables:
cnt: a hash table (Python'sCounter) recording the frequency of each character in the current substrings[i..j]. Sincescontains only lowercase English letters, it holds at most26entries.mx: the maximum frequency among all characters in the current substring.v: the number of distinct characters in the current substring.ans: the length of the longest balanced substring found so far.
Step-by-step walkthrough:
-
Outer loop — fix the start. For each starting index
i, reset the state: create a freshcnt, and setmx = 0andv = 0. This guarantees the state describes only the current windows[i..j]. -
Inner loop — extend the end. For each ending index
jfromiton - 1, absorb the characters[j]:- Increment its frequency:
cnt[s[j]] += 1. - Update the maximum frequency:
mx = max(mx, cnt[s[j]]). Only the newly added character could raise the maximum, so this single comparison suffices. - If
cnt[s[j]] == 1, the characters[j]is appearing in this window for the first time, so increment the distinct-character count:v += 1.
- Increment its frequency:
-
Constant-time balance check. The substring
s[i..j]has lengthj - i + 1. Withvdistinct characters each appearing at mostmxtimes, the length can never exceedmx * v. Equalitymx * v == j - i + 1holds exactly when every distinct character appears precisely
mxtimes — i.e., the substring is balanced. When the check passes, update the answer:ans = max(ans, j - i + 1) -
Return the result. After all pairs
(i, j)have been examined,ansholds the length of the longest balanced substring.
Implementation:
class Solution:
def longestBalanced(self, s: str) -> int:
n = len(s)
ans = 0
for i in range(n):
cnt = Counter()
mx = v = 0
for j in range(i, n):
cnt[s[j]] += 1
mx = max(mx, cnt[s[j]])
if cnt[s[j]] == 1:
v += 1
if mx * v == j - i + 1:
ans = max(ans, j - i + 1)
return ans
Why the incremental updates are correct:
mxnever decreases whilejgrows for a fixedi, because characters are only added to the window — so comparing against the count of the latest character is enough.vonly changes when a character's count transitions from0to1, which is detected bycnt[s[j]] == 1right after the increment.- Both invariants hold precisely because the window only ever expands within the inner loop; the state is rebuilt from scratch whenever
iadvances.
Complexity analysis:
- Time complexity:
O(n^2), wherenis the length ofs. There areO(n^2)pairs(i, j), and each inner-loop iteration performs onlyO(1)work thanks to the incremental updates and themx * vcheck. - Space complexity:
O(|Σ|), where|Σ| = 26is the size of the alphabet, used by the frequency tablecnt.
Example Walkthrough
Let's trace the algorithm on the input s = "aaabb" (n = 5). The expected answer is 4, achieved by the substring "aabb", where 'a' and 'b' each appear exactly 2 times.
We initialize ans = 0 and run the outer loop over every starting index i. For each i, we reset cnt, mx = 0, and v = 0, then extend j.
Outer loop: i = 0
j | char | cnt after update | mx | v | check mx * v == j - i + 1 | ans |
|---|---|---|---|---|---|---|
| 0 | a | {a: 1} | 1 | 1 | 1 * 1 == 1 ✓ | 1 |
| 1 | a | {a: 2} | 2 | 1 | 2 * 1 == 2 ✓ | 2 |
| 2 | a | {a: 3} | 3 | 1 | 3 * 1 == 3 ✓ | 3 |
| 3 | b | {a: 3, b: 1} | 3 | 2 | 3 * 2 == 4? No (6 ≠ 4) | 3 |
| 4 | b | {a: 3, b: 2} | 3 | 2 | 3 * 2 == 5? No (6 ≠ 5) | 3 |
Notice how the run of identical characters "aaa" keeps passing the check (one distinct character is always balanced), but once 'b' joins with a smaller frequency, mx * v overshoots the actual length, correctly rejecting "aaab" and "aaabb".
Outer loop: i = 1 (state reset to empty)
j | char | cnt after update | mx | v | check mx * v == j - i + 1 | ans |
|---|---|---|---|---|---|---|
| 1 | a | {a: 1} | 1 | 1 | 1 * 1 == 1 ✓ | 3 |
| 2 | a | {a: 2} | 2 | 1 | 2 * 1 == 2 ✓ | 3 |
| 3 | b | {a: 2, b: 1} | 2 | 2 | 2 * 2 == 3? No (4 ≠ 3) | 3 |
| 4 | b | {a: 2, b: 2} | 2 | 2 | 2 * 2 == 4 ✓ | 4 |
At (i, j) = (1, 4) the substring is "aabb": there are v = 2 distinct characters, the maximum frequency is mx = 2, and mx * v = 4 exactly matches the length 4. This means every distinct character attains the maximum frequency, so the substring is balanced — ans is updated to 4.
This window also illustrates the non-monotonic nature of the property: "aab" (at j = 3) fails the check, yet extending it by one more character restores balance.
Outer loop: i = 2
j | char | cnt | mx | v | check | ans |
|---|---|---|---|---|---|---|
| 2 | a | {a: 1} | 1 | 1 | 1 == 1 ✓ | 4 |
| 3 | b | {a: 1, b: 1} | 1 | 2 | 2 == 2 ✓ (substring "ab") | 4 |
| 4 | b | {a: 1, b: 2} | 2 | 2 | 4 == 3? No | 4 |
Outer loops: i = 3 and i = 4
i = 3:"b"passes (length 1) and"bb"passes (mx * v = 2 * 1 == 2), but neither beatsans = 4.i = 4:"b"passes with length 1.
Result
After examining all (i, j) pairs, the algorithm returns ans = 4, corresponding to the longest balanced substring "aabb".
The walkthrough highlights the two pillars of the approach: the state (cnt, mx, v) is updated in O(1) per extension, and the single equation mx * v == j - i + 1 replaces a full frequency comparison, keeping the total cost at O(n^2).
Solution Implementation
1from collections import Counter
2
3
4class Solution:
5 def longestBalanced(self, s: str) -> int:
6 n = len(s)
7 max_len = 0
8
9 # Enumerate every possible starting index of the substring
10 for i in range(n):
11 char_count = Counter() # Frequency of each character in s[i..j]
12 max_freq = 0 # Highest frequency among characters in s[i..j]
13 distinct = 0 # Number of distinct characters in s[i..j]
14
15 # Extend the substring to every possible ending index
16 for j in range(i, n):
17 char_count[s[j]] += 1
18
19 # Update the maximum frequency seen so far
20 max_freq = max(max_freq, char_count[s[j]])
21
22 # A new distinct character appears when its count becomes 1
23 if char_count[s[j]] == 1:
24 distinct += 1
25
26 # The substring is balanced if every distinct character
27 # occurs exactly max_freq times, i.e.
28 # max_freq * distinct == length of the substring
29 if max_freq * distinct == j - i + 1:
30 max_len = max(max_len, j - i + 1)
31
32 return max_len
331import java.util.Arrays;
2
3class Solution {
4 public int longestBalanced(String s) {
5 int n = s.length();
6 // Frequency array for lowercase letters 'a' to 'z'
7 int[] charCount = new int[26];
8 // Length of the longest balanced substring found so far
9 int maxLength = 0;
10
11 // Enumerate every possible starting index of the substring
12 for (int i = 0; i < n; ++i) {
13 // Reset character frequencies for the new starting index
14 Arrays.fill(charCount, 0);
15 // maxFreq: highest frequency of any character in s[i..j]
16 // distinctCount: number of distinct characters in s[i..j]
17 int maxFreq = 0, distinctCount = 0;
18
19 // Extend the substring one character at a time
20 for (int j = i; j < n; ++j) {
21 int charIndex = s.charAt(j) - 'a';
22
23 // If this character appears for the first time in the window,
24 // increase the distinct character count
25 if (++charCount[charIndex] == 1) {
26 ++distinctCount;
27 }
28
29 // Update the maximum frequency within the current window
30 maxFreq = Math.max(maxFreq, charCount[charIndex]);
31
32 // The substring s[i..j] is balanced if and only if
33 // maxFreq * distinctCount equals its length, meaning
34 // every distinct character appears exactly maxFreq times
35 if (maxFreq * distinctCount == j - i + 1) {
36 maxLength = Math.max(maxLength, j - i + 1);
37 }
38 }
39 }
40 return maxLength;
41 }
42}
431class Solution {
2public:
3 int longestBalanced(string s) {
4 int n = s.size();
5 // Frequency counter for each lowercase letter ('a' to 'z')
6 vector<int> freq(26, 0);
7 int ans = 0;
8
9 // Enumerate every possible starting index of a substring
10 for (int i = 0; i < n; ++i) {
11 // Reset the frequency counter for the new starting index
12 fill(freq.begin(), freq.end(), 0);
13
14 int maxFreq = 0; // Highest frequency among characters in s[i..j]
15 int distinctCount = 0; // Number of distinct characters in s[i..j]
16
17 // Extend the substring one character at a time
18 for (int j = i; j < n; ++j) {
19 int c = s[j] - 'a';
20
21 // If this character appears for the first time, it is a new distinct character
22 if (++freq[c] == 1) {
23 ++distinctCount;
24 }
25
26 // Update the maximum frequency seen so far in this substring
27 maxFreq = max(maxFreq, freq[c]);
28
29 // A substring is balanced when every distinct character occurs
30 // exactly maxFreq times, i.e. maxFreq * distinctCount == length.
31 if (maxFreq * distinctCount == j - i + 1) {
32 ans = max(ans, j - i + 1);
33 }
34 }
35 }
36
37 return ans;
38 }
39};
401/**
2 * Finds the length of the longest balanced substring of s.
3 * A substring is balanced if every distinct character in it
4 * appears exactly the same number of times.
5 *
6 * Approach: Enumerate all substrings with a double loop.
7 * For each starting index, extend the ending index while
8 * maintaining character counts. A substring of length L with
9 * `distinct` distinct characters, where the maximum frequency
10 * of any character is `maxFreq`, is balanced if and only if
11 * maxFreq * distinct === L (this forces every character's
12 * count to equal maxFreq).
13 *
14 * Time complexity: O(n^2), Space complexity: O(26) = O(1)
15 *
16 * @param s - the input string consisting of lowercase letters
17 * @returns the length of the longest balanced substring
18 */
19function longestBalanced(s: string): number {
20 const n: number = s.length;
21 let ans: number = 0;
22
23 // Enumerate every possible starting index of the substring
24 for (let i = 0; i < n; ++i) {
25 // count[k] holds occurrences of character ('a' + k) in s[i..j]
26 const count: number[] = Array(26).fill(0);
27 // maxFreq: highest frequency among characters in the window
28 // distinct: number of distinct characters in the window
29 let maxFreq: number = 0;
30 let distinct: number = 0;
31
32 // Extend the substring one character at a time
33 for (let j = i; j < n; ++j) {
34 const c: number = s.charCodeAt(j) - 97;
35
36 // A new distinct character enters the window
37 if (++count[c] === 1) {
38 ++distinct;
39 }
40
41 // Update the maximum frequency seen so far
42 maxFreq = Math.max(maxFreq, count[c]);
43
44 // The substring is balanced when every distinct character
45 // occurs exactly maxFreq times, i.e. maxFreq * distinct
46 // equals the substring length
47 if (maxFreq * distinct === j - i + 1) {
48 ans = Math.max(ans, j - i + 1);
49 }
50 }
51 }
52
53 return ans;
54}
55Time and Space Complexity
-
Time Complexity:
O(n²), wherenis the length of the strings. The outer loop enumerates every starting indexi, and the inner loop extends the ending indexjfromiton - 1. Inside the inner loop, all operations — updating the countercnt[s[j]], maintaining the maximum frequencymx, tracking the number of distinct charactersv, and checking the balanced conditionmx * v == j - i + 1— takeO(1)time. Therefore, the total work isO(n × n) = O(n²). -
Space Complexity:
O(|Σ|), where|Σ|is the size of the character set (here|Σ| = 26for lowercase English letters). The countercntholds at most|Σ|distinct keys, and it is re-created for each starting index rather than accumulating, so the extra space never exceedsO(|Σ|). All remaining variables (mx,v,ans) use onlyO(1)space.
Pattern Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall: Forgetting to Reset the Window State When the Starting Index Advances
The Problem:
A frequent mistake is declaring char_count, max_freq, and distinct outside the outer loop, intending to "reuse" them across different starting positions:
# WRONG: state is shared across all starting indices
class Solution:
def longestBalanced(self, s: str) -> int:
n = len(s)
max_len = 0
char_count = Counter() # initialized only once — bug!
max_freq = 0
distinct = 0
for i in range(n):
for j in range(i, n):
char_count[s[j]] += 1
max_freq = max(max_freq, char_count[s[j]])
if char_count[s[j]] == 1:
distinct += 1
if max_freq * distinct == j - i + 1:
max_len = max(max_len, j - i + 1)
return max_len
Every value in the algorithm's state is defined relative to the current window s[i..j]. Once i advances, the old window no longer exists, yet the leftover counts make char_count describe a mixture of multiple windows. Consequences:
char_countaccumulates garbage. Frequencies from earlier windows inflate the counts, sochar_count[s[j]] == 1rarely triggers anddistinctbecomes meaningless.max_freqis "sticky." It only ever grows, so it carries a maximum from a window that no longer exists. The checkmax_freq * distinct == j - i + 1then compares unrelated quantities, producing both false positives and false negatives.- The bug can be subtle in testing: on small inputs like
"ab"the wrong code may still return the right answer, while longer inputs (e.g.,"aabbcc") silently fail.
A closely related variant of this mistake is trying to "fix" the reuse by subtracting s[i-1] from the counter when i advances (sliding the left edge) but forgetting that max_freq cannot be decreased in O(1). Removing a character may lower the true maximum frequency, and there is no constant-time way to recompute it from a plain counter — which is exactly why this problem doesn't admit a straightforward two-pointer sliding window.
The Solution:
Re-initialize all per-window state inside the outer loop, so each starting index begins with a clean slate:
for i in range(n):
char_count = Counter() # fresh state for the window starting at i
max_freq = 0
distinct = 0
for j in range(i, n):
char_count[s[j]] += 1
max_freq = max(max_freq, char_count[s[j]])
if char_count[s[j]] == 1:
distinct += 1
if max_freq * distinct == j - i + 1:
max_len = max(max_len, j - i + 1)
The correctness of the incremental updates rests on a single invariant: within a fixed i, the window only ever expands. Under expansion-only updates:
max_freqcan only increase, so comparing it against the newest character's count is sufficient.distinctonly changes when a count transitions from0to1.
Resetting on every new i is precisely what preserves this invariant. If you ever feel tempted to shrink the window instead, remember that the maximum frequency is not efficiently maintainable under deletions — rebuilding from scratch per starting index is the simple, correct choice and still yields the intended O(n²) time with O(26) space.
Quick sanity tests to catch this bug:
| Input | Correct Output | Typical Buggy Output |
|---|---|---|
"aabbcc" | 6 | varies (often < 6) |
"abab" | 4 | varies |
"aaab" | 2 ("ab") | varies |
Testing with strings where the best balanced substring does not start at index 0 is the most reliable way to expose stale-state bugs, since windows starting at i = 0 are unaffected by the missing reset.
Ready to land your dream job?
Unlock your dream job with a 5-minute quiz for a personalized study roadmap!
Get My RoadmapDepth first search is equivalent to which of the tree traversal order?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion If you prefer videos here's a video that explains recursion in a fun and easy way Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask him
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity describes how the time needed
Want a Structured Path to Master System Design Too? Don’t Miss This!