Facebook Pixel

3721. Longest Balanced Subarray II

HardSegment TreeArrayHash TableDivide and ConquerPrefix Sum
LeetCode ↗

Problem Description

You are given an integer array nums.

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

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

Key points to keep in mind:

  • Only distinct values are counted. For example, if the subarray is [2, 2, 3], it contains one distinct even number (2) and one distinct odd number (3), so it is balanced — even though the value 2 appears twice.
  • The comparison is between the count of unique even values and the count of unique odd values within the subarray, not the total occurrences of even and odd elements.
  • A subarray with zero distinct even numbers and zero distinct odd numbers would trivially satisfy the condition, but since subarrays must be non-empty, every subarray contains at least one number.

For example:

  • Given nums = [2, 3, 2, 5], the subarray [2, 3] has one distinct even number (2) and one distinct odd number (3), so it is balanced. A longer balanced subarray may exist, such as [3, 2, 5]? No — that one has one distinct even (2) but two distinct odds (3, 5), so it is not balanced. However, [2, 3, 2] has one distinct even (2) and one distinct odd (3), giving a balanced subarray of length 3.
  • If no balanced subarray exists (e.g., nums = [1, 1, 1], which contains only odd numbers), the answer is 0.

The goal is to find the maximum possible length among all such balanced subarrays.

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

How We Pick the Algorithm

Why Ordered Set / Fenwick / Segment Tree?

This problem maps to Ordered Set / Fenwick / Segment Tree through a short path in the full flowchart.

Tree orgraph?noSortedsearch /rangeyesOrdered Set /Fenwick /Segment Tree

The distinct-count constraint turns the problem into a dynamic prefix-balance query with lazy range updates, requiring a segment tree for the leftmost position search.

Open in Flowchart

Intuition

The condition "distinct even count equals distinct odd count" naturally suggests turning the problem into a balance/prefix-sum problem. If we assign +1 to every distinct odd number and -1 to every distinct even number, then a subarray is balanced exactly when the sum of these contributions inside it is 0.

This is similar to the classic problem of finding the longest subarray with equal numbers of 0s and 1s, where we map values to +1/-1 and look for two positions with the same prefix sum. If now[i] denotes the prefix balance up to index i, then the subarray (pos, i] is balanced whenever now[i] = now[pos], and its length is i - pos. To maximize the length, for each right endpoint i we want the earliest position pos whose prefix balance equals the current one.

The tricky part is the word distinct. A number should only contribute once per subarray, no matter how many times it appears. This breaks the simple static prefix-sum trick, because whether an element "counts" depends on the left boundary of the subarray: an occurrence of x at index i matters only for subarrays that start after the previous occurrence of x.

The standard way to handle this is to think of each occurrence as contributing to a range of prefix positions:

  • When we see value x at index i, it adds its contribution (+1 if odd, -1 if even) to all prefix sums at positions i, i+1, \ldots, n.
  • But if x appeared before at position last[x], that earlier occurrence's contribution must be revoked for the range starting at last[x], because for any subarray containing both occurrences, only one should count. Effectively the contribution "moves" from the old occurrence to the new one.

So as the right endpoint i sweeps from left to right, the array of prefix balances is no longer fixed — it gets range updates (add +1 or -1 over a suffix). After each update, we need to find the smallest position pos such that the (dynamically maintained) prefix balance at pos equals the current total balance now.

This is exactly what a lazy segment tree over the prefix positions can do:

  • It supports range addition to handle adding/revoking contributions over suffixes.
  • Each node stores the minimum and maximum value in its interval. Since each update changes values by ±1 on a contiguous range, the set of values within any interval is "dense" — every integer between the interval's min and max actually occurs. Therefore, checking mn <= now <= mx is enough to know whether the target value exists in a subtree, which lets us binary search on the tree for the leftmost position holding value now in O(log n) time.

Combining these pieces — prefix balance, a hash table last to detect repeated values, and a segment tree with min/max plus lazy range updates for fast leftmost-equal queries — gives an O(n \log n) sweep: for each right endpoint, update contributions, query the earliest matching prefix position pos, and update the answer with i - pos.

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

Solution Approach

Segment Tree + Prefix Sum + Hash Table

We maintain a segment tree over the prefix positions 0, 1, \ldots, n, where the value at position p represents the prefix balance after processing the first p elements (with contributions adjusted for distinctness). Position 0 represents the empty prefix with balance 0.

Step 1: Define the segment tree node

Each node covers an interval [l, r] and stores:

  • mn / mx: the minimum and maximum prefix balance within the interval;
  • lazy: a pending range-addition tag.

The tree supports two operations:

  • modify(u, l, r, v): add v to every position in [l, r] (range addition with lazy propagation);
  • query(u, target): find the leftmost position whose value equals target.

The query works by walking down the tree: at each node, first push down the lazy tag, then check whether the left child satisfies mn <= target <= mx. Because all updates are ±1 over contiguous ranges, adjacent positions differ by at most 1, so every integer in [mn, mx] is guaranteed to appear in that subtree — the membership check is exact. If the left child can contain target, descend left (to find the earliest position); otherwise descend right. A leaf returns its position. Note that the target is always present in the tree, since position 0 is never modified except by suffix updates that also affect now — more precisely, the current balance now always equals the value at the latest position i, so the query never fails.

Step 2: Sweep the right endpoint and maintain contributions

We iterate i from 1 to n over the elements x = nums[i-1], maintaining:

  • last: a hash table mapping each value to the index of its most recent occurrence;
  • now: the current total balance, i.e., the value stored at position i (or n) in the tree.

For each element x, let its contribution be:

det = +1  if x is odd
det = -1  if x is even

Then:

  1. Revoke the previous occurrence. If x appeared before at last[x], its old contribution applied to all positions >= last[x]. Since only one occurrence should count per subarray, undo it with modify(1, last[x], n, -det) and update now -= det. Conceptually, this transfers the contribution of x from its old position to the new one.
  2. Add the current occurrence. Record last[x] = i, then apply modify(1, i, n, det) and update now += det. Now, for every left boundary pos, the value at position pos in the tree is exactly the balance of the subarray (pos, i]... rather, the prefix value such that the subarray balance is now - value(pos).
  3. Query the earliest matching prefix. The subarray (pos, i] is balanced precisely when the tree value at pos equals now. Call pos = query(1, now) to get the leftmost such position, and update:
ans = max(ans, i - pos)

Why the invariant holds

For a fixed right endpoint i, the tree value at position pos equals:

(distinct odds) - (distinct evens) over the prefix, counting each value
only at its last occurrence within [1, i]

A value x whose last occurrence in [1, i] is at index j contributes to positions pos >= j. For the subarray (pos, i], the value x is included if and only if j > pos, which means its contribution is present in value(i) = now but absent from value(pos) exactly when x lies inside the subarray. Hence:

balance of (pos, i] = now - value(pos)

and the subarray is balanced if and only if value(pos) = now.

Step 3: Return the answer

After processing all right endpoints, ans holds the length of the longest balanced subarray (it stays 0 if none exists).

Complexity Analysis

  • Time complexity: O(n log n). Each element triggers at most two range modifications and one tree binary search, each costing O(log n).
  • Space complexity: O(n) for the segment tree (about 4(n+1) nodes) and the hash table last.

Walkthrough on nums = [2, 3, 2, 5]

ixactionnowquery(now)posi - pos
12add -1 on [1,4]-1leftmost -1 is 10
23add +1 on [2,4]0leftmost 0 is 02
32revoke -1 at [1,4], add -1 on [3,4]0leftmost 0 is 03
45add +1 on [4,4]1leftmost 1 is 40

The answer is 3, corresponding to the balanced subarray [2, 3, 2] (one distinct even, one distinct odd).

Example Walkthrough

Let's trace the algorithm on nums = [2, 3, 2, 5] (so n = 4).

We maintain a segment tree over prefix positions 0..4. Conceptually, the tree holds an array value[0..4], where value[pos] is the adjusted prefix balance (distinct odds minus distinct evens, with each value counted only at its last occurrence so far). Initially everything is 0:

value = [0, 0, 0, 0, 0],  now = 0,  last = {},  ans = 0

Step i = 1, x = 2 (even, det = -1)

  • 2 has not been seen before, so there is nothing to revoke.
  • Record last[2] = 1, apply modify([1, 4], -1), and set now = -1.
value = [0, -1, -1, -1, -1],  now = -1
  • query(-1) → leftmost position with value -1 is pos = 1.
  • ans = max(0, 1 - 1) = 0. The candidate subarray (1, 1] is empty, so no gain.

Step i = 2, x = 3 (odd, det = +1)

  • First occurrence of 3; nothing to revoke.
  • Record last[3] = 2, apply modify([2, 4], +1), and set now = 0.
value = [0, -1, 0, 0, 0],  now = 0
  • query(0) → during the descent, the left subtree reports mn = -1, mx = 0, and since values change by ±1 between neighbors, 0 is guaranteed to exist there — we descend left and land on pos = 0.
  • ans = max(0, 2 - 0) = 2. This corresponds to subarray (0, 2] = [2, 3]: one distinct even, one distinct odd. Balanced ✓

Step i = 3, x = 2 (even, det = -1) — the interesting case

  • 2 was seen at last[2] = 1, so its old contribution must move:
    • Revoke: modify([1, 4], +1), now = 0 - (-1) = 1:
      value = [0, 0, 1, 1, 1]
    • Re-add at the new position: last[2] = 3, modify([3, 4], -1), now = 1 + (-1) = 0:
      value = [0, 0, 1, 0, 0],  now = 0
  • Notice what this accomplished: position 1 no longer "remembers" the old 2, because for any subarray starting at pos = 1 or pos = 2, the second 2 at index 3 is the one that counts — and it must count only once.
  • query(0) → leftmost position with value 0 is pos = 0.
  • ans = max(2, 3 - 0) = 3. Subarray (0, 3] = [2, 3, 2]: distinct evens {2}, distinct odds {3} — sizes 1 = 1. Balanced ✓ Length 3.

Step i = 4, x = 5 (odd, det = +1)

  • First occurrence of 5; record last[5] = 4, apply modify([4, 4], +1), now = 1.
value = [0, 0, 1, 0, 1],  now = 1
  • query(1) → leftmost position with value 1 is pos = 2.
  • i - pos = 4 - 2 = 2. This finds subarray (2, 4] = [2, 5]: one distinct even, one distinct odd — balanced, but shorter than what we already have.
  • ans = max(3, 2) = 3.

Summary table

ixTree updatesvalue[0..4] after updatesnowposi - posans
12[1,4] -1[0, -1, -1, -1, -1]-1100
23[2,4] +1[0, -1, 0, 0, 0]0022
32revoke [1,4] +1, then [3,4] -1[0, 0, 1, 0, 0]0033
45[4,4] +1[0, 0, 1, 0, 1]1223

The final answer is 3, achieved by the balanced subarray [2, 3, 2].

The key invariant visible throughout the trace: at every step, the subarray (pos, i] has balance now - value[pos], so it is balanced exactly when value[pos] = now — and the segment tree's min/max-guided descent finds the leftmost such pos in O(log n), maximizing the length for each right endpoint.

Solution Implementation

1from typing import List
2
3
4class Solution:
5    def longestBalanced(self, nums: List[int]) -> int:
6        """
7        Find the length of the longest subarray in which the number of
8        DISTINCT odd values equals the number of DISTINCT even values.
9
10        Idea:
11        - Assign each value a weight: +1 if odd, -1 if even.
12        - For every prefix ending at index i, maintain a segment tree over
13          positions [0, n] where tree[j] = sum of weights of all distinct
14          values whose LAST occurrence lies in [1, j].
15        - The distinct-value balance of subarray (j, i] is then
16          total_balance - tree[j]. It is zero exactly when
17          tree[j] == total_balance.
18        - Since adjacent leaves differ by exactly +1 / -1, the leftmost
19          position holding a target value can be located by walking down
20          the tree (intermediate value property), maximizing i - j.
21        """
22        n = len(nums)
23
24        class Node:
25            """A segment tree node covering the index range [left, right]."""
26            __slots__ = ("left", "right", "min_val", "max_val", "lazy")
27
28            def __init__(self) -> None:
29                self.left = self.right = 0       # range boundaries
30                self.min_val = self.max_val = 0  # min / max leaf value in range
31                self.lazy = 0                    # pending range-add value
32
33        tree = [Node() for _ in range((n + 1) * 4)]
34
35        def build(node: int, left: int, right: int) -> None:
36            """Initialize the node for range [left, right] and recurse."""
37            tree[node].left, tree[node].right = left, right
38            tree[node].min_val = tree[node].max_val = tree[node].lazy = 0
39            if left == right:
40                return
41            mid = (left + right) >> 1
42            build(node << 1, left, mid)
43            build(node << 1 | 1, mid + 1, right)
44
45        def apply_update(node: int, value: int) -> None:
46            """Apply a range-add of `value` to this node."""
47            tree[node].min_val += value
48            tree[node].max_val += value
49            tree[node].lazy += value
50
51        def push_down(node: int) -> None:
52            """Propagate the pending lazy value to both children."""
53            if tree[node].lazy:
54                apply_update(node << 1, tree[node].lazy)
55                apply_update(node << 1 | 1, tree[node].lazy)
56                tree[node].lazy = 0
57
58        def push_up(node: int) -> None:
59            """Recompute this node's aggregates from its children."""
60            tree[node].min_val = min(tree[node << 1].min_val, tree[node << 1 | 1].min_val)
61            tree[node].max_val = max(tree[node << 1].max_val, tree[node << 1 | 1].max_val)
62
63        def update(node: int, left: int, right: int, value: int) -> None:
64            """Add `value` to every position in [left, right]."""
65            if tree[node].left >= left and tree[node].right <= right:
66                apply_update(node, value)
67                return
68            push_down(node)
69            mid = (tree[node].left + tree[node].right) >> 1
70            if left <= mid:
71                update(node << 1, left, right, value)
72            if right > mid:
73                update(node << 1 | 1, left, right, value)
74            push_up(node)
75
76        def query(node: int, target: int) -> int:
77            """
78            Return the leftmost position whose value equals `target`.
79            Valid because adjacent leaf values differ by exactly 1,
80            so target lies in a subtree iff min <= target <= max.
81            """
82            if tree[node].left == tree[node].right:
83                return tree[node].left
84            push_down(node)
85            if tree[node << 1].min_val <= target <= tree[node << 1].max_val:
86                return query(node << 1, target)
87            return query(node << 1 | 1, target)
88
89        # Build the tree over positions 0..n (position 0 = empty prefix).
90        build(1, 0, n)
91
92        last_pos = {}            # value -> index (1-based) of its last occurrence
93        total_balance = 0        # distinct-value balance of the whole prefix
94        ans = 0
95
96        for i, x in enumerate(nums, start=1):
97            delta = 1 if (x & 1) else -1  # +1 for odd, -1 for even
98
99            # If x appeared before, cancel its previous contribution,
100            # which covered the suffix [last_pos[x], n].
101            if x in last_pos:
102                update(1, last_pos[x], n, -delta)
103                total_balance -= delta
104
105            # Record x's new last occurrence and add its contribution
106            # to the suffix [i, n].
107            last_pos[x] = i
108            update(1, i, n, delta)
109            total_balance += delta
110
111            # Leftmost j with tree[j] == total_balance gives the longest
112            # balanced subarray (j, i].
113            pos = query(1, total_balance)
114            ans = max(ans, i - pos)
115
116        return ans
117
1import java.util.HashMap;
2import java.util.Map;
3
4/**
5 * Idea:
6 * - Treat each distinct odd number as +1, and each distinct even number as -1.
7 * - Maintain prefix sums representing the odd/even balance.
8 * - When a number appears again, remove its previous contribution
9 *   (so only the latest occurrence of each value counts).
10 * - Use a segment tree with lazy propagation to maintain the min/max
11 *   prefix sums under range-add updates.
12 * - Binary search on the segment tree to find the earliest index whose
13 *   prefix sum equals the current prefix sum, which yields the longest
14 *   balanced subarray ending at the current position.
15 */
16class Solution {
17
18    /**
19     * Segment tree node covering a range of prefix-sum indices.
20     */
21    static class Node {
22        int left, right;        // range covered by this node: [left, right]
23        int minSum, maxSum;     // minimum / maximum prefix sum within the range
24        int lazyAdd;            // pending range-add value (lazy propagation)
25    }
26
27    /**
28     * Segment tree supporting:
29     * - range add (add a value to all prefix sums in a range)
30     * - descending search for the smallest index holding a given prefix sum
31     */
32    static class SegmentTree {
33        Node[] tree;
34
35        SegmentTree(int size) {
36            tree = new Node[size << 2];
37            for (int i = 0; i < tree.length; i++) {
38                tree[i] = new Node();
39            }
40            build(1, 0, size);
41        }
42
43        /**
44         * Build the tree over [left, right] with all prefix sums initialized to 0.
45         */
46        void build(int node, int left, int right) {
47            tree[node].left = left;
48            tree[node].right = right;
49            tree[node].minSum = 0;
50            tree[node].maxSum = 0;
51            tree[node].lazyAdd = 0;
52            if (left == right) {
53                return;
54            }
55            int mid = (left + right) >> 1;
56            build(node << 1, left, mid);
57            build(node << 1 | 1, mid + 1, right);
58        }
59
60        /**
61         * Add value to every prefix sum whose index lies in [left, right].
62         */
63        void modify(int node, int left, int right, int value) {
64            // Current node range fully covered: apply lazily and stop
65            if (tree[node].left >= left && tree[node].right <= right) {
66                apply(node, value);
67                return;
68            }
69            pushdown(node);
70            int mid = (tree[node].left + tree[node].right) >> 1;
71            if (left <= mid) {
72                modify(node << 1, left, right, value);
73            }
74            if (right > mid) {
75                modify(node << 1 | 1, left, right, value);
76            }
77            pushup(node);
78        }
79
80        /**
81         * Binary search on the tree: find the smallest index whose
82         * prefix sum equals target.
83         *
84         * Correctness note: prefix sums change by exactly 1 between
85         * adjacent indices, so if target lies within [minSum, maxSum]
86         * of a subtree, that subtree must contain an index with that
87         * exact prefix sum.
88         */
89        int query(int node, int target) {
90            // Reached a leaf: this is the answer index
91            if (tree[node].left == tree[node].right) {
92                return tree[node].left;
93            }
94            pushdown(node);
95            int leftChild = node << 1;
96            int rightChild = node << 1 | 1;
97            // Prefer the left child to obtain the smallest index
98            if (tree[leftChild].minSum <= target && target <= tree[leftChild].maxSum) {
99                return query(leftChild, target);
100            }
101            return query(rightChild, target);
102        }
103
104        /**
105         * Apply a range-add value to a node and record it lazily.
106         */
107        void apply(int node, int value) {
108            tree[node].minSum += value;
109            tree[node].maxSum += value;
110            tree[node].lazyAdd += value;
111        }
112
113        /**
114         * Recompute this node's aggregates from its children.
115         */
116        void pushup(int node) {
117            tree[node].minSum = Math.min(tree[node << 1].minSum, tree[node << 1 | 1].minSum);
118            tree[node].maxSum = Math.max(tree[node << 1].maxSum, tree[node << 1 | 1].maxSum);
119        }
120
121        /**
122         * Propagate the pending lazy value down to both children.
123         */
124        void pushdown(int node) {
125            if (tree[node].lazyAdd != 0) {
126                apply(node << 1, tree[node].lazyAdd);
127                apply(node << 1 | 1, tree[node].lazyAdd);
128                tree[node].lazyAdd = 0;
129            }
130        }
131    }
132
133    /**
134     * Returns the length of the longest subarray in which the number of
135     * distinct odd values equals the number of distinct even values.
136     */
137    public int longestBalanced(int[] nums) {
138        int n = nums.length;
139        SegmentTree segmentTree = new SegmentTree(n);
140
141        // lastIndex.get(x) = most recent 1-based position where value x appeared
142        Map<Integer, Integer> lastIndex = new HashMap<>();
143
144        int currentSum = 0; // prefix sum at the current right endpoint
145        int answer = 0;     // length of the longest balanced subarray found
146
147        // Enumerate the right endpoint (1-based index i)
148        for (int i = 1; i <= n; i++) {
149            int value = nums[i - 1];
150            // Odd contributes +1, even contributes -1
151            int delta = (value & 1) == 1 ? 1 : -1;
152
153            // If the value appeared before, cancel its previous contribution
154            // over the suffix starting at its last position
155            if (lastIndex.containsKey(value)) {
156                segmentTree.modify(1, lastIndex.get(value), n, -delta);
157                currentSum -= delta;
158            }
159
160            // Record the new occurrence and add its contribution
161            // over the suffix starting at position i
162            lastIndex.put(value, i);
163            segmentTree.modify(1, i, n, delta);
164            currentSum += delta;
165
166            // Find the earliest position whose prefix sum matches the
167            // current one; the subarray between them is balanced
168            int position = segmentTree.query(1, currentSum);
169            answer = Math.max(answer, i - position);
170        }
171
172        return answer;
173    }
174}
175
1/*
2 * Segment tree node.
3 * It maintains:
4 *  - left, right : the index range [left, right] covered by this node
5 *  - minVal      : minimum prefix sum within this range
6 *  - maxVal      : maximum prefix sum within this range
7 *  - lazy        : pending lazy-propagation value (range add)
8 */
9class Node {
10public:
11    int left = 0, right = 0;
12    int minVal = 0, maxVal = 0;
13    int lazy = 0;
14};
15
16/*
17 * Segment tree supporting:
18 *  1. Range add (add a value to all prefix sums in a range)
19 *  2. Query for the smallest index whose prefix sum equals a target value
20 */
21class SegmentTree {
22public:
23    explicit SegmentTree(int n) {
24        tree.resize(n << 2);
25        // Build the tree over the index range [0, n]
26        build(1, 0, n);
27    }
28
29    /*
30     * Add value to all prefix sums in range [left, right].
31     */
32    void modify(int u, int left, int right, int value) {
33        if (tree[u].left >= left && tree[u].right <= right) {
34            apply(u, value);
35            return;
36        }
37        pushdown(u);
38        int mid = (tree[u].left + tree[u].right) >> 1;
39        if (left <= mid) {
40            modify(u << 1, left, right, value);
41        }
42        if (right > mid) {
43            modify(u << 1 | 1, left, right, value);
44        }
45        pushup(u);
46    }
47
48    /*
49     * Binary search on the segment tree.
50     * Find the smallest index pos such that prefixSum[pos] == target.
51     *
52     * Key observation:
53     * If target lies within [minVal, maxVal] of a segment, then since
54     * adjacent prefix sums differ by exactly 1, a position with prefix
55     * sum equal to target must exist inside that segment.
56     */
57    int query(int u, int target) {
58        if (tree[u].left == tree[u].right) {
59            return tree[u].left;
60        }
61        pushdown(u);
62        int leftChild = u << 1, rightChild = u << 1 | 1;
63        // Prefer the left child to obtain the smallest index
64        if (tree[leftChild].minVal <= target && target <= tree[leftChild].maxVal) {
65            return query(leftChild, target);
66        }
67        return query(rightChild, target);
68    }
69
70private:
71    vector<Node> tree;
72
73    /*
74     * Build an empty segment tree over [left, right].
75     * All initial prefix sums are zero.
76     */
77    void build(int u, int left, int right) {
78        tree[u].left = left;
79        tree[u].right = right;
80        tree[u].minVal = tree[u].maxVal = 0;
81        tree[u].lazy = 0;
82        if (left == right) {
83            return;
84        }
85        int mid = (left + right) >> 1;
86        build(u << 1, left, mid);
87        build(u << 1 | 1, mid + 1, right);
88    }
89
90    /*
91     * Apply a range-add of value to node u.
92     */
93    void apply(int u, int value) {
94        tree[u].minVal += value;
95        tree[u].maxVal += value;
96        tree[u].lazy += value;
97    }
98
99    /*
100     * Recompute parent information from its two children.
101     */
102    void pushup(int u) {
103        tree[u].minVal = min(tree[u << 1].minVal, tree[u << 1 | 1].minVal);
104        tree[u].maxVal = max(tree[u << 1].maxVal, tree[u << 1 | 1].maxVal);
105    }
106
107    /*
108     * Propagate the lazy tag down to both children.
109     */
110    void pushdown(int u) {
111        if (tree[u].lazy != 0) {
112            apply(u << 1, tree[u].lazy);
113            apply(u << 1 | 1, tree[u].lazy);
114            tree[u].lazy = 0;
115        }
116    }
117};
118
119class Solution {
120public:
121    int longestBalanced(vector<int>& nums) {
122        int n = nums.size();
123        SegmentTree segmentTree(n);
124
125        /*
126         * lastPos[x] = the most recent index (1-based) where value x appeared
127         */
128        unordered_map<int, int> lastPos;
129
130        int prefixSum = 0; // current prefix sum at the right endpoint
131        int ans = 0;       // length of the longest balanced subarray
132
133        /*
134         * Enumerate the right endpoint i (1-based) of the subarray.
135         */
136        for (int i = 1; i <= n; ++i) {
137            int x = nums[i - 1];
138
139            /*
140             * Contribution of value x:
141             *  +1 if x is odd
142             *  -1 if x is even
143             */
144            int delta = (x & 1) ? 1 : -1;
145
146            /*
147             * If x appeared before, remove its previous contribution
148             * from all prefix sums starting at its last position.
149             */
150            if (lastPos.count(x)) {
151                segmentTree.modify(1, lastPos[x], n, -delta);
152                prefixSum -= delta;
153            }
154
155            /*
156             * Record the new position of x and add its contribution
157             * to all prefix sums from index i onward.
158             */
159            lastPos[x] = i;
160            segmentTree.modify(1, i, n, delta);
161            prefixSum += delta;
162
163            /*
164             * Find the smallest position pos such that
165             * prefixSum[pos] == prefixSum, meaning the subarray
166             * (pos, i] is balanced.
167             */
168            int pos = segmentTree.query(1, prefixSum);
169
170            /*
171             * Update the answer with the length of this balanced subarray.
172             */
173            ans = max(ans, i - pos);
174        }
175
176        return ans;
177    }
178};
179
1/**
2 * Segment tree node definition.
3 * Each node maintains the range [left, right], the minimum and maximum
4 * values within that range, and a lazy tag for pending range updates.
5 */
6interface SegmentTreeNode {
7    left: number;   // Left boundary of the range covered by this node
8    right: number;  // Right boundary of the range covered by this node
9    minVal: number; // Minimum value in this range
10    maxVal: number; // Maximum value in this range
11    lazy: number;   // Lazy propagation tag (pending addition)
12}
13
14// Global segment tree storage
15let tree: SegmentTreeNode[];
16
17/**
18 * Build the segment tree over the range [left, right].
19 * @param node  Current node index in the tree array
20 * @param left  Left boundary of the range
21 * @param right Right boundary of the range
22 */
23function build(node: number, left: number, right: number): void {
24    tree[node].left = left;
25    tree[node].right = right;
26    if (left === right) return;
27    const mid = (left + right) >> 1;
28    build(node << 1, left, mid);
29    build((node << 1) | 1, mid + 1, right);
30}
31
32/**
33 * Apply an addition of `val` to the given node:
34 * update its min/max values and accumulate the lazy tag.
35 * @param node Current node index
36 * @param val  Value to add
37 */
38function apply(node: number, val: number): void {
39    tree[node].minVal += val;
40    tree[node].maxVal += val;
41    tree[node].lazy += val;
42}
43
44/**
45 * Push the lazy tag of the current node down to its two children.
46 * @param node Current node index
47 */
48function pushdown(node: number): void {
49    if (tree[node].lazy !== 0) {
50        apply(node << 1, tree[node].lazy);
51        apply((node << 1) | 1, tree[node].lazy);
52        tree[node].lazy = 0;
53    }
54}
55
56/**
57 * Recompute the min/max of the current node from its two children.
58 * @param node Current node index
59 */
60function pushup(node: number): void {
61    tree[node].minVal = Math.min(tree[node << 1].minVal, tree[(node << 1) | 1].minVal);
62    tree[node].maxVal = Math.max(tree[node << 1].maxVal, tree[(node << 1) | 1].maxVal);
63}
64
65/**
66 * Range update: add `val` to every position in [left, right].
67 * @param node  Current node index
68 * @param left  Left boundary of the update range
69 * @param right Right boundary of the update range
70 * @param val   Value to add
71 */
72function modify(node: number, left: number, right: number, val: number): void {
73    // Current node range is fully covered by the update range
74    if (tree[node].left >= left && tree[node].right <= right) {
75        apply(node, val);
76        return;
77    }
78    pushdown(node);
79    const mid = (tree[node].left + tree[node].right) >> 1;
80    if (left <= mid) modify(node << 1, left, right, val);
81    if (right > mid) modify((node << 1) | 1, left, right, val);
82    pushup(node);
83}
84
85/**
86 * Find the leftmost position whose stored value equals `target`.
87 * The search prefers the left subtree whenever the target value
88 * lies within its [min, max] range.
89 * @param node   Current node index
90 * @param target Target value to locate
91 * @returns The leftmost position holding the target value
92 */
93function query(node: number, target: number): number {
94    // Reached a leaf: return its position
95    if (tree[node].left === tree[node].right) return tree[node].left;
96    pushdown(node);
97    // Go left if the target value can exist in the left subtree
98    if (tree[node << 1].minVal <= target && target <= tree[node << 1].maxVal) {
99        return query(node << 1, target);
100    }
101    return query((node << 1) | 1, target);
102}
103
104/**
105 * Compute the length of the longest balanced subarray, i.e. the longest
106 * subarray in which the number of distinct odd values equals the number
107 * of distinct even values.
108 *
109 * Idea:
110 * - Treat each distinct odd value as +1 and each distinct even value as -1.
111 * - For every prefix ending at index i, the segment tree position j stores
112 *   the "balance" of the subarray (j, i] considering only the latest
113 *   occurrence of each value.
114 * - When a value reappears, its previous contribution is cancelled on the
115 *   prefix range [lastIndex, n] so that only the newest occurrence counts.
116 * - A balanced subarray ending at i corresponds to a position j with
117 *   balance equal to the current total, so we query the leftmost such j.
118 *
119 * @param nums Input array of numbers
120 * @returns Length of the longest balanced subarray
121 */
122function longestBalanced(nums: number[]): number {
123    const n = nums.length;
124
125    // Initialize the segment tree nodes over positions [0, n]
126    tree = Array.from({ length: (n + 1) * 4 }, () => ({
127        left: 0,
128        right: 0,
129        minVal: 0,
130        maxVal: 0,
131        lazy: 0,
132    }));
133    build(1, 0, n);
134
135    // Map from value to the index (1-based) of its latest occurrence
136    const lastIndex = new Map<number, number>();
137    let currentSum = 0; // Current total balance over the whole prefix
138    let result = 0;     // Length of the longest balanced subarray found
139
140    nums.forEach((num, idx) => {
141        const i = idx + 1; // Convert to 1-based index
142        // Odd values contribute +1, even values contribute -1
143        const delta = num & 1 ? 1 : -1;
144
145        // If the value appeared before, cancel its previous contribution
146        if (lastIndex.has(num)) {
147            modify(1, lastIndex.get(num)!, n, -delta);
148            currentSum -= delta;
149        }
150
151        // Record the latest occurrence and add the new contribution
152        lastIndex.set(num, i);
153        modify(1, i, n, delta);
154        currentSum += delta;
155
156        // Find the leftmost position whose balance equals the current total;
157        // the subarray (position, i] is then balanced
158        const position = query(1, currentSum);
159        result = Math.max(result, i - position);
160    });
161
162    return result;
163}
164

Time and Space Complexity

  • Time Complexity: O(n log n), where n is the length of the array nums.

    • The loop enumerates each right endpoint, running n iterations in total.
    • In each iteration, the code performs at most two range update operations (modify) and one binary-search-style query (query) on the segment tree.
    • Each modify call recurses down the tree to a depth of O(log n), applying lazy tags and pushing values up/down along the path, so it costs O(log n) per call.
    • Each query call descends from the root to a leaf, choosing the left or right child based on whether target lies within [mn, mx] of the left subtree, which also takes O(log n).
    • Building the segment tree with build(1, 0, n) visits every node exactly once, costing O(n).
    • Therefore the total time is O(n) + n × O(log n) = O(n log n).
  • Space Complexity: O(n), where n is the length of the array nums.

    • The segment tree array tr allocates (n + 1) * 4 nodes, each with constant-size fields (l, r, mn, mx, lazy), occupying O(n) space.
    • The hash table last, which records the most recent index of each distinct value, stores at most n entries, taking O(n) space.
    • The recursion depth of build, modify, and query is bounded by the tree height O(log n), which is dominated by the O(n) terms above.

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

Common Pitfalls

Pitfall 1: Forgetting to revoke the previous occurrence of a duplicate value

This is the most frequent and most damaging mistake. The whole correctness of the prefix-balance invariant rests on each value contributing exactly once, at its latest occurrence. A tempting but wrong simplification looks like this:

for i, x in enumerate(nums, start=1):
    delta = 1 if (x & 1) else -1
    update(1, i, n, delta)          # WRONG: old occurrence still counted
    total_balance += delta
    pos = query(1, total_balance)
    ans = max(ans, i - pos)

With this version, an input like [2, 2, 3] is mishandled: the value 2 adds -1 twice, so the tree behaves as if there were two distinct evens. The query then looks for the wrong target and either misses the balanced subarray [2, 2, 3] (one distinct even, one distinct odd) or reports a bogus length.

Why it breaks: the invariant states that tree[j] equals the distinct balance of values whose last occurrence lies in [1, j]. If the old occurrence is not cancelled, a duplicate value is counted at both positions, so total_balance - tree[j] no longer equals the balance of subarray (j, i].

Fix: before inserting the current occurrence, undo the old suffix update covering [last_pos[x], n], effectively moving the value's contribution to its new position:

if x in last_pos:
    update(1, last_pos[x], n, -delta)   # cancel the stale contribution
    total_balance -= delta
last_pos[x] = i
update(1, i, n, delta)
total_balance += delta

Pitfall 2: Querying for 0 instead of total_balance

It is easy to think "a balanced subarray has balance 0, so search for the leftmost position holding 0." But tree[j] stores the prefix balance, not the subarray balance. The subarray (j, i] has balance total_balance - tree[j], so the correct search target is:

pos = query(1, total_balance)   # NOT query(1, 0)

Searching for 0 only works coincidentally when total_balance == 0, and it silently returns wrong answers otherwise (the test [2, 3, 2, 5] exposes this at i = 3, where total_balance = 0 happens to mask the bug — try [2, 3, 5, 4] instead).


Pitfall 3: Building the tree over [1, n] and dropping position 0

Position 0 represents the empty prefix and is the left boundary that yields subarrays starting at index 1 (i.e., the full prefix [1, i]). If the tree only covers [1, n]:

  • subarrays that start at the very beginning of the array can never be found, and
  • the query may even fail, because the target value total_balance is no longer guaranteed to exist anywhere in the tree.

Always build over [0, n]:

build(1, 0, n)

and never apply any element update to position 0 itself (updates start at i >= 1).


Pitfall 4: Missing push_down inside query

The query walks from the root to a leaf reading min_val / max_val of the children. If a node still carries a pending lazy tag, its children's aggregates are stale, and the descent direction (left vs. right child) can be chosen incorrectly — producing a non-leftmost position or an outright wrong leaf. The fix is to propagate before inspecting children:

def query(node, target):
    if tree[node].left == tree[node].right:
        return tree[node].left
    push_down(node)                       # essential — do not skip
    if tree[node << 1].min_val <= target <= tree[node << 1].max_val:
        return query(node << 1, target)
    return query(node << 1 | 1, target)

A related subtlety: the membership test min <= target <= max is only exact because every update is a ±1 range-add over a suffix, which keeps adjacent leaf values differing by at most 1 (an intermediate-value property). If you adapt this template to a problem with arbitrary deltas, this check becomes a necessary-but-not-sufficient condition and the query logic must be redesigned.


Pitfall 5: Recursion depth on large inputs

The recursive build, update, and query each descend O(log n) levels, which is safe, but build recurses into every node and Python's default recursion limit (1000) can be hit through deep call chains on very large n combined with other recursion in the program. Two safeguards:

import sys
sys.setrecursionlimit(2 * 10**5)

or convert build to an iterative initialization. Alternatively, since all initial values are 0, you can skip build entirely by setting boundaries lazily — but then tree[node].left/right must be computed from parameters passed down the recursion rather than read from the node.

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:

Which of the following shows the order of node visit in a Breadth-first Search?


Recommended Readings

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

Load More