3825. Longest Strictly Increasing Subsequence With Non-Zero Bitwise AND
Problem Description
You are given an integer array nums.
Your task is to find a subsequence of nums that satisfies two conditions at the same time:
- The subsequence must be strictly increasing. This means each element in the chosen subsequence is greater than the one before it.
- The bitwise AND of all elements in the subsequence must be non-zero. In other words, when you apply the
ANDoperation across every number in the subsequence, the result should not be0.
A subsequence is a sequence that can be derived from the array by deleting some or no elements without changing the order of the remaining elements. The subsequence must be non-empty.
Return the length of the longest such subsequence. If no subsequence meets both requirements, return 0.
Key points to understand:
-
The bitwise
ANDof a group of numbers is non-zero only if there exists at least one bit position where all the numbers have a1. For example, the numbers6(110),7(111), and14(1110) all share a1at bit position1and bit position2, so theirAND(110=6) is non-zero. -
You are combining a classic "longest strictly increasing subsequence" requirement with the extra constraint that all selected numbers must share a common
1bit.
How We Pick the Algorithm
Why Binary Search?
This problem maps to Binary Search through a short path in the full flowchart.
For each bit position, finding the longest strictly increasing subsequence among numbers that share that bit uses binary search for LIS construction.
Open in FlowchartIntuition
The challenge in this problem comes from the two conditions that must hold together: the subsequence has to be strictly increasing, and its bitwise AND must be non-zero. The increasing part alone is the well-known "Longest Increasing Subsequence" (LIS) problem. The tricky addition is the non-zero AND requirement.
Let's focus on what it means for the bitwise AND of a group of numbers to be non-zero. The AND operation keeps a bit set to 1 only if that bit is 1 in every number. So the final AND is non-zero if and only if there is at least one bit position where all the chosen numbers have a 1.
This observation is the key that unlocks the problem. Instead of trying to track the AND value while building the subsequence (which would be complicated), we can flip the perspective: rather than asking "what is the AND of this subsequence?", we ask "is there a common bit shared by every number?"
If we fix a single bit position i, then any subsequence made only of numbers that have a 1 at bit i automatically has a non-zero AND, because bit i survives the AND operation for all of them. This means once we restrict ourselves to numbers sharing bit i, the non-zero AND condition is guaranteed for free, and we only need to worry about finding the longest strictly increasing subsequence among those numbers.
So the plan becomes:
- Enumerate every possible bit position
i(from0up to the highest bit appearing innums). - For each bit
i, collect all numbers that have a1at that position, keeping their original order. - Run the standard LIS algorithm on this filtered list to get the longest valid subsequence sharing bit
i. - The answer is the maximum LIS length found across all bit positions.
Why is enumerating just single bits enough? Because if the optimal subsequence has a non-zero AND, that AND itself has at least one bit set to 1. That particular bit is shared by every element of the subsequence, so when we enumerate that exact bit, the entire optimal subsequence will be included in our filtered list and captured by the LIS computation. We never miss the best answer.
For the LIS part, we use the efficient patience-sorting style approach with binary search (bisect_left), maintaining an array g where g[j] holds the smallest possible tail value of an increasing subsequence of length j + 1. Using bisect_left ensures the subsequence stays strictly increasing, and the final length of g gives the LIS length for that filtered list.
Solution Approach
We use the pattern Enumeration + Longest Increasing Subsequence (LIS). The idea is to enumerate each bit position, filter the numbers that share that bit, and compute the LIS on the filtered list. Below is a step-by-step walk-through of the implementation.
Step 1: Determine how many bits we need to check
We only need to consider bit positions that actually appear in the data. The highest such position is bounded by the largest number in nums:
m = max(nums).bit_length()
Here m is the number of bits required to represent the maximum value. So valid bit positions range from 0 to m - 1.
Step 2: Enumerate each bit position
For each bit i in range(m), we build a filtered list arr containing only the numbers whose bit i is set to 1. The order of the original array is preserved, which is essential because subsequences must respect the original ordering:
arr = [x for x in nums if x >> i & 1]
The expression x >> i & 1 shifts x right by i positions and masks the lowest bit, giving 1 if bit i of x is set and 0 otherwise. Every number in this arr shares bit i, so any subsequence drawn from arr is guaranteed to have a non-zero bitwise AND.
Step 3: Compute the LIS on the filtered list
For each filtered list, we run the helper function lis, which uses the classic patience-sorting / greedy with binary search technique:
def lis(arr: List[int]) -> int:
g = []
for x in arr:
j = bisect_left(g, x)
if j == len(g):
g.append(x)
else:
g[j] = x
return len(g)
Here is how this works:
gis a list whereg[j]stores the smallest possible tail value of an increasing subsequence of lengthj + 1.- For each element
x, we usebisect_left(g, x)to find the leftmost positionjwherexcould be placed.- If
j == len(g), thenxis larger than every tail ing, so it extends the longest subsequence found so far, and we append it. - Otherwise,
xreplacesg[j], keeping the tail as small as possible so future numbers have a better chance of extending the sequence.
- If
- The use of
bisect_left(rather thanbisect_right) ensures the subsequence remains strictly increasing. Ifxequals an existing value,bisect_leftlands on that value's position and replaces it, preventing equal elements from being counted as part of the same increasing run. - The final length of
gequals the length of the longest strictly increasing subsequence.
Step 4: Take the maximum across all bits
We track the best LIS length found over all bit positions and return it:
ans = 0
for i in range(m):
arr = [x for x in nums if x >> i & 1]
ans = max(ans, lis(arr))
return ans
Since the optimal valid subsequence must share at least one common 1 bit, enumerating that bit will include every element of the optimal subsequence in arr, so the maximum is guaranteed to capture the correct answer. If no number has a particular bit set, arr is empty and lis returns 0, which naturally contributes nothing to the maximum.
Complexity Analysis
- Time complexity:
O(m × n × log n), wherenis the length ofnumsandmis the number of bits (at most around30for typical integer ranges). For each of thembit positions, buildingarrcostsO(n), and the LIS computation costsO(n × log n)due to the binary search per element. - Space complexity:
O(n)for the filtered listarrand thegarray used during the LIS computation.
Example Walkthrough
Let's trace through the solution with a small example:
nums = [3, 1, 6, 7, 4]
In binary, these numbers are:
| Number | Binary | bit 0 | bit 1 | bit 2 |
|---|---|---|---|---|
| 3 | 011 | 1 | 1 | 0 |
| 1 | 001 | 1 | 0 | 0 |
| 6 | 110 | 0 | 1 | 1 |
| 7 | 111 | 1 | 1 | 1 |
| 4 | 100 | 0 | 0 | 1 |
Step 1: Determine the number of bits
The maximum value is 7 (111), so m = max(nums).bit_length() = 3. We will check bit positions 0, 1, and 2.
Step 2 & 3: Enumerate each bit, filter, and run LIS
Bit 0 (rightmost bit) — keep numbers where x >> 0 & 1 == 1:
arr = [3, 1, 7] (numbers 3, 1, 7 all have bit 0 set)
Run LIS on [3, 1, 7]:
x = 3:g = [],bisect_leftgives0, equalslen(g), append →g = [3]x = 1:bisect_left([3], 1) = 0, replace →g = [1]x = 7:bisect_left([1], 7) = 1, equalslen(g), append →g = [1, 7]
LIS length = 2 (e.g., subsequence [3, 7] or [1, 7]). Both share bit 0, so their AND is non-zero (3 & 7 = 3, 1 & 7 = 1).
Bit 1 (middle bit) — keep numbers where x >> 1 & 1 == 1:
arr = [3, 6, 7] (numbers 3, 6, 7 all have bit 1 set)
Run LIS on [3, 6, 7]:
x = 3:g = []→ append →g = [3]x = 6:bisect_left([3], 6) = 1, append →g = [3, 6]x = 7:bisect_left([3, 6], 7) = 2, append →g = [3, 6, 7]
LIS length = 3 (subsequence [3, 6, 7]). Verify: 3 & 6 & 7 = 2, which is non-zero. ✓
Bit 2 (leftmost bit) — keep numbers where x >> 2 & 1 == 1:
arr = [6, 7, 4] (numbers 6, 7, 4 all have bit 2 set)
Run LIS on [6, 7, 4]:
x = 6:g = []→ append →g = [6]x = 7:bisect_left([6], 7) = 1, append →g = [6, 7]x = 4:bisect_left([6, 7], 4) = 0, replace →g = [4, 7]
LIS length = 2 (e.g., subsequence [6, 7]). Verify: 6 & 7 = 6, non-zero. ✓
Step 4: Take the maximum across all bits
| Bit | Filtered arr | LIS length |
|---|---|---|
| 0 | [3, 1, 7] | 2 |
| 1 | [3, 6, 7] | 3 |
| 2 | [6, 7, 4] | 2 |
The maximum is 3, achieved at bit position 1 with the subsequence [3, 6, 7].
Answer: 3
This confirms the core insight: by fixing a single shared bit (bit 1 here), every number in the filtered list automatically guarantees a non-zero AND, so we only needed to solve the standard strictly increasing subsequence problem on that filtered list.
Solution Implementation
1from typing import List
2from bisect import bisect_left
3
4
5class Solution:
6 def longestSubsequence(self, nums: List[int]) -> int:
7 # Compute the length of the Longest Non-Decreasing Subsequence (LIS variant).
8 # Uses patience-sorting: 'tails' holds the smallest possible tail
9 # value for an increasing subsequence of each length.
10 def longest_increasing_subsequence(arr: List[int]) -> int:
11 tails = []
12 for value in arr:
13 # Find the first position where 'value' can replace an existing tail.
14 pos = bisect_left(tails, value)
15 if pos == len(tails):
16 # 'value' extends the longest subsequence found so far.
17 tails.append(value)
18 else:
19 # Replace to keep the tail as small as possible for future extensions.
20 tails[pos] = value
21 return len(tails)
22
23 ans = 0
24 # Number of bits needed to represent the largest number in 'nums'.
25 max_bits = max(nums).bit_length()
26
27 # For each bit position, consider only the numbers that have that bit set,
28 # then compute the LIS over this filtered list.
29 for bit in range(max_bits):
30 filtered = [value for value in nums if value >> bit & 1]
31 ans = max(ans, longest_increasing_subsequence(filtered))
32
33 return ans
341class Solution {
2 /**
3 * Finds the length of the longest subsequence where every adjacent pair
4 * of elements shares at least one common set bit (i.e., their bitwise AND
5 * is non-zero). Since a subsequence chained by a common bit must have all
6 * "linked" elements sharing the SAME bit position when treated greedily,
7 * we examine each bit position independently and find the Longest
8 * Increasing Subsequence (LIS) among numbers that have that bit set.
9 *
10 * @param nums the input array
11 * @return the maximum length found across all bit positions
12 */
13 public int longestSubsequence(int[] nums) {
14 int ans = 0;
15
16 // Determine the maximum value to know how many bits we need to inspect.
17 int max = 0;
18 for (int x : nums) {
19 max = Math.max(max, x);
20 }
21
22 // Number of significant bits in the maximum value.
23 int bitCount = 32 - Integer.numberOfLeadingZeros(max);
24
25 // For each bit position, collect numbers having that bit set,
26 // then compute the LIS over that filtered list.
27 for (int bit = 0; bit < bitCount; bit++) {
28 List<Integer> filtered = new ArrayList<>();
29 for (int x : nums) {
30 // Keep the value only if its current bit is 1.
31 if (((x >> bit) & 1) == 1) {
32 filtered.add(x);
33 }
34 }
35 ans = Math.max(ans, lis(filtered));
36 }
37
38 return ans;
39 }
40
41 /**
42 * Computes the length of the Longest Increasing Subsequence (strictly
43 * increasing) using the patience-sorting / binary-search technique.
44 *
45 * @param arr the list of values, in their original relative order
46 * @return the length of the longest strictly increasing subsequence
47 */
48 private int lis(List<Integer> arr) {
49 // 'tails' holds the smallest possible tail value for an increasing
50 // subsequence of each length; it remains sorted in ascending order.
51 List<Integer> tails = new ArrayList<>();
52
53 for (int x : arr) {
54 // Binary search for the first element in 'tails' that is >= x.
55 int left = 0, right = tails.size();
56 while (left < right) {
57 int mid = (left + right) >>> 1;
58 if (tails.get(mid) >= x) {
59 right = mid;
60 } else {
61 left = mid + 1;
62 }
63 }
64
65 if (left == tails.size()) {
66 // x extends the longest subsequence found so far.
67 tails.add(x);
68 } else {
69 // Replace to keep the tail value minimal for that length.
70 tails.set(left, x);
71 }
72 }
73
74 return tails.size();
75 }
76}
771class Solution {
2public:
3 int longestSubsequence(vector<int>& nums) {
4 // Compute the length of the Longest Increasing Subsequence (strictly increasing)
5 // using the classic patience-sorting / binary-search technique in O(n log n).
6 auto computeLIS = [&](const vector<int>& arr) {
7 vector<int> tails; // tails[k] = smallest possible tail of an increasing subsequence of length k+1
8 for (int value : arr) {
9 // Find the first element >= value (strict LIS uses lower_bound).
10 auto it = lower_bound(tails.begin(), tails.end(), value);
11 if (it == tails.end()) {
12 // value extends the longest subsequence so far.
13 tails.push_back(value);
14 } else {
15 // Replace to keep the tail as small as possible.
16 *it = value;
17 }
18 }
19 return static_cast<int>(tails.size());
20 };
21
22 int ans = 0;
23
24 // Determine how many bits we need to inspect based on the maximum value.
25 int maxValue = ranges::max(nums);
26 int bitCount = (maxValue == 0) ? 0 : 32 - __builtin_clz(maxValue);
27
28 // For each bit position, gather the numbers whose i-th bit is set,
29 // then measure the LIS among those numbers. The best across all bits is the answer.
30 for (int i = 0; i < bitCount; ++i) {
31 vector<int> filtered;
32 ranges::copy_if(nums, back_inserter(filtered), [&](int x) {
33 return (x >> i) & 1; // keep numbers with the i-th bit set
34 });
35 ans = max(ans, computeLIS(filtered));
36 }
37
38 return ans;
39 }
40};
411/**
2 * Binary search equivalent of C++ std::lower_bound.
3 * Returns the index of the first element in `arr` that is >= `value`.
4 * If no such element exists, returns `arr.length`.
5 */
6function lowerBound(arr: number[], value: number): number {
7 let low = 0;
8 let high = arr.length;
9 while (low < high) {
10 const mid = (low + high) >> 1;
11 if (arr[mid] < value) {
12 // Element is too small; search the right half.
13 low = mid + 1;
14 } else {
15 // Element is a candidate; narrow toward the left.
16 high = mid;
17 }
18 }
19 return low;
20}
21
22/**
23 * Compute the length of the Longest Increasing Subsequence (strictly increasing)
24 * using the patience-sorting / binary-search technique in O(n log n).
25 */
26function computeLIS(arr: number[]): number {
27 // tails[k] = smallest possible tail of an increasing subsequence of length k + 1
28 const tails: number[] = [];
29 for (const value of arr) {
30 // Find the first element >= value (strict LIS uses lower_bound).
31 const idx = lowerBound(tails, value);
32 if (idx === tails.length) {
33 // value extends the longest subsequence so far.
34 tails.push(value);
35 } else {
36 // Replace to keep the tail as small as possible.
37 tails[idx] = value;
38 }
39 }
40 return tails.length;
41}
42
43/**
44 * For each bit position, gather the numbers whose i-th bit is set,
45 * then measure the LIS among those numbers. The best across all bits is the answer.
46 */
47function longestSubsequence(nums: number[]): number {
48 let ans = 0;
49
50 // Determine how many bits we need to inspect based on the maximum value.
51 const maxValue = Math.max(...nums);
52 // Number of significant bits; if maxValue is 0 there is nothing to inspect.
53 const bitCount = maxValue === 0 ? 0 : 32 - Math.clz32(maxValue);
54
55 for (let i = 0; i < bitCount; i++) {
56 // Keep only the numbers with the i-th bit set.
57 const filtered = nums.filter((x) => ((x >> i) & 1) === 1);
58 // Track the best LIS length found across all bit positions.
59 ans = Math.max(ans, computeLIS(filtered));
60 }
61
62 return ans;
63}
64Time and Space Complexity
Time Complexity: O(log M × n × log n)
The algorithm works as follows:
- The outer loop iterates over each bit position from
0tom - 1, wherem = max(nums).bit_length(). This givesO(log M)iterations, whereMis the maximum value in the array. - Within each iteration, building the filtered array
arr(containing elements whosei-th bit is set) takesO(n)time, wherenis the length of the array. - For each filtered array, the
lisfunction computes the longest increasing subsequence using binary search (bisect_left). Processing each of the up tonelements requires anO(log n)binary search, so each call tolistakesO(n × log n).
Therefore, the total time complexity is O(log M × (n + n × log n)) = O(log M × n × log n).
Space Complexity: O(n)
The space usage is dominated by:
- The filtered array
arr, which holds at mostnelements. - The auxiliary list
gused in thelisfunction, which holds at mostnelements.
Both require O(n) space, so the overall space complexity is O(n). Here, n and M are the length of the array and the maximum value in the array, respectively.
Common Pitfalls
Pitfall 1: Using bisect_right instead of bisect_left, breaking the strictly increasing requirement
The most common mistake is treating this like a non-decreasing subsequence problem and reaching for bisect_right. The problem explicitly requires a strictly increasing subsequence, so duplicate values must not be counted within the same run.
Why it's wrong: Consider arr = [3, 3, 3] (all share some bit). With bisect_right, each 3 would find an insertion position past the previous 3, causing tails to grow to length 3 — incorrectly reporting an "increasing" subsequence [3, 3, 3]. The correct strictly increasing answer is 1.
# WRONG — counts equal elements as increasing pos = bisect_right(tails, value) # [3, 3, 3] -> returns 3 # CORRECT — equal elements replace in place, length stays 1 pos = bisect_left(tails, value) # [3, 3, 3] -> returns 1
With bisect_left, when value equals an existing tail, it lands on that value's index and overwrites it rather than appending, so equal elements never extend the sequence.
Pitfall 2: Forgetting that a single element is a valid answer
A subsequence of length 1 is always strictly increasing, and its bitwise AND is just the element itself. As long as any number in nums is non-zero, the answer is at least 1.
Why this matters: A naive solution that only looks for subsequences of length ≥ 2 (e.g., comparing pairs) would wrongly return 0 for input like nums = [8]. The bit-enumeration approach handles this automatically: the bit set in 8 produces filtered = [8], and lis([8]) returns 1. Just make sure not to add a manual filter that excludes single-element results.
# nums = [8] -> bit 3 gives filtered = [8] -> lis returns 1 ✓
Pitfall 3: Calling max(nums) on a potentially empty array, or mishandling all-zero input
max(nums).bit_length() assumes nums is non-empty. While the constraints usually guarantee at least one element, two edge cases deserve attention:
- All elements are
0:max(nums)is0, and0.bit_length()is0, somax_bits = 0, the loop never runs, and the function returns0. This is actually correct — any subsequence containing only zeros has an AND of0— but it's worth verifying the loop body genuinely never executes rather than assuming it does.
# nums = [0, 0, 0] -> max_bits = 0 -> loop skipped -> returns 0 ✓
- Empty
nums:max([])raisesValueError. If empty input is possible, guard against it:
if not nums:
return 0
max_bits = max(nums).bit_length()
Pitfall 4: Misunderstanding why enumerating bits independently is sufficient
It can seem suspicious to compute the LIS per bit and take the maximum, rather than considering combinations of shared bits. The key insight: a subsequence has a non-zero AND if and only if there exists at least one bit position set in every chosen element.
Why independent enumeration is correct: The optimal valid subsequence shares some common bit b. When the loop reaches bit b, all elements of that optimal subsequence appear in filtered (in original order), so its full length is captured by lis(filtered). You never need to reason about multiple shared bits simultaneously — guaranteeing one shared bit is enough for a non-zero AND, and the max over all bits covers every possibility.
A flawed alternative — requiring elements to share their entire bit pattern, or intersecting bits greedily as you build the sequence — overcomplicates the logic and produces incorrect (too short) results.
Ready to land your dream job?
Unlock your dream job with a 5-minute quiz for a personalized study roadmap!
Get My RoadmapWhat's the relationship between a tree and a graph?
Recommended Readings
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 If you prefer videos here's a video that explains recursion in a fun and easy way 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 him
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity describes how the time needed
Want a Structured Path to Master System Design Too? Don’t Miss This!