Facebook Pixel

2166. Design Bitset

MediumDesignArrayHash TableString
Leetcode Link

Problem Description

You need to implement a Bitset class that efficiently manages a collection of bits (0s and 1s). The class should support the following operations:

Constructor:

  • Bitset(size): Creates a bitset with size bits, all initially set to 0.

Methods:

  • fix(idx): Sets the bit at index idx to 1. If it's already 1, nothing changes.
  • unfix(idx): Sets the bit at index idx to 0. If it's already 0, nothing changes.
  • flip(): Inverts all bits in the bitset - every 0 becomes 1 and every 1 becomes 0.
  • all(): Returns true if every bit in the bitset is 1, otherwise returns false.
  • one(): Returns true if at least one bit in the bitset is 1, otherwise returns false.
  • count(): Returns the total number of 1 bits in the bitset.
  • toString(): Returns a string representation of the bitset where the character at position i represents the bit at index i.

The solution uses a clever dual-array approach where:

  • Array a stores the current state of the bitset
  • Array b stores the flipped state of the bitset
  • Variable cnt tracks the count of 1s in array a

This design makes the flip() operation extremely efficient (O(1)) by simply swapping the two arrays and adjusting the count, rather than iterating through all bits. When fixing or unfixing individual bits, both arrays are updated to maintain consistency. The toString() method returns the joined representation of array a.

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

Intuition

The key challenge in this problem is handling the flip() operation efficiently. A naive approach would iterate through all bits and flip each one, taking O(n) time. However, when we need to flip frequently, this becomes a bottleneck.

The breakthrough insight is that instead of actually flipping all bits, we can maintain what the flipped version would look like at all times. Think of it like having a photo and its negative - when you need the negative, you don't process the photo each time, you just switch to the pre-computed negative.

This leads us to maintain two arrays:

  • Array a: represents the current state of our bitset
  • Array b: represents the complement (flipped version) of our bitset

When flip() is called, we simply swap these arrays - what was the normal becomes the flipped, and what was flipped becomes the normal. This reduces the flip operation from O(n) to O(1).

To make this work, we need to ensure both arrays stay synchronized during fix() and unfix() operations. When we set a bit to 1 in array a, we must set the corresponding bit to 0 in array b (since it's the complement). Similarly, when we set a bit to 0 in array a, we set it to 1 in array b.

We also maintain a counter cnt to track the number of 1s in array a. This allows us to answer count(), all(), and one() queries in O(1) time without scanning the entire array. When we flip, the new count becomes size - cnt (all the 0s become 1s and vice versa).

This design trades space (using two arrays instead of one) for time efficiency, making all operations except toString() run in constant time.

Solution Approach

The implementation uses two character arrays and a counter to efficiently manage the bitset operations:

Data Structure:

  • self.a: Character array storing the current bitset state (initially all '0')
  • self.b: Character array storing the complement of the bitset (initially all '1')
  • self.cnt: Integer counter tracking the number of '1' bits in array a

Constructor (__init__):

self.a = ['0'] * size  # Current state: all zeros
self.b = ['1'] * size  # Complement: all ones
self.cnt = 0           # Count of 1s in array a

Initializes both arrays and sets the counter to 0 since all bits start as '0'.

fix(idx) Method:

if self.a[idx] == '0':
    self.a[idx] = '1'
    self.cnt += 1
self.b[idx] = '0'
  • Only updates array a and increments counter if the bit was '0'
  • Always sets the corresponding bit in array b to '0' (complement of '1')

unfix(idx) Method:

if self.a[idx] == '1':
    self.a[idx] = '0'
    self.cnt -= 1
self.b[idx] = '1'
  • Only updates array a and decrements counter if the bit was '1'
  • Always sets the corresponding bit in array b to '1' (complement of '0')

flip() Method:

self.a, self.b = self.b, self.a
self.cnt = len(self.a) - self.cnt
  • Swaps the two arrays in O(1) time using Python's tuple unpacking
  • Updates the counter: if there were cnt ones before, there are now size - cnt ones (all zeros became ones)

Query Methods:

  • all(): Returns self.cnt == len(self.a) - checks if all bits are '1'
  • one(): Returns self.cnt > 0 - checks if at least one bit is '1'
  • count(): Returns self.cnt - directly returns the maintained counter
  • toString(): Returns ''.join(self.a) - joins the character array into a string

All operations except toString() run in O(1) time. The toString() method requires O(n) time to concatenate the array elements. The space complexity is O(n) for storing the two arrays.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example with a bitset of size 5 to illustrate how the dual-array approach works:

Initial State after Bitset(5):

a = ['0', '0', '0', '0', '0']  (current state)
b = ['1', '1', '1', '1', '1']  (complement)
cnt = 0

Operation 1: fix(1) - Set index 1 to '1'

Before: a[1] = '0', so we update it
After:  a = ['0', '1', '0', '0', '0']
        b = ['1', '0', '1', '1', '1']  (b[1] set to '0' as complement)
        cnt = 1

Operation 2: fix(3) - Set index 3 to '1'

Before: a[3] = '0', so we update it
After:  a = ['0', '1', '0', '1', '0']
        b = ['1', '0', '1', '0', '1']  (b[3] set to '0' as complement)
        cnt = 2

Operation 3: flip() - Invert all bits

Instead of iterating through all bits, we simply swap arrays:
Before swap: a = ['0', '1', '0', '1', '0'], b = ['1', '0', '1', '0', '1']
After swap:  a = ['1', '0', '1', '0', '1']  (old b becomes new a)
             b = ['0', '1', '0', '1', '0']  (old a becomes new b)
             cnt = 5 - 2 = 3  (size - old_cnt)

Notice how after the flip:

  • The bits that were '0' in the original (indices 0, 2, 4) are now '1'
  • The bits that were '1' in the original (indices 1, 3) are now '0'
  • This happened in O(1) time by swapping references!

Operation 4: count() - Get number of 1s

Returns: cnt = 3  (no array traversal needed)

Operation 5: unfix(0) - Set index 0 to '0'

Before: a[0] = '1', so we update it
After:  a = ['0', '0', '1', '0', '1']
        b = ['1', '1', '0', '1', '0']  (b[0] set to '1' as complement)
        cnt = 2

Operation 6: toString() - Get string representation

Returns: "00101"  (joining array a)

The key insight is that array b always maintains the exact opposite of array a. When we need to flip everything, instead of changing n bits individually, we just swap our perspective - what was the inverse becomes the primary, achieving O(1) flip operation.

Solution Implementation

1class Bitset:
2    def __init__(self, size: int):
3        """
4        Initialize a Bitset of given size with all bits set to 0.
5      
6        Args:
7            size: The number of bits in the bitset
8        """
9        # Main array representing the current state of bits
10        self.bits = ['0'] * size
11        # Complement array (inverse of bits) for O(1) flip operation
12        self.complement = ['1'] * size
13        # Counter to track number of '1' bits in the main array
14        self.ones_count = 0
15
16    def fix(self, idx: int) -> None:
17        """
18        Set the bit at index idx to 1.
19      
20        Args:
21            idx: Index of the bit to set to 1
22        """
23        # Only increment counter if bit was previously 0
24        if self.bits[idx] == '0':
25            self.bits[idx] = '1'
26            self.ones_count += 1
27        # Update complement array accordingly
28        self.complement[idx] = '0'
29
30    def unfix(self, idx: int) -> None:
31        """
32        Set the bit at index idx to 0.
33      
34        Args:
35            idx: Index of the bit to set to 0
36        """
37        # Only decrement counter if bit was previously 1
38        if self.bits[idx] == '1':
39            self.bits[idx] = '0'
40            self.ones_count -= 1
41        # Update complement array accordingly
42        self.complement[idx] = '1'
43
44    def flip(self) -> None:
45        """
46        Flip all bits in the bitset (0 becomes 1, 1 becomes 0).
47        Achieved in O(1) by swapping main and complement arrays.
48        """
49        # Swap the main array with its complement
50        self.bits, self.complement = self.complement, self.bits
51        # Update the count of ones (total bits minus current ones)
52        self.ones_count = len(self.bits) - self.ones_count
53
54    def all(self) -> bool:
55        """
56        Check if all bits are set to 1.
57      
58        Returns:
59            True if all bits are 1, False otherwise
60        """
61        return self.ones_count == len(self.bits)
62
63    def one(self) -> bool:
64        """
65        Check if at least one bit is set to 1.
66      
67        Returns:
68            True if at least one bit is 1, False otherwise
69        """
70        return self.ones_count > 0
71
72    def count(self) -> int:
73        """
74        Get the count of bits set to 1.
75      
76        Returns:
77            Number of bits currently set to 1
78        """
79        return self.ones_count
80
81    def toString(self) -> str:
82        """
83        Get string representation of the bitset.
84      
85        Returns:
86            String containing all bits in order
87        """
88        return ''.join(self.bits)
89
1class Bitset {
2    // Primary bit array storing the actual bitset values
3    private char[] bitArray;
4  
5    // Complement array storing the flipped version of bitArray
6    private char[] complementArray;
7  
8    // Counter tracking the number of '1' bits in bitArray
9    private int onesCount;
10
11    /**
12     * Initializes a Bitset of given size with all bits initially set to 0
13     * @param size The size of the bitset
14     */
15    public Bitset(int size) {
16        bitArray = new char[size];
17        complementArray = new char[size];
18      
19        // Initialize primary array with all zeros
20        Arrays.fill(bitArray, '0');
21      
22        // Initialize complement array with all ones (opposite of primary)
23        Arrays.fill(complementArray, '1');
24      
25        // Initially no bits are set to 1
26        onesCount = 0;
27    }
28
29    /**
30     * Sets the bit at index idx to 1
31     * @param idx The index of the bit to set
32     */
33    public void fix(int idx) {
34        // Only increment count if bit was previously 0
35        if (bitArray[idx] == '0') {
36            bitArray[idx] = '1';
37            onesCount++;
38        }
39        // Update complement array to maintain inverse relationship
40        complementArray[idx] = '0';
41    }
42
43    /**
44     * Sets the bit at index idx to 0
45     * @param idx The index of the bit to unset
46     */
47    public void unfix(int idx) {
48        // Only decrement count if bit was previously 1
49        if (bitArray[idx] == '1') {
50            bitArray[idx] = '0';
51            onesCount--;
52        }
53        // Update complement array to maintain inverse relationship
54        complementArray[idx] = '1';
55    }
56
57    /**
58     * Flips all bits in the bitset (0 becomes 1, 1 becomes 0)
59     * Achieves O(1) time complexity by swapping arrays
60     */
61    public void flip() {
62        // Swap the primary and complement arrays
63        char[] temp = bitArray;
64        bitArray = complementArray;
65        complementArray = temp;
66      
67        // Update count: new ones count = total bits - previous ones count
68        onesCount = bitArray.length - onesCount;
69    }
70
71    /**
72     * Checks if all bits are set to 1
73     * @return true if all bits are 1, false otherwise
74     */
75    public boolean all() {
76        return onesCount == bitArray.length;
77    }
78
79    /**
80     * Checks if at least one bit is set to 1
81     * @return true if at least one bit is 1, false otherwise
82     */
83    public boolean one() {
84        return onesCount > 0;
85    }
86
87    /**
88     * Returns the count of bits set to 1
89     * @return The number of 1s in the bitset
90     */
91    public int count() {
92        return onesCount;
93    }
94
95    /**
96     * Returns string representation of the bitset
97     * @return String containing the bit sequence
98     */
99    public String toString() {
100        return String.valueOf(bitArray);
101    }
102}
103
104/**
105 * Your Bitset object will be instantiated and called as such:
106 * Bitset obj = new Bitset(size);
107 * obj.fix(idx);
108 * obj.unfix(idx);
109 * obj.flip();
110 * boolean param_4 = obj.all();
111 * boolean param_5 = obj.one();
112 * int param_6 = obj.count();
113 * String param_7 = obj.toString();
114 */
115
1class Bitset {
2private:
3    string currentBits;      // Current bitset representation
4    string complementBits;   // Complement of current bitset (0s become 1s, 1s become 0s)
5    int oneCount;           // Count of '1' bits in currentBits
6  
7public:
8    /**
9     * Constructor: Initialize a bitset of given size with all bits set to 0
10     * @param size: The number of bits in the bitset
11     */
12    Bitset(int size) {
13        currentBits = string(size, '0');      // Initialize all bits to 0
14        complementBits = string(size, '1');   // Complement starts with all 1s
15        oneCount = 0;                          // Initially no bits are set
16    }
17  
18    /**
19     * Set the bit at given index to 1
20     * @param idx: The index of the bit to set
21     */
22    void fix(int idx) {
23        if (currentBits[idx] == '0') {
24            currentBits[idx] = '1';
25            ++oneCount;
26        }
27        complementBits[idx] = '0';  // Update complement accordingly
28    }
29  
30    /**
31     * Set the bit at given index to 0
32     * @param idx: The index of the bit to unset
33     */
34    void unfix(int idx) {
35        if (currentBits[idx] == '1') {
36            currentBits[idx] = '0';
37            --oneCount;
38        }
39        complementBits[idx] = '1';  // Update complement accordingly
40    }
41  
42    /**
43     * Flip all bits in the bitset (0 becomes 1, 1 becomes 0)
44     * Achieved in O(1) by swapping current and complement strings
45     */
46    void flip() {
47        swap(currentBits, complementBits);
48        oneCount = currentBits.size() - oneCount;  // Update count after flip
49    }
50  
51    /**
52     * Check if all bits are set to 1
53     * @return true if all bits are 1, false otherwise
54     */
55    bool all() {
56        return oneCount == currentBits.size();
57    }
58  
59    /**
60     * Check if at least one bit is set to 1
61     * @return true if at least one bit is 1, false otherwise
62     */
63    bool one() {
64        return oneCount > 0;
65    }
66  
67    /**
68     * Get the count of bits set to 1
69     * @return The number of 1s in the bitset
70     */
71    int count() {
72        return oneCount;
73    }
74  
75    /**
76     * Get string representation of the current bitset
77     * @return The bitset as a string of '0's and '1's
78     */
79    string toString() {
80        return currentBits;
81    }
82};
83
84/**
85 * Your Bitset object will be instantiated and called as such:
86 * Bitset* obj = new Bitset(size);
87 * obj->fix(idx);
88 * obj->unfix(idx);
89 * obj->flip();
90 * bool param_4 = obj->all();
91 * bool param_5 = obj->one();
92 * int param_6 = obj->count();
93 * string param_7 = obj->toString();
94 */
95
1// Current bitset representation
2let currentBits: string;
3
4// Complement of current bitset (0s become 1s, 1s become 0s)
5let complementBits: string;
6
7// Count of '1' bits in currentBits
8let oneCount: number;
9
10/**
11 * Initialize a bitset of given size with all bits set to 0
12 * @param size - The number of bits in the bitset
13 */
14function Bitset(size: number): void {
15    // Initialize all bits to 0
16    currentBits = '0'.repeat(size);
17  
18    // Complement starts with all 1s
19    complementBits = '1'.repeat(size);
20  
21    // Initially no bits are set
22    oneCount = 0;
23}
24
25/**
26 * Set the bit at given index to 1
27 * @param idx - The index of the bit to set
28 */
29function fix(idx: number): void {
30    // Convert strings to arrays for manipulation
31    const currentArray = currentBits.split('');
32    const complementArray = complementBits.split('');
33  
34    // Only increment count if bit was previously 0
35    if (currentArray[idx] === '0') {
36        currentArray[idx] = '1';
37        oneCount++;
38    }
39  
40    // Update complement accordingly
41    complementArray[idx] = '0';
42  
43    // Convert arrays back to strings
44    currentBits = currentArray.join('');
45    complementBits = complementArray.join('');
46}
47
48/**
49 * Set the bit at given index to 0
50 * @param idx - The index of the bit to unset
51 */
52function unfix(idx: number): void {
53    // Convert strings to arrays for manipulation
54    const currentArray = currentBits.split('');
55    const complementArray = complementBits.split('');
56  
57    // Only decrement count if bit was previously 1
58    if (currentArray[idx] === '1') {
59        currentArray[idx] = '0';
60        oneCount--;
61    }
62  
63    // Update complement accordingly
64    complementArray[idx] = '1';
65  
66    // Convert arrays back to strings
67    currentBits = currentArray.join('');
68    complementBits = complementArray.join('');
69}
70
71/**
72 * Flip all bits in the bitset (0 becomes 1, 1 becomes 0)
73 * Achieved in O(1) by swapping current and complement strings
74 */
75function flip(): void {
76    // Swap current and complement bitsets
77    const temp = currentBits;
78    currentBits = complementBits;
79    complementBits = temp;
80  
81    // Update count after flip (total bits minus previous count)
82    oneCount = currentBits.length - oneCount;
83}
84
85/**
86 * Check if all bits are set to 1
87 * @returns true if all bits are 1, false otherwise
88 */
89function all(): boolean {
90    return oneCount === currentBits.length;
91}
92
93/**
94 * Check if at least one bit is set to 1
95 * @returns true if at least one bit is 1, false otherwise
96 */
97function one(): boolean {
98    return oneCount > 0;
99}
100
101/**
102 * Get the count of bits set to 1
103 * @returns The number of 1s in the bitset
104 */
105function count(): number {
106    return oneCount;
107}
108
109/**
110 * Get string representation of the current bitset
111 * @returns The bitset as a string of '0's and '1's
112 */
113function toString(): string {
114    return currentBits;
115}
116

Time and Space Complexity

Time Complexity:

  • __init__(size): O(size) - Creates two arrays of length size and initializes them
  • fix(idx): O(1) - Only updates a single index in both arrays and potentially updates the counter
  • unfix(idx): O(1) - Only updates a single index in both arrays and potentially updates the counter
  • flip(): O(1) - Swaps references between the two arrays and updates the counter
  • all(): O(1) - Simple comparison operation
  • one(): O(1) - Simple comparison operation
  • count(): O(1) - Returns the counter value
  • toString(): O(size) - Joins all elements of array a into a string

Space Complexity:

  • Overall: O(size) - The class maintains two arrays a and b, each of length size, plus a constant amount of additional variables (cnt)
  • The space is dominated by the two character arrays storing '0' and '1' values

Key Optimization: The clever use of two complementary arrays (a and b) allows the flip() operation to run in O(1) time by simply swapping references, rather than iterating through all bits which would take O(size) time. Array b always maintains the flipped version of array a, enabling this constant-time flip operation at the cost of doubling the space requirement.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Forgetting to Update the Complement Array

A critical mistake is only updating the main array (self.bits) in fix() and unfix() methods without updating the complement array. This breaks the invariant that the complement array should always be the inverse of the main array.

Incorrect Implementation:

def fix(self, idx: int) -> None:
    if self.bits[idx] == '0':
        self.bits[idx] = '1'
        self.ones_count += 1
    # MISSING: self.complement[idx] = '0'

Why This Fails: When flip() is called, the arrays are swapped. If the complement wasn't properly maintained, the bitset will have incorrect values after flipping.

Solution: Always update both arrays to maintain the invariant:

def fix(self, idx: int) -> None:
    if self.bits[idx] == '0':
        self.bits[idx] = '1'
        self.ones_count += 1
    self.complement[idx] = '0'  # Always update complement

2. Incorrect Count Update After Flip

Another common error is forgetting to update or incorrectly calculating ones_count after a flip operation.

Incorrect Implementation:

def flip(self) -> None:
    self.bits, self.complement = self.complement, self.bits
    # WRONG: Forgetting to update ones_count
    # OR WRONG: self.ones_count = self.ones_count

Why This Fails: After flipping, all 0s become 1s and vice versa. If there were k ones before, there are now size - k ones.

Solution:

def flip(self) -> None:
    self.bits, self.complement = self.complement, self.bits
    self.ones_count = len(self.bits) - self.ones_count

3. Redundant Counter Updates

Updating the counter even when the bit value doesn't change leads to incorrect counts.

Incorrect Implementation:

def fix(self, idx: int) -> None:
    self.bits[idx] = '1'
    self.ones_count += 1  # WRONG: Always incrementing
    self.complement[idx] = '0'

Why This Fails: If the bit at idx is already '1', incrementing the counter again will give an incorrect count.

Solution: Check the current value before updating:

def fix(self, idx: int) -> None:
    if self.bits[idx] == '0':  # Only update if changing
        self.bits[idx] = '1'
        self.ones_count += 1
    self.complement[idx] = '0'

4. Using Wrong Data Types

Using a list of integers instead of characters might seem natural but can cause issues with string concatenation.

Incorrect Implementation:

def __init__(self, size: int):
    self.bits = [0] * size  # Using integers
    self.complement = [1] * size
  
def toString(self) -> str:
    return ''.join(self.bits)  # TypeError: sequence item 0: expected str instance, int found

Solution: Use character arrays or convert properly:

# Option 1: Use characters from the start
self.bits = ['0'] * size
self.complement = ['1'] * size

# Option 2: Convert when needed
def toString(self) -> str:
    return ''.join(str(bit) for bit in self.bits)
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following is a min heap?


Recommended Readings

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

Load More