1542. Find Longest Awesome Substring
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.
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 stateans
: Variable to track the maximum length found
Algorithm Steps:
-
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
- Set
-
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)
- Convert character to digit:
-
Check for all-even digit counts:
- If current state
st
exists in dictionaryd
, it means we've seen this exact state before at indexd[st]
- The substring between
d[st] + 1
andi
has all digits appearing even times - Update answer:
ans = max(ans, i - d[st])
- If
st
is new, store it:d[st] = i
- If current state
-
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)])
- Calculate the state that differs by only bit
- For each possible digit
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 digitv
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 EvaluatorExample 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
ind
: No - Check single-bit flips:
- For digit 3:
8 ^ 8 = 0
is ind
at position -1 - Length =
0 - (-1) = 1
- Update
ans = max(1, 1) = 1
- For digit 3:
- 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
ind
: No - Check single-bit flips:
- For digit 2:
12 ^ 4 = 8
is ind
at position 0 - Length =
1 - 0 = 1
- For digit 3:
12 ^ 8 = 4
not ind
- For digit 2:
- 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
ind
: No - Check single-bit flips:
- For digit 2:
28 ^ 4 = 24
not ind
- For digit 3:
28 ^ 8 = 20
not ind
- For digit 4:
28 ^ 16 = 12
is ind
at position 1 - Length =
2 - 1 = 1
- For digit 2:
- 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
ind
: No - Check single-bit flips:
- For digit 3:
24 ^ 8 = 16
not ind
- For digit 4:
24 ^ 16 = 8
is ind
at position 0 - Length =
3 - 0 = 3
- Update
ans = max(1, 3) = 3
- For digit 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
ind
: No - Check single-bit flips (only showing matches):
- No matches that improve
ans
- No matches that improve
- 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
ind
: No - Check single-bit flips:
- For digit 1:
18 ^ 2 = 16
not ind
- For digit 4:
18 ^ 16 = 2
not ind
- For digit 1:
- 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
ind
: Yes, at position 4! - Length =
6 - 4 = 2
- Check single-bit flips:
- For digit 1:
26 ^ 2 = 24
is ind
at position 3 - Length =
6 - 3 = 3
- For digit 3:
26 ^ 8 = 18
is ind
at position 5 - Length =
6 - 5 = 1
- For digit 4:
26 ^ 16 = 10
not ind
- For digit 1:
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)
whereC = 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.
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
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!