Facebook Pixel

3825. Longest Strictly Increasing Subsequence With Non-Zero Bitwise AND

Medium
LeetCode ↗

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:

  1. The subsequence must be strictly increasing. This means each element in the chosen subsequence is greater than the one before it.
  2. The bitwise AND of all elements in the subsequence must be non-zero. In other words, when you apply the AND operation across every number in the subsequence, the result should not be 0.

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 AND of a group of numbers is non-zero only if there exists at least one bit position where all the numbers have a 1. For example, the numbers 6 (110), 7 (111), and 14 (1110) all share a 1 at bit position 1 and bit position 2, so their AND (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 1 bit.

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

How We Pick the Algorithm

Why Binary Search?

This problem maps to Binary Search through a short path in the full flowchart.

Sortedinput orbinaryyesSubsequenceproblem?yesBinary Search

For each bit position, finding the longest strictly increasing subsequence among numbers that share that bit uses binary search for LIS construction.

Open in Flowchart

Intuition

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 (from 0 up to the highest bit appearing in nums).
  • For each bit i, collect all numbers that have a 1 at 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:

  • g is a list where g[j] stores the smallest possible tail value of an increasing subsequence of length j + 1.
  • For each element x, we use bisect_left(g, x) to find the leftmost position j where x could be placed.
    • If j == len(g), then x is larger than every tail in g, so it extends the longest subsequence found so far, and we append it.
    • Otherwise, x replaces g[j], keeping the tail as small as possible so future numbers have a better chance of extending the sequence.
  • The use of bisect_left (rather than bisect_right) ensures the subsequence remains strictly increasing. If x equals an existing value, bisect_left lands 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 g equals 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), where n is the length of nums and m is the number of bits (at most around 30 for typical integer ranges). For each of the m bit positions, building arr costs O(n), and the LIS computation costs O(n × log n) due to the binary search per element.
  • Space complexity: O(n) for the filtered list arr and the g array 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:

NumberBinarybit 0bit 1bit 2
3011110
1001100
6110011
7111111
4100001

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_left gives 0, equals len(g), append → g = [3]
  • x = 1: bisect_left([3], 1) = 0, replace → g = [1]
  • x = 7: bisect_left([1], 7) = 1, equals len(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

BitFiltered arrLIS 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
34
1class 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}
77
1class 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};
41
1/**
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}
64

Time 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 0 to m - 1, where m = max(nums).bit_length(). This gives O(log M) iterations, where M is the maximum value in the array.
  • Within each iteration, building the filtered array arr (containing elements whose i-th bit is set) takes O(n) time, where n is the length of the array.
  • For each filtered array, the lis function computes the longest increasing subsequence using binary search (bisect_left). Processing each of the up to n elements requires an O(log n) binary search, so each call to lis takes O(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 most n elements.
  • The auxiliary list g used in the lis function, which holds at most n elements.

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) is 0, and 0.bit_length() is 0, so max_bits = 0, the loop never runs, and the function returns 0. This is actually correct — any subsequence containing only zeros has an AND of 0 — 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([]) raises ValueError. 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 Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

What's the relationship between a tree and a graph?


Recommended Readings

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

Load More