3714. Longest Balanced Substring II
Problem Description
You are given a string s that contains only three possible characters: 'a', 'b', and 'c'.
A substring of s (a contiguous, non-empty sequence of characters within the string) is called balanced if every distinct character that appears in it occurs the same number of times.
Your task is to return the length of the longest balanced substring of s.
A few examples to clarify what "balanced" means:
"aaa"is balanced — only one distinct character ('a') appears, so the condition holds trivially."aabb"is balanced — both'a'and'b'appear exactly2times."abcabc"is balanced —'a','b', and'c'each appear exactly2times."aab"is not balanced —'a'appears2times while'b'appears only1time.
Note that a balanced substring does not need to contain all three characters. It only requires that whichever characters do appear share an equal count. This naturally splits the problem into three cases to consider:
- Substrings made of exactly one distinct character (e.g.,
"bbbb"). - Substrings made of exactly two distinct characters with equal counts (e.g.,
"abab"). - Substrings made of all three distinct characters with equal counts (e.g.,
"cbacba").
The answer is the maximum length achievable across all such balanced substrings.
How We Pick the Algorithm
Why Prefix Sums?
This problem maps to Prefix Sums through a short path in the full flowchart.
Using prefix differences of character counts as a hash key detects equal-frequency substrings by finding matching prefix state positions.
Open in FlowchartIntuition
The first key observation is that a balanced substring can only look like one of three shapes, since s contains at most three distinct characters:
- It uses one distinct character — then it is automatically balanced, no matter its length.
- It uses two distinct characters — then those two characters must appear an equal number of times.
- It uses all three characters — then all three counts must be equal.
Trying every substring and counting characters would be far too slow (O(n^2) substrings or worse). So instead of checking substrings directly, we ask a smarter question: how can we detect "equal counts" efficiently?
This is where the classic prefix sum + hash table trick comes in. The idea is:
- The count of a character inside a substring
s[i+1..j]equals its prefix count atjminus its prefix count ati. - Two characters
aandbhave equal counts ins[i+1..j]exactly when the differencecount(a) - count(b)is the same at positionsiandj.
So, if we walk through the string maintaining a running difference d = count(a) - count(b), and we see the same value of d at two different positions, the substring between them is balanced. By storing the first position where each d value appears in a hash table, we can always find the longest such substring in one pass.
Each of the three cases reduces to this same trick with a slightly different "signature":
- One character: no math needed — just scan for the longest run of identical consecutive characters.
- Two characters: use the single difference
d = count(a) - count(b)as the signature. One subtlety: the substring must contain only those two characters, so we process each maximal segment consisting of justaandbindependently, resetting the hash table whenever the third character interrupts the segment. - Three characters: one difference is not enough to pin down three equal counts, but two differences are. We use the pair
(count(a) - count(b), count(b) - count(c))as the signature. If this pair repeats at two positions, then in the substring between them,aandbincreased equally, andbandcincreased equally — hence all three counts are equal. No segment-splitting is needed here, because extra characters cannot "hide": any character appearing in the substring is captured by the counts.
Each case is solved in linear time with a single pass, so the final answer is simply the maximum over all three cases — covering every possible form a balanced substring can take.
Pattern Learn more about Prefix Sum patterns.
Solution Approach
The solution computes the best answer for each of the three cases separately, then returns the maximum. Each case is handled by its own helper function.
Case 1: One distinct character — calc1(s)
This is a straightforward scan for the longest run of identical consecutive characters:
- Use a pointer
ito mark the start of a run. - Advance a second pointer
jwhiles[j] == s[i]. - The run length is
j - i; update the answerres = max(res, j - i). - Jump
itojand repeat until the end of the string.
Every run like "aaa" or "cc" is balanced by definition, so the longest run is the best answer for this case.
Case 2: Two distinct characters — calc2(s, a, b)
This handles substrings consisting of exactly the characters a and b with equal counts. The function is called three times — once per character pair: (a, b), (b, c), (a, c).
Key points of the implementation:
-
Skip irrelevant segments: the inner
while i < n and s[i] not in (a, b)loop skips over any character that is notaorb. A valid two-character balanced substring can never cross such a position, so each maximal segment of onlya/bcharacters is processed independently. -
Running difference: within a segment, maintain
d, where eachacontributes+1and eachbcontributes-1:d += 1 if s[i] == a else -1 -
Hash table of first occurrences:
posmaps each value ofdto the earliest index where it occurred. It is initialized aspos = {0: i - 1}at the start of each segment, meaning "the difference is0just before the segment begins." -
Detecting balance: if the current
dwas seen before at indexpos[d], then the substrings[pos[d]+1 .. i]has an equal number ofas andbs, so update:res = max(res, i - pos[d])Otherwise, record
pos[d] = i. Storing only the first occurrence guarantees the longest possible match later.
One nuance: this signature also matches substrings where d returns to the same value with zero occurrences of one character (e.g., a run of only as never repeats a d value, so that's fine), and pure single-character runs are already covered by calc1, so no answers are missed.
Case 3: Three distinct characters — calc3(s)
A single difference cannot encode three equal counts, so we use a pair of differences as the signature:
-
Maintain a
Countercntof all characters seen so far (a prefix count). -
At each index
i, compute the key:k = (cnt["a"] - cnt["b"], cnt["b"] - cnt["c"]) -
The hash table
posmaps each key to its first occurrence index, seeded withpos = {(0, 0): -1}to handle substrings starting at index0. -
If
krepeats at indicespos[k]andi, then over the substrings[pos[k]+1 .. i]:count(a) - count(b)did not change →aandbincreased equally,count(b) - count(c)did not change →bandcincreased equally,
so all three counts grew by the same amount, making the substring balanced. Update
res = max(res, i - pos[k]).
Unlike Case 2, no segment-splitting is needed here — the prefix counts automatically account for every character in the substring.
Combining the cases
x = calc1(s) # one character
y = max(calc2(s, "a", "b"), calc2(s, "b", "c"), calc2(s, "a", "c")) # two characters
z = calc3(s) # three characters
return max(x, y, z)
Complexity Analysis
- Time complexity:
O(n)—calc1is a single pass, each of the threecalc2calls is a single pass, andcalc3is a single pass withO(1)work per character (the keys are small fixed-size tuples). - Space complexity:
O(n)— the hash tablesposmay store up toO(n)distinct difference values or pairs in the worst case.
Example Walkthrough
Let's trace the full solution on the input:
s = "aabbcc" (indices 0..5)
The expected answer is 6, since the whole string contains 'a', 'b', and 'c' exactly 2 times each. Let's see how each case discovers its best candidate.
Case 1: calc1(s) — longest run of one character
Scanning for maximal runs of identical characters:
| Run | Indices | Length |
|---|---|---|
"aa" | 0–1 | 2 |
"bb" | 2–3 | 2 |
"cc" | 4–5 | 2 |
Best for this case: x = 2.
Case 2: calc2(s, a, b) — two characters with equal counts
Take the pair ('a', 'b') as the representative trace. The maximal segment containing only 'a'/'b' is s[0..3] = "aabb" (the 'c's at indices 4–5 break the segment). Initialize pos = {0: -1} and run the difference d (+1 for 'a', -1 for 'b'):
i | s[i] | d after update | Seen before? | Action |
|---|---|---|---|---|
| 0 | a | 1 | No | record pos[1] = 0 |
| 1 | a | 2 | No | record pos[2] = 1 |
| 2 | b | 1 | Yes, at index 0 | res = max(res, 2 - 0) = 2 |
| 3 | b | 0 | Yes, at index −1 | res = max(res, 3 - (-1)) = 4 |
The match d = 0 at index 3 corresponds to s[0..3] = "aabb" — two 'a's and two 'b's. ✓
The pair ('b', 'c') similarly finds "bbcc" (length 4) inside its segment s[2..5]. The pair ('a', 'c') finds nothing: its segments are "aa" and "cc" separately (the 'b's split them and reset pos), and d never repeats within either one.
Best for this case: y = max(4, 4, 0) = 4.
Case 3: calc3(s) — all three characters with equal counts
Track the signature k = (cnt[a] − cnt[b], cnt[b] − cnt[c]), with pos = {(0,0): -1}:
i | s[i] | cnt (a, b, c) | key k | Seen before? | Action |
|---|---|---|---|---|---|
| 0 | a | (1, 0, 0) | (1, 0) | No | record at 0 |
| 1 | a | (2, 0, 0) | (2, 0) | No | record at 1 |
| 2 | b | (2, 1, 0) | (1, 1) | No | record at 2 |
| 3 | b | (2, 2, 0) | (0, 2) | No | record at 3 |
| 4 | c | (2, 2, 1) | (0, 1) | No | record at 4 |
| 5 | c | (2, 2, 2) | (0, 0) | Yes, at −1 | res = 5 − (−1) = 6 |
At index 5 the key (0, 0) repeats its seeded occurrence at index −1. Between those positions, count(a) − count(b) and count(b) − count(c) both stayed unchanged, so all three counts grew equally — the substring s[0..5] = "aabbcc" is balanced. ✓
Best for this case: z = 6.
Combining
answer = max(x, y, z) = max(2, 4, 6) = 6
Every helper ran in a single linear pass, and the longest balanced substring is the entire string "aabbcc" with length 6.
Solution Implementation
1from collections import Counter
2
3
4class Solution:
5 def longestBalanced(self, s: str) -> int:
6 def calc_one_char(s: str) -> int:
7 """Case 1: substring made of a single distinct character.
8 Any run of identical characters is balanced, so return
9 the length of the longest such run."""
10 best = 0
11 i, n = 0, len(s)
12 while i < n:
13 # Extend j to the end of the current run of s[i]
14 j = i + 1
15 while j < n and s[j] == s[i]:
16 j += 1
17 best = max(best, j - i)
18 i = j # Jump to the start of the next run
19 return best
20
21 def calc_two_chars(s: str, a: str, b: str) -> int:
22 """Case 2: substring containing exactly the two characters a and b.
23 Within each maximal segment consisting only of a/b, track the
24 running difference (count of a minus count of b). If the same
25 difference appears twice, the substring between those positions
26 has equal numbers of a and b."""
27 best = 0
28 i, n = 0, len(s)
29 while i < n:
30 # Skip characters that are neither a nor b
31 while i < n and s[i] not in (a, b):
32 i += 1
33 # first_pos maps a difference value to its earliest index
34 first_pos = {0: i - 1}
35 diff = 0
36 # Process one maximal segment of only a/b characters
37 while i < n and s[i] in (a, b):
38 diff += 1 if s[i] == a else -1
39 if diff in first_pos:
40 # Same difference seen before -> balanced substring
41 best = max(best, i - first_pos[diff])
42 else:
43 first_pos[diff] = i
44 i += 1
45 return best
46
47 def calc_three_chars(s: str) -> int:
48 """Case 3: substring containing all three characters a, b, c.
49 Use a 2D prefix-difference key (cnt_a - cnt_b, cnt_b - cnt_c).
50 If the same key appears at two indices, the substring between
51 them has equal counts of a, b, and c."""
52 first_pos = {(0, 0): -1} # Key -> earliest index with that key
53 cnt = Counter()
54 best = 0
55 for i, ch in enumerate(s):
56 cnt[ch] += 1
57 key = (cnt["a"] - cnt["b"], cnt["b"] - cnt["c"])
58 if key in first_pos:
59 # Same key seen before -> all three counts equal in between
60 best = max(best, i - first_pos[key])
61 else:
62 first_pos[key] = i
63 return best
64
65 # Combine the three cases and take the overall maximum
66 ans_one = calc_one_char(s)
67 ans_two = max(
68 calc_two_chars(s, "a", "b"),
69 calc_two_chars(s, "b", "c"),
70 calc_two_chars(s, "a", "c"),
71 )
72 ans_three = calc_three_chars(s)
73 return max(ans_one, ans_two, ans_three)
741class Solution {
2
3 public int longestBalanced(String s) {
4 char[] chars = s.toCharArray();
5
6 // Case 1: substring consists of exactly one distinct character.
7 int oneChar = calc1(chars);
8
9 // Case 2: substring consists of exactly two distinct characters,
10 // each appearing the same number of times. Try all three pairs.
11 int twoChars = Math.max(
12 calc2(chars, 'a', 'b'),
13 Math.max(calc2(chars, 'b', 'c'), calc2(chars, 'a', 'c'))
14 );
15
16 // Case 3: substring contains all three characters,
17 // each appearing the same number of times.
18 int threeChars = calc3(chars);
19
20 return Math.max(oneChar, Math.max(twoChars, threeChars));
21 }
22
23 /**
24 * Finds the longest run of a single repeated character.
25 * A block of identical characters is trivially balanced.
26 */
27 private int calc1(char[] s) {
28 int best = 0;
29 int i = 0;
30 int n = s.length;
31 while (i < n) {
32 // Extend j to the end of the current run of identical characters.
33 int j = i + 1;
34 while (j < n && s[j] == s[i]) {
35 j++;
36 }
37 best = Math.max(best, j - i);
38 i = j; // Jump to the start of the next run.
39 }
40 return best;
41 }
42
43 /**
44 * Finds the longest substring made up only of characters {a, b}
45 * in which a and b occur equally often.
46 *
47 * Technique: within each maximal segment containing only a/b,
48 * track the running difference d = count(a) - count(b).
49 * If the same difference appears at two indices, the substring
50 * between them is balanced.
51 */
52 private int calc2(char[] s, char a, char b) {
53 int best = 0;
54 int i = 0;
55 int n = s.length;
56 while (i < n) {
57 // Skip characters that are neither a nor b.
58 while (i < n && s[i] != a && s[i] != b) {
59 i++;
60 }
61
62 // firstIndexOfDiff maps a running difference to the earliest
63 // index at which it was seen within the current segment.
64 Map<Integer, Integer> firstIndexOfDiff = new HashMap<>();
65 firstIndexOfDiff.put(0, i - 1); // Difference 0 before the segment starts.
66
67 int diff = 0;
68 // Process the maximal segment consisting only of a/b.
69 while (i < n && (s[i] == a || s[i] == b)) {
70 diff += (s[i] == a) ? 1 : -1;
71
72 Integer earliest = firstIndexOfDiff.get(diff);
73 if (earliest != null) {
74 // Equal difference at two positions => balanced in between.
75 best = Math.max(best, i - earliest);
76 } else {
77 firstIndexOfDiff.put(diff, i);
78 }
79 i++;
80 }
81 }
82 return best;
83 }
84
85 /**
86 * Finds the longest substring in which 'a', 'b' and 'c'
87 * all occur the same number of times.
88 *
89 * Technique: track the pair of running differences
90 * (count(a) - count(b), count(b) - count(c)).
91 * If the same pair reappears, the substring between the two
92 * positions has equal counts of all three characters.
93 */
94 private int calc3(char[] s) {
95 // firstIndexOfKey maps an encoded difference pair to the earliest
96 // index at which it occurred.
97 Map<Long, Integer> firstIndexOfKey = new HashMap<>();
98 firstIndexOfKey.put(f(0, 0), -1); // Both differences are 0 before the string starts.
99
100 int[] count = new int[3]; // Running counts of 'a', 'b', 'c'.
101 int best = 0;
102
103 for (int i = 0; i < s.length; i++) {
104 count[s[i] - 'a']++;
105
106 int diffAB = count[0] - count[1];
107 int diffBC = count[1] - count[2];
108 long key = f(diffAB, diffBC);
109
110 Integer earliest = firstIndexOfKey.get(key);
111 if (earliest != null) {
112 // Same difference pair seen before => balanced substring in between.
113 best = Math.max(best, i - earliest);
114 } else {
115 firstIndexOfKey.put(key, i);
116 }
117 }
118 return best;
119 }
120
121 /**
122 * Encodes a pair of (possibly negative) differences into a single long key.
123 * Offsetting by 100000 makes both values non-negative; casting to long
124 * before shifting prevents int overflow.
125 */
126 private long f(int x, int y) {
127 return ((long) (x + 100000) << 20) | (y + 100000);
128 }
129}
1301class Solution {
2public:
3 int longestBalanced(string s) {
4 // Case 1: substrings containing exactly one distinct character.
5 int best1 = calc1(s);
6
7 // Case 2: substrings containing exactly two distinct characters,
8 // checked for every unordered pair among 'a', 'b', 'c'.
9 int best2 = max({calc2(s, 'a', 'b'), calc2(s, 'b', 'c'), calc2(s, 'a', 'c')});
10
11 // Case 3: substrings containing all three characters with equal counts.
12 int best3 = calc3(s);
13
14 return max({best1, best2, best3});
15 }
16
17private:
18 // Longest run consisting of a single repeated character.
19 int calc1(const string& s) {
20 int res = 0;
21 int i = 0, n = s.size();
22 while (i < n) {
23 // Extend j to the end of the current run of identical characters.
24 int j = i + 1;
25 while (j < n && s[j] == s[i]) {
26 ++j;
27 }
28 res = max(res, j - i);
29 i = j;
30 }
31 return res;
32 }
33
34 // Longest substring made up only of characters a and b
35 // in which both characters appear the same number of times.
36 int calc2(const string& s, char a, char b) {
37 int res = 0;
38 int i = 0, n = s.size();
39 while (i < n) {
40 // Skip characters that are neither a nor b;
41 // they break any candidate substring.
42 while (i < n && s[i] != a && s[i] != b) {
43 ++i;
44 }
45
46 // Map from prefix difference (count(a) - count(b))
47 // to the earliest index where that difference occurred.
48 unordered_map<int, int> firstPos;
49 firstPos[0] = i - 1;
50
51 int diff = 0;
52 // Process the maximal segment containing only a and b.
53 while (i < n && (s[i] == a || s[i] == b)) {
54 diff += (s[i] == a) ? 1 : -1;
55 auto it = firstPos.find(diff);
56 if (it != firstPos.end()) {
57 // Same difference seen before: the substring in between
58 // has equal counts of a and b.
59 res = max(res, i - it->second);
60 } else {
61 firstPos[diff] = i;
62 }
63 ++i;
64 }
65 }
66 return res;
67 }
68
69 // Encode the pair (x, y) into a single 64-bit key.
70 // Offsets keep both values non-negative before packing.
71 static long long f(int x, int y) {
72 return ((long long) (x + 100000) << 20) | (long long) (y + 100000);
73 }
74
75 // Longest substring in which 'a', 'b', and 'c' all occur
76 // the same number of times.
77 int calc3(const string& s) {
78 // Map from the encoded difference pair to the earliest prefix index.
79 unordered_map<long long, int> firstPos;
80 firstPos[f(0, 0)] = -1;
81
82 int cnt[3] = {0, 0, 0};
83 int res = 0;
84
85 for (int i = 0; i < (int) s.size(); ++i) {
86 char c = s[i];
87 ++cnt[c - 'a'];
88
89 // Two independent differences fully describe the balance state:
90 // equal differences imply equal counts of all three characters
91 // within the enclosed substring.
92 int x = cnt[0] - cnt[1];
93 int y = cnt[1] - cnt[2];
94 long long key = f(x, y);
95
96 auto it = firstPos.find(key);
97 if (it != firstPos.end()) {
98 // Same state seen before: the substring in between is balanced.
99 res = max(res, i - it->second);
100 } else {
101 firstPos[key] = i;
102 }
103 }
104 return res;
105 }
106};
1071/**
2 * Returns the length of the longest balanced substring of s.
3 * A substring is balanced if every distinct character in it
4 * appears the same number of times.
5 *
6 * The answer is the maximum over three cases:
7 * 1. The substring contains exactly one distinct character.
8 * 2. The substring contains exactly two distinct characters.
9 * 3. The substring contains all three characters 'a', 'b', 'c'.
10 */
11function longestBalanced(s: string): number {
12 // Case 1: substrings made of a single repeated character
13 const singleCharBest = calc1(s);
14
15 // Case 2: substrings containing exactly two distinct characters,
16 // checked for every unordered pair among 'a', 'b', 'c'
17 const twoCharBest = Math.max(
18 calc2(s, 'a', 'b'),
19 calc2(s, 'b', 'c'),
20 calc2(s, 'a', 'c')
21 );
22
23 // Case 3: substrings containing all three characters
24 const threeCharBest = calc3(s);
25
26 return Math.max(singleCharBest, twoCharBest, threeCharBest);
27}
28
29/**
30 * Case 1: finds the longest run of identical consecutive characters.
31 * Such a run is trivially balanced because it contains one distinct
32 * character only.
33 */
34function calc1(s: string): number {
35 let best = 0;
36 const n = s.length;
37 let i = 0;
38
39 while (i < n) {
40 // Extend j to the end of the current run of equal characters
41 let j = i + 1;
42 while (j < n && s[j] === s[i]) {
43 j++;
44 }
45 // Length of the run is j - i
46 best = Math.max(best, j - i);
47 // Jump to the start of the next run
48 i = j;
49 }
50 return best;
51}
52
53/**
54 * Case 2: finds the longest substring that consists only of the two
55 * characters a and b, with equal counts of each, and that actually
56 * contains both characters.
57 *
58 * Technique: treat a as +1 and b as -1 within each maximal block
59 * composed solely of a/b. Two prefix positions with the same running
60 * sum delimit a balanced substring. The earliest index for each sum
61 * is stored in a map.
62 */
63function calc2(s: string, a: string, b: string): number {
64 let best = 0;
65 const n = s.length;
66 let i = 0;
67
68 while (i < n) {
69 // Skip characters that are neither a nor b
70 while (i < n && s[i] !== a && s[i] !== b) {
71 i++;
72 }
73
74 // Map: running difference -> earliest index where it occurred.
75 // Sum 0 corresponds to the position just before the block starts.
76 const firstIndexOfDiff = new Map<number, number>();
77 firstIndexOfDiff.set(0, i - 1);
78
79 // Running difference: count(a) - count(b)
80 let diff = 0;
81
82 // Scan the maximal block containing only a and b
83 while (i < n && (s[i] === a || s[i] === b)) {
84 diff += s[i] === a ? 1 : -1;
85
86 const prev = firstIndexOfDiff.get(diff);
87 if (prev !== undefined) {
88 // Equal diffs => the substring (prev, i] is balanced
89 best = Math.max(best, i - prev);
90 } else {
91 // Record the first occurrence of this diff value
92 firstIndexOfDiff.set(diff, i);
93 }
94 i++;
95 }
96 }
97 return best;
98}
99
100/**
101 * Case 3: finds the longest substring in which 'a', 'b' and 'c'
102 * all appear the same number of times.
103 *
104 * Technique: maintain prefix counts of each letter. Two prefixes give
105 * a balanced substring exactly when both pairwise differences
106 * (cntA - cntB, cntB - cntC) match. The earliest index of each
107 * difference pair is stored in a map keyed by an encoded string.
108 */
109function calc3(s: string): number {
110 // Map: encoded difference pair -> earliest prefix index
111 const firstIndexOfState = new Map<string, number>();
112 // The empty prefix (index -1) has both differences equal to 0
113 firstIndexOfState.set(key(0, 0), -1);
114
115 // Prefix counts of 'a', 'b', 'c'
116 const cnt = [0, 0, 0];
117 let best = 0;
118
119 for (let i = 0; i < s.length; i++) {
120 // Convert character to index 0 ('a'), 1 ('b'), or 2 ('c')
121 const c = s.charCodeAt(i) - 97;
122 cnt[c]++;
123
124 // Pairwise differences fully describe the balance state
125 const diffAB = cnt[0] - cnt[1];
126 const diffBC = cnt[1] - cnt[2];
127 const stateKey = key(diffAB, diffBC);
128
129 const prev = firstIndexOfState.get(stateKey);
130 if (prev !== undefined) {
131 // Same state => the substring (prev, i] is balanced
132 best = Math.max(best, i - prev);
133 } else {
134 // Record the first occurrence of this state
135 firstIndexOfState.set(stateKey, i);
136 }
137 }
138 return best;
139}
140
141/**
142 * Encodes a pair of integers into a single string key
143 * suitable for use in a Map.
144 */
145function key(x: number, y: number): string {
146 return x + '#' + y;
147}
148Time and Space Complexity
Time Complexity: O(n), where n is the length of the string s.
calc1(s): The two pointersiandjeach traverse the string at most once in total, sinceijumps tojafter every inner loop. This givesO(n).calc2(s, a, b): Although there are nestedwhileloops, the indexionly moves forward and never resets. Each character is visited exactly once per call, so each call costsO(n). It is invoked3times (for the pairs("a","b"),("b","c"),("a","c")), which is3 * O(n) = O(n). The dictionary lookups and insertions onposareO(1)on average.calc3(s): A single pass over the string withO(1)average-time hash map operations on the key(cnt["a"] - cnt["b"], cnt["b"] - cnt["c"]), givingO(n).
Summing all parts: O(n) + O(n) + O(n) = O(n).
Space Complexity: O(n), where n is the length of the string s.
calc1(s): Uses only a constant number of variables, i.e.,O(1).calc2(s, a, b): The dictionaryposstores prefix differencesd, which can take up toO(n)distinct values in the worst case (e.g., a string of alla's), soO(n).calc3(s): The dictionaryposstores tuple keys(cnt["a"] - cnt["b"], cnt["b"] - cnt["c"]). Since each indexiinserts at most one new key,posholds at mostn + 1entries, i.e.,O(n). TheCountercntholds at most3entries, which isO(1).
The dominant term is the hash map storage, so the overall space complexity is O(n).
Pattern Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Forgetting to split segments in the two-character case
The most frequent mistake is applying the "running difference + hash table" trick for the two-character case over the entire string, without skipping the third character:
# WRONG: scans the whole string for the pair (a, b)
def calc_two_chars(s, a, b):
first_pos = {0: -1}
diff = 0
best = 0
for i, ch in enumerate(s):
if ch == a:
diff += 1
elif ch == b:
diff -= 1
# characters other than a/b silently ignored <-- BUG
if diff in first_pos:
best = max(best, i - first_pos[diff])
else:
first_pos[diff] = i
return best
Why it is wrong: ignoring the third character means the matched substring may contain
it. The check only proves count(a) == count(b); it says nothing about the third character
sandwiched inside. For example, with s = "accb" and the pair (a, b):
- At index
3,diffreturns to0, so the code reports the substring"accb"(length 4) as balanced. - But
"accb"has countsa=1, b=1, c=2— not balanced. The correct answer for this string is2("cc").
A two-character balanced substring can never cross an occurrence of the third character, so
each maximal segment consisting only of a/b must be processed independently.
Fix: skip over foreign characters and reset the state per segment, exactly as in the correct code:
while i < n: while i < n and s[i] not in (a, b): i += 1 first_pos = {0: i - 1} # reset BOTH the map and the base index diff = 0 while i < n and s[i] in (a, b): ...
Note the related sub-pitfall here: when resetting, the base entry must be {0: i - 1}
(the position just before the segment), not {0: -1}. Reusing -1 after skipping
characters produces lengths that wrongly span the skipped region.
Pitfall 2: Updating the hash table on every occurrence
In both calc_two_chars and calc_three_chars, writing
first_pos[key] = i # executed unconditionally <-- BUG
overwrites the earliest index with the latest one. The longest balanced substring between two equal signatures requires the first occurrence as the left anchor. Always guard the write:
if key in first_pos:
best = max(best, i - first_pos[key])
else:
first_pos[key] = i
Pitfall 3: Assuming the two-character pass covers single-character runs
A run like "aaaa" never repeats a difference value (diff goes 1, 2, 3, 4), so
calc_two_chars reports 0 for it. If you drop the dedicated one-character scan
(calc_one_char), an input such as s = "bbbb" returns 0 instead of 4. All three
cases are genuinely necessary — none subsumes another:
calc_one_charhandles"aaaa"(missed by the other two),calc_two_charshandles"abab"(missed bycalc_three_chars, sincecnt[c]lags),calc_three_charshandles"abcabc"(missed bycalc_two_chars, which splits on the third character).
Pitfall 4: Using a single difference as the key in the three-character case
Trying to reuse the two-character trick with one scalar such as cnt[a] - cnt[b] (or the
sum of pairwise differences) cannot encode three equal counts: the key can repeat while
cnt[c] drifts arbitrarily. Three equal counts impose two independent constraints, so
the key must be a 2-tuple, e.g. (cnt["a"] - cnt["b"], cnt["b"] - cnt["c"]). Seeding it
with {(0, 0): -1} is also essential — omitting that entry misses every balanced substring
that starts at index 0.
Ready to land your dream job?
Unlock your dream job with a 5-minute quiz for a personalized study roadmap!
Get My RoadmapHow many times is a tree node visited in a depth first search?
Recommended Readings
Prefix Sum Technique Explained Prefix Sum A prefix sum transforms an array into a new array of running totals For an input array arr define prefix k as the sum of all elements before index k prefix 0 0 prefix 1 arr 0 prefix 2 arr 0 arr 1 and so on The power
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
Want a Structured Path to Master System Design Too? Don’t Miss This!