Facebook Pixel

3858. Minimum Bitwise OR From Grid

Medium
LeetCode ↗

Problem Description

You are given a 2D integer array grid of size m x n.

You must select exactly one integer from each row of the grid.

Return an integer denoting the minimum possible bitwise OR of the selected integers from each row.

 

In other words, you have a grid with m rows and n columns. From each of the m rows, you pick a single element. After picking one element per row, you compute the bitwise OR of all the chosen elements. Among all possible ways of selecting one element from each row, your task is to find the selection that produces the smallest possible bitwise OR value, and return that value.

For example, if the grid has rows [1, 6, 3], [4, 2, 9], and [7, 2, 8], you must pick exactly one number from each of the three rows, then OR them together. You want to choose the combination whose OR result is as small as possible.

Recall that the bitwise OR operation (|) sets a bit to 1 if that bit is 1 in any of the operands. Because OR can only turn bits on (never off), adding more numbers can only keep the result the same or make it larger. The challenge is to balance the choices across all rows so that the fewest high-value bits get set in the final combined result.

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

How We Pick the Algorithm

Why Greedy Algorithms?

This problem maps to Greedy Algorithms through a short path in the full flowchart.

Max/minoptimizationproblem?yesGreedyapproachworks?yesGreedyAlgorithms

Greedy bit-by-bit construction from high to low, keeping each bit off if any per-row choice allows it.

Open in Flowchart

Intuition

The key observation is that the bitwise OR operation can only turn bits on, never off. This means that once a bit appears in the result, it stays there no matter what else we OR. Since higher bits contribute much larger values than lower bits (a single high bit can outweigh all the lower bits combined), our top priority should always be to avoid setting the highest possible bits first.

This naturally leads to a greedy, bit-by-bit decision from the most significant bit down to the least significant bit. For each bit position, we ask: "Is it possible to construct a valid selection (one element per row) such that this bit, together with all the higher bits we've already committed to keeping as 0, stays 0?" If yes, we greedily keep this bit at 0, because doing so reduces the answer more than anything we could save on the lower bits later. If no, we are forced to accept this bit as 1 and move on.

To check whether a bit can stay 0, we track the bits we are trying to keep off. Let ans hold the bits we've already been forced to set to 1. When testing bit i, we form a mask = ans | ((1 << i) - 1). This mask represents all the bits we are allowed to have set: the bits already in ans, plus every bit below i (since we haven't decided those yet, we let them be free). The bits we're trying to keep 0 are exactly the ones not in this mask, which includes bit i and any higher bits we previously chose to keep 0.

Now for each row, we need at least one element x that doesn't violate this requirement — meaning x uses only the allowed bits, expressed as (x | mask) == mask. If every row can supply such an element, then bit i can safely remain 0, so we leave ans unchanged. But if any row has no valid element, then no matter how we pick, that bit (or a forbidden higher bit) must turn on, so we are forced to set bit i in ans with ans |= 1 << i.

By processing bits from high to low and locking in the smallest achievable value at each step, the greedy choices combine to give the globally minimum possible bitwise OR. The reason greedy works here is the dominance of higher bits: saving one high bit is always worth more than saving any combination of lower bits, so committing to the best high-bit decision first never causes us to miss a better overall answer.

Solution Approach

The implementation uses a greedy bitwise construction pattern, building the answer one bit at a time from the most significant bit to the least significant. Below is a step-by-step walkthrough of how the code works.

Step 1: Determine how many bits we need to consider.

mx = max(map(max, grid))
m = mx.bit_length()

We first find the largest value mx across the entire grid using max(map(max, grid)). The number of bits we need to examine is mx.bit_length(), since no element in the grid can have a bit set beyond this position. This bounds the range of bit positions we iterate over.

Step 2: Initialize the answer.

ans = 0

We start with ans = 0, representing the optimistic assumption that we can keep every bit off. We will only set bits in ans when we are forced to.

Step 3: Iterate over bit positions from high to low.

for i in range(m - 1, -1, -1):
    mask = ans | ((1 << i) - 1)

For each bit position i (starting from the highest), we build a mask that represents the allowed bits:

  • ans contributes the bits we have already been forced to set to 1.
  • (1 << i) - 1 contributes all bits below position i, which we leave free since they are decided later.

The bits not in mask are the ones we are currently trying to keep 0 — namely bit i and any higher bits we previously committed to as 0.

Step 4: Check whether each row can satisfy the mask.

for row in grid:
    found = False
    for x in row:
        if (x | mask) == mask:
            found = True
            break
    if not found:
        ans |= 1 << i
        break

For every row, we look for at least one element x that uses only the allowed bits. The condition (x | mask) == mask is true exactly when x has no bits outside of mask, meaning picking x would not turn on bit i or any forbidden higher bit. As soon as we find such an element, we mark found = True and stop scanning that row.

If any row fails to provide a valid element (found stays False), it is impossible to keep bit i off, so we set it with ans |= 1 << i and break out early — there's no need to check the remaining rows for this bit since the decision is already made.

Step 5: Return the result.

return ans

After processing all bit positions, ans holds exactly the bits that had to be 1, which is the minimum possible bitwise OR.

Complexity Analysis

  • Time complexity: O(B * m * n), where B is the number of bits (about 30 for typical integer ranges), and m * n is the total number of grid elements. For each of the B bit positions, we may scan every element of the grid.
  • Space complexity: O(1), since we only use a constant number of integer variables (mx, m, ans, mask) regardless of input size.

Data structures and patterns used:

  • Bit masking is the core technique, using mask to encode the set of allowed bits and the test (x | mask) == mask to verify feasibility.
  • Greedy bit-by-bit decision making drives the algorithm: by resolving higher-value bits first and locking in the best choice at each step, the locally optimal decisions combine into the globally minimum OR.

Example Walkthrough

Let's trace through the algorithm using a small grid:

grid = [[1, 6, 3],
        [4, 2, 9],
        [7, 2, 8]]

We must pick exactly one number from each row and minimize the bitwise OR of the three picks.

Step 1: Determine the number of bits.

The maximum value in the grid is 9 (max(map(max, grid))). Since 9 in binary is 1001, 9.bit_length() == 4. So we examine bit positions 3, 2, 1, 0.

For reference, here are the binary forms (4 bits):

ValueBinary
10001
60110
30011
40100
20010
91001
70111
81000

Step 2: Initialize. ans = 0 (we hope to keep every bit off).

Step 3–4: Process each bit from high (3) to low (0).


Bit i = 3 (value 8)

mask = ans | ((1 << 3) - 1) = 0 | 0111 = 0111. Allowed bits: 0,1,2. We're trying to keep bit 3 off.

Check each row for some x with (x | mask) == mask (i.e., x has no bits above bit 2):

  • Row [1, 6, 3]: 1 = 00010001 | 0111 = 0111 = mask ✓ found.
  • Row [4, 2, 9]: 4 = 01000100 | 0111 = 0111 = mask ✓ found.
  • Row [7, 2, 8]: 7 = 01110111 | 0111 = 0111 = mask ✓ found.

Every row can supply a value with bit 3 off. Keep bit 3 = 0. ans stays 0000.


Bit i = 2 (value 4)

mask = ans | ((1 << 2) - 1) = 0000 | 0011 = 0011. Allowed bits: 0,1. We're trying to keep bits 3 and 2 off.

Check each row for (x | 0011) == 0011 (only bits 0 and 1 allowed):

  • Row [1, 6, 3]: 1 = 00010001 | 0011 = 0011 ✓ (also 3 = 0011 works).
  • Row [4, 2, 9]: 4=01000111✗, 2=00100011 ✓ found.
  • Row [7, 2, 8]: 7=01110111✗, 2=00100011 ✓ found.

Every row succeeds. Keep bit 2 = 0. ans stays 0000.


Bit i = 1 (value 2)

mask = ans | ((1 << 1) - 1) = 0000 | 0001 = 0001. Allowed bits: 0 only. We're trying to keep bits 3, 2, 1 off.

Check each row for (x | 0001) == 0001 (only bit 0 allowed):

  • Row [1, 6, 3]: 1=00010001 ✓ found.
  • Row [4, 2, 9]: 4=01000101✗, 2=00100011✗, 9=10011001✗ → no valid element!

Row 2 cannot keep bit 1 off. So bit 1 must turn on: ans |= 1 << 1ans = 0010. We break early without checking row 3.


Bit i = 0 (value 1)

mask = ans | ((1 << 0) - 1) = 0010 | 0000 = 0010. Allowed bits: bit 1 (already forced) only — bit 0 must stay off.

Check each row for (x | 0010) == 0010:

  • Row [1, 6, 3]: 1=00010011✗, 6=01100110✗, 3=00110011✗ → no valid element!

Row 1 cannot keep bit 0 off. So bit 0 must turn on: ans |= 1 << 0ans = 0011. Break early.


Step 5: Return the result.

ans = 0011 = 3.

Verification: One selection achieving OR = 3 is 3 (row 0), 2 (row 1), 2 (row 2): 3 | 2 | 2 = 0011 | 0010 | 0010 = 0011 = 3. Bits 3 and 2 were successfully kept off; bits 1 and 0 were unavoidable. The greedy choice of locking in the highest bits as 0 first yields the global minimum OR of 3.

Solution Implementation

1from typing import List
2
3
4class Solution:
5    def minimumOR(self, grid: List[List[int]]) -> int:
6        # Find the maximum value to determine how many bits we need to consider.
7        max_value = max(map(max, grid))
8        bit_count = max_value.bit_length()
9
10        # 'answer' is built greedily from the highest bit down to the lowest.
11        answer = 0
12        for bit in range(bit_count - 1, -1, -1):
13            # Tentatively assume the current bit is 0.
14            # mask = bits already fixed in 'answer' + all lower bits set to 1 (allowed to be anything).
15            mask = answer | ((1 << bit) - 1)
16
17            for row in grid:
18                # Check whether this row has an element that is a subset of 'mask'.
19                # (x | mask == mask) means every set bit of x is already in mask.
20                found = any((x | mask) == mask for x in row)
21
22                # If some row cannot satisfy the constraint with the current bit as 0,
23                # then this bit must be set to 1 in the answer.
24                if not found:
25                    answer |= 1 << bit
26                    break
27
28        return answer
29```
30
31**Key points / perspectives:**
32
331. **Greedy correctness:** Setting a high bit costs more than any combination of lower bits, so we always prefer keeping higher bits as `0`. We only set a bit when it's unavoidable.
34
352. **Subset check `(x | mask) == mask`:** This is equivalent to `(x & ~mask) == 0`, meaning `x` has no bits outside of `mask`. Both forms are valid; the original `|` form is kept for clarity.
36
373. **Alternative using `all()`:** You could invert the logic for the outer loop:
38   ```python3
39   if not all(any((x | mask) == mask for x in row) for row in grid):
40       answer |= 1 << bit
41
1class Solution {
2    public int minimumOR(int[][] grid) {
3        // Track the maximum value in the grid to know how many bits we need to inspect.
4        int maxValue = 0;
5        for (int[] row : grid) {
6            for (int value : row) {
7                maxValue = Math.max(maxValue, value);
8            }
9        }
10
11        // Number of significant bits in maxValue (the highest bit index + 1).
12        int bitCount = 32 - Integer.numberOfLeadingZeros(maxValue);
13
14        // The answer is built bit by bit, from the most significant bit downward.
15        int answer = 0;
16
17        // Greedily decide each bit: try to keep it as 0 if possible.
18        for (int bit = bitCount - 1; bit >= 0; bit--) {
19            // Candidate mask assumes the current bit is 0.
20            // It includes:
21            //   - bits already fixed in 'answer' (higher bits)
22            //   - all lower bits (we don't care about them yet, so allow them freely)
23            int mask = answer | ((1 << bit) - 1);
24
25            // Check every row: each row must contribute at least one element
26            // whose set bits are entirely covered by 'mask'.
27            for (int[] row : grid) {
28                boolean found = false;
29                for (int value : row) {
30                    // (value | mask) == mask means 'value' has no bits outside 'mask'.
31                    if ((value | mask) == mask) {
32                        found = true;
33                        break;
34                    }
35                }
36
37                // If some row has no element that fits under the mask,
38                // we are forced to set the current bit to 1.
39                if (!found) {
40                    answer |= 1 << bit;
41                    break;
42                }
43            }
44        }
45
46        return answer;
47    }
48}
49
1class Solution {
2public:
3    int minimumOR(vector<vector<int>>& grid) {
4        // Find the maximum value across the entire grid to
5        // determine how many bits we need to consider.
6        int max_value = 0;
7        for (const auto& row : grid) {
8            max_value = max(max_value, ranges::max(row));
9        }
10
11        // Number of significant bits in max_value.
12        // (32 - count of leading zeros) gives the position of the highest set bit + 1.
13        int bit_count = 32 - __builtin_clz(max_value);
14
15        // The answer we build greedily, one bit at a time.
16        int answer = 0;
17
18        // Greedily decide each bit from the most significant down to the least.
19        // We prefer to keep a bit at 0; we only set it if it's unavoidable.
20        for (int bit = bit_count - 1; bit >= 0; --bit) {
21            // mask = bits already committed in the answer
22            //        OR all lower bits ((1 << bit) - 1, which are still "free").
23            // An element "fits" if all of its set bits are within this mask.
24            int mask = answer | ((1 << bit) - 1);
25
26            // Check whether every row can supply at least one element
27            // whose set bits stay inside the current mask.
28            for (const auto& row : grid) {
29                bool found = false;
30                for (int value : row) {
31                    // (value | mask) == mask means value's bits are a subset of mask.
32                    if ((value | mask) == mask) {
33                        found = true;
34                        break;
35                    }
36                }
37
38                // If some row has no fitting element, we are forced to
39                // set the current bit in the answer, then stop checking.
40                if (!found) {
41                    answer |= 1 << bit;
42                    break;
43                }
44            }
45        }
46
47        return answer;
48    }
49};
50
1function minimumOR(grid: number[][]): number {
2    // Find the maximum value in the grid to determine the highest relevant bit.
3    let maxValue = 0;
4    for (const row of grid) {
5        maxValue = Math.max(maxValue, Math.max(...row));
6    }
7
8    // Number of significant bits in maxValue.
9    // Math.clz32 counts leading zeros in a 32-bit integer.
10    const bitCount = maxValue === 0 ? 0 : 32 - Math.clz32(maxValue);
11
12    // The answer being constructed bit by bit (from high bit to low bit).
13    let answer = 0;
14
15    // Greedily decide each bit, starting from the most significant.
16    for (let bit = bitCount - 1; bit >= 0; bit--) {
17        // mask = bits already locked in the answer, plus all lower bits
18        // optimistically assumed to be allowed (set to 1).
19        // We try to keep the current `bit` as 0.
20        const mask = answer | ((1 << bit) - 1);
21
22        for (const row of grid) {
23            // Check whether this row has at least one element that fits
24            // entirely within the mask, i.e. (x | mask) === mask means
25            // x introduces no bits outside the mask.
26            let found = false;
27            for (const value of row) {
28                if ((value | mask) === mask) {
29                    found = true;
30                    break;
31                }
32            }
33
34            // If no element in this row fits, the current bit cannot stay 0;
35            // it must be forced to 1 in the answer.
36            if (!found) {
37                answer |= 1 << bit;
38                break;
39            }
40        }
41    }
42
43    return answer;
44}
45

Time and Space Complexity

Time Complexity: O(m * n * log(mx))

Let the grid have m rows and n columns, and let mx be the maximum value in the grid.

  • Computing mx = max(map(max, grid)) takes O(m * n) time, since it scans all elements.
  • m = mx.bit_length() is O(log(mx)), which we denote as B = log(mx) bit positions.
  • The outer loop runs B times (once for each bit position from high to low).
  • For each bit position, the code iterates over all rows (m rows) and, within each row, scans up to n elements to check the condition (x | mask) == mask.
  • Therefore, the nested loops cost O(B * m * n) in total.

Combining both parts, the overall time complexity is O(m * n + m * n * log(mx)) = O(m * n * log(mx)).

Space Complexity: O(1)

The algorithm only uses a constant number of auxiliary variables (mx, m, ans, mask, found, loop indices). No additional data structures that scale with the input size are allocated, so the extra space is O(1).

Common Pitfalls

Pitfall: Treating the bits as independent and locking them in too early

A very common mistake is to assume that once you decide bit i can be 0, every row will simultaneously offer a single element that keeps all the already-fixed-to-zero bits off. People often write the inner check against mask without carrying forward the bits already forced into answer, like this:

# WRONG: mask only considers the current bit, ignoring previously fixed bits.
for bit in range(bit_count - 1, -1, -1):
    mask = (1 << bit) - 1            # <-- missing 'answer |'
    for row in grid:
        found = any((x | mask) == mask for x in row)
        if not found:
            answer |= 1 << bit
            break

Why this is wrong: The greedy decision for bit i must respect all earlier commitments. When you successfully kept bits k > i at 0, you implicitly promised that the same single pick per row avoids those higher bits too. If mask drops the answer term, you allow a row to choose an element that satisfies bit i but turns on a higher bit you already committed to keeping 0. The bits are not independent because you pick one element per row, and that element's OR contribution affects every bit at once.

Concrete failure example:

grid = [[7, 4], [6, 1]]
# Binary:
#   7 = 111, 4 = 100
#   6 = 110, 1 = 001

With the buggy mask = (1 << bit) - 1:

  • Bit 2: row0 has 4=100 (fails, 100|011 != 011), so bit 2 is forced → answer = 100. Correct so far.
  • Bit 1: buggy mask = 011 (ignores answer). Row0: 4|011=111 != 011, 7|011=111 != 011 → fails → bit 1 set.
  • This may diverge from the truth because the check no longer verifies that the same element keeping bit 1 off also keeps the committed bit 2 off.

The correct version keeps mask = answer | ((1 << bit) - 1), so each row must find one element whose set bits are a subset of the allowed region (previously-fixed bits + all lower free bits). This guarantees a consistent, simultaneously-valid choice across all bits.

Solution

Always fold the accumulated answer into the mask so the feasibility test honors every prior commitment:

class Solution:
    def minimumOR(self, grid: List[List[int]]) -> int:
        max_value = max(map(max, grid))
        bit_count = max_value.bit_length()

        answer = 0
        for bit in range(bit_count - 1, -1, -1):
            # Carry forward fixed bits in 'answer'; allow all lower bits freely.
            mask = answer | ((1 << bit) - 1)

            for row in grid:
                # Each row needs ONE element whose bits are a subset of 'mask'.
                if not any((x | mask) == mask for x in row):
                    answer |= 1 << bit
                    break

        return answer

Secondary pitfall: Off-by-one in the bit range

Using range(bit_count, -1, -1) (looping one extra high bit) wastes work but is harmless, while using mx.bit_length() correctly gives the highest meaningful bit index as bit_count - 1. A more dangerous variant is hardcoding a fixed width like range(30, -1, -1) — fine for constrained inputs, but it silently breaks if values exceed 2^30. Prefer max_value.bit_length() so the range adapts to the actual data and you never miss a high bit that some element sets.

Ready to land your dream job?

Unlock your dream job with a 5-minute quiz for a personalized study roadmap!

Get My Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?


Recommended Readings

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

Load More