Facebook Pixel

1542. Find Longest Awesome Substring

HardBit ManipulationHash TableString
Leetcode Link

Problem Description

You are given a string s consisting of digit characters ('0' through '9'). An awesome substring is a non-empty substring that can be rearranged (by swapping characters any number of times) to form a palindrome.

Your task is to find the length of the longest awesome substring in s.

A palindrome has a special property: all characters appear an even number of times, except for at most one character which can appear an odd number of times (this would be the middle character in odd-length palindromes).

For example:

  • "3242133" is awesome because it can be rearranged to "3241423" which is a palindrome
  • "12321" is already a palindrome, so it's awesome
  • "1221" can form palindrome "2112" or "1221"

The key insight is that for a substring to be rearrangeable into a palindrome:

  • At most one digit can appear an odd number of times
  • All other digits must appear an even number of times

The solution uses bit manipulation to track the parity (odd/even count) of each digit. A 10-bit integer st represents the state where bit i is 1 if digit i appears an odd number of times in the current prefix, and 0 if it appears an even number of times.

For a substring s[j..i] to be awesome, the XOR of states at positions i and j-1 should either be:

  • 0 (all digits appear even times)
  • A power of 2 (exactly one digit appears odd times)

The algorithm maintains a hash map of first occurrences of each state and checks for valid awesome substrings by comparing current state with previously seen states.

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

Intuition

The key observation is that a string can be rearranged into a palindrome if and only if at most one character appears an odd number of times. Why? In a palindrome, characters are mirrored around the center. For even-length palindromes, every character must have a matching pair. For odd-length palindromes, exactly one character sits in the middle without a pair.

Since we're dealing with digits 0-9, we can track the parity (odd/even count) of each digit using bits. Think of a 10-bit number where each bit represents whether a digit (0-9) appears an odd or even number of times. If bit i is 1, digit i appears odd times; if 0, even times.

As we traverse the string, we can maintain this bit state using XOR operations. When we encounter digit d, we flip the d-th bit using st ^= (1 << d). This naturally tracks parity because XOR toggles bits - if a digit appeared even times (bit = 0) and we see it again, it becomes odd (bit = 1), and vice versa.

Now comes the clever part: for any substring s[j..i] to be awesome, we need to check if the digits in that substring have the right parity pattern (at most one odd). Instead of checking every possible substring directly, we can use prefix states.

If we know the parity state at position i and position j-1, the XOR of these two states gives us the parity of the substring s[j..i]. Why? Because XOR cancels out the common prefix s[0..j-1], leaving only the substring we care about.

For the substring to be awesome, this XOR result should either be:

  • 0 (all digits appear even times - forms an even-length palindrome)
  • A power of 2 like 1, 10, 100 in binary (exactly one digit appears odd times - forms an odd-length palindrome with that digit in the middle)

By storing the first occurrence of each state in a hash map, we can efficiently find the longest awesome substring ending at each position. We check if the current state was seen before (gives all-even substring) or if flipping any single bit of the current state was seen before (gives exactly-one-odd substring).

Solution Approach

The implementation uses state compression with prefix sum technique to efficiently find the longest awesome substring.

Data Structures Used:

  • st: An integer representing the current parity state (10 bits for digits 0-9)
  • d: A dictionary/hash map storing the first occurrence index of each state
  • ans: Variable to track the maximum length found

Algorithm Steps:

  1. Initialize the state tracking:

    • Set st = 0 (initially all digits have even count, i.e., count = 0)
    • Initialize dictionary d = {0: -1} to handle substrings starting from index 0
    • Set ans = 1 as minimum awesome substring has length 1
  2. Process each character: For each character at index i:

    • Convert character to digit: v = int(c)
    • Update state by flipping the bit for this digit: st ^= (1 << v)
  3. Check for all-even digit counts:

    • If current state st exists in dictionary d, it means we've seen this exact state before at index d[st]
    • The substring between d[st] + 1 and i has all digits appearing even times
    • Update answer: ans = max(ans, i - d[st])
    • If st is new, store it: d[st] = i
  4. Check for exactly-one-odd digit count:

    • For each possible digit v from 0 to 9:
      • Calculate the state that differs by only bit v: st ^ (1 << v)
      • If this modified state exists in dictionary, the substring between those positions has exactly digit v appearing odd times
      • Update answer: ans = max(ans, i - d[st ^ (1 << v)])

Why this works:

  • When st matches a previous state, the XOR between them is 0, meaning all digits in the substring have even counts
  • When st ^ (1 << v) matches a previous state, the XOR between current state and that previous state is (1 << v), meaning only digit v has odd count
  • By checking all 10 possible single-bit differences, we cover all cases where exactly one digit appears odd times

Time Complexity: O(n × 10) where n is the length of string, as we check 10 possible digit flips for each character.

Space Complexity: O(2^10) in worst case for the dictionary, as there are at most 2^10 = 1024 possible states.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's trace through the string s = "3242133" to find the longest awesome substring.

Initial Setup:

  • st = 0 (binary: 0000000000 - all digits have even count)
  • d = {0: -1} (state 0 seen at position -1)
  • ans = 1

Step-by-step processing:

i = 0, char = '3':

  • Digit v = 3
  • Update state: st = 0 ^ (1 << 3) = 8 (binary: 0000001000)
  • Check if st = 8 in d: No
  • Check single-bit flips:
    • For digit 3: 8 ^ 8 = 0 is in d at position -1
    • Length = 0 - (-1) = 1
    • Update ans = max(1, 1) = 1
  • Add to dictionary: d[8] = 0

i = 1, char = '2':

  • Digit v = 2
  • Update state: st = 8 ^ (1 << 2) = 12 (binary: 0000001100)
  • Check if st = 12 in d: No
  • Check single-bit flips:
    • For digit 2: 12 ^ 4 = 8 is in d at position 0
    • Length = 1 - 0 = 1
    • For digit 3: 12 ^ 8 = 4 not in d
  • Add to dictionary: d[12] = 1

i = 2, char = '4':

  • Digit v = 4
  • Update state: st = 12 ^ (1 << 4) = 28 (binary: 0000011100)
  • Check if st = 28 in d: No
  • Check single-bit flips:
    • For digit 2: 28 ^ 4 = 24 not in d
    • For digit 3: 28 ^ 8 = 20 not in d
    • For digit 4: 28 ^ 16 = 12 is in d at position 1
    • Length = 2 - 1 = 1
  • Add to dictionary: d[28] = 2

i = 3, char = '2':

  • Digit v = 2
  • Update state: st = 28 ^ (1 << 2) = 24 (binary: 0000011000)
  • Check if st = 24 in d: No
  • Check single-bit flips:
    • For digit 3: 24 ^ 8 = 16 not in d
    • For digit 4: 24 ^ 16 = 8 is in d at position 0
    • Length = 3 - 0 = 3
    • Update ans = max(1, 3) = 3
  • Add to dictionary: d[24] = 3

i = 4, char = '1':

  • Digit v = 1
  • Update state: st = 24 ^ (1 << 1) = 26 (binary: 0000011010)
  • Check if st = 26 in d: No
  • Check single-bit flips (only showing matches):
    • No matches that improve ans
  • Add to dictionary: d[26] = 4

i = 5, char = '3':

  • Digit v = 3
  • Update state: st = 26 ^ (1 << 3) = 18 (binary: 0000010010)
  • Check if st = 18 in d: No
  • Check single-bit flips:
    • For digit 1: 18 ^ 2 = 16 not in d
    • For digit 4: 18 ^ 16 = 2 not in d
  • Add to dictionary: d[18] = 5

i = 6, char = '3':

  • Digit v = 3
  • Update state: st = 18 ^ (1 << 3) = 26 (binary: 0000011010)
  • Check if st = 26 in d: Yes, at position 4!
  • Length = 6 - 4 = 2
  • Check single-bit flips:
    • For digit 1: 26 ^ 2 = 24 is in d at position 3
    • Length = 6 - 3 = 3
    • For digit 3: 26 ^ 8 = 18 is in d at position 5
    • Length = 6 - 5 = 1
    • For digit 4: 26 ^ 16 = 10 not in d

Final Result: ans = 3

The longest awesome substring is "242" (from index 1 to 3), which can be rearranged to form palindrome "424".

Solution Implementation

1class Solution:
2    def longestAwesome(self, s: str) -> int:
3        # Bitmask to track parity (odd/even count) of each digit 0-9
4        # Bit i is 1 if digit i appears odd number of times, 0 if even
5        parity_mask = 0
6      
7        # Dictionary to store first occurrence index of each parity state
8        # Key: parity mask, Value: earliest index where this mask occurred
9        first_occurrence = {0: -1}  # Empty prefix has mask 0 at index -1
10      
11        # Initialize result with minimum possible length
12        max_length = 1
13      
14        # Process each character in the string
15        for current_index, char in enumerate(s):
16            # Convert character to digit
17            digit = int(char)
18          
19            # Toggle the bit for this digit in the parity mask
20            # XOR flips the bit: 0->1 (even->odd) or 1->0 (odd->even)
21            parity_mask ^= 1 << digit
22          
23            # Case 1: Check if we've seen this exact parity mask before
24            # If yes, substring between these positions has all even counts (palindrome)
25            if parity_mask in first_occurrence:
26                max_length = max(max_length, current_index - first_occurrence[parity_mask])
27            else:
28                # Record first occurrence of this parity mask
29                first_occurrence[parity_mask] = current_index
30          
31            # Case 2: Check for palindromes with exactly one odd-count digit
32            # Try flipping each bit to see if that state was seen before
33            for digit_to_flip in range(10):
34                # Create mask with one bit different (one digit has different parity)
35                target_mask = parity_mask ^ (1 << digit_to_flip)
36              
37                # If this mask exists, substring between positions forms palindrome
38                # with exactly one odd-count digit
39                if target_mask in first_occurrence:
40                    max_length = max(max_length, current_index - first_occurrence[target_mask])
41      
42        return max_length
43
1class Solution {
2    public int longestAwesome(String s) {
3        // Array to store the first occurrence index of each bitmask state
4        // Size 1024 = 2^10 for digits 0-9 (each digit represented by 1 bit)
5        int[] firstOccurrence = new int[1024];
6      
7        // Initialize all positions to -1 (not seen yet)
8        Arrays.fill(firstOccurrence, -1);
9      
10        // Empty prefix has bitmask 0 at position 0
11        firstOccurrence[0] = 0;
12      
13        // Current bitmask representing parity of digit frequencies
14        int bitmask = 0;
15      
16        // Maximum length of awesome substring found
17        int maxLength = 1;
18      
19        // Iterate through each character position (1-indexed for prefix calculation)
20        for (int position = 1; position <= s.length(); position++) {
21            // Get current digit
22            int digit = s.charAt(position - 1) - '0';
23          
24            // Toggle the bit corresponding to current digit
25            // XOR flips the parity: even->odd, odd->even
26            bitmask ^= (1 << digit);
27          
28            // Case 1: Check if we've seen this exact bitmask before
29            // If yes, substring between has all even frequencies (forms palindrome)
30            if (firstOccurrence[bitmask] >= 0) {
31                maxLength = Math.max(maxLength, position - firstOccurrence[bitmask]);
32            } else {
33                // Record first occurrence of this bitmask
34                firstOccurrence[bitmask] = position;
35            }
36          
37            // Case 2: Check all possible single-bit differences
38            // This allows exactly one digit to have odd frequency (center of palindrome)
39            for (int checkDigit = 0; checkDigit < 10; checkDigit++) {
40                // Create bitmask with one bit different
41                int targetBitmask = bitmask ^ (1 << checkDigit);
42              
43                // If we've seen this bitmask before, we can form an awesome substring
44                if (firstOccurrence[targetBitmask] >= 0) {
45                    maxLength = Math.max(maxLength, position - firstOccurrence[targetBitmask]);
46                }
47            }
48        }
49      
50        return maxLength;
51    }
52}
53
1class Solution {
2public:
3    int longestAwesome(string s) {
4        // Array to store the first occurrence index of each bitmask state
5        // Size 1024 = 2^10 for 10 possible digits (0-9)
6        vector<int> firstOccurrence(1024, -1);
7      
8        // Empty prefix has bitmask 0 at index 0
9        firstOccurrence[0] = 0;
10      
11        // Current bitmask state representing parity of digit frequencies
12        int currentMask = 0;
13      
14        // Maximum length of awesome substring found
15        int maxLength = 1;
16      
17        // Iterate through each character position (1-indexed for easier calculation)
18        for (int i = 1; i <= s.size(); ++i) {
19            // Get the current digit value
20            int digit = s[i - 1] - '0';
21          
22            // Toggle the bit corresponding to current digit in the mask
23            currentMask ^= (1 << digit);
24          
25            // Check if we've seen this exact mask before (all digits have even frequency)
26            if (firstOccurrence[currentMask] != -1) {
27                maxLength = max(maxLength, i - firstOccurrence[currentMask]);
28            } else {
29                // Record the first occurrence of this mask state
30                firstOccurrence[currentMask] = i;
31            }
32          
33            // Check all possible masks that differ by exactly one bit
34            // (allowing one digit to have odd frequency for palindrome)
35            for (int bitPosition = 0; bitPosition < 10; ++bitPosition) {
36                int targetMask = currentMask ^ (1 << bitPosition);
37              
38                if (firstOccurrence[targetMask] != -1) {
39                    maxLength = max(maxLength, i - firstOccurrence[targetMask]);
40                }
41            }
42        }
43      
44        return maxLength;
45    }
46};
47
1/**
2 * Finds the length of the longest awesome substring.
3 * An awesome substring is one that can be rearranged to form a palindrome.
4 * A palindrome can have at most one character with odd frequency.
5 * 
6 * @param s - Input string containing only digits 0-9
7 * @returns The length of the longest awesome substring
8 */
9function longestAwesome(s: string): number {
10    // Array to store the first occurrence index of each bitmask state
11    // Size 1024 = 2^10 for all possible states of 10 digits (0-9)
12    const firstOccurrenceIndex: number[] = Array(1024).fill(-1);
13  
14    // Initialize variables
15    let bitmaskState: number = 0;  // Current bitmask representing odd/even count of digits
16    let maxLength: number = 1;      // Maximum length of awesome substring found
17  
18    // Mark that empty prefix (state 0) occurs at index 0
19    firstOccurrenceIndex[0] = 0;
20
21    // Iterate through each character in the string
22    for (let currentIndex = 1; currentIndex <= s.length; ++currentIndex) {
23        // Get the numeric value of current digit
24        const digit: number = s.charCodeAt(currentIndex - 1) - '0'.charCodeAt(0);
25      
26        // Toggle the bit corresponding to current digit in the bitmask
27        bitmaskState ^= 1 << digit;
28
29        // Check if we've seen this exact bitmask state before
30        // If yes, the substring between those indices has all even frequencies
31        if (firstOccurrenceIndex[bitmaskState] >= 0) {
32            maxLength = Math.max(maxLength, currentIndex - firstOccurrenceIndex[bitmaskState]);
33        } else {
34            // First time seeing this state, record its index
35            firstOccurrenceIndex[bitmaskState] = currentIndex;
36        }
37
38        // Check all possible states that differ by exactly one bit
39        // These represent substrings with exactly one odd frequency digit
40        for (let digitToToggle = 0; digitToToggle < 10; ++digitToToggle) {
41            const targetState: number = bitmaskState ^ (1 << digitToToggle);
42          
43            if (firstOccurrenceIndex[targetState] >= 0) {
44                maxLength = Math.max(maxLength, currentIndex - firstOccurrenceIndex[targetState]);
45            }
46        }
47    }
48
49    return maxLength;
50}
51

Time and Space Complexity

The time complexity is O(n × C), where n is the length of the string s and C is the number of types of digit characters (which is 10 for digits 0-9).

  • The outer loop iterates through each character in the string, contributing O(n).
  • For each character, we perform XOR operations and dictionary lookups in O(1) time.
  • The inner loop iterates through all 10 possible digits (0-9), contributing O(C) where C = 10.
  • Therefore, the overall time complexity is O(n × 10) = O(n × C).

The space complexity is O(2^C), where C is the number of types of digit characters.

  • The dictionary d stores states represented by bitmasks.
  • Each digit (0-9) can be either present an odd or even number of times, represented by a bit being 1 or 0.
  • With 10 different digits, there are 2^10 = 1024 possible states that can be stored in the dictionary.
  • Therefore, the space complexity is O(2^10) = O(2^C).

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

Common Pitfalls

1. Forgetting to Initialize the Empty Prefix State

Pitfall: A common mistake is forgetting to initialize first_occurrence[0] = -1. Without this initialization, substrings starting from index 0 won't be properly handled.

Example of the bug:

# WRONG: Missing initialization
first_occurrence = {}  # Should be {0: -1}

Why it matters: When the entire prefix from index 0 to i forms a valid palindrome (all even counts), we need to calculate the length as i - (-1) = i + 1. Without the initial state, we'd miss these valid substrings.

Solution: Always initialize with first_occurrence = {0: -1} to handle substrings starting from the beginning.

2. Updating Dictionary Before Checking

Pitfall: Adding the current state to the dictionary before checking if it already exists will always find the current index, resulting in zero-length substrings.

Example of the bug:

# WRONG: Update before check
parity_mask ^= 1 << digit
first_occurrence[parity_mask] = current_index  # Don't do this first!
if parity_mask in first_occurrence:  # This will always be True now
    max_length = max(max_length, current_index - first_occurrence[parity_mask])  # Always 0!

Solution: Always check for existing states first, then only update if it's a new state:

if parity_mask in first_occurrence:
    max_length = max(max_length, current_index - first_occurrence[parity_mask])
else:
    first_occurrence[parity_mask] = current_index

3. Overwriting Previous Occurrences

Pitfall: Updating first_occurrence[parity_mask] every time we see a state, instead of keeping only the first occurrence.

Example of the bug:

# WRONG: Always updating
first_occurrence[parity_mask] = current_index  # This overwrites the first occurrence

Why it matters: We want the longest substring, so we need the earliest occurrence of each state. Overwriting with later occurrences would give us shorter substrings.

Solution: Only store the first occurrence of each state using the conditional check shown in pitfall #2.

4. Not Checking Single-Character Substrings

Pitfall: Initializing max_length = 0 instead of max_length = 1.

Why it matters: Every single character is a palindrome by itself, so the minimum answer should be 1 (unless the string is empty, which typically isn't a test case).

Solution: Initialize with max_length = 1 to account for single-character palindromes.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

A person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".

What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?


Recommended Readings

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

Load More