Set bits: The number of 1s in the binary representation of a number
- Naive approach: Check each bit individually
- Brian Kernighan's algorithm: Efficient method to count set bits
- Built-in functions: Use language-specific functions
For number 181 (binary: 10110101), we need to count the number of 1s
n = n & (n-1) removes the rightmost set bit in each iteration
Repeat until n becomes 0, counting each iteration
int countSetBits(int n) {
int count = 0;
while (n) {
n = n & (n-1); // Remove rightmost set bit
count++;
}
return count;
}
The number of set bits in 181 is 5 (binary: 10110101 has 5 ones)
• Time complexity: O(number of set bits) instead of O(log n)
• Bitwise AND: n & (n-1) removes the rightmost set bit
• Efficiency: Only iterates as many times as there are set bits
Subset generation: Using binary representation to represent inclusion/exclusion of elements
Each bit represents whether an element is included in the subset (1) or not (0)
From 0 to 2^n - 1, each number represents a unique subset
For each number, check which bits are set to determine subset elements
void generateSubsets(vector& arr) {
int n = arr.size();
for (int i = 0; i < (1 << n); i++) { // 2^n subsets
vector subset;
for (int j = 0; j < n; j++) {
if (i & (1 << j)) { // Check if j-th bit is set
subset.push_back(arr[j]);
}
}
print(subset); // Print current subset
}
}
Generated all 2^n subsets using bit manipulation, where each bit represents element inclusion
• Binary representation: Each bit corresponds to an element in the set
• Range: Iterate from 0 to 2^n - 1 to cover all possible combinations
• Efficiency: O(n * 2^n) time complexity for generating all subsets
XOR sum: Sum of XOR values for all pairs (i,j) where i < j
XOR is associative and commutative, and a ^ a = 0
For each bit position, count how many pairs contribute to the result
For each bit position, contribution = (count of 1s) * (count of 0s) * (1 << bit_position)
long long xorSum(vector& arr) {
long long result = 0;
int n = arr.size();
for (int bit = 0; bit < 32; bit++) {
long long countOnes = 0;
for (int num : arr) {
if (num & (1 << bit)) {
countOnes++;
}
}
long long countZeros = n - countOnes;
result += countOnes * countZeros * (1LL << bit);
}
return result;
}
The sum of XOR values for all pairs in [1,2,3] is 6, computed efficiently using bit analysis
• Bit-wise analysis: Process each bit position independently
• Contribution calculation: Each bit contributes based on count of 1s and 0s
• Efficiency: O(32n) instead of O(n²) brute force approach
Definitions & Concepts
- Binary representation: Numbers expressed in base-2 using only 0s and 1s
- Set bits: Number of 1s in the binary representation of a number
- Bit manipulation: Operations performed directly on binary representations
- Bitmasking: Using binary numbers to represent sets or states
Core Rules & Principles
- Bitwise operations: AND (&), OR (|), XOR (^), NOT (~), left shift (<<), right shift (>>)
- Set bit identification: n & (1 << i) checks if the i-th bit is set
- Set bit removal: n & (n-1) removes the rightmost set bit
- Parity: XOR of all bits gives parity of the number of set bits
Step-by-Step Methods
- Basic counting: Iterate through all bits and count 1s
- Kernighan's algorithm: Use n = n & (n-1) to efficiently count set bits
- Lookup table: Precompute results for smaller chunks
- Divide and conquer: Process bits in groups
Examples (Simple → Advanced)
- Simple: Count set bits in a single number: countSetBits(5) = 2 (binary: 101)
- Intermediate: Generate all subsets using bit manipulation
- Advanced: Calculate XOR sums of all pairs efficiently
Tips & Tricks
- Power of 2 check: (n & (n-1)) == 0 indicates n is a power of 2
- Rightmost set bit: n & (-n) isolates the rightmost set bit
- Toggle all bits: ~n flips all bits (be careful with signed integers)
- Fast multiplication/division: Left/right shifts for powers of 2
Common Pitfalls
- Sign extension: Be careful with signed integers during bit operations
- Overflow: Shifting beyond the integer width causes undefined behavior
- Zero handling: Special cases when n = 0 in iterative algorithms
- Integer limits: Consider using long long for large calculations
Memorization Aids
- Kernighan's algorithm: n & (n-1) removes rightmost set bit
- Subset generation: 1 << n creates 2^n combinations
- Bit checking: n & (1 << i) tests if i-th bit is set
- Set bit isolation: n & (-n) gets rightmost set bit
Round up to power of 2: Use bit manipulation to find the next power of two
If n is already a power of 2, return n
Subtract 1 to get n-1, then set all bits to the right of the highest set bit
Add 1 to get the next power of two
int nextPowerOfTwo(int n) {
if (n <= 1) return 1;
n--;
n |= n >> 1;
n |= n >> 2;
n |= n >> 4;
n |= n >> 8;
n |= n >> 16;
return n + 1;
}
Efficiently finds the next power of two using bitwise operations
Bit reversal: Swap bits from opposite ends toward the center
Exchange pairs of adjacent bits
Exchange pairs of 2-bit groups
Repeat with 4-bit, 8-bit, and 16-bit groups
uint32_t reverseBits(uint32_t n) {
n = ((n & 0xAAAAAAAA) >> 1) | ((n & 0x55555555) << 1);
n = ((n & 0xCCCCCCCC) >> 2) | ((n & 0x33333333) << 2);
n = ((n & 0xF0F0F0F0) >> 4) | ((n & 0x0F0F0F0F) << 4);
n = ((n & 0xFF00FF00) >> 8) | ((n & 0x00FF00FF) << 8);
n = (n >> 16) | (n << 16);
return n;
}
Efficiently reverses all bits using divide-and-conquer approach
Subset enumeration: Using 0 to 2^n - 1 to represent all subsets
Parity checks: Using XOR to determine odd/even occurrences
Single element problems: Finding unique elements using XOR properties
- Brute force: Try all possibilities using bitmasks
- Optimized counting: Process each bit position independently
- Mathematical properties: Use XOR and AND properties
- Dynamic programming: With bitmasks for state representation
• Use unsigned integers when performing bit operations
• Be careful with operator precedence in complex expressions
• Document bit manipulation code thoroughly
• Consider edge cases like 0 and negative numbers
Questions & Answers
Question: What is Brian Kernighan's algorithm and why is it more efficient than naive counting?
Answer: Brian Kernighan's algorithm uses the property that n & (n-1) removes the rightmost set bit from n.
In the naive approach, we check all bits (O(log n)), but in Kernighan's algorithm, we only iterate as many times as there are set bits (O(number of set bits)). For sparse numbers with few set bits, this is significantly faster.
Example: For n = 12 (binary: 1100), the algorithm only runs 2 iterations (for the 2 set bits) instead of 4 (log₂12 ≈ 4).
Question: How do I know when to use bit manipulation instead of regular arithmetic?
Answer: Consider bit manipulation when you notice these patterns:
- Problems involving powers of 2 or binary representations
- Need to track inclusion/exclusion (subset problems)
- Operations like finding unique elements or pairs
- Parity checks or toggling states
- Problems where you need to optimize O(n²) to O(n) or O(log n)
Bit operations are typically faster and can lead to elegant solutions for these types of problems.
Question: Can you explain how (n & (n-1)) removes the rightmost set bit?
Answer: When you subtract 1 from a number, it flips all bits starting from the rightmost set bit:
If n = 12 (binary: 1100), then n-1 = 11 (binary: 1011)
n & (n-1) = 1100 & 1011 = 1000
The rightmost set bit (at position 2) and all trailing zeros are cleared. This works because subtracting 1 borrows from the rightmost set bit, turning it to 0 and setting all trailing zeros to 1s. The AND operation then clears those bits.