898. Bitwise ORs of Subarrays
Problem Description
The problem presents us with a task: given an array arr
of integers, we are required to find the total number of distinct bitwise OR results that can be obtained from all possible non-empty contiguous subarrays of arr
.
To understand the problem better, let's clarify some concepts:
-
Bitwise OR (
|
): A binary operation that takes two bit patterns of equal length and performs the logical inclusive OR operation on each pair of corresponding bits. The result in each position is1
if at least one of the bits is1
. -
Subarray: A sequence of elements within an array that is contiguous (elements are consecutive without gaps) and non-empty.
-
Distinct: Each unique value should be counted only once in the final tally.
The problem, therefore, requires us to explore and assess every possible subarray that can be generated from the given array, perform the bitwise OR operation on its elements, and count the unique outcomes.
Intuition
The intuitive approach to solving this problem might involve a brute force strategy - calculating the bitwise OR for every subarray and then utilizing a set to count the distinct values. However, this approach would result in a high time complexity due to the multiple nested loops required (each subarray is recursively built starting from each element).
The solution code, however, uses a clever strategy that reduces the amount of redundant computation:
-
Iterative Building: It takes advantage of the fact that the bitwise OR of a subarray ending at position
i
depends upon the bitwise OR result of the subarray ending ati-1
. Theprev
variable is used to accumulate the bitwise OR result till the current element, so that for the next element, we don't have to start from scratch. -
Early Breaking: It recognizes that if a new subarray's bitwise OR equals the accumulated OR (
prev
), we can stop early because all subsequent subarrays will only give the same or greater OR results, which we would already have encountered due to the nature of the OR operation building on previous subarrays. -
Set for Uniqueness: The set
s
is used to store unique bitwise OR results. This way, every time we add a new OR result, duplicate values are inherently avoided.
The main algorithm goes as follows:
- Initialize a set
s
to keep track of distinct bitwise OR results. - Loop over each element
v
in the array:- Start a
prev
accumulator that holds the bitwise OR up to the current element. - Loop backwards from the current element to the beginning of the array:
- Calculate the bitwise OR for the current subarray and add it to the set
s
. - Once the current subarray OR equals the accumulated
prev
OR, break early.
- Calculate the bitwise OR for the current subarray and add it to the set
- Start a
- Finally, return the length of set
s
, which represents the number of distinct bitwise ORs.
By using the set and early breaking, the solution immensely reduces the number of calculations compared to the brute force approach, and therefore, is much more efficient.
Learn more about Dynamic Programming patterns.
Solution Approach
The implementation of the reference solution makes use of sets and two pointers to efficiently compute the distinct bitwise ORs of all subarrays. Here's how the approach works in detail:
-
We initialize an empty set
s
that will hold the distinct bitwise OR results. Apart from this, we have aprev
variable that is used to keep track of the bitwise OR up to the current element in the outer loop, which goes through each elementi
in the array. -
As we iterate over each element
v
in the array using an indexi
, we first update theprev
variable to hold the bitwise OR between itself and the current element (prev |= v
). This newprev
will be the bitwise OR of all elements from the beginning of the array up to the current elementi
. -
In the inner loop, we start from the current element and go backwards through the array using another index
j
. We create a new variablecurr
to hold the result of bitwise ORs for the subarrays ending in the current elementi
. -
During each iteration of the inner loop, we update
curr
to be the bitwise OR of itself and the currentj
th element (curr |= arr[j]
). -
After updating
curr
, we add it to the sets
. This operation ensures that only distinct OR results are kept, as sets do not store duplicate elements. -
The early breaking condition is checked (
if curr == prev
), which is based on the understanding that once the current subarray's OR matches the overall OR up to the current element (prev
), all subsequent larger subarrays will yield the same OR value and have been considered before. When this condition is met, the inner loop breaks, avoiding unnecessary computation. -
Lastly, once both loops are done, the length of set
s
will represent the number of distinct bitwise ORs that can be formed from all the subarrays ofarr
. This is the result that the function returns.
The above steps form an efficient algorithm as it reduces the number of subarrays we need to check, leveraging the properties of the bitwise OR operation and making use of sets to maintain distinct entries. Such an optimization is essential to pass all test cases on platforms like LeetCode, where the input size can be large and brute force methods would result in a timeout error.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
To illustrate the solution approach, let's take a small example:
Let's say we have an array arr
= [1, 2, 3].
We will apply the solution approach step by step:
-
Initialize a set and variables:
- Set
s
= {} - Variable
prev
is initialized.
- Set
-
Outer loop - Iterate over each element in the array:
- For element 1 (
i=0
), setprev = 1
. - For element 2 (
i=1
),prev
becomesprev | 2 = 1 | 2 = 3
. - For element 3 (
i=2
),prev
becomesprev | 3 = 3 | 3 = 3
.
- For element 1 (
-
Inner loop - Backward iteration from current element:
- At
i=0
: Only one subarray[1]
:curr = 1
- Set
s
becomes {1}
- At
i=1
: Subarrays[2]
,[1, 2]
:curr = 2
, add to sets
-> {1, 2}curr = 2 | 1 = 3
, add to sets
-> {1, 2, 3}- Since
curr
now equalsprev
, inner loop breaks early.
- At
i=2
: Subarrays[3]
,[2, 3]
, but we stop at[2, 3]
because:curr = 3
, already in sets
, no need to add.- Loop attempts to compute
curr = 3 | 2
, but sinceprev
is already3
, the inner loop breaks.
- At
-
Early Breaking:
- Utilized at each step in the inner loop when
curr
matchesprev
.
- Utilized at each step in the inner loop when
-
Final Result:
- The length of set
s
is 3, representing the distinct bitwise OR results: {1, 2, 3}.
- The length of set
Each bitwise OR operation only computes new results, while duplicates are ignored due to the set data structure. By breaking early from the inner loop, we avoid unnecessary computation, optimizing the process. This compact example covers all the main elements present in the solution approach, demonstrating its efficiency and how it leads to the final answer.
Solution Implementation
1class Solution:
2 def subarrayBitwiseORs(self, arr: List[int]) -> int:
3 # Initialize a set to store unique bitwise OR results
4 unique_or_results = set()
5 # 'prev' will hold the cumulative OR result of the current iteration
6 prev = 0
7 # Iterate over the input array with both value and index
8 for index, value in enumerate(arr):
9 # Update 'prev' by taking the OR with the current value
10 prev |= value
11 # 'current' will hold the cumulative OR result starting from 'index' going back to the start
12 current = 0
13 # Iterate backwards from the current index to the start of the array
14 for j in range(index, -1, -1):
15 # Update 'current' by taking the OR with the value at j
16 current |= arr[j]
17 # Add 'current' to the set of unique OR results
18 unique_or_results.add(current)
19 # If 'current' equals 'prev', no new unique values can be found by continuing; break
20 if current == prev:
21 break
22 # Return the number of unique bitwise OR results found
23 return len(unique_or_results)
24
1class Solution {
2 public int subarrayBitwiseORs(int[] arr) {
3 // We use a set to store unique values of bitwise ORs for all subarrays
4 Set<Integer> uniqueBitwiseORs = new HashSet<>();
5
6 // We iterate through each element in the array
7 for (int i = 0; i < arr.length; ++i) {
8 // 'aggregate' will hold the cumulative bitwise OR value up to the current element
9 int aggregate = 0;
10
11 // We iterate from the current element down to the start of the array
12 for (int j = i; j >= 0; --j) {
13 // We calculate the bitwise OR from the current element to the 'jth' element
14 aggregate |= arr[j];
15
16 // Add the current subarray's bitwise OR to the set
17 uniqueBitwiseORs.add(aggregate);
18
19 /* If the current aggregate value is the same as the previous
20 aggregate value, all future aggregates will also be the same
21 due to the properties of bitwise OR, so we break out early. */
22 if (aggregate == (aggregate | arr[i])) {
23 break;
24 }
25 }
26 }
27
28 // Return the number of unique bitwise ORs found
29 return uniqueBitwiseORs.size();
30 }
31}
32
1#include <vector>
2#include <unordered_set>
3
4class Solution {
5public:
6 // Function to count the number of distinct bitwise OR values of all subarrays.
7 int subarrayBitwiseORs(vector<int>& arr) {
8 unordered_set<int> uniqueOrValues; // To store unique OR values of subarrays.
9 int currentOr = 0; // To store the running OR value of the current subarray.
10
11 // Iterate over each element in the array.
12 for (int i = 0; i < arr.size(); ++i) {
13 currentOr |= arr[i]; // Update the running OR with the current element.
14 int subarrayOr = 0; // Used to calculate OR for each possible subarray ending at 'i'.
15
16 // Iterate from the current element to the beginning of the array.
17 for (int j = i; j >= 0; --j) {
18 subarrayOr |= arr[j]; // Update the OR for the subarray ending at 'i' starting at 'j'.
19 uniqueOrValues.insert(subarrayOr); // Store the calculated OR in the set.
20
21 // Break the loop if the subarray OR equals the currently calculated OR (all bits already set).
22 if (subarrayOr == currentOr) break;
23 }
24 }
25
26 // The size of the set represents the number of distinct OR values of all subarrays.
27 return uniqueOrValues.size();
28 }
29};
30
1// The TypeScript standard library already includes Set, so we don't need
2// a separate import for unordered_set as we would in C++.
3
4// Function to count the number of distinct bitwise OR values of all subsequences.
5function subarrayBitwiseORs(arr: number[]): number {
6 let uniqueOrValues: Set<number> = new Set(); // To store unique OR values of subarrays.
7 let currentOr: number = 0; // To store the running OR value of the current subarray.
8
9 // Iterate over each element in the array.
10 for (let i = 0; i < arr.length; i++) {
11 currentOr |= arr[i]; // Update the running OR with the current element.
12 let subarrayOr: number = 0; // Used to calculate OR for each possible subarray ending at 'i'.
13
14 // Iterate from the current element to the beginning of the array.
15 for (let j = i; j >= 0; j--) {
16 subarrayOr |= arr[j]; // Update the OR for the subarray ending at 'i' starting at 'j'.
17 uniqueOrValues.add(subarrayOr); // Store the calculated OR in the set.
18
19 // Break the loop if the subarray OR equals the currently calculated OR (all bits already set).
20 if (subarrayOr === currentOr) break;
21 }
22 }
23
24 // The size of the set represents the number of distinct OR values of all subarrays.
25 return uniqueOrValues.size;
26}
27
28export { subarrayBitwiseORs }; // Export the function to be available for import in other modules.
29
Time and Space Complexity
The given code aims to find the number of distinct subarray bitwise ORs. To do this, it iterates over the given array and computes the OR of elements from the current element to all previous elements by keeping a record of the previous OR in prev
and the current progression in curr
.
Time Complexity
The time complexity of this algorithm mainly depends on the number of iterations within the double-loop structure.
- The outer loop runs exactly
n
times wheren
is the number of elements inarr
. - The inner loop runs up to
i+1
times in the worst case (whencurr
never equalsprev
early).
However, due to the properties of the bitwise OR operation, repetitions are likely to occur much earlier, resulting in earlier breaks from the inner loop. Specifically, the sequence of ORs will eventually stablize into a set of values that does not grow with each additional OR operation. The actual number of unique elements in these OR sequences across all iterations is bounded by a factor much smaller than n^2
.
While it's difficult to put a precise bound on this without specifics about the input distribution, let's denote the average unique sequence length as k
(which is considerably smaller than n
due to the saturation of OR operations). Therefore, the total number of operations is approximately O(n*k)
.
However, it is important to note that k
is not guaranteed to be a constant and its relation with n
can depend heavily on the input, implying that in the worst case the time complexity could tend towards O(n^2)
, but in practical scenarios, it is expected to perform significantly better.
Space Complexity
The space complexity is due to the set s
that is used to store the unique subarray OR results.
- In the worst case, each subarray OR could be unique, which means the set could grow to the size of the sum of all subarray counts. As with the time complexity argument, this won't actually occur due to the saturation of bitwise ORs.
Let m
represent the maximum possible unique OR values which can be much less than the total subarray count of roughly n*(n+1)/2
. Therefore, the space complexity can be approximated as O(m)
.
In conclusion, the time complexity of the code is approximately O(n*k)
(with k
being influenced by the input nature and much smaller than n
) and space complexity is around O(m)
for storing the unique OR results set, where m
represents the maximum number of unique OR values across all subarrays.
Learn more about how to find time and space complexity quickly using problem constraints.
What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
LeetCode 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 we
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 algomonster s3 us east 2 amazonaws com recursion jpg You first
Want a Structured Path to Master System Design Too? Don’t Miss This!