AlgoMaster Logo

Longest Nice Substring

easyFrequency8 min readUpdated June 23, 2026

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 'b' without a 'B' (or the reverse) disqualifies the entire substring.

Two facts shape the solutions. The string is short (at most 100 characters), so a brute-force scan over all substrings is fast enough. And if a character in the string has no counterpart anywhere, it can never be part of a nice substring. That character splits the string into two halves, and the answer must lie entirely within one of them. That property leads to a divide and conquer approach.

Key Constraints:

  • 1 <= s.length <= 100 → With n at most 100, an O(n^3) brute force runs in about 10^6 operations, within time limits. A more efficient solution is optional, not required.
  • s consists of uppercase and lowercase English letters → At most 52 distinct characters (26 lowercase, 26 uppercase). A 26-bit mask per case is enough to track which letters appear.

Approach 1: Brute Force

Intuition

Check every possible substring and test whether it is nice. For each substring, collect its characters into a set, then verify that every character in the set has its case counterpart in the set as well.

With n at most 100, there are about 5,000 substrings, and testing each one takes O(n) time. That is roughly 500,000 operations.

We want the longest nice substring, breaking ties toward the earliest one. We track the best length and its starting position as we scan, and only update when we find a strictly longer substring, so the earliest of two equal-length results is kept.

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 test all substrings. Any substring that includes index 0 ('Y') has no 'y' until index 6, and any substring that spans index 2 ('z') contains a 'z' with no 'Z', so none of those are nice. The nice substrings all sit between index 3 and index 5. The substring "aA" (indices 3-4) is nice because both 'a' and 'A' are present. Extending to "aAa" (indices 3-5) stays nice. Extending to "aAay" (indices 3-6) adds 'y' without 'Y', so it is not nice. No substring of length 4 or more is nice, so the best is "aAa" with length 3, recorded the first time it appears.

Result:

0
a
1
A
2
a
result

Code

This checks every substring independently, including ones that contain a character with no counterpart, which can never be nice. The next approach uses those characters to split the string and avoid that wasted work.

Approach 2: Divide and Conquer

Intuition

If a character appears in the string but its case counterpart does not, that character can never be part of any nice substring. Any nice substring must lie entirely on one side of it.

That gives a recursive strategy. Scan the string for a character whose counterpart is missing, split the string at that character, and recursively find the longest nice substring in each piece. The answer is the longest result across the pieces.

When every character in a piece has its counterpart present, that piece is itself nice and we return it unchanged. That is the 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

Divide and conquer creates substring copies at each recursion level. The next approach drops the recursion and the set, checking all substrings iteratively while reducing the niceness test to a single integer comparison.

Approach 3: Bitmask Optimization

Intuition

Represent character presence with two 26-bit masks: lowerMask for lowercase letters and upperMask for uppercase letters. Bit 0 stands for a/A, bit 1 for b/B, and so on.

For a substring starting at a fixed index, extend the end one character at a time and update the masks. The substring is nice when lowerMask == upperMask, since matching bits mean every lowercase letter that appears has its uppercase counterpart and the reverse.

This checks all substrings like the brute force, but the niceness test is a single comparison of two integers instead of a scan over a set. The asymptotic cost stays O(n^2), with a much smaller constant factor.

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