3209. Number of Subarrays With AND Value of K
Problem Description
Given an array of integers nums
and an integer k
, you need to find the total number of contiguous subarrays where the bitwise AND of all elements in the subarray equals k
.
A subarray is a contiguous part of the array. For example, if nums = [1, 2, 3]
, the subarrays are [1]
, [2]
, [3]
, [1,2]
, [2,3]
, and [1,2,3]
.
The bitwise AND operation takes two numbers and performs AND on each bit position. For a subarray from index l
to r
, you calculate nums[l] & nums[l+1] & ... & nums[r]
and check if this result equals k
.
For example, if nums = [1, 1, 1]
and k = 1
:
- Subarray
[1]
has AND value =1
(appears 3 times) - Subarray
[1, 1]
has AND value =1 & 1 = 1
(appears 2 times) - Subarray
[1, 1, 1]
has AND value =1 & 1 & 1 = 1
(appears 1 time) - Total count = 6
The solution uses a hash table to track all possible AND values ending at each position. For each new element, it updates these values by performing AND with the new element, counts occurrences of value k
, and accumulates the result.
Intuition
The key insight is that bitwise AND has a special property: it can only decrease or stay the same as we add more elements, never increase. This is because AND operation can only turn bits from 1 to 0, never from 0 to 1.
Consider what happens when we fix the right endpoint of our subarray at position i
. All possible subarrays ending at i
have left endpoints ranging from 0
to i
. As we extend the left boundary further left (including more elements), the AND value either stays the same or decreases.
Since each number in the array is at most 10^9
(roughly 30 bits), and each AND operation can potentially turn off at least one bit, there can be at most 30 distinct AND values for all subarrays ending at any position. This is a crucial observation that makes the problem tractable.
Instead of checking all possible subarrays (which would be O(n²)), we can maintain a frequency map of all distinct AND values for subarrays ending at the current position. When we move to the next position i+1
:
- Each existing AND value gets combined with
nums[i+1]
through AND operation - We also start a new subarray containing just
nums[i+1]
This way, we only need to update at most 30 values per position, making the algorithm efficient. We use a Counter (hash table) to track how many subarrays produce each AND value. Whenever we see the value k
in our counter, we add its frequency to our answer.
The transition from position i
to i+1
naturally maintains all possible AND values and their counts, allowing us to solve the problem in a single pass through the array.
Learn more about Segment Tree and Binary Search patterns.
Solution Approach
The solution uses a hash table to track all possible AND values for subarrays ending at each position. Here's how the implementation works:
We maintain two counters:
pre
: stores the frequency of AND values for subarrays ending at the previous positioncur
: stores the frequency of AND values for subarrays ending at the current position
For each element x
in nums
:
-
Create a new counter for current position: Initialize
cur = Counter()
to store AND values ending at this position. -
Update existing subarrays: For each AND value
y
inpre
with frequencyv
:- Calculate
x & y
- this extends all subarrays ending at the previous position to include the current element - Add the frequency to
cur
:cur[x & y] += v
- This handles all subarrays that started before the current position
- Calculate
-
Add single-element subarray: Increment
cur[x] += 1
to account for the subarray containing only the current element. -
Count subarrays with AND = k: Add
cur[k]
to the answer. This gives us the number of subarrays ending at the current position with AND value equal tok
. -
Move to next position: Set
pre = cur
to prepare for the next iteration.
The algorithm processes each element once, and for each element, it updates at most 30 distinct AND values (due to the bit limitation). This gives us a time complexity of O(n × 30) = O(n).
For example, with nums = [1, 1, 2]
and k = 0
:
- Position 0:
cur = {1: 1}
, no subarrays with AND = 0 - Position 1:
cur = {1: 2}
(from1&1=1
twice), no subarrays with AND = 0 - Position 2:
cur = {0: 2, 2: 1}
(from1&2=0
twice, and single2
), add 2 to answer
The space complexity is O(30) = O(1) since we store at most 30 distinct values in the counter.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with nums = [3, 7, 3, 1]
and k = 3
.
Initial Setup:
ans = 0
(our answer counter)pre = Counter()
(empty, tracks AND values from previous position)
Processing nums[0] = 3:
- Create new
cur = Counter()
- No previous values in
pre
to extend - Add single-element subarray:
cur[3] += 1
, socur = {3: 1}
- Check if k=3 exists:
cur[3] = 1
, soans += 1
→ans = 1
- Update
pre = cur = {3: 1}
Processing nums[1] = 7:
- Create new
cur = Counter()
- Extend previous subarrays: For
3
inpre
with frequency 1:- Calculate
7 & 3 = 3
(binary: 111 & 011 = 011) - Add to cur:
cur[3] += 1
, socur = {3: 1}
- Calculate
- Add single-element subarray:
cur[7] += 1
, socur = {3: 1, 7: 1}
- Check if k=3 exists:
cur[3] = 1
, soans += 1
→ans = 2
- Update
pre = cur = {3: 1, 7: 1}
Processing nums[2] = 3:
- Create new
cur = Counter()
- Extend previous subarrays:
- For
3
inpre
:3 & 3 = 3
, socur[3] += 1
- For
7
inpre
:3 & 7 = 3
, socur[3] += 1
- Now
cur = {3: 2}
- For
- Add single-element subarray:
cur[3] += 1
, socur = {3: 3}
- Check if k=3 exists:
cur[3] = 3
, soans += 3
→ans = 5
- Update
pre = cur = {3: 3}
Processing nums[3] = 1:
- Create new
cur = Counter()
- Extend previous subarrays:
- For
3
inpre
with frequency 3:1 & 3 = 1
(binary: 001 & 011 = 001) - So
cur[1] += 3
, givingcur = {1: 3}
- For
- Add single-element subarray:
cur[1] += 1
, socur = {1: 4}
- Check if k=3 exists:
cur[3] = 0
, soans += 0
→ans = 5
Final Answer: 5
The 5 subarrays with AND value equal to 3 are:
[3]
at index 0[3, 7]
from indices 0-1[7, 3]
from indices 1-2[3]
at index 2[3, 7, 3]
from indices 0-2
Solution Implementation
1from typing import List
2from collections import Counter
3
4class Solution:
5 def countSubarrays(self, nums: List[int], k: int) -> int:
6 # Total count of valid subarrays
7 result = 0
8
9 # Store AND results of subarrays ending at previous position
10 # Key: AND result, Value: count of subarrays with that AND result
11 previous_and_counts = Counter()
12
13 # Process each element in the array
14 for current_num in nums:
15 # Store AND results of subarrays ending at current position
16 current_and_counts = Counter()
17
18 # Extend all previous subarrays by including current element
19 # For each previous AND result, compute new AND with current element
20 for and_result, count in previous_and_counts.items():
21 new_and_result = current_num & and_result
22 current_and_counts[new_and_result] += count
23
24 # Add subarray containing only the current element
25 current_and_counts[current_num] += 1
26
27 # Count how many subarrays ending at current position have AND equal to k
28 result += current_and_counts[k]
29
30 # Update previous counts for next iteration
31 previous_and_counts = current_and_counts
32
33 return result
34
1class Solution {
2 public long countSubarrays(int[] nums, int k) {
3 long totalCount = 0;
4
5 // Map to store AND results from previous iteration
6 // Key: AND result, Value: count of subarrays with that AND result
7 Map<Integer, Integer> previousAndResults = new HashMap<>();
8
9 // Iterate through each element in the array
10 for (int currentNum : nums) {
11 // Map to store AND results for current iteration
12 Map<Integer, Integer> currentAndResults = new HashMap<>();
13
14 // Extend all previous subarrays by including current element
15 for (Map.Entry<Integer, Integer> entry : previousAndResults.entrySet()) {
16 int previousAndValue = entry.getKey();
17 int count = entry.getValue();
18
19 // Calculate new AND value by combining with current number
20 int newAndValue = currentNum & previousAndValue;
21
22 // Add or update the count for this AND value
23 currentAndResults.merge(newAndValue, count, Integer::sum);
24 }
25
26 // Add subarray containing only the current element
27 currentAndResults.merge(currentNum, 1, Integer::sum);
28
29 // Count subarrays ending at current position with AND value equal to k
30 totalCount += currentAndResults.getOrDefault(k, 0);
31
32 // Update previous results for next iteration
33 previousAndResults = currentAndResults;
34 }
35
36 return totalCount;
37 }
38}
39
1class Solution {
2public:
3 long long countSubarrays(vector<int>& nums, int k) {
4 long long result = 0;
5
6 // Map to store the frequency of AND values ending at the previous position
7 unordered_map<int, int> previousAndCounts;
8
9 // Iterate through each element in the array
10 for (int currentNum : nums) {
11 // Map to store the frequency of AND values ending at the current position
12 unordered_map<int, int> currentAndCounts;
13
14 // Extend all subarrays ending at the previous position to include currentNum
15 for (auto& [andValue, frequency] : previousAndCounts) {
16 // Calculate new AND value when extending the subarray
17 int newAndValue = currentNum & andValue;
18 currentAndCounts[newAndValue] += frequency;
19 }
20
21 // Count the subarray containing only the current element
22 currentAndCounts[currentNum]++;
23
24 // Add the count of subarrays ending at current position with AND value equal to k
25 result += currentAndCounts[k];
26
27 // Update previousAndCounts for the next iteration
28 previousAndCounts = currentAndCounts;
29 }
30
31 return result;
32 }
33};
34
1/**
2 * Counts the number of subarrays where the bitwise AND of all elements equals k
3 * @param nums - The input array of numbers
4 * @param k - The target value for bitwise AND result
5 * @returns The count of valid subarrays
6 */
7function countSubarrays(nums: number[], k: number): number {
8 let totalCount: number = 0;
9
10 // Map to store bitwise AND results and their frequencies from previous iteration
11 // Key: bitwise AND result, Value: count of subarrays with this result
12 let previousAndResults: Map<number, number> = new Map<number, number>();
13
14 // Iterate through each element in the array
15 for (const currentNum of nums) {
16 // Map to store bitwise AND results for subarrays ending at current position
17 const currentAndResults: Map<number, number> = new Map<number, number>();
18
19 // Extend all previous subarrays by including the current element
20 for (const [previousAndValue, frequency] of previousAndResults) {
21 // Calculate new AND result by including current element
22 const newAndValue: number = currentNum & previousAndValue;
23
24 // Update frequency count for this AND result
25 currentAndResults.set(
26 newAndValue,
27 (currentAndResults.get(newAndValue) || 0) + frequency
28 );
29 }
30
31 // Add subarray containing only the current element
32 currentAndResults.set(
33 currentNum,
34 (currentAndResults.get(currentNum) || 0) + 1
35 );
36
37 // Add count of subarrays with AND result equal to k
38 totalCount += currentAndResults.get(k) || 0;
39
40 // Update previous results for next iteration
41 previousAndResults = currentAndResults;
42 }
43
44 return totalCount;
45}
46
Time and Space Complexity
The time complexity is O(n × log M)
, where n
is the length of the array nums
and M
is the maximum value in the array nums
.
The key insight is that for any number, the bitwise AND operation can only decrease or maintain the value, never increase it. Starting from any value x
, repeatedly applying AND operations will create at most O(log M)
distinct values before reaching 0, since each bit position can only change from 1 to 0 (never from 0 to 1).
In the algorithm:
- The outer loop iterates through all
n
elements - For each element, we iterate through the
pre
Counter, which contains at mostO(log M)
distinct AND results - Each operation inside the loops takes
O(1)
time
Therefore, the total time complexity is O(n × log M)
.
The space complexity is O(log M)
. At any point in the algorithm, the Counter cur
(and subsequently pre
) stores at most O(log M)
distinct values, representing all possible AND results of subarrays ending at the current position. Since we only maintain two Counters (pre
and cur
) and each can have at most O(log M)
entries, the space complexity is O(log M)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Not Handling the Case Where k Has Bits That Don't Exist in Any Array Element
A common mistake is not recognizing that if k
has any bit set to 1 that is never set to 1 in any element of the array, the answer should immediately be 0. The bitwise AND operation can only preserve or clear bits, never set new ones.
Pitfall Example:
# If nums = [4, 2, 8] (binary: [100, 010, 1000]) and k = 3 (binary: 011) # k requires bits 0 and 1 to be set, but no element has both bits set # The AND of any subarray cannot produce 3
Solution: Add an early termination check:
def countSubarrays(self, nums: List[int], k: int) -> int:
# Check if k is achievable by ANDing all elements
all_bits_or = 0
for num in nums:
all_bits_or |= num
# If k has bits that don't exist in any element, return 0
if k & ~all_bits_or != 0:
return 0
# Continue with the original algorithm...
2. Assuming AND Values Always Decrease Monotonically
While it's true that extending a subarray can only maintain or decrease the AND value, developers might incorrectly assume they can break early when the AND becomes less than k
. However, this is wrong because we need to count ALL subarrays, not just find one.
Pitfall Example:
# WRONG: Breaking early when AND < k for and_result, count in previous_and_counts.items(): new_and_result = current_num & and_result if new_and_result < k: # WRONG! break # This skips valid counting current_and_counts[new_and_result] += count
Solution: Process all entries without early termination based on the AND value:
# CORRECT: Process all previous AND values for and_result, count in previous_and_counts.items(): new_and_result = current_num & and_result current_and_counts[new_and_result] += count
3. Memory Inefficiency with Large Numbers
While the Counter approach works well for 32-bit integers (limiting distinct AND values to ~30), using a regular dictionary without cleanup could accumulate entries with zero counts over iterations, potentially causing memory issues in edge cases.
Solution: Clean up zero-count entries or use defaultdict more carefully:
def countSubarrays(self, nums: List[int], k: int) -> int:
result = 0
previous_and_counts = {} # Use regular dict for better control
for current_num in nums:
current_and_counts = {}
# Process previous AND values
for and_result, count in previous_and_counts.items():
new_and_result = current_num & and_result
current_and_counts[new_and_result] = current_and_counts.get(new_and_result, 0) + count
# Add single element subarray
current_and_counts[current_num] = current_and_counts.get(current_num, 0) + 1
# Count subarrays with AND = k
result += current_and_counts.get(k, 0)
# Only keep non-zero counts
previous_and_counts = {key: val for key, val in current_and_counts.items() if val > 0}
return result
What is the best way of checking if an element exists in an unsorted array once in terms of time complexity? Select the best that applies.
Recommended Readings
Segment Tree Faster Range Queries For this article we want to introduce the idea of a Segment Tree Segment Trees allow us to quickly perform range queries as well as range updates Suppose we have an array and we want to know the sum of a particular range of numbers as well
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
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
Want a Structured Path to Master System Design Too? Don’t Miss This!