LeetCode Daily Challenge-2044 Count Number of Maximum Bitwise-OR Subsets

Anni HuangAnni Huang
8 min read

Problem Description

Given an integer array nums, we need to:

  1. Find the maximum possible bitwise OR of any subset of nums
  2. Return the count of different non-empty subsets that achieve this maximum OR

Key Insights:

  • The maximum possible bitwise OR is always the OR of all elements (since OR only sets bits, never clears them)
  • We need to count all subsets that produce this maximum OR value
  • Two subsets are different if they have different indices, even if they contain the same values

Example:

  • Input: nums = [3, 1]
  • Maximum OR = 3 | 1 = 3
  • Subsets: [3] (OR=3), [1] (OR=1), [3,1] (OR=3)
  • Count of subsets with max OR = 2

Algorithm Walkthrough

Method 1: Backtracking with Pruning

The first solution uses backtracking with pruning optimization:

Key Optimization - Smart Pruning:

if(currOr == targetOr) {
    count = count + (1 << (nums.length - index));
    return;
}

When we achieve the maximum OR, instead of exploring each remaining subset individually, we calculate that there are 2^(remaining elements) valid subsets from this point.

Example for [3, 1]:

  • When we include 3 first, we achieve maxOr=3
  • Remaining elements: [1]
  • Valid subsets from this point: {} and {1} = 2^1 = 2 subsets
  • So we add 2 to count (representing [3] and [3,1])

Method 2: Memoization Approach

The memoization approach uses a different strategy:

Core Logic:

// At each index, we have two choices:
// 1. Skip current element
int count = backtrackMemo(nums, index + 1, currentOr, maxOr, memo);

// 2. Include current element  
count += backtrackMemo(nums, index + 1, currentOr | nums[index], maxOr, memo);

Memoization Key:

  • Uses "index,currentOr" as the key
  • Caches results for each (position, current_OR_value) state
  • Avoids recalculating when we encounter the same state again

Example Trace for [3, 1]:

  1. backtrack(0, 0) - at index 0 with OR=0
  2. Skip 3: backtrack(1, 0) - at index 1 with OR=0
  3. Include 3: backtrack(1, 3) - at index 1 with OR=3
  4. From (1, 0): skip 1 → backtrack(2, 0) → return 0
  5. From (1, 0): include 1 → backtrack(2, 1) → return 0
  6. From (1, 3): skip 1 → backtrack(2, 3) → return 1
  7. From (1, 3): include 1 → backtrack(2, 3) → return 1 (cached)

Key Differences:

AspectPruning MethodMemoization Method
ExplorationFor-loop based, explores from current indexBinary choice: include/exclude current element
OptimizationMathematical pruning when target reachedCaches overlapping subproblems
Base CaseEarly termination on target ORProcess all elements, check at end
MemoryO(n) recursion stackO(n × M) for cache storage

Complexity Analysis

Method 1: Backtracking with Pruning

  • Time Complexity: O(2^n) worst case, but pruning significantly reduces actual runtime
  • Space Complexity: O(n) for recursion stack

Method 2: Memoization

  • Time Complexity: O(n × M) where M is the number of unique OR values possible
  • Space Complexity: O(n × M) for memoization cache
  • When Better: When there are many overlapping subproblems (repeated (index, currentOr) states)

Full Solution

Java

Method 1: Backtracking with Pruning

class Solution {
    int count = 0;

    public int countMaxOrSubsets(int[] nums) {
        int maxOr = 0;
        for(int a : nums) maxOr |= a;
        backtrack(nums, maxOr, 0, 0);
        return count;
    }

    void backtrack(int[] nums, int targetOr, int currOr, int index) {
        if(currOr == targetOr) {
            // Pruning: count all possible subsets from remaining elements
            count = count + (1 << (nums.length - index));
            return;
        }

        for(int i = index; i < nums.length; i++) {
            backtrack(nums, targetOr, currOr | nums[i], i + 1);
        }
    }
}

Method 2: Memoization

class Solution {
    /**
     * Optimized version using memoization.
     * 
     * Time Complexity: O(n × M) where M is number of unique OR values
     * Space Complexity: O(n × M) for memoization
     */
    public int countMaxOrSubsets(int[] nums) {
        int maxOr = 0;
        for (int num : nums) {
            maxOr |= num;
        }

        Map<String, Integer> memo = new HashMap<>();
        return backtrackMemo(nums, 0, 0, maxOr, memo);
    }

    private int backtrackMemo(int[] nums, int index, int currentOr, int maxOr, 
                             Map<String, Integer> memo) {
        if (index == nums.length) {
            return currentOr == maxOr ? 1 : 0;
        }

        String key = index + "," + currentOr;
        if (memo.containsKey(key)) {
            return memo.get(key);
        }

        // Skip current element
        int count = backtrackMemo(nums, index + 1, currentOr, maxOr, memo);

        // Include current element
        count += backtrackMemo(nums, index + 1, currentOr | nums[index], maxOr, memo);

        memo.put(key, count);
        return count;
    }
}

C

// Method 1: Pruning
int count;

void backtrack(int* nums, int numsSize, int targetOr, int currOr, int index) {
    if(currOr == targetOr) {
        count += (1 << (numsSize - index));
        return;
    }

    for(int i = index; i < numsSize; i++) {
        backtrack(nums, numsSize, targetOr, currOr | nums[i], i + 1);
    }
}

int countMaxOrSubsets(int* nums, int numsSize) {
    count = 0;
    int maxOr = 0;

    for(int i = 0; i < numsSize; i++) {
        maxOr |= nums[i];
    }

    backtrack(nums, numsSize, maxOr, 0, 0);
    return count;
}

// Method 2: Memoization (using simple hash)
#define MAX_STATES 10000
int memo[MAX_STATES];
int used[MAX_STATES];

int hash_function(int index, int currentOr) {
    return (index * 1024 + currentOr) % MAX_STATES;
}

int backtrackMemo(int* nums, int numsSize, int index, int currentOr, int maxOr) {
    if (index == numsSize) {
        return currentOr == maxOr ? 1 : 0;
    }

    int key = hash_function(index, currentOr);
    if (used[key]) {
        return memo[key];
    }

    int count = backtrackMemo(nums, numsSize, index + 1, currentOr, maxOr);
    count += backtrackMemo(nums, numsSize, index + 1, currentOr | nums[index], maxOr);

    memo[key] = count;
    used[key] = 1;
    return count;
}

C++

class Solution {
public:
    // Method 1: Pruning
    int count = 0;

    int countMaxOrSubsets(vector<int>& nums) {
        count = 0;
        int maxOr = 0;
        for(int num : nums) maxOr |= num;
        backtrack(nums, maxOr, 0, 0);
        return count;
    }

private:
    void backtrack(vector<int>& nums, int targetOr, int currOr, int index) {
        if(currOr == targetOr) {
            count += (1 << (nums.size() - index));
            return;
        }

        for(int i = index; i < nums.size(); i++) {
            backtrack(nums, targetOr, currOr | nums[i], i + 1);
        }
    }
};

// Method 2: Memoization
class Solution {
private:
    unordered_map<string, int> memo;

    int backtrackMemo(vector<int>& nums, int index, int currentOr, int maxOr) {
        if (index == nums.size()) {
            return currentOr == maxOr ? 1 : 0;
        }

        string key = to_string(index) + "," + to_string(currentOr);
        if (memo.find(key) != memo.end()) {
            return memo[key];
        }

        int count = backtrackMemo(nums, index + 1, currentOr, maxOr);
        count += backtrackMemo(nums, index + 1, currentOr | nums[index], maxOr);

        memo[key] = count;
        return count;
    }

public:
    int countMaxOrSubsets(vector<int>& nums) {
        int maxOr = 0;
        for(int num : nums) maxOr |= num;

        memo.clear();
        return backtrackMemo(nums, 0, 0, maxOr);
    }
};

C

public class Solution {
    // Method 1: Pruning
    private int count = 0;

    public int CountMaxOrSubsets(int[] nums) {
        count = 0;
        int maxOr = 0;
        foreach(int num in nums) maxOr |= num;

        Backtrack(nums, maxOr, 0, 0);
        return count;
    }

    private void Backtrack(int[] nums, int targetOr, int currOr, int index) {
        if(currOr == targetOr) {
            count += (1 << (nums.Length - index));
            return;
        }

        for(int i = index; i < nums.Length; i++) {
            Backtrack(nums, targetOr, currOr | nums[i], i + 1);
        }
    }
}

// Method 2: Memoization
public class Solution {
    private Dictionary<string, int> memo;

    public int CountMaxOrSubsets(int[] nums) {
        int maxOr = 0;
        foreach(int num in nums) maxOr |= num;

        memo = new Dictionary<string, int>();
        return BacktrackMemo(nums, 0, 0, maxOr);
    }

    private int BacktrackMemo(int[] nums, int index, int currentOr, int maxOr) {
        if (index == nums.Length) {
            return currentOr == maxOr ? 1 : 0;
        }

        string key = $"{index},{currentOr}";
        if (memo.ContainsKey(key)) {
            return memo[key];
        }

        int count = BacktrackMemo(nums, index + 1, currentOr, maxOr);
        count += BacktrackMemo(nums, index + 1, currentOr | nums[index], maxOr);

        memo[key] = count;
        return count;
    }
}

Python

class Solution:
    # Method 1: Pruning
    def countMaxOrSubsets(self, nums: List[int]) -> int:
        max_or = 0
        for num in nums:
            max_or |= num

        self.count = 0

        def backtrack(index, curr_or):
            if curr_or == max_or:
                self.count += (1 << (len(nums) - index))
                return

            for i in range(index, len(nums)):
                backtrack(i + 1, curr_or | nums[i])

        backtrack(0, 0)
        return self.count

    # Method 2: Memoization
    def countMaxOrSubsetsMemo(self, nums: List[int]) -> int:
        max_or = 0
        for num in nums:
            max_or |= num

        memo = {}

        def backtrack(index, current_or):
            if index == len(nums):
                return 1 if current_or == max_or else 0

            key = (index, current_or)
            if key in memo:
                return memo[key]

            # Skip current element
            count = backtrack(index + 1, current_or)
            # Include current element
            count += backtrack(index + 1, current_or | nums[index])

            memo[key] = count
            return count

        return backtrack(0, 0)

Golang

// Method 1: Pruning
func countMaxOrSubsets(nums []int) int {
    maxOr := 0
    for _, num := range nums {
        maxOr |= num
    }

    count := 0

    var backtrack func(int, int)
    backtrack = func(index, currOr int) {
        if currOr == maxOr {
            count += (1 << (len(nums) - index))
            return
        }

        for i := index; i < len(nums); i++ {
            backtrack(i + 1, currOr | nums[i])
        }
    }

    backtrack(0, 0)
    return count
}

// Method 2: Memoization
func countMaxOrSubsetsMemo(nums []int) int {
    maxOr := 0
    for _, num := range nums {
        maxOr |= num
    }

    memo := make(map[string]int)

    var backtrack func(int, int) int
    backtrack = func(index, currentOr int) int {
        if index == len(nums) {
            if currentOr == maxOr {
                return 1
            }
            return 0
        }

        key := fmt.Sprintf("%d,%d", index, currentOr)
        if val, exists := memo[key]; exists {
            return val
        }

        count := backtrack(index + 1, currentOr)
        count += backtrack(index + 1, currentOr | nums[index])

        memo[key] = count
        return count
    }

    return backtrack(0, 0)
}

When to Use Each Method:

  • Pruning Method: Better for sparse problems where max OR is achieved quickly
  • Memoization Method: Better when there are many repeated subproblems, especially with smaller OR value ranges
0
Subscribe to my newsletter

Read articles from Anni Huang directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Anni Huang
Anni Huang

I am Anni HUANG, a software engineer with 3 years of experience in IDE development and Chatbot.