Facebook Pixel

2981. Find Longest Special Substring That Occurs Thrice I

MediumHash TableStringBinary SearchCountingSliding Window
Leetcode Link

Problem Description

You need to find the length of the longest "special substring" that appears at least 3 times in a given string s.

A special substring is defined as a substring that consists of only a single repeated character. For example:

  • "aaa" is special (all 'a's)
  • "bb" is special (all 'b's)
  • "abc" is NOT special (different characters)

The task is to:

  1. Find all possible special substrings in the string s
  2. Identify which of these special substrings occur at least 3 times
  3. Return the length of the longest such substring

If no special substring appears at least 3 times, return -1.

Example walkthrough:

  • If s = "aaaa", the special substrings are:

    • "a" appears 4 times
    • "aa" appears 3 times
    • "aaa" appears 2 times
    • "aaaa" appears 1 time

    The longest special substring appearing at least 3 times is "aa" with length 2.

Key points:

  • The string s consists only of lowercase English letters
  • A substring must be contiguous (consecutive characters)
  • The same special substring can overlap in different occurrences
  • You're looking for the maximum length among all qualifying special substrings
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key observation is that if a special substring of length x appears at least 3 times, then a special substring of length x-1 must also appear at least 3 times. This is because any substring of length x contains multiple substrings of length x-1.

For example, if "aaa" appears 3 times, then "aa" must appear at least 3 times (actually more, since each "aaa" contains two "aa" substrings).

This monotonic property creates a pattern like: true, true, true, ..., false, false as we increase the length. We want to find the last true position (maximum valid length). Using our standard binary search template that finds the first true, we can invert the condition: define feasible(x) as "length x does NOT work" (i.e., no special substring of length x appears at least 3 times). Then the pattern becomes false, false, ..., true, true and we find the first infeasible length, then subtract 1.

To efficiently count occurrences of special substrings of a given length, we scan through consecutive runs of the same character. When we encounter a run of length L, it contains max(0, L - x + 1) special substrings of length x. For example, "aaaaa" (length 5) contains 3 substrings of length 3: starting at positions 0, 1, and 2.

The algorithm combines these two ideas:

  1. Binary search using the template with first_true_index to find the first length that does NOT work
  2. For each candidate length, scan through the string to find all runs of identical characters
  3. Count how many special substrings of that length each run contributes
  4. Check if any character has at least 3 special substrings of that length

Learn more about Binary Search and Sliding Window patterns.

Solution Implementation

1from collections import defaultdict
2
3class Solution:
4    def maximumLength(self, s: str) -> int:
5        """
6        Find the maximum length of a special substring that occurs at least 3 times.
7        A special substring consists of a single repeated character.
8        """
9        string_length = len(s)
10
11        def is_infeasible(target_length: int) -> bool:
12            """
13            Check if target_length is infeasible (no special substring of this
14            length appears at least 3 times).
15            Returns True if infeasible, False if feasible.
16            """
17            if target_length == 0:
18                return False  # Length 0 is always feasible (edge case)
19
20            occurrence_count = defaultdict(int)
21
22            # Find consecutive character runs
23            i = 0
24            while i < string_length:
25                j = i + 1
26                while j < string_length and s[j] == s[i]:
27                    j += 1
28
29                # Count substrings of target_length from this run
30                group_length = j - i
31                possible_substrings = max(0, group_length - target_length + 1)
32                occurrence_count[s[i]] += possible_substrings
33
34                i = j
35
36            # Return True if infeasible (no character has 3+ occurrences)
37            return max(occurrence_count.values()) < 3
38
39        # Binary search using the template: find first infeasible length
40        left, right = 1, string_length
41        first_true_index = -1
42
43        while left <= right:
44            mid = (left + right) // 2
45            if is_infeasible(mid):
46                first_true_index = mid
47                right = mid - 1
48            else:
49                left = mid + 1
50
51        # Handle edge cases
52        if first_true_index == -1:
53            # All lengths are feasible (shouldn't happen with proper bounds)
54            return string_length
55        if first_true_index == 1:
56            # Even length 1 is infeasible
57            return -1
58
59        # Return the last feasible length
60        return first_true_index - 1
61
1class Solution {
2    private String inputString;
3    private int stringLength;
4
5    /**
6     * Finds the maximum length of a special substring that appears at least 3 times.
7     * A special substring consists of only a single repeated character.
8     */
9    public int maximumLength(String s) {
10        this.inputString = s;
11        this.stringLength = s.length();
12
13        // Binary search using template: find first infeasible length
14        int left = 1;
15        int right = stringLength;
16        int firstTrueIndex = -1;
17
18        while (left <= right) {
19            int mid = left + (right - left) / 2;
20
21            if (isInfeasible(mid)) {
22                firstTrueIndex = mid;
23                right = mid - 1;
24            } else {
25                left = mid + 1;
26            }
27        }
28
29        // Handle edge cases
30        if (firstTrueIndex == -1) {
31            return stringLength;  // All lengths feasible
32        }
33        if (firstTrueIndex == 1) {
34            return -1;  // Even length 1 infeasible
35        }
36
37        return firstTrueIndex - 1;
38    }
39
40    /**
41     * Checks if the given length is infeasible (no special substring of this
42     * length appears at least 3 times).
43     * Returns true if infeasible, false if feasible.
44     */
45    private boolean isInfeasible(int targetLength) {
46        int[] occurrenceCount = new int[26];
47
48        for (int i = 0; i < stringLength;) {
49            int j = i + 1;
50
51            while (j < stringLength && inputString.charAt(j) == inputString.charAt(i)) {
52                j++;
53            }
54
55            int charIndex = inputString.charAt(i) - 'a';
56            occurrenceCount[charIndex] += Math.max(0, j - i - targetLength + 1);
57
58            if (occurrenceCount[charIndex] >= 3) {
59                return false;  // Feasible - found 3+ occurrences
60            }
61
62            i = j;
63        }
64
65        return true;  // Infeasible - no character has 3+ occurrences
66    }
67}
68
1class Solution {
2public:
3    int maximumLength(string s) {
4        int stringLength = s.size();
5
6        // Lambda: returns true if length is infeasible (no 3+ occurrences)
7        auto isInfeasible = [&](int targetLength) {
8            int occurrenceCount[26]{};
9
10            for (int i = 0; i < stringLength;) {
11                int j = i + 1;
12                while (j < stringLength && s[j] == s[i]) {
13                    ++j;
14                }
15
16                int charIndex = s[i] - 'a';
17                int segmentLength = j - i;
18                occurrenceCount[charIndex] += max(0, segmentLength - targetLength + 1);
19
20                if (occurrenceCount[charIndex] >= 3) {
21                    return false;  // Feasible
22                }
23
24                i = j;
25            }
26            return true;  // Infeasible
27        };
28
29        // Binary search using template: find first infeasible length
30        int left = 1, right = stringLength;
31        int firstTrueIndex = -1;
32
33        while (left <= right) {
34            int mid = left + (right - left) / 2;
35
36            if (isInfeasible(mid)) {
37                firstTrueIndex = mid;
38                right = mid - 1;
39            } else {
40                left = mid + 1;
41            }
42        }
43
44        // Handle edge cases
45        if (firstTrueIndex == -1) {
46            return stringLength;
47        }
48        if (firstTrueIndex == 1) {
49            return -1;
50        }
51
52        return firstTrueIndex - 1;
53    }
54};
55
1/**
2 * Finds the maximum length of a substring that appears at least 3 times in the string
3 * where the substring consists of only one unique character
4 */
5function maximumLength(s: string): number {
6    const stringLength: number = s.length;
7
8    /**
9     * Returns true if targetLength is infeasible (no 3+ occurrences)
10     */
11    const isInfeasible = (targetLength: number): boolean => {
12        const characterCounts: number[] = Array(26).fill(0);
13
14        let currentIndex: number = 0;
15        while (currentIndex < stringLength) {
16            let nextIndex: number = currentIndex + 1;
17            while (nextIndex < stringLength && s[nextIndex] === s[currentIndex]) {
18                nextIndex++;
19            }
20
21            const characterIndex: number = s[currentIndex].charCodeAt(0) - 'a'.charCodeAt(0);
22            const segmentLength: number = nextIndex - currentIndex;
23            const possibleSubstrings: number = Math.max(0, segmentLength - targetLength + 1);
24            characterCounts[characterIndex] += possibleSubstrings;
25
26            if (characterCounts[characterIndex] >= 3) {
27                return false;  // Feasible
28            }
29
30            currentIndex = nextIndex;
31        }
32
33        return true;  // Infeasible
34    };
35
36    // Binary search using template: find first infeasible length
37    let left: number = 1;
38    let right: number = stringLength;
39    let firstTrueIndex: number = -1;
40
41    while (left <= right) {
42        const mid: number = Math.floor((left + right) / 2);
43
44        if (isInfeasible(mid)) {
45            firstTrueIndex = mid;
46            right = mid - 1;
47        } else {
48            left = mid + 1;
49        }
50    }
51
52    // Handle edge cases
53    if (firstTrueIndex === -1) {
54        return stringLength;
55    }
56    if (firstTrueIndex === 1) {
57        return -1;
58    }
59
60    return firstTrueIndex - 1;
61}
62

Solution Approach

The solution implements a binary search combined with a sliding window counting technique.

Binary Search Template Setup

We use the standard binary search template to find the first length that does NOT work:

left, right = 1, n
first_true_index = -1

while left <= right:
    mid = (left + right) // 2
    if not feasible(mid):  # length mid does NOT have 3+ occurrences
        first_true_index = mid
        right = mid - 1
    else:
        left = mid + 1

# first_true_index is the first infeasible length
# Answer is first_true_index - 1 (last feasible length)

The feasible(x) function returns True if length x is infeasible (no special substring of length x appears at least 3 times). This inverts the natural condition so we can use the standard "find first true" template.

Feasible Function Implementation

The feasible check determines if there exists a special substring of length x appearing at least 3 times:

  1. Initialize a counter: Use defaultdict(int) to count occurrences for each character.

  2. Traverse the string with two pointers to find consecutive runs:

    i = 0
    while i < n:
        j = i + 1
        while j < n and s[j] == s[i]:
            j += 1
        # Process run from i to j-1
        i = j
  3. Count special substrings in each run:

    • For a run of length L = j - i
    • Number of special substrings of length x = max(0, L - x + 1)
    • Add this count to cnt[s[i]]
  4. Return the inverse for template compatibility:

    return max(cnt.values()) < 3  # True if infeasible

Final Result

After binary search completes:

  • If first_true_index == 1, even length 1 doesn't work, return -1
  • If first_true_index == -1, all lengths work (shouldn't happen with proper bounds)
  • Otherwise, return first_true_index - 1 as the maximum valid length

The time complexity is O(n log n) where each binary search iteration takes O(n) time to scan the string.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the solution with s = "aaabaaabcaaab".

Step 1: Binary Search Setup

  • String length n = 13
  • Binary search range: left = 1, right = 13
  • first_true_index = -1 (tracks first infeasible length)

Step 2: Binary Search Iterations

First iteration: mid = (1 + 13) / 2 = 7

  • Check if length 7 is infeasible (no special substring appears ≥ 3 times)
  • Scanning the string for runs:
    • "aaa" (length 3): contributes max(0, 3-7+1) = 0 substrings
    • All runs have length ≤ 3, so no length-7 special substrings exist
  • No character has 3+ occurrences → infeasible = True
  • Update: first_true_index = 7, right = 6

Second iteration: mid = (1 + 6) / 2 = 3

  • Check if length 3 is infeasible
  • Scanning for runs:
    • "aaa" at position 0: contributes 3-3+1 = 1 substring "aaa"
    • "aaa" at position 4: contributes 1 substring "aaa"
    • "aaa" at position 9: contributes 1 substring "aaa"
  • Count: 'a' → 3 occurrences of length-3 special substrings
  • Length 3 works! → infeasible = False
  • Update: left = 4

Third iteration: mid = (4 + 6) / 2 = 5

  • Check if length 5 is infeasible
  • All runs have length ≤ 3, so no length-5 special substrings exist
  • infeasible = True
  • Update: first_true_index = 5, right = 4

Fourth iteration: mid = (4 + 4) / 2 = 4

  • Check if length 4 is infeasible
  • All runs have length ≤ 3, so no length-4 special substrings exist
  • infeasible = True
  • Update: first_true_index = 4, right = 3

Step 3: Binary Search Termination

  • left = 4 > right = 3 → loop ends
  • first_true_index = 4 (first infeasible length)
  • Return first_true_index - 1 = 3

The answer is 3 because "aaa" appears exactly 3 times in the string, making it the longest special substring that meets the requirement.

Time and Space Complexity

Time Complexity: O(n × log n)

The algorithm uses binary search on the answer space from 0 to n, which takes O(log n) iterations. In each iteration, the check function is called.

The check function iterates through the string once with two pointers (i and j), taking O(n) time to identify consecutive segments of identical characters. For each segment, it performs constant-time operations to update the count in the dictionary. The max(cnt.values()) operation at the end takes O(|Σ|) time where |Σ| is the size of the character set (26 for lowercase English letters).

Since |Σ| = 26 is a constant and n dominates, the time complexity per check call is O(n). Therefore, the overall time complexity is O(n × log n).

Space Complexity: O(|Σ|) or O(1)

The space is primarily used by the cnt dictionary in the check function, which stores at most |Σ| entries (one for each distinct character in the string). Since |Σ| = 26 for lowercase English letters, this is effectively O(1) constant space.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Using Wrong Binary Search Template

Pitfall: Not using the standard binary search template with first_true_index tracking.

Common mistakes:

# WRONG: Using while left < right with different update rules
while left < right:
    mid = (left + right + 1) >> 1
    if check(mid):
        left = mid
    else:
        right = mid - 1

Correct approach using template:

# CORRECT: Standard template with first_true_index
left, right = 1, n
first_true_index = -1

while left <= right:
    mid = (left + right) // 2
    if feasible(mid):  # Define feasible as "condition becomes TRUE"
        first_true_index = mid
        right = mid - 1
    else:
        left = mid + 1

2. Incorrect Counting of Overlapping Substrings

Pitfall: Incorrectly counting how many special substrings of length x can be formed from a consecutive run.

Incorrect approach:

# WRONG: Dividing the group length by target length
possible_substrings = group_length // target_length

Why it's wrong: For a run like "aaaa" (length 4) and target length 2:

  • Wrong calculation: 4 // 2 = 2
  • Correct count: 3 overlapping substrings at positions [0:2], [1:3], [2:4]

Correct approach:

# CORRECT: Using sliding window formula
possible_substrings = max(0, group_length - target_length + 1)

3. Forgetting to Invert the Condition for "Find Maximum"

Pitfall: The template finds the first true position, but we need the last feasible position (maximum length). Must invert the condition.

Wrong:

# WRONG: feasible returns True when length works
def feasible(x):
    return max(counts.values()) >= 3  # True = works

Correct:

# CORRECT: feasible returns True when length does NOT work (infeasible)
def is_infeasible(x):
    return max(counts.values()) < 3  # True = doesn't work

# Then: first_true_index - 1 = last feasible length

4. Forgetting Edge Cases

Pitfall: Not handling when first_true_index == 1 (even length 1 is infeasible).

Wrong:

return first_true_index - 1  # Returns 0 when should return -1

Correct:

if first_true_index == 1:
    return -1  # No valid length exists
return first_true_index - 1
Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How would you design a stack which has a function min that returns the minimum element in the stack, in addition to push and pop? All push, pop, min should have running time O(1).


Recommended Readings

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

Load More