3721. Longest Balanced Subarray II
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 value2appears 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 length3. - If no balanced subarray exists (e.g.,
nums = [1, 1, 1], which contains only odd numbers), the answer is0.
The goal is to find the maximum possible length among all such balanced subarrays.
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.
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 FlowchartIntuition
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
xat indexi, it adds its contribution (+1if odd,-1if even) to all prefix sums at positionsi, i+1, \ldots, n. - But if
xappeared before at positionlast[x], that earlier occurrence's contribution must be revoked for the range starting atlast[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
±1on a contiguous range, the set of values within any interval is "dense" — every integer between the interval's min and max actually occurs. Therefore, checkingmn <= now <= mxis enough to know whether the target value exists in a subtree, which lets us binary search on the tree for the leftmost position holding valuenowinO(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): addvto every position in[l, r](range addition with lazy propagation);query(u, target): find the leftmost position whose value equalstarget.
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 positioni(orn) in the tree.
For each element x, let its contribution be:
det = +1 if x is odd det = -1 if x is even
Then:
- Revoke the previous occurrence. If
xappeared before atlast[x], its old contribution applied to all positions>= last[x]. Since only one occurrence should count per subarray, undo it withmodify(1, last[x], n, -det)and updatenow -= det. Conceptually, this transfers the contribution ofxfrom its old position to the new one. - Add the current occurrence. Record
last[x] = i, then applymodify(1, i, n, det)and updatenow += det. Now, for every left boundarypos, the value at positionposin the tree is exactly the balance of the subarray(pos, i]... rather, the prefix value such that the subarray balance isnow - value(pos). - Query the earliest matching prefix. The subarray
(pos, i]is balanced precisely when the tree value atposequalsnow. Callpos = 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 costingO(log n). - Space complexity:
O(n)for the segment tree (about4(n+1)nodes) and the hash tablelast.
Walkthrough on nums = [2, 3, 2, 5]
i | x | action | now | query(now) → pos | i - pos |
|---|---|---|---|---|---|
| 1 | 2 | add -1 on [1,4] | -1 | leftmost -1 is 1 | 0 |
| 2 | 3 | add +1 on [2,4] | 0 | leftmost 0 is 0 | 2 |
| 3 | 2 | revoke -1 at [1,4], add -1 on [3,4] | 0 | leftmost 0 is 0 | 3 |
| 4 | 5 | add +1 on [4,4] | 1 | leftmost 1 is 4 | 0 |
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)
2has not been seen before, so there is nothing to revoke.- Record
last[2] = 1, applymodify([1, 4], -1), and setnow = -1.
value = [0, -1, -1, -1, -1], now = -1
query(-1)→ leftmost position with value-1ispos = 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, applymodify([2, 4], +1), and setnow = 0.
value = [0, -1, 0, 0, 0], now = 0
query(0)→ during the descent, the left subtree reportsmn = -1, mx = 0, and since values change by±1between neighbors,0is guaranteed to exist there — we descend left and land onpos = 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
2was seen atlast[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
- Revoke:
- Notice what this accomplished: position
1no longer "remembers" the old2, because for any subarray starting atpos = 1orpos = 2, the second2at index3is the one that counts — and it must count only once. query(0)→ leftmost position with value0ispos = 0.ans = max(2, 3 - 0) = 3. Subarray(0, 3] = [2, 3, 2]: distinct evens{2}, distinct odds{3}— sizes1 = 1. Balanced ✓ Length3.
Step i = 4, x = 5 (odd, det = +1)
- First occurrence of
5; recordlast[5] = 4, applymodify([4, 4], +1),now = 1.
value = [0, 0, 1, 0, 1], now = 1
query(1)→ leftmost position with value1ispos = 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
i | x | Tree updates | value[0..4] after updates | now | pos | i - pos | ans |
|---|---|---|---|---|---|---|---|
| 1 | 2 | [1,4] -1 | [0, -1, -1, -1, -1] | -1 | 1 | 0 | 0 |
| 2 | 3 | [2,4] +1 | [0, -1, 0, 0, 0] | 0 | 0 | 2 | 2 |
| 3 | 2 | revoke [1,4] +1, then [3,4] -1 | [0, 0, 1, 0, 0] | 0 | 0 | 3 | 3 |
| 4 | 5 | [4,4] +1 | [0, 0, 1, 0, 1] | 1 | 2 | 2 | 3 |
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
1171import 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}
1751/*
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};
1791/**
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}
164Time and Space Complexity
-
Time Complexity:
O(n log n), wherenis the length of the arraynums.- The loop enumerates each right endpoint, running
niterations 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
modifycall recurses down the tree to a depth ofO(log n), applying lazy tags and pushing values up/down along the path, so it costsO(log n)per call. - Each
querycall descends from the root to a leaf, choosing the left or right child based on whethertargetlies within[mn, mx]of the left subtree, which also takesO(log n). - Building the segment tree with
build(1, 0, n)visits every node exactly once, costingO(n). - Therefore the total time is
O(n) + n × O(log n) = O(n log n).
- The loop enumerates each right endpoint, running
-
Space Complexity:
O(n), wherenis the length of the arraynums.- The segment tree array
trallocates(n + 1) * 4nodes, each with constant-size fields (l,r,mn,mx,lazy), occupyingO(n)space. - The hash table
last, which records the most recent index of each distinct value, stores at mostnentries, takingO(n)space. - The recursion depth of
build,modify, andqueryis bounded by the tree heightO(log n), which is dominated by theO(n)terms above.
- The segment tree array
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_balanceis 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 RoadmapWhich of the following shows the order of node visit in a Breadth-first Search?

Recommended Readings
Segment Tree Introduction A segment tree stores information about intervals of an array It supports two operations efficiently update one element and query an aggregate value over a contiguous range In this introduction the aggregate is sum so the operations are update k value and sum arr left right The
https assets algo monster cover_photos dnd svg Divide and Conquer Divide and conquer solves one large problem by repeatedly breaking it into smaller subproblems of the same type solving those subproblems then combining their answers A common template is split solve recursively then merge Merge sort is a classic example
Prefix Sum Technique Explained Prefix Sum A prefix sum transforms an array into a new array of running totals For an input array arr define prefix k as the sum of all elements before index k prefix 0 0 prefix 1 arr 0 prefix 2 arr 0 arr 1 and so on The power
Want a Structured Path to Master System Design Too? Don’t Miss This!