2401. Longest Nice Subarray
Problem Description
You have an array nums
containing positive integers.
A subarray is called nice if for every pair of elements at different positions in the subarray, their bitwise AND equals 0
.
Your task is to find the length of the longest nice subarray.
Key points to understand:
- A subarray must be contiguous (consecutive elements from the original array)
- For a subarray to be nice, ANY two elements from different positions must have bitwise AND =
0
- A subarray of length
1
is always considered nice (since there are no pairs to check)
For example, if we have elements a
, b
, and c
in a subarray, for it to be nice:
a & b = 0
a & c = 0
b & c = 0
This means that for any bit position, at most one element in the nice subarray can have a 1
at that position. If two or more elements have a 1
at the same bit position, their bitwise AND would not be 0
.
The solution uses a sliding window approach with two pointers. It maintains a mask
variable that tracks the bitwise OR of all elements in the current window. When adding a new element x
, if mask & x != 0
, it means x
shares some bit positions with existing elements in the window, so we need to shrink the window from the left until no bits overlap. The mask
is updated by XORing out elements when removing them from the left, and ORing in new elements when adding them from the right.
Intuition
The key insight is understanding what makes a subarray "nice". For any two numbers to have a bitwise AND of 0
, they cannot share any 1
bits at the same position.
Think about it this way: if number a
has a 1
at bit position 3, and number b
also has a 1
at bit position 3, then a & b
will have a 1
at position 3, making it non-zero. So for a subarray to be nice, each bit position can have at most one number with a 1
at that position.
This observation leads us to a crucial realization: we can track all the "occupied" bit positions in our current subarray using a single variable. If we OR all numbers in our subarray together, the result tells us exactly which bit positions are currently occupied by at least one 1
.
For example, if we have [5, 2]
where 5 = 101
and 2 = 010
in binary, their OR gives us 111
, showing that bit positions 0, 1, and 2 are occupied.
Now, when we try to add a new number to our subarray, we can quickly check if it conflicts with existing numbers by doing mask & new_number
. If this result is non-zero, it means the new number has 1
s at positions that are already occupied - a conflict!
This naturally suggests a sliding window approach:
- Expand the window by adding elements from the right
- If a new element conflicts with the current window (shares bit positions), shrink from the left until there's no conflict
- Keep track of the maximum window size achieved
The beauty of using OR for the mask is that when we remove an element from the left, we can update the mask by XORing it out (since we know that element's bits were unique in the window). When adding to the right, we OR it in to mark its bit positions as occupied.
Learn more about Sliding Window patterns.
Solution Approach
The solution uses a two-pointer sliding window technique with bit manipulation to efficiently find the longest nice subarray.
Variables Used:
l
andr
: Left and right pointers defining our sliding windowmask
: Tracks all occupied bit positions in the current window using bitwise ORans
: Stores the maximum window size found so far
Algorithm Steps:
-
Initialize variables: Start with
ans = mask = l = 0
. The right pointerr
will be controlled by the enumerate function. -
Expand window from right: For each element
x
at positionr
:- Check if
mask & x
is non-zero (conflict exists)
- Check if
-
Shrink window from left when needed:
while mask & x: mask ^= nums[l] l += 1
- If
mask & x != 0
, the new element shares bit positions with existing elements - Remove elements from the left by XORing them out of the mask:
mask ^= nums[l]
- Move left pointer forward:
l += 1
- Continue until no conflict exists (
mask & x == 0
)
- If
-
Add new element to window:
mask |= x
- OR the new element into the mask to mark its bit positions as occupied
-
Update maximum length:
ans = max(ans, r - l + 1)
- Calculate current window size as
r - l + 1
- Keep track of the maximum size seen
- Calculate current window size as
Why XOR for removal and OR for addition?
- When adding:
mask |= x
sets all bits that are1
inx
to1
in the mask - When removing:
mask ^= nums[l]
flips the bits ofnums[l]
in the mask. Since we knownums[l]
's bits are unique in the window (no overlap with other elements), XORing effectively removes only its bits from the mask
Time Complexity: O(n)
where n
is the length of the array. Each element is visited at most twice (once by right pointer, once by left pointer).
Space Complexity: O(1)
as we only use a constant amount of extra variables.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with nums = [5, 2, 8, 1]
In binary:
5 = 101
2 = 010
8 = 1000
1 = 001
Initial state: l = 0, mask = 0, ans = 0
Step 1: r = 0, x = 5 (binary: 101)
- Check:
mask & 5 = 0 & 101 = 0
(no conflict) - Add to window:
mask = 0 | 101 = 101
- Update answer:
ans = max(0, 0-0+1) = 1
- Window:
[5]
, mask shows bits 0 and 2 are occupied
Step 2: r = 1, x = 2 (binary: 010)
- Check:
mask & 2 = 101 & 010 = 0
(no conflict) - Add to window:
mask = 101 | 010 = 111
- Update answer:
ans = max(1, 1-0+1) = 2
- Window:
[5, 2]
, mask shows bits 0, 1, and 2 are occupied
Step 3: r = 2, x = 8 (binary: 1000)
- Check:
mask & 8 = 111 & 1000 = 0
(no conflict) - Add to window:
mask = 111 | 1000 = 1111
- Update answer:
ans = max(2, 2-0+1) = 3
- Window:
[5, 2, 8]
, mask shows bits 0, 1, 2, and 3 are occupied
Step 4: r = 3, x = 1 (binary: 001)
- Check:
mask & 1 = 1111 & 001 = 1
(conflict! bit 0 is shared) - Shrink window:
- Remove
nums[0] = 5
:mask = 1111 ^ 101 = 1010
- Move left:
l = 1
- Check again:
mask & 1 = 1010 & 001 = 0
(no more conflict)
- Remove
- Add to window:
mask = 1010 | 001 = 1011
- Update answer:
ans = max(3, 3-1+1) = 3
- Window:
[2, 8, 1]
Final answer: 3 (the longest nice subarray is [5, 2, 8]
with length 3)
Notice how when we encountered 1
, it conflicted with 5
(both have bit 0 set). By removing 5
from the left, we resolved the conflict and could include 1
in our window.
Solution Implementation
1class Solution:
2 def longestNiceSubarray(self, nums: List[int]) -> int:
3 """
4 Find the length of the longest subarray where no two elements share any common bits.
5 A subarray is "nice" if for every pair of elements in it, their bitwise AND equals 0.
6
7 Args:
8 nums: List of non-negative integers
9
10 Returns:
11 Length of the longest nice subarray
12 """
13 # Initialize variables
14 max_length = 0 # Maximum length of nice subarray found so far
15 bit_mask = 0 # Bitmask tracking all set bits in current window
16 left = 0 # Left pointer of sliding window
17
18 # Iterate through array with right pointer
19 for right, current_num in enumerate(nums):
20 # Check if current number shares any bits with existing window
21 # If yes, shrink window from left until no conflict
22 while bit_mask & current_num:
23 # Remove leftmost number from window by XORing its bits out
24 bit_mask ^= nums[left]
25 left += 1
26
27 # Add current number to window by ORing its bits into mask
28 bit_mask |= current_num
29
30 # Update maximum length if current window is longer
31 current_length = right - left + 1
32 max_length = max(max_length, current_length)
33
34 return max_length
35
1class Solution {
2 public int longestNiceSubarray(int[] nums) {
3 int maxLength = 0; // Maximum length of nice subarray found
4 int bitMask = 0; // Bitmask to track which bits are currently set in the window
5 int left = 0; // Left pointer of the sliding window
6
7 // Iterate through the array with right pointer
8 for (int right = 0; right < nums.length; right++) {
9 // Shrink window from left while there's bit conflict with nums[right]
10 // If (bitMask & nums[right]) != 0, it means nums[right] shares at least one bit with existing numbers
11 while ((bitMask & nums[right]) != 0) {
12 // Remove nums[left] from the bitmask and move left pointer forward
13 bitMask ^= nums[left];
14 left++;
15 }
16
17 // Add nums[right] to the bitmask (all numbers in window have no common bits)
18 bitMask |= nums[right];
19
20 // Update maximum length of nice subarray
21 maxLength = Math.max(maxLength, right - left + 1);
22 }
23
24 return maxLength;
25 }
26}
27
1class Solution {
2public:
3 int longestNiceSubarray(vector<int>& nums) {
4 int maxLength = 0; // Maximum length of nice subarray found
5 int bitMask = 0; // Bitmask to track which bits are currently set in the window
6
7 // Use sliding window with two pointers
8 int left = 0;
9 for (int right = 0; right < nums.size(); ++right) {
10 // If nums[right] shares any set bits with current mask,
11 // shrink window from left until no bits overlap
12 while (bitMask & nums[right]) {
13 // Remove nums[left] from the bitmask using XOR
14 bitMask ^= nums[left];
15 left++;
16 }
17
18 // Add nums[right] to the bitmask using OR
19 bitMask |= nums[right];
20
21 // Update maximum length if current window is larger
22 maxLength = max(maxLength, right - left + 1);
23 }
24
25 return maxLength;
26 }
27};
28
1/**
2 * Finds the length of the longest subarray where no two elements have common set bits.
3 * A "nice" subarray means all pairs of elements have bitwise AND equal to 0.
4 *
5 * @param nums - Array of positive integers
6 * @returns The length of the longest nice subarray
7 */
8function longestNiceSubarray(nums: number[]): number {
9 // Maximum length of nice subarray found so far
10 let maxLength: number = 0;
11
12 // Bitmask to track all set bits in current window
13 // If a bit is set in mask, it means some number in current window has that bit set
14 let currentBitmask: number = 0;
15
16 // Sliding window with left and right pointers
17 let leftPointer: number = 0;
18
19 for (let rightPointer: number = 0; rightPointer < nums.length; rightPointer++) {
20 // Check if current number shares any set bits with numbers in window
21 // If yes, shrink window from left until no common bits exist
22 while ((currentBitmask & nums[rightPointer]) !== 0) {
23 // Remove leftmost number from bitmask using XOR
24 currentBitmask ^= nums[leftPointer];
25 leftPointer++;
26 }
27
28 // Add current number to the window by setting its bits in mask
29 currentBitmask |= nums[rightPointer];
30
31 // Update maximum length if current window is larger
32 const currentWindowLength: number = rightPointer - leftPointer + 1;
33 maxLength = Math.max(maxLength, currentWindowLength);
34 }
35
36 return maxLength;
37}
38
Time and Space Complexity
Time Complexity: O(n)
The algorithm uses a sliding window approach with two pointers (l
and r
). The right pointer r
iterates through each element in the array exactly once via the enumerate(nums)
loop. While there is an inner while
loop that moves the left pointer l
, each element can only be added to and removed from the window once. Specifically, each element enters the window when r
reaches it (via mask |= x
) and exits at most once when l
passes it (via mask ^= nums[l]
). Since each of the n
elements is processed at most twice (once when entering and once when leaving the window), the total number of operations is bounded by 2n
, giving us a time complexity of O(n)
, where n
is the length of the array nums
.
Space Complexity: O(1)
The algorithm uses only a constant amount of extra space. The variables ans
, mask
, l
, r
, and x
are all single integers that don't scale with the input size. The mask
variable stores a bitwise OR of the current window elements, but since it's just a single integer (not an array or data structure that grows with input), it requires O(1)
space regardless of the input size.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Bit Removal Using OR Instead of XOR
A common mistake is trying to remove elements from the bit mask using subtraction or OR operations instead of XOR.
Incorrect approach:
while mask & x: mask -= nums[l] # Wrong! Arithmetic subtraction doesn't work for bit removal # OR mask |= ~nums[l] # Wrong! This sets unwanted bits l += 1
Why this fails:
- Arithmetic subtraction doesn't properly remove individual bits from a bitmask
- Using OR with complement sets all bits except those in nums[l], corrupting the mask
Correct approach:
while mask & x: mask ^= nums[l] # Correct! XOR flips the bits, effectively removing them l += 1
2. Misunderstanding the "Nice" Condition
Some might think checking adjacent elements is sufficient, but the condition requires ALL pairs in the subarray to have AND = 0.
Incorrect understanding:
# Wrong! Only checking consecutive elements
for i in range(len(nums) - 1):
if nums[i] & nums[i+1] == 0:
# extend subarray
Correct understanding: Every element in the nice subarray must have completely disjoint bit positions from every other element. The mask tracks ALL occupied bit positions across the entire window.
3. Off-by-One Error in Length Calculation
Common mistake:
ans = max(ans, r - l) # Wrong! Missing the +1
Correct calculation:
ans = max(ans, r - l + 1) # Correct! Inclusive count
The length should be right - left + 1
because both endpoints are inclusive. For example, a subarray from index 2 to 4 has length 3 (indices 2, 3, 4), not 2.
4. Not Handling Single Elements
Forgetting that a single element always forms a nice subarray.
Incomplete solution:
if len(nums) == 1:
return 1 # Unnecessary special case
The sliding window approach naturally handles this - when the window contains just one element, r - l + 1 = 1
, which is correct.
A heap is a ...?
Recommended Readings
https assets algo monster cover_photos stack svg Sliding Window Maximum Monotonic Stack We have an array and a sliding window defined by a start index and an end index The sliding window moves from left of the array to right There are always k elements in the window The window
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!