AlgoMaster Logo

Longest Nice Substring

Last Updated: March 29, 2026

easy

Understanding the Problem

We need to find the longest substring where every letter that shows up has both its uppercase and lowercase versions present in that same substring. The word "substring" means a contiguous sequence of characters, not a subsequence.

What makes this different from simpler substring problems is the "both cases" constraint. It is not enough for a character to appear. Its counterpart must also be present. A single lonely 'b' without a 'B' (or vice versa) disqualifies the entire substring.

A couple of things to notice right away. First, the string is short (at most 100 characters), so even brute force approaches will work. Second, if a character in the string has no counterpart anywhere, it can never be part of a nice substring. That character effectively "breaks" the string, and the answer must lie entirely on one side of it. This observation opens the door to a divide and conquer approach.

Key Constraints:

  • 1 <= s.length <= 100 → With n at most 100, even O(n^3) brute force runs in about 10^6 operations, well within time limits. There is no pressure to find a hyper-optimized solution.
  • s consists of uppercase and lowercase English letters → At most 52 distinct characters (26 lowercase + 26 uppercase). We can use fixed-size arrays or bitmasks to track character presence efficiently.

Approach 1: Brute Force

Intuition

The simplest approach is exactly what it sounds like: check every possible substring and see if it is nice. For each substring, collect all the characters into a set, then verify that for every character in the set, its case counterpart is also in the set.

With n at most 100, there are at most about 5,000 substrings, and checking each one takes at most O(n) time. That is roughly 500,000 operations, which is nothing for modern hardware.

We want the longest nice substring, and in case of ties, the earliest one. So we track the maximum length and the starting position as we go.

Algorithm

  1. Initialize variables to track the best starting index and best length found so far.
  2. For each starting index i from 0 to n-1, iterate over ending indices j from i+1 to n.
  3. For the substring s[i..j], collect all characters into a set.
  4. Check if the substring is nice: for every character c in the set, verify that both its uppercase and lowercase versions are in the set.
  5. If the substring is nice and its length is greater than the current best, update the best.
  6. Return the best substring, or an empty string if none was found.

Example Walkthrough

Input:

0
Y
1
a
2
z
3
a
4
A
5
a
6
y
s

We check all substrings. Starting at index 3, the substring "aA" (indices 3-4) is nice because both 'a' and 'A' are present. Extending to "aAa" (indices 3-5) is still nice. But extending further to "aAay" adds 'y' without 'Y', breaking niceness.

Result:

0
a
1
A
2
a
result

Code

This approach works but checks every substring independently, even ones containing characters that can never be part of a nice substring. What if we could identify those "deal-breaker" characters and use them to split the problem into smaller pieces?

Approach 2: Divide and Conquer

Intuition

Here is the key insight: if a character appears in the string but its case counterpart does not, that character can never be part of any nice substring. It is a "deal-breaker." Any nice substring must lie entirely on one side of that character.

This gives us a natural recursive strategy. Scan the string and find any character whose counterpart is missing. Split the string at that character. Then recursively find the longest nice substring in each piece. The answer is the longest result across all pieces.

When we reach a string where every character has its counterpart present, the entire string is nice, and we return it as-is. This is our base case.

Algorithm

  1. If the string length is less than 2, return an empty string (a single character can never be nice).
  2. Build a set of all characters in the string.
  3. Scan through the string looking for any character whose case counterpart is not in the set.
  4. If no such character exists, the entire string is nice. Return it.
  5. If we find a "bad" character, split the string at that character.
  6. Recursively solve each piece.
  7. Return the longest result. In case of a tie, the earlier one wins.

Example Walkthrough

1Build set {Y,a,z,A,y}. Scan for bad chars: 'z' has no 'Z'
0
Y
1
a
2
z
bad: z
3
a
4
A
5
a
6
y
1/6

Code

The divide and conquer approach is elegant but creates substring copies at each recursion level. What if we could check all substrings without recursion, using bitmasks to make the niceness check a single integer comparison?

Approach 3: Bitmask Optimization

Intuition

Instead of using sets and recursion, we can represent character presence with two bitmasks: one for lowercase letters and one for uppercase letters. Each bit position corresponds to a letter (bit 0 = a/A, bit 1 = b/B, and so on).

For any substring, we maintain a lowerMask and an upperMask as we extend the window. The substring is nice when lowerMask == upperMask, meaning every lowercase letter that appears also has its uppercase counterpart, and vice versa.

This approach goes back to checking all substrings (like the brute force), but the niceness check becomes a single comparison of two integers instead of iterating through a set. This makes it significantly faster in practice, even though the asymptotic complexity is O(n^2).

Algorithm

  1. Initialize variables to track the best starting index and best length.
  2. For each starting index i, initialize two bitmasks to 0.
  3. Extend the window by iterating j from i to n-1.
  4. For each character at position j, set the appropriate bit in lowerMask or upperMask.
  5. If lowerMask == upperMask and the current window is longer than the best, update the best.
  6. Return the best substring.

Example Walkthrough

1Start i=0: scan substrings starting at 'Y'
0
i
Y
1
a
2
z
3
a
4
A
5
a
6
y
1/7

Code