Facebook Pixel

3714. Longest Balanced Substring II

MediumHash TableStringPrefix Sum
LeetCode ↗

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 exactly 2 times.
  • "abcabc" is balanced — 'a', 'b', and 'c' each appear exactly 2 times.
  • "aab" is not balanced — 'a' appears 2 times while 'b' appears only 1 time.

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:

  1. Substrings made of exactly one distinct character (e.g., "bbbb").
  2. Substrings made of exactly two distinct characters with equal counts (e.g., "abab").
  3. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

How We Pick the Algorithm

Why Prefix Sums?

This problem maps to Prefix Sums through a short path in the full flowchart.

Tree orgraph?noPrefixsums /range sum?yesPrefix Sums

Using prefix differences of character counts as a hash key detects equal-frequency substrings by finding matching prefix state positions.

Open in Flowchart

Intuition

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:

  1. It uses one distinct character — then it is automatically balanced, no matter its length.
  2. It uses two distinct characters — then those two characters must appear an equal number of times.
  3. 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 at j minus its prefix count at i.
  • Two characters a and b have equal counts in s[i+1..j] exactly when the difference count(a) - count(b) is the same at positions i and j.

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 just a and b independently, 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, a and b increased equally, and b and c increased 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 i to mark the start of a run.
  • Advance a second pointer j while s[j] == s[i].
  • The run length is j - i; update the answer res = max(res, j - i).
  • Jump i to j and 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 not a or b. A valid two-character balanced substring can never cross such a position, so each maximal segment of only a/b characters is processed independently.

  • Running difference: within a segment, maintain d, where each a contributes +1 and each b contributes -1:

    d += 1 if s[i] == a else -1
  • Hash table of first occurrences: pos maps each value of d to the earliest index where it occurred. It is initialized as pos = {0: i - 1} at the start of each segment, meaning "the difference is 0 just before the segment begins."

  • Detecting balance: if the current d was seen before at index pos[d], then the substring s[pos[d]+1 .. i] has an equal number of as and bs, 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 Counter cnt of 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 pos maps each key to its first occurrence index, seeded with pos = {(0, 0): -1} to handle substrings starting at index 0.

  • If k repeats at indices pos[k] and i, then over the substring s[pos[k]+1 .. i]:

    • count(a) - count(b) did not change → a and b increased equally,
    • count(b) - count(c) did not change → b and c increased 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)calc1 is a single pass, each of the three calc2 calls is a single pass, and calc3 is a single pass with O(1) work per character (the keys are small fixed-size tuples).
  • Space complexity: O(n) — the hash tables pos may store up to O(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:

RunIndicesLength
"aa"0–12
"bb"2–32
"cc"4–52

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'):

is[i]d after updateSeen before?Action
0a1Norecord pos[1] = 0
1a2Norecord pos[2] = 1
2b1Yes, at index 0res = max(res, 2 - 0) = 2
3b0Yes, at index −1res = 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}:

is[i]cnt (a, b, c)key kSeen before?Action
0a(1, 0, 0)(1, 0)Norecord at 0
1a(2, 0, 0)(2, 0)Norecord at 1
2b(2, 1, 0)(1, 1)Norecord at 2
3b(2, 2, 0)(0, 2)Norecord at 3
4c(2, 2, 1)(0, 1)Norecord at 4
5c(2, 2, 2)(0, 0)Yes, at −1res = 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)
74
1class 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}
130
1class 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};
107
1/**
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}
148

Time and Space Complexity

Time Complexity: O(n), where n is the length of the string s.

  • calc1(s): The two pointers i and j each traverse the string at most once in total, since i jumps to j after every inner loop. This gives O(n).
  • calc2(s, a, b): Although there are nested while loops, the index i only moves forward and never resets. Each character is visited exactly once per call, so each call costs O(n). It is invoked 3 times (for the pairs ("a","b"), ("b","c"), ("a","c")), which is 3 * O(n) = O(n). The dictionary lookups and insertions on pos are O(1) on average.
  • calc3(s): A single pass over the string with O(1) average-time hash map operations on the key (cnt["a"] - cnt["b"], cnt["b"] - cnt["c"]), giving O(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 dictionary pos stores prefix differences d, which can take up to O(n) distinct values in the worst case (e.g., a string of all a's), so O(n).
  • calc3(s): The dictionary pos stores tuple keys (cnt["a"] - cnt["b"], cnt["b"] - cnt["c"]). Since each index i inserts at most one new key, pos holds at most n + 1 entries, i.e., O(n). The Counter cnt holds at most 3 entries, which is O(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, diff returns to 0, so the code reports the substring "accb" (length 4) as balanced.
  • But "accb" has counts a=1, b=1, c=2not balanced. The correct answer for this string is 2 ("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_char handles "aaaa" (missed by the other two),
  • calc_two_chars handles "abab" (missed by calc_three_chars, since cnt[c] lags),
  • calc_three_chars handles "abcabc" (missed by calc_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 Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

How many times is a tree node visited in a depth first search?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More