Minimize OR of Remaining Elements Using Operations
HardPrompt
You are given a 0-indexed integer array nums and an integer k.
In one operation, you can pick any index i of nums such that 0 <= i < nums.length - 1 and replace nums[i] and nums[i + 1] with a single occurrence of nums[i] & nums[i + 1], where & represents the bitwise AND operator.
Return the minimum possible value of the bitwise OR of the remaining elements of nums after applying at most k operations.
Example 1:
Input: nums = [3,5,3,2,7], k = 2
Output: 3
Explanation: Let's do the following operations:
1. Replace nums[0] and nums[1] with (nums[0] & nums[1]) so that nums becomes equal to [1,3,2,7].
2. Replace nums[2] and nums[3] with (nums[2] & nums[3]) so that nums becomes equal to [1,3,2].
The bitwise-or of the final array is 3.
It can be shown that 3 is the minimum possible value of the bitwise OR of the remaining elements of nums after applying at most k operations.Example 2:
Input: nums = [7,3,15,14,2,8], k = 4
Output: 2
Explanation: Let's do the following operations:
1. Replace nums[0] and nums[1] with (nums[0] & nums[1]) so that nums becomes equal to [3,15,14,2,8].
2. Replace nums[0] and nums[1] with (nums[0] & nums[1]) so that nums becomes equal to [3,14,2,8].
3. Replace nums[0] and nums[1] with (nums[0] & nums[1]) so that nums becomes equal to [2,2,8].
4. Replace nums[1] and nums[2] with (nums[1] & nums[2]) so that nums becomes equal to [2,0].
The bitwise-or of the final array is 2.
It can be shown that 2 is the minimum possible value of the bitwise OR of the remaining elements of nums after applying at most k operations.Example 3:
Input: nums = [10,7,10,3,9,14,9,4], k = 1
Output: 15
Explanation: Without applying any operations, the bitwise-or of nums is 15.
It can be shown that 15 is the minimum possible value of the bitwise OR of the remaining elements of nums after applying at most k operations.
Constraints:
1 <= nums.length <= 1050 <= nums[i] < 2300 <= k < nums.length
Approaches
3 approaches with complexity analysis and trade-offs.
This approach combines a greedy strategy on the bits of the result with a dynamic programming solution to a subproblem. The main idea is that to minimize a number, we should try to make its most significant bits zero. We iterate from the MSB downwards, deciding for each bit whether it can be zero in the final answer.
For each bit b, we test if it's possible to achieve a final OR value where bit b is 0, given the decisions we've made for higher bits. This check is formulated as a subproblem: can we partition the nums array into at least n - k contiguous groups, such that the bitwise AND of each group satisfies the bit constraints imposed by our target answer?
A straightforward DP solves this subproblem in O(N^2), leading to an overall complexity that is too slow but establishes a correct framework.
Algorithm
- The core idea is to determine the bits of the minimum possible OR value one by one, from the most significant bit (MSB) to the least significant bit (LSB).
- We iterate from bit 29 down to 0. For each bit
b, we try to make this bit 0 in our final answer. - Let
ansbe the minimum OR value we are building. Initially,ansis all ones. We try to turn off bitbinans. Let this bepotential_ans = ans & ~(1 << b). - We then check if it's possible to perform at most
koperations such that the bitwise OR of the final array is at mostpotential_ans. This is equivalent to checking if for every elementyin the final array,y | potential_ans == potential_ans. - The
check(target)function determines if this is possible. It does so by finding the maximum number of groups (m_max) we can partitionnumsinto, where each group's bitwise AND is valid with respect totarget. Ifm_max >= nums.length - k, the check passes. - This version of
checkuses a dynamic programming approach. Letdp[i]be the maximum number of valid groups for the prefixnums[0...i-1]. - The transition is
dp[i] = 1 + max(dp[j])for all0 <= j < iwhere the subarraynums[j...i-1]forms a valid group (its AND is compatible withtarget). - Calculating this DP naively takes
O(N^2)time.
Walkthrough
The algorithm determines the minimal OR value bit by bit. We start with a candidate answer of all ones and try to turn off each bit from 30 down to 0.
To check if a bit b can be zero, we call a function check(target). This function must verify if we can partition nums into m groups (using n-m operations) such that m >= n-k and for each group g_i, (AND of g_i) | target == target. This is equivalent to finding the maximum number of valid groups we can form.
We can find this maximum using dynamic programming. Let dp[i] be the maximum number of valid groups we can partition nums[0...i-1] into. The recurrence relation is:
dp[i] = 1 + max(dp[j]) over all 0 <= j < i such that (nums[j] & ... & nums[i-1]) | target == target.
This DP requires two nested loops to compute, resulting in an O(N^2) complexity for the check function. Since we call this for each bit, the total time complexity is O(B * N^2), where B is the number of bits (around 30).
class Solution { public int minimizeOrOfRemainingElements(int[] nums, int k) { int n = nums.length; int ans = -1; // Represents all bits set to 1 for (int b = 29; b >= 0; b--) { int potentialAns = ans & ~(1 << b); if (isPossible(nums, k, potentialAns)) { ans = potentialAns; } } return ~ans; } private boolean isPossible(int[] nums, int k, int targetMask) { int n = nums.length; int[] dp = new int[n + 1]; // dp[i] = max number of valid groups in nums[0...i-1] // A group is valid if its AND result has 0s where targetMask has 1s. // (group_and & targetMask) == 0 for (int i = 1; i <= n; i++) { dp[i] = -1; // Impossible int currentAnd = -1; for (int j = i - 1; j >= 0; j--) { currentAnd &= nums[j]; if ((currentAnd & targetMask) == 0) { if (dp[j] != -1) { dp[i] = Math.max(dp[i], dp[j] + 1); } } } } return dp[n] != -1 && n - dp[n] <= k; }}Note: The code uses a targetMask where set bits must be zero in the group ANDs. The final answer is ~ans because ans stores this mask.
Complexity
Time
O(B * N^2), where B is the number of bits (e.g., 30) and N is the length of `nums`. The outer loop runs B times, and the `check` function is O(N^2).
Space
O(N) for the DP array used within the `check` function.
Trade-offs
Pros
The bitwise greedy strategy is a correct high-level approach for this type of problem.
It breaks down the problem into a more manageable subproblem.
Cons
The time complexity of
O(B * N^2)is too high for the given constraints (Nup to 10^5), and this solution will time out.
This approach builds upon the previous one by optimizing the check function. The O(N^2) DP to find the maximum number of valid partitions is the bottleneck. We can optimize the DP transition from O(N) to O(B) by pre-calculating information about the positions of zeros for each bit.
Specifically, for each prefix of the array and for each bit, we find the last index where that bit was zero. This last_zero table allows us to quickly determine the valid range of starting points j for a group ending at i-1. This reduces the complexity of the check function to O(B*N), making the overall solution O(B^2 * N).
Algorithm
- The overall bitwise greedy structure remains the same as the previous approach.
- The
check(target)function is optimized. The key observation is that theO(N^2)DP can be sped up. - The transition
dp[i] = 1 + max(dp[j])for validjcan be optimized. A groupnums[j...i-1]is valid if for every bitbthat must be zero, there is at least one number innums[j...i-1]with bitbbeing zero. - We can precompute
last_zero[p][b], the index of the last number at or beforepwith bitbas zero. This takesO(B*N). - For a fixed
iandtarget, the condition onjbecomesj <= min_{b | bit b must be 0} (last_zero[i-1][b]). Let this boundary beJ. - The DP transition becomes
dp[i] = 1 + max(dp[0...J]). We can maintain a running maximum ofdpvalues to make thisO(1). - Calculating
Jfor eachitakesO(B). So thecheckfunction becomesO(B*N). - The precomputation of
last_zerois done outside the main loop.
Walkthrough
We keep the bitwise greedy framework but optimize the check function. The goal is to compute dp[i] = 1 + max_{0 <= j <= J[i-1]} dp[j] faster.
-
Precomputation: We create a table
last_zero[i][b]that stores the index of the most recent elementnums[p](withp <= i) where bitbis 0. This table can be filled inO(B*N)time. -
Optimized
checkfunction: Insidecheck(target), for eachifrom 1 ton, we need to find the valid starting range for a group ending ati-1. A groupnums[j...i-1]is valid if its AND is compatible withtarget. This means for every bitbthat must be zero in the result, the subarraynums[j...i-1]must contain at least one number with bitbbeing zero. Using ourlast_zerotable, this meansjmust be less than or equal tolast_zero[i-1][b]for all such required zero bitsb. So, the latest possible startjisJ = min(last_zero[i-1][b])over all required zero bitsb. This takesO(B)time to compute for eachi. -
DP with Running Maximum: The DP transition is now
dp[i] = 1 + max_dp[J], wheremax_dp[J]is the maximum value indp[0...J]. We can maintain thismax_dparray alongside thedparray. Thecheckfunction now takesO(B*N).
class Solution { public int minimizeOrOfRemainingElements(int[] nums, int k) { int n = nums.length; int ans = -1; // All 1s // Precompute last_zero table int[][] last_zero = new int[n][30]; for (int b = 0; b < 30; b++) { int last = -1; for (int i = 0; i < n; i++) { if (((nums[i] >> b) & 1) == 0) { last = i; } last_zero[i][b] = last; } } for (int b = 29; b >= 0; b--) { int potentialAns = ans & ~(1 << b); if (isPossible(nums, k, potentialAns, last_zero)) { ans = potentialAns; } } return ~ans; } private boolean isPossible(int[] nums, int k, int targetMask, int[][] last_zero) { int n = nums.length; int[] dp = new int[n + 1]; int[] max_dp = new int[n + 1]; for (int i = 1; i <= n; i++) { int min_last = n; for (int b = 0; b < 30; b++) { if (((targetMask >> b) & 1) == 1) { min_last = Math.min(min_last, last_zero[i - 1][b]); } } int j_boundary = min_last; if (j_boundary == -1) { dp[i] = -1; // Impossible to form a valid group ending at i-1 } else { dp[i] = max_dp[j_boundary] + 1; } max_dp[i] = Math.max(max_dp[i - 1], dp[i]); } return max_dp[n] > 0 && n - max_dp[n] <= k; }}Complexity
Time
O(B^2 * N). The main loop runs B times. The `check` function takes O(B*N) due to the inner loop to calculate `j_boundary` for each of the N elements.
Space
O(B*N) for the `last_zero` table.
Trade-offs
Pros
Significantly faster than the naive
O(B*N^2)approach.The precomputation idea is a powerful technique for range-based problems.
Cons
The time complexity of
O(B^2 * N)might still be too slow ifNis large.The space complexity of
O(B*N)for thelast_zerotable can be large (e.g.,30 * 10^5 * 4bytes ≈ 12 MB).
This is the most efficient approach, optimizing the check function's runtime from O(B*N) to O(N). The total time complexity becomes O(B*N).
The optimization comes from recognizing that the j_boundary array, which determines the DP transitions, doesn't need to be recomputed from scratch in every iteration of the main bitwise-greedy loop. As we decide on more bits to be zero in our answer, the constraints on partitions only get stricter. The j_boundary for a new mask can be calculated in O(N) from the j_boundary of the previous mask. This eliminates the O(B) factor from the check function, leading to a fully optimized solution.
Algorithm
- This approach refines the previous one to achieve the optimal time complexity.
- The bitwise greedy framework and the
last_zeroprecomputation remain. - The key insight is that when we go from checking bit
bto bitb-1, the set of required zero bits only grows. The mask for bitbisM_b. The mask forb-1will beM_{b-1} = M_borM_{b-1} = M_b | (1 << (b-1)). - Let
J(i, mask)be thej_boundaryfor indexiand a given mask. We haveJ(i, mask | (1 << b)) = min(J(i, mask), last_zero[i][b]). - Instead of recomputing the
j_boundaryarray from scratch inO(B*N)inside eachcheck, we can maintain aJ_currentarray for the current best answer's mask. - In each step of the main loop (for bit
b), we compute aJ_testarray fromJ_currentinO(N)time:J_test[i] = min(J_current[i], last_zero[i][b]). - We then run the
O(N)DP using thisJ_testarray. - If the check passes, we update
J_current = J_testfor the next iteration.
Walkthrough
This approach optimizes the calculation of the j_boundary array across the main loop's iterations.
-
Precomputation: We still precompute the
last_zero[i][b]table inO(B*N)time. -
Stateful Optimization: We introduce an array
J_currentof sizen.J_current[i]will store thej_boundaryfornums[0...i]corresponding to the mask of the current best answerans. -
Main Loop: We iterate from bit
b = 29down to 0. a. Create a temporaryJ_testarray. For eachifrom 0 ton-1, calculateJ_test[i] = min(J_current[i], last_zero[i][b]). This takesO(N). ThisJ_testarray represents the boundaries if we were to add bitbto our set of required zero bits. b. Call a modifiedcheckfunction that takes this precomputedJ_testarray as input. Thischeckfunction now only needs to run theO(N)DP. c. The DP isdp[i] = 1 + max_dp[J_test[i-1]]. d. Ifcheckreturns true, it means setting bitbto zero is possible. We update our answeransand, crucially, updateJ_current = J_testto carry the tighter boundaries to the next iteration.
This way, each of the B iterations of the main loop takes O(N) time (for updating J and running the DP), making the total time complexity O(B*N).
class Solution { public int minimizeOrOfRemainingElements(int[] nums, int k) { int n = nums.length; int ans = 0; int[][] last_zero = new int[n][30]; for (int b = 0; b < 30; b++) { int last = -1; for (int i = 0; i < n; i++) { if (((nums[i] >> b) & 1) == 0) { last = i; } last_zero[i][b] = last; } } int[] j_current = new int[n]; for(int i=0; i<n; i++) j_current[i] = i; // Initially, no constraints for (int b = 29; b >= 0; b--) { int[] j_test = new int[n]; for (int i = 0; i < n; i++) { j_test[i] = Math.min(j_current[i], last_zero[i][b]); } if (isPossible(n, k, j_test)) { // It's possible to make bit b zero, so we don't add it to ans. j_current = j_test; } else { // Must have bit b as 1. ans |= (1 << b); } } return ans; } private boolean isPossible(int n, int k, int[] j_boundaries) { int[] dp = new int[n + 1]; int[] max_dp = new int[n + 1]; for (int i = 1; i <= n; i++) { int boundary = j_boundaries[i - 1]; if (boundary == -1) { dp[i] = -1; // Impossible } else { dp[i] = max_dp[boundary] + 1; } max_dp[i] = Math.max(max_dp[i - 1], dp[i]); } return max_dp[n] > 0 && n - max_dp[n] <= k; }}Complexity
Time
O(B*N), where B is the number of bits and N is the length of `nums`. Precomputation is `O(B*N)`, and the main loop runs B times with `O(N)` work inside.
Space
O(B*N) for the `last_zero` table. Additional `O(N)` space is used for DP arrays and boundary arrays.
Trade-offs
Pros
This is the most efficient solution with optimal time complexity.
It correctly solves the problem within the given constraints.
Cons
The implementation is more complex, requiring careful state management across calls to the check function.
The
O(B*N)space complexity might still be a concern for very largeNon memory-constrained systems, though it's generally acceptable.
Solutions
Solution
class Solution {public int minOrAfterOperations(int[] nums, int k) { int ans = 0, rans = 0; for (int i = 29; i >= 0; i--) { int test = ans + (1 << i); int cnt = 0; int val = 0; for (int num : nums) { if (val == 0) { val = test & num; } else { val &= test & num; } if (val != 0) { cnt++; } } if (cnt > k) { rans += (1 << i); } else { ans += (1 << i); } } return rans; }}class Solution {public: int minOrAfterOperations(vector<int> &nums, int k) { int ans = 0, rans = 0; for (int i = 29; i >= 0; i--) { int test = ans + (1 << i); int cnt = 0; int val = 0; for (auto it : nums) { if (val == 0) { val = test & it; } else { val &= test & it; } if (val) { cnt++; } } if (cnt > k) { rans += (1 << i); } else { ans += (1 << i); } } return rans; }};class Solution: def minOrAfterOperations(self, nums: List[int], k: int) -> int: ans = 0 rans = 0 for i in range(29, - 1, - 1): test = ans + (1 << i) cnt = 0 val = 0 for num in nums: if val == 0: val = test & num else: val &= test & num if val: cnt += 1 if cnt > k: rans += 1 << i else: ans += 1 << i return ransVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.