3445. Maximum Difference Between Even and Odd Frequency II
Problem Description
You are given a string s
consisting of characters from '0' to '4', and an integer k
. Your task is to find a substring of s
that maximizes a specific frequency difference.
The substring must satisfy these conditions:
- It has a length of at least
k
characters - It contains a character
a
that appears an odd number of times - It contains a character
b
that appears an even number of times (but not zero times)
Your goal is to find the maximum value of freq[a] - freq[b]
, where freq[a]
is the number of times character a
appears in the substring, and freq[b]
is the number of times character b
appears in the substring.
Note that:
- Characters
a
andb
must be different - The substring can contain other characters besides
a
andb
- Character
b
must appear at least once (non-zero even frequency means 2, 4, 6, etc.)
For example, if you have a substring where character '1' appears 5 times (odd) and character '2' appears 2 times (even), the frequency difference would be 5 - 2 = 3.
The function should return the maximum such difference across all valid substrings of s
.
Intuition
Since the string only contains 5 distinct characters ('0' to '4'), we can enumerate all possible pairs (a, b)
where a
is the character we want to have odd frequency and b
is the character we want to have even frequency. This gives us at most 5 × 4 = 20
combinations to check.
For each character pair (a, b)
, we need to find the substring that maximizes freq[a] - freq[b]
while satisfying the parity constraints. The key insight is that we can use a sliding window approach combined with prefix state tracking.
The challenge is handling the parity constraints efficiently. We observe that:
- To maximize
freq[a] - freq[b]
, we wantfreq[a]
as large as possible andfreq[b]
as small as possible - But we need
freq[a]
to be odd andfreq[b]
to be non-zero even
Here's where the clever part comes in: instead of checking all possible substrings, we can track the parity states of cumulative counts. For any substring [i, j]
, the frequency of a character in that substring equals its cumulative count at position j
minus its cumulative count at position i-1
.
The parity of this difference depends on the parities of both cumulative counts. If we want the frequency in the substring to be odd, we need the cumulative counts at the two endpoints to have different parities. If we want it to be even, we need them to have the same parity.
By maintaining a 2D array t[2][2]
that stores the minimum value of (cumulative_a - cumulative_b)
for each parity combination of cumulative counts seen so far, we can efficiently find the best starting position for our substring. When we're at position r
with current cumulative counts, we look for a previous position with the right parity combination that minimizes the prefix contribution, thus maximizing the substring's frequency difference.
The sliding window ensures we maintain the minimum length constraint k
, while the parity tracking ensures we satisfy the odd/even frequency requirements.
Learn more about Prefix Sum and Sliding Window patterns.
Solution Approach
The implementation uses a sliding window with prefix state compression to efficiently find the optimal substring for each character pair.
Main Loop Structure:
We enumerate all possible character pairs (a, b)
where a ≠ b
. For each pair, we process the string using a sliding window approach.
Key Variables:
curA
,curB
: Count of charactersa
andb
in the current window[l+1, r]
preA
,preB
: Cumulative count of charactersa
andb
before positionl
(i.e., in[0, l]
)l
: Position before the left boundary of the window (initialized to -1)r
: Right boundary of the windowt[2][2]
: 2D array storing minimum values ofpreA - preB
for each parity combination
Sliding Window Logic:
- We iterate through the string with
r
as the right pointer - For each position
r
, we updatecurA
andcurB
based on whether the current character equalsa
orb
- We shrink the window from the left when two conditions are met:
- Window size is at least
k
:r - l >= k
- Character
b
appears at least twice in the window:curB - preB >= 2
(ensuring non-zero even frequency)
- Window size is at least
Window Shrinking: When shrinking, we:
- Record the state at the left boundary:
t[preA & 1][preB & 1] = min(t[preA & 1][preB & 1], preA - preB)
- Move
l
forward and updatepreA
andpreB
accordingly
Answer Calculation:
For each position r
, we calculate the potential answer using:
ans = max(ans, curA - curB - t[(curA & 1) ^ 1][curB & 1])
This formula works because:
curA - curB
is the total frequency difference from position 0 tor
- We subtract
t[(curA & 1) ^ 1][curB & 1]
to get the frequency difference in the substring (curA & 1) ^ 1
ensures we find a prefix with opposite parity fora
(making the substring count odd)curB & 1
ensures we find a prefix with the same parity forb
(making the substring count even)
Why This Works:
The substring [l+1, r]
has:
- Frequency of
a
=curA - preA
- Frequency of
b
=curB - preB
- Frequency difference =
(curA - preA) - (curB - preB) = (curA - curB) - (preA - preB)
By storing the minimum preA - preB
for each parity state, we maximize the frequency difference while ensuring the parity constraints are satisfied.
The algorithm processes each character pair in O(n)
time, giving an overall time complexity of O(5 × 4 × n) = O(n)
.
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 walk through a concrete example with s = "02211"
and k = 2
.
We'll focus on one character pair: a = '1'
(want odd frequency) and b = '2'
(want even frequency).
Initial Setup:
l = -1
(position before the window starts)preA = 0, preB = 0
(no characters before position -1)curA = 0, curB = 0
(cumulative counts)t[2][2] = [[INF, INF], [INF, INF]]
(minimum values for each parity state)
Step-by-step processing:
Position r = 0 (char = '0'):
- Neither 'a' nor 'b', so
curA = 0, curB = 0
- Window size:
0 - (-1) = 1 < k
, don't shrink - Skip answer calculation (window too small)
Position r = 1 (char = '2'):
- This is 'b', so
curB = 1
- Window size:
1 - (-1) = 2 >= k
, can potentially shrink - Check if
curB - preB >= 2
:1 - 0 = 1 < 2
, don't shrink yet - Skip answer calculation (need even frequency for 'b', but have odd)
Position r = 2 (char = '2'):
- This is 'b', so
curB = 2
- Window size:
2 - (-1) = 3 >= k
- Check if
curB - preB >= 2
:2 - 0 = 2 >= 2
, can shrink! - Shrink window:
- Record state:
t[preA & 1][preB & 1] = t[0][0] = min(INF, 0 - 0) = 0
- Move
l
from -1 to 0 - Update prefix counts:
preA = 0
,preB = 0
(char at position 0 is '0')
- Record state:
- Calculate answer:
- Need:
curA
odd (currently 0, even) andcurB
even (currently 2, even ✓) - Look for prefix with opposite parity for A:
t[(0 & 1) ^ 1][2 & 1] = t[1][0]
t[1][0] = INF
, so no valid answer yet
- Need:
Position r = 3 (char = '1'):
- This is 'a', so
curA = 1
- Window size:
3 - 0 = 3 >= k
- Check if
curB - preB >= 2
:2 - 0 = 2 >= 2
, can shrink! - Shrink window:
- Record state:
t[0][0] = min(0, 0 - 0) = 0
- Move
l
from 0 to 1 - Update prefix counts:
preA = 0
,preB = 1
(char at position 1 is '2')
- Record state:
- Calculate answer:
- Need:
curA
odd (currently 1, odd ✓) andcurB
even (currently 2, even ✓) - Look for prefix with opposite parity for A and same for B:
t[(1 & 1) ^ 1][2 & 1] = t[0][0] = 0
- Answer =
(curA - curB) - t[0][0] = (1 - 2) - 0 = -1
- Need:
Position r = 4 (char = '1'):
- This is 'a', so
curA = 2
- Window size:
4 - 1 = 3 >= k
- Check if
curB - preB >= 2
:2 - 1 = 1 < 2
, don't shrink - Calculate answer:
- Need:
curA
odd (currently 2, even) andcurB
even (currently 2, even ✓) - Look for prefix with opposite parity for A:
t[(2 & 1) ^ 1][2 & 1] = t[1][0] = INF
- No valid answer from this position
- Need:
Result for this pair (a='1', b='2'): Maximum difference = -1
The algorithm would continue checking all other character pairs. For instance, with a = '2'
and b = '1'
, we might find a substring "2211" where '2' appears 2 times (even, but we want odd) and '1' appears 2 times (even ✓), but this wouldn't be valid since '2' needs odd frequency.
The key insight is how the parity tracking ensures we only consider valid substrings: when we calculate the answer, we specifically look for a prefix state that, when "subtracted" from the current state, gives us the desired parities (odd for 'a', even for 'b').
Solution Implementation
1class Solution:
2 def maxDifference(self, S: str, k: int) -> int:
3 # Convert string of digits to list of integers
4 digits = list(map(int, S))
5 max_diff = float('-inf')
6
7 # Try all pairs of different digits (a, b) from 0-4
8 for digit_a in range(5):
9 for digit_b in range(5):
10 # Skip if both digits are the same
11 if digit_a == digit_b:
12 continue
13
14 # Current counts of digit_a and digit_b in the window
15 current_count_a = 0
16 current_count_b = 0
17
18 # Previous counts (before the window)
19 prev_count_a = 0
20 prev_count_b = 0
21
22 # Store minimum (count_a - count_b) for each parity combination
23 # min_diff[parity_a][parity_b] stores the minimum difference
24 # for given parities of count_a and count_b
25 min_diff = [[float('inf'), float('inf')],
26 [float('inf'), float('inf')]]
27
28 # Left pointer of the sliding window (exclusive)
29 left = -1
30
31 # Iterate through the string with right pointer
32 for right, digit in enumerate(digits):
33 # Update current counts
34 current_count_a += (digit == digit_a)
35 current_count_b += (digit == digit_b)
36
37 # Shrink window from left while maintaining constraints
38 # Window size must be at least k and we need at least 2 occurrences of digit_b
39 while right - left >= k and current_count_b - prev_count_b >= 2:
40 # Update minimum difference for current parity combination
41 min_diff[prev_count_a & 1][prev_count_b & 1] = min(
42 min_diff[prev_count_a & 1][prev_count_b & 1],
43 prev_count_a - prev_count_b
44 )
45
46 # Move left pointer and update previous counts
47 left += 1
48 prev_count_a += (digits[left] == digit_a)
49 prev_count_b += (digits[left] == digit_b)
50
51 # Calculate maximum difference using opposite parity for count_a
52 # This ensures we're comparing segments with different parity
53 max_diff = max(
54 max_diff,
55 current_count_a - current_count_b - min_diff[(current_count_a & 1) ^ 1][current_count_b & 1]
56 )
57
58 return max_diff
59
1class Solution {
2 public int maxDifference(String S, int k) {
3 char[] charArray = S.toCharArray();
4 int stringLength = charArray.length;
5 final int NEGATIVE_INFINITY = Integer.MAX_VALUE / 2;
6 int maxResult = -NEGATIVE_INFINITY;
7
8 // Try all pairs of different digits (0-4)
9 for (int digitA = 0; digitA < 5; ++digitA) {
10 for (int digitB = 0; digitB < 5; ++digitB) {
11 // Skip if both digits are the same
12 if (digitA == digitB) {
13 continue;
14 }
15
16 // Current counts of digitA and digitB in the window
17 int currentCountA = 0;
18 int currentCountB = 0;
19
20 // Prefix counts of digitA and digitB up to left pointer
21 int prefixCountA = 0;
22 int prefixCountB = 0;
23
24 // Minimum values table indexed by parity of counts
25 // minTable[parityA][parityB] stores minimum (countA - countB) value
26 // for subarrays with matching parity
27 int[][] minTable = {{NEGATIVE_INFINITY, NEGATIVE_INFINITY},
28 {NEGATIVE_INFINITY, NEGATIVE_INFINITY}};
29
30 // Sliding window with left and right pointers
31 for (int left = -1, right = 0; right < stringLength; ++right) {
32 // Update current counts for the new character at right pointer
33 currentCountA += charArray[right] == '0' + digitA ? 1 : 0;
34 currentCountB += charArray[right] == '0' + digitB ? 1 : 0;
35
36 // Shrink window from left while maintaining constraints:
37 // 1. Window size is at least k (right - left >= k)
38 // 2. The excluded part has at least 2 occurrences of digitB
39 while (right - left >= k && currentCountB - prefixCountB >= 2) {
40 // Update minimum table with the prefix values
41 int parityA = prefixCountA & 1;
42 int parityB = prefixCountB & 1;
43 minTable[parityA][parityB] = Math.min(minTable[parityA][parityB],
44 prefixCountA - prefixCountB);
45
46 // Move left pointer and update prefix counts
47 ++left;
48 prefixCountA += charArray[left] == '0' + digitA ? 1 : 0;
49 prefixCountB += charArray[left] == '0' + digitB ? 1 : 0;
50 }
51
52 // Calculate maximum difference using current window and stored minimums
53 // We need opposite parity for A (XOR with 1) and same parity for B
54 int oppositeParityA = (currentCountA & 1) ^ 1;
55 int currentParityB = currentCountB & 1;
56 maxResult = Math.max(maxResult,
57 currentCountA - currentCountB - minTable[oppositeParityA][currentParityB]);
58 }
59 }
60 }
61
62 return maxResult;
63 }
64}
65
1class Solution {
2public:
3 int maxDifference(string s, int k) {
4 const int n = s.size();
5 const int INF = INT_MAX / 2;
6 int maxDiff = -INF;
7
8 // Try all pairs of different digits (a, b) from '0' to '4'
9 for (int digitA = 0; digitA < 5; ++digitA) {
10 for (int digitB = 0; digitB < 5; ++digitB) {
11 // Skip if both digits are the same
12 if (digitA == digitB) {
13 continue;
14 }
15
16 // Current counts of digitA and digitB in the window
17 int currentCountA = 0, currentCountB = 0;
18 // Previous counts (left boundary counts)
19 int prevCountA = 0, prevCountB = 0;
20
21 // Minimum values table indexed by parity of counts
22 // minTable[parityA][parityB] stores minimum (countA - countB)
23 // for positions with given parities
24 int minTable[2][2] = {{INF, INF}, {INF, INF}};
25
26 // Left pointer of the sliding window (exclusive)
27 int left = -1;
28
29 // Iterate through the string with right pointer
30 for (int right = 0; right < n; ++right) {
31 // Update current counts
32 currentCountA += (s[right] == '0' + digitA);
33 currentCountB += (s[right] == '0' + digitB);
34
35 // Shrink window while conditions are met:
36 // 1. Window size is at least k
37 // 2. Count of digitB in window is at least 2
38 while (right - left >= k && currentCountB - prevCountB >= 2) {
39 // Store minimum difference for current parity combination
40 int parityA = prevCountA & 1;
41 int parityB = prevCountB & 1;
42 minTable[parityA][parityB] = min(minTable[parityA][parityB],
43 prevCountA - prevCountB);
44
45 // Move left pointer and update previous counts
46 ++left;
47 prevCountA += (s[left] == '0' + digitA);
48 prevCountB += (s[left] == '0' + digitB);
49 }
50
51 // Update maximum difference using opposite parity for A
52 // This ensures the substring length is even
53 int oppositeParityA = (currentCountA & 1) ^ 1;
54 int currentParityB = currentCountB & 1;
55 maxDiff = max(maxDiff,
56 currentCountA - currentCountB - minTable[oppositeParityA][currentParityB]);
57 }
58 }
59 }
60
61 return maxDiff;
62 }
63};
64
1/**
2 * Finds the maximum difference between counts of two different digits
3 * in substrings of length at least k
4 * @param S - Input string containing digits 0-4
5 * @param k - Minimum substring length constraint
6 * @returns Maximum difference between counts of two digits
7 */
8function maxDifference(S: string, k: number): number {
9 // Convert string to array of numbers
10 const digits: number[] = S.split('').map(Number);
11 let maxDiff: number = -Infinity;
12
13 // Try all pairs of different digits (a, b) where a and b are from 0 to 4
14 for (let digitA = 0; digitA < 5; digitA++) {
15 for (let digitB = 0; digitB < 5; digitB++) {
16 // Skip if both digits are the same
17 if (digitA === digitB) {
18 continue;
19 }
20
21 // Current counts of digitA and digitB in the current window
22 let currentCountA: number = 0;
23 let currentCountB: number = 0;
24 // Previous counts of digitA and digitB (before left pointer)
25 let previousCountA: number = 0;
26 let previousCountB: number = 0;
27
28 // Store minimum values of (countA - countB) for different parities
29 // minDiffTable[parityA][parityB] stores min(countA - countB)
30 // where countA has parity parityA and countB has parity parityB
31 const minDiffTable: number[][] = [
32 [Infinity, Infinity],
33 [Infinity, Infinity],
34 ];
35
36 let leftPointer: number = -1;
37
38 // Sliding window approach with right pointer
39 for (let rightPointer = 0; rightPointer < digits.length; rightPointer++) {
40 const currentDigit: number = digits[rightPointer];
41
42 // Update current counts
43 currentCountA += currentDigit === digitA ? 1 : 0;
44 currentCountB += currentDigit === digitB ? 1 : 0;
45
46 // Shrink window from left while maintaining constraints
47 // Ensure window size >= k and at least 2 occurrences of digitB in window
48 while (rightPointer - leftPointer >= k && currentCountB - previousCountB >= 2) {
49 // Update minimum difference table based on previous counts' parities
50 const parityPrevA: number = previousCountA & 1;
51 const parityPrevB: number = previousCountB & 1;
52 minDiffTable[parityPrevA][parityPrevB] = Math.min(
53 minDiffTable[parityPrevA][parityPrevB],
54 previousCountA - previousCountB
55 );
56
57 // Move left pointer and update previous counts
58 leftPointer++;
59 previousCountA += digits[leftPointer] === digitA ? 1 : 0;
60 previousCountB += digits[leftPointer] === digitB ? 1 : 0;
61 }
62
63 // Calculate maximum difference using current counts and stored minimum
64 // Use opposite parity for countA to ensure valid substring
65 const oppositeParityA: number = (currentCountA & 1) ^ 1;
66 const parityCurrentB: number = currentCountB & 1;
67 maxDiff = Math.max(
68 maxDiff,
69 currentCountA - currentCountB - minDiffTable[oppositeParityA][parityCurrentB]
70 );
71 }
72 }
73 }
74
75 return maxDiff;
76}
77
Time and Space Complexity
Time Complexity: O(n × |Σ|²)
, where n
is the length of string S
and |Σ|
is the alphabet size (5 in this problem).
The analysis breaks down as follows:
- The outer two nested loops iterate through all pairs of digits
(a, b)
wherea ≠ b
, giving us5 × 5 - 5 = 20
iterations, which isO(|Σ|²)
- For each pair
(a, b)
, the inner loop iterates through the string once with indexr
from0
ton-1
- The while loop inside moves the left pointer
l
forward, but across all iterations ofr
, the pointerl
moves at mostn
times total (amortizedO(1)
per iteration) - All operations inside the loops (comparisons, arithmetic, array access) are
O(1)
- Therefore, the total time complexity is
O(|Σ|² × n)
Space Complexity: O(n + 1) = O(n)
The analysis breaks down as follows:
- The list
s
stores the converted integer values of the input string, requiringO(n)
space - The 2D array
t
is of fixed size2 × 2
, which isO(1)
- All other variables (
ans
,curA
,curB
,preA
,preB
,l
,r
,x
,a
,b
) use constant spaceO(1)
- Therefore, the total space complexity is
O(n)
Note: The reference answer states O(1)
space complexity, which would be accurate if the input string could be processed character by character without creating the list s
. However, the given code creates s = list(map(int, S))
, which uses O(n)
additional space.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Parity Matching for Valid Substrings
The Pitfall:
The most common mistake is misunderstanding how the parity matching works when calculating the answer. Developers often incorrectly assume they need to check if the substring itself has odd count for a
and even count for b
, but the code actually checks the difference between cumulative counts and stored prefix counts.
Why it happens:
The formula max_diff[(current_count_a & 1) ^ 1][current_count_b & 1]
is counterintuitive. It looks for a prefix with:
- Opposite parity for
a
(using XOR with 1) - Same parity for
b
This ensures that the substring [left+1, right]
has:
- Odd count for
a
:(current_count_a - prev_count_a)
is odd when parities differ - Even count for
b
:(current_count_b - prev_count_b)
is even when parities are the same
Solution: Add explicit validation to verify the substring meets requirements:
# After calculating the potential answer, validate the substring
if min_diff[(current_count_a & 1) ^ 1][current_count_b & 1] != float('inf'):
# Calculate actual substring counts
substring_count_a = current_count_a - stored_prev_count_a
substring_count_b = current_count_b - stored_prev_count_b
# Verify constraints
if substring_count_a % 2 == 1 and substring_count_b % 2 == 0 and substring_count_b > 0:
max_diff = max(max_diff, substring_count_a - substring_count_b)
2. Forgetting to Initialize min_diff Array Properly
The Pitfall:
Not handling the case where no valid prefix exists yet. The min_diff
array starts with float('inf')
values, but we might try to use these infinite values in calculations before any valid prefix is stored.
Why it happens: The algorithm assumes that once we shrink the window enough times, we'll have valid prefixes stored. However, for small inputs or specific character distributions, we might attempt to calculate an answer before any valid prefix exists.
Solution: Check if a valid prefix exists before using it:
# Before calculating max_diff
if min_diff[(current_count_a & 1) ^ 1][current_count_b & 1] != float('inf'):
max_diff = max(
max_diff,
current_count_a - current_count_b - min_diff[(current_count_a & 1) ^ 1][current_count_b & 1]
)
3. Misunderstanding the Window Shrinking Condition
The Pitfall:
The condition current_count_b - prev_count_b >= 2
ensures that character b
appears at least twice in the window, guaranteeing a non-zero even count. However, developers might mistakenly think this guarantees an even count, forgetting that 3, 5, 7, etc., are also possible.
Why it happens: The condition only ensures a minimum count of 2, not that the count is even. The even count is enforced through the parity matching mechanism, not this condition.
Solution:
The window shrinking should continue as long as we can maintain at least 2 occurrences of b
:
# More robust shrinking that ensures we keep valid windows
while right - left >= k:
window_b_count = current_count_b - prev_count_b
# Only shrink if we can maintain at least 2 occurrences of b
if window_b_count >= 2:
# Store the state
min_diff[prev_count_a & 1][prev_count_b & 1] = min(
min_diff[prev_count_a & 1][prev_count_b & 1],
prev_count_a - prev_count_b
)
# Check if we can shrink further without violating constraints
next_digit = digits[left + 1] if left + 1 < len(digits) else -1
if next_digit == digit_b and window_b_count <= 2:
break # Can't shrink without losing valid b count
left += 1
prev_count_a += (digits[left] == digit_a)
prev_count_b += (digits[left] == digit_b)
4. Not Handling Edge Cases with Return Value
The Pitfall:
Returning float('-inf')
when no valid substring exists, which might not be the expected behavior.
Solution: Define clear behavior for when no valid substring exists:
# At the end of the function
return max_diff if max_diff != float('-inf') else -1 # or 0, depending on requirements
Which of the following problems can be solved with backtracking (select multiple)
Recommended Readings
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
https assets algo monster cover_photos stack svg Sliding Window Maximum Monotonic Stack We have an array and a sliding window defined by a start index and an end index The sliding window moves from left of the array to right There are always k elements in the window The window
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
Want a Structured Path to Master System Design Too? Don’t Miss This!