Facebook Pixel

3719. Longest Balanced Subarray I

MediumSegment TreeArrayHash TableDivide and ConquerPrefix Sum
LeetCode ↗

Problem Description

You are given an integer array nums.

A subarray is a contiguous, non-empty sequence of elements within the array. A subarray is called balanced if the number of distinct even numbers in it is equal to the number of distinct odd numbers in it.

Your task is to return the length of the longest balanced subarray.

A few key points to keep in mind:

  • Only distinct values are counted. For example, if the subarray is [2, 2, 3], the count of distinct even numbers is 1 (just the value 2), and the count of distinct odd numbers is also 1 (the value 3), so this subarray is balanced with length 3.
  • The subarray must be contiguous — you cannot skip elements.
  • If multiple balanced subarrays exist, you only need the length of the longest one.

Example:

For nums = [2, 2, 3]:

  • The subarray [2, 3] has 1 distinct even number and 1 distinct odd number — balanced, length 2.
  • The subarray [2, 2, 3] also has 1 distinct even and 1 distinct odd — balanced, length 3.

So the answer is 3.

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

How We Pick the Algorithm

Why Hash Table / Counting?

This problem maps to Hash Table / Counting through a short path in the full flowchart.

Tree orgraph?noFastlookup orcounting?yesHash Table /Counting

Tracking distinct even and odd values in a growing window requires a hash set to determine whether each new element is a first occurrence.

Open in Flowchart

Intuition

The condition for a balanced subarray depends on distinct even and odd values, not just raw counts. This makes the problem tricky for classic tricks like prefix sums, because adding one more element to a subarray may or may not change the distinct counts — it depends on whether that value has already appeared inside the current window.

This observation points us toward a more direct strategy: since the "distinct" property is tied to a specific window's contents, the most natural way to handle it is to fix the left endpoint i and then extend the right endpoint j one step at a time. As the window grows to the right, elements are only ever added, never removed — and that's the key insight. When a window only grows, maintaining the set of seen values is easy:

  • Keep a hash set vis of values already in the window.
  • When nums[j] is new (not in vis), it contributes one new distinct value — increment either the even counter or the odd counter based on its parity (nums[j] & 1).
  • When nums[j] is a repeat, the distinct counts don't change at all.

With a counter array cnt of size 2 tracking distinct evens (cnt[0]) and distinct odds (cnt[1]), checking whether the current window [i, j] is balanced becomes a constant-time comparison cnt[0] == cnt[1]. Whenever it holds, the window length j - i + 1 is a candidate for the answer.

By restarting the set and counters for every left endpoint, we examine all O(n^2) subarrays, but each extension step costs only O(1) amortized work thanks to the hash set. This brute-force-with-bookkeeping approach is simple, correct, and avoids the difficulty of trying to shrink a window (where removing an element would require knowing whether other copies of it still remain inside).

Pattern Learn more about Segment Tree, Divide and Conquer and Prefix Sum patterns.

Solution Approach

Pattern used: Enumeration (brute force over left endpoints) + Hash Table for de-duplication.

We enumerate every possible left endpoint i, then extend the right endpoint j from i to the end of the array, maintaining the distinct counts incrementally as the window grows.

Step-by-step walkthrough:

  1. Initialize the answer ans = 0 and let n be the length of nums.

  2. Outer loop — fix the left endpoint i: For each i from 0 to n - 1, reset the bookkeeping structures:

    • cnt = [0, 0] — a counter array where cnt[0] is the number of distinct even values and cnt[1] is the number of distinct odd values in the current window.
    • vis = set() — a hash set storing all values already seen in the window [i, j].
  3. Inner loop — extend the right endpoint j: For each j from i to n - 1:

    • New value check: If nums[j] is not in vis, it is a brand-new distinct value, so:
      • Increment the appropriate counter using its parity: cnt[nums[j] & 1] += 1. The bitwise trick nums[j] & 1 evaluates to 0 for even numbers and 1 for odd numbers, which maps directly to the index in cnt.
      • Add nums[j] to vis.
    • Repeat value: If nums[j] is already in vis, the distinct counts are unchanged, so we do nothing.
    • Balance check: If cnt[0] == cnt[1], the window [i, j] is balanced, so update the answer: ans = max(ans, j - i + 1).
  4. Return ans after all pairs (i, j) have been considered.

Code:

class Solution:
    def longestBalanced(self, nums: List[int]) -> int:
        n = len(nums)
        ans = 0
        for i in range(n):
            cnt = [0, 0]
            vis = set()
            for j in range(i, n):
                if nums[j] not in vis:
                    cnt[nums[j] & 1] += 1
                    vis.add(nums[j])
                if cnt[0] == cnt[1]:
                    ans = max(ans, j - i + 1)
        return ans

Why this works: Every subarray of nums corresponds to exactly one pair (i, j) with i <= j, and the double loop visits all such pairs. Because the window only ever grows to the right for a fixed i, the hash set vis lets us detect duplicates in O(1) average time, keeping the counters exact at every step.

Complexity Analysis:

  • Time complexity: O(n^2) — there are O(n^2) pairs (i, j), and each inner-loop step does only constant-time set lookup, insertion, and comparison.
  • Space complexity: O(n) — the hash set vis can hold up to n distinct values in the worst case; cnt uses constant space.

Example Walkthrough

Let's trace the algorithm on nums = [1, 2, 2, 3]. The expected answer is 3, achieved by the subarray [1, 2, 2] (or [2, 2, 3]), which contains 1 distinct even value (2) and 1 distinct odd value (1 or 3).

Left endpoint i = 0 — reset cnt = [0, 0], vis = {}:

jnums[j]New value?Updatecnt (even, odd)Balanced?ans
01Yescnt[1] += 1, vis = {1}[0, 1]No0
12Yescnt[0] += 1, vis = {1, 2}[1, 1]Yes → length 22
22No (repeat)nothing changes[1, 1]Yes → length 33
33Yescnt[1] += 1, vis = {1, 2, 3}[1, 2]No3

Notice the key moment at j = 2: the second 2 is already in vis, so the distinct counts stay [1, 1] and the window [1, 2, 2] remains balanced — the window grows to length 3 "for free."

Left endpoint i = 1 — reset cnt = [0, 0], vis = {}:

jnums[j]New value?cntBalanced?ans
12Yes[1, 0]No3
22No[1, 0]No3
33Yes[1, 1]Yes → length 33

The window [2, 2, 3] is also balanced with length 3, but it doesn't improve the answer.

Left endpoint i = 2 — windows [2] gives cnt = [1, 0] (not balanced), then [2, 3] gives cnt = [1, 1] (balanced, length 2, no improvement).

Left endpoint i = 3 — the window [3] gives cnt = [0, 1], never balanced.

All pairs (i, j) have been examined, and the final answer is 3.

This trace highlights the two pillars of the approach: the hash set vis keeps duplicates from inflating the counts (the second 2 changed nothing), and the constant-time check cnt[0] == cnt[1] lets us validate every one of the O(n^2) windows with only O(1) extra work per extension.

Solution Implementation

1from typing import List
2
3
4class Solution:
5    def longestBalanced(self, nums: List[int]) -> int:
6        n = len(nums)
7        max_length = 0
8
9        # Enumerate every possible starting index of the subarray
10        for left in range(n):
11            # count[0]: number of distinct even values in the current window
12            # count[1]: number of distinct odd values in the current window
13            count = [0, 0]
14            # Track distinct values already counted in the window [left, right]
15            seen = set()
16
17            # Extend the subarray to the right one element at a time
18            for right in range(left, n):
19                # Only count a value the first time it appears in the window
20                if nums[right] not in seen:
21                    # nums[right] & 1 is 0 for even numbers, 1 for odd numbers
22                    count[nums[right] & 1] += 1
23                    seen.add(nums[right])
24
25                # The window is balanced when distinct evens == distinct odds
26                if count[0] == count[1]:
27                    max_length = max(max_length, right - left + 1)
28
29        return max_length
30
1class Solution {
2    public int longestBalanced(int[] nums) {
3        int n = nums.length;
4        int ans = 0;
5
6        // Enumerate every possible starting index of the subarray
7        for (int i = 0; i < n; ++i) {
8            // Track the distinct values seen in the current subarray [i, j]
9            Set<Integer> visited = new HashSet<>();
10
11            // count[0]: number of distinct even values
12            // count[1]: number of distinct odd values
13            int[] count = new int[2];
14
15            // Extend the subarray to the right, one element at a time
16            for (int j = i; j < n; ++j) {
17                // Only count the value if it appears for the first time in [i, j]
18                if (visited.add(nums[j])) {
19                    // nums[j] & 1 is 0 for even numbers and 1 for odd numbers
20                    ++count[nums[j] & 1];
21                }
22
23                // The subarray is balanced when the number of distinct
24                // even values equals the number of distinct odd values
25                if (count[0] == count[1]) {
26                    ans = Math.max(ans, j - i + 1);
27                }
28            }
29        }
30
31        return ans;
32    }
33}
34
1class Solution {
2public:
3    int longestBalanced(vector<int>& nums) {
4        int n = nums.size();
5        int ans = 0;
6
7        // Enumerate every possible starting index of the subarray.
8        for (int i = 0; i < n; ++i) {
9            // Track the distinct values seen in the current window [i, j].
10            unordered_set<int> visited;
11
12            // count[0]: number of distinct even values in the window.
13            // count[1]: number of distinct odd values in the window.
14            int count[2]{};
15
16            // Extend the subarray to the right one element at a time.
17            for (int j = i; j < n; ++j) {
18                // Only count a value the first time it appears in the window,
19                // since we care about distinct elements.
20                if (!visited.contains(nums[j])) {
21                    visited.insert(nums[j]);
22                    ++count[nums[j] & 1];  // nums[j] & 1 == 1 for odd, 0 for even.
23                }
24
25                // The subarray is balanced when the number of distinct even
26                // values equals the number of distinct odd values.
27                if (count[0] == count[1]) {
28                    ans = max(ans, j - i + 1);
29                }
30            }
31        }
32
33        return ans;
34    }
35};
36
1/**
2 * Finds the length of the longest subarray in which the number of
3 * distinct even values equals the number of distinct odd values.
4 *
5 * @param nums - The input array of integers.
6 * @returns The length of the longest balanced subarray.
7 */
8function longestBalanced(nums: number[]): number {
9    const n: number = nums.length;
10    // Stores the maximum length found so far
11    let ans: number = 0;
12
13    // Enumerate every possible starting index of the subarray
14    for (let i = 0; i < n; ++i) {
15        // Tracks the distinct values seen in the current subarray
16        const visited: Set<number> = new Set<number>();
17        // count[0]: number of distinct even values
18        // count[1]: number of distinct odd values
19        const count: number[] = Array(2).fill(0);
20
21        // Extend the subarray to every possible ending index
22        for (let j = i; j < n; ++j) {
23            // Only count a value the first time it appears
24            if (!visited.has(nums[j])) {
25                visited.add(nums[j]);
26                // nums[j] & 1 is 0 for even numbers, 1 for odd numbers
27                ++count[nums[j] & 1];
28            }
29
30            // If distinct evens equal distinct odds, the subarray is balanced
31            if (count[0] === count[1]) {
32                ans = Math.max(ans, j - i + 1);
33            }
34        }
35    }
36
37    return ans;
38}
39

Time and Space Complexity

  • Time complexity: O(n^2), where n is the length of the array nums. The code uses two nested loops: the outer loop iterates over every starting index i, and the inner loop scans every ending index j from i to n - 1. Inside the inner loop, all operations — checking membership in the set vis, updating the counter cnt, and comparing cnt[0] with cnt[1] — take O(1) time on average. Therefore, the total work is O(n) * O(n) = O(n^2).

  • Space complexity: O(n), where n is the length of the array nums. For each starting index i, the set vis may store up to n - i distinct elements in the worst case (when all elements in the subarray are unique), requiring O(n) space. The counter array cnt only uses constant space O(1), so the overall space is dominated by vis, giving O(n).

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

Common Pitfalls

Pitfall 1: Forgetting to reset seen and count for each new left endpoint

The most frequent mistake is declaring the hash set and counters outside the outer loop:

# WRONG
count = [0, 0]
seen = set()
for left in range(n):
    for right in range(left, n):
        if nums[right] not in seen:
            count[nums[right] & 1] += 1
            seen.add(nums[right])
        ...

Once left advances, the window [left, right] no longer contains the elements before left, but seen still remembers them. This causes genuinely new values to be skipped and counters to drift, producing wrong (usually inflated) answers.

Fix: Re-initialize count = [0, 0] and seen = set() at the start of every iteration of the outer loop, exactly as the correct solution does. Each left endpoint defines a fresh window, so the bookkeeping must start from scratch.


Pitfall 2: Counting occurrences instead of distinct values

A subtle misreading of the problem leads to counting every even/odd element rather than distinct even/odd values:

# WRONG — counts duplicates multiple times
for right in range(left, n):
    count[nums[right] & 1] += 1   # no de-duplication

For nums = [2, 2, 3], this incorrectly computes 2 evens vs. 1 odd for the full array and misses the correct answer of 3.

Fix: Guard the increment with a membership check (if nums[right] not in seen) so each value contributes to the counter at most once per window.


Pitfall 3: Attempting to shrink the window with naive removal (broken sliding window)

It is tempting to convert this into a classic two-pointer sliding window and "remove" nums[left] when the left pointer moves:

# WRONG — removal logic is incorrect with duplicates
seen.remove(nums[left])
count[nums[left] & 1] -= 1

This fails because the same value may still exist elsewhere in the window. Removing it from seen and decrementing the counter is only valid when its last remaining occurrence leaves the window. Correct removal requires a full frequency map (value → occurrence count), and even then the "balanced" condition is not monotonic, so a standard shrink/grow sliding window does not directly apply.

Fix: Either stick with the enumeration approach (reset per left endpoint, as in the given solution), or if optimizing, maintain a Counter of occurrences and only decrement the distinct counter when a value's frequency drops to zero — while recognizing that the window logic must check balance at every position, not rely on monotonic shrinking.


Pitfall 4: Classifying parity incorrectly for negative numbers with % 2

Using nums[right] % 2 == 1 to test for odd numbers is a trap in some languages. In Python -3 % 2 is 1, so it happens to work, but in C++/Java -3 % 2 is -1, and the check silently misclassifies negative odd numbers.

Fix: Use the bitwise trick nums[right] & 1 (as the solution does) or compare % 2 != 0 instead of == 1. In Python & 1 works uniformly for negatives because of two's-complement semantics on the lowest bit.


Pitfall 5: Updating the answer only when the window grows

Some implementations check the balance condition inside the if nums[right] not in seen block:

# WRONG — balance check skipped for duplicate elements
if nums[right] not in seen:
    count[nums[right] & 1] += 1
    seen.add(nums[right])
    if count[0] == count[1]:          # only checked on new values
        max_length = max(max_length, right - left + 1)

A duplicate element does not change the distinct counts, but it does extend the window length. For nums = [2, 3, 3], the window [2, 3, 3] is balanced with length 3, yet the check above never fires at right = 2 and reports 2 instead.

Fix: Perform the count[0] == count[1] comparison on every iteration of the inner loop, outside the de-duplication branch.

Ready to land your dream job?

Unlock your dream job with a 5-minute quiz for a personalized study roadmap!

Get My Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

Consider the classic dynamic programming of longest increasing subsequence:

Find the length of the longest subsequence of a given sequence such that all elements of the subsequence are sorted in increasing order.

For example, the length of LIS for [50, 3, 10, 7, 40, 80] is 4 and LIS is [3, 7, 40, 80].

What is the recurrence relation?


Recommended Readings

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

Load More