1. Bitwise Operators & Masks
An int is just 32 bits sitting in memory, and C++ gives you direct
operators on those bits: & (AND), | (OR), ^
(XOR), ~ (NOT), << (left shift) and >>
(right shift). A mask is just an integer whose bit pattern you build on
purpose so that combining it with another integer isolates, sets, or clears the bits
you care about.
// 1 << k is an int with only bit k set -- the building block for every mask below
int mask = 1 << 3; // 0b00001000
// Check if bit k is set
bool isSet(int n, int k) {
return (n & (1 << k)) != 0;
}
// Set bit k to 1
int setBit(int n, int k) {
return n | (1 << k);
}
// Clear bit k to 0
int clearBit(int n, int k) {
return n & ~(1 << k);
}
// Toggle bit k
int toggleBit(int n, int k) {
return n ^ (1 << k);
}
// n << 1 doubles n; n >> 1 halves n (integer division, rounding toward negative infinity
// for signed types) -- both run in O(1), which is why shifting beats *2 or /2 in tight loops
Reading these back in English builds the intuition you'll lean on constantly:
n & (n - 1) clears the lowest set bit, n & -n isolates
it, and n & 1 tells you if n is odd. None of these need a
loop — each is a single O(1) CPU instruction.
&, | and ^ bind looser than comparison operators like == and < in C++. if (n & 1 == 0) parses as n & (1 == 0), not what you want — always wrap bitwise expressions used in a condition in explicit parentheses: if ((n & 1) == 0).
2. The XOR Trick Family
XOR has two properties that make it the single most reused operator in interview bit
tricks: x ^ x == 0 (a value cancels itself out) and x ^ 0 == x
(zero is the identity element). Combined with the fact that XOR is commutative and
associative — order and grouping don't matter — you can XOR a whole collection together
and every value that appears an even number of times disappears.
#include <vector>
using namespace std;
// Every value appears twice except one -- find the one that appears once.
// O(n) time, O(1) space -- no hash set needed.
int singleNumber(const vector<int>& nums) {
int result = 0;
for (int x : nums) result ^= x; // pairs cancel to 0, the lone value survives
return result;
}
// nums contains n distinct values from 0..n, one is missing -- find it.
// XOR every index 0..n with every value in nums; every present value cancels
// with its index-twin, leaving only the missing number.
int missingNumber(const vector<int>& nums) {
int result = nums.size(); // account for index n up front
for (int i = 0; i < (int)nums.size(); i++) {
result ^= i;
result ^= nums[i];
}
return result;
}
The same idea extends further: to find two numbers that each appear once while every
other number appears twice, XOR the whole array first — the result is
a ^ b for the two lone values. Because a != b, that XOR has at
least one set bit; picking any one set bit and splitting the array into "bit set" and
"bit clear" groups puts a and b in different groups, and
XOR-ing each group separately recovers both values in a second O(n) pass.
You'll see a ^= b; b ^= a; a ^= b; as a way to swap without a temp variable. It's clever but fragile — it silently breaks if a and b alias the same memory location, and it's no faster than std::swap on a modern compiler. Know it for interviews, but use std::swap(a, b) in real code.
3. Counting Set Bits: Brian Kernighan's Algorithm
The naive way to count how many bits are set in an integer checks all 32 bit positions
one at a time — O(32), effectively O(1) but wasteful. Brian Kernighan's
algorithm does better by only doing work proportional to the number of set
bits: repeatedly clear the lowest set bit with n & (n - 1) until
n becomes zero, counting how many clears it took.
// n & (n - 1) always clears exactly the lowest set bit of n.
// Example: n = 0b1011000, n - 1 = 0b1010111, n & (n-1) = 0b1010000
int countSetBits(unsigned int n) {
int count = 0;
while (n != 0) {
n &= (n - 1); // drop the lowest set bit
count++;
}
return count; // O(number of set bits), not O(32)
}
// GCC/Clang also expose a built-in that compiles to a single hardware instruction
// where available -- use it directly in real code, but know how to derive
// countSetBits by hand for an interview.
int countSetBitsBuiltin(unsigned int n) {
return __builtin_popcount(n);
}
Why does n & (n - 1) clear exactly the lowest set bit? Subtracting 1
flips every trailing zero to a 1 and flips the lowest set bit to a 0; ANDing that with
the original n keeps every higher bit unchanged but zeroes out that lowest
set bit and all the trailing zeros beneath it, which were already zero.
A power of two has exactly one set bit, so n & (n - 1) == 0 (for n > 0) is a one-line, branch-free power-of-two check — a favorite quick warm-up question precisely because it tests whether you understand why the trick works, not just that it exists.
4. Subset Enumeration via Bitmasking
A set of n elements has exactly 2ᵃ subsets, and every
one of them can be represented as an n-bit number: bit i is 1
if element i is included, 0 if it's excluded. That means looping a mask
from 0 to (1 << n) - 1 and reading off which bits are set
visits every subset exactly once — no recursion required.
#include <vector>
using namespace std;
// Generate the power set of nums (all 2^n subsets) via bitmasking.
// O(2^n * n) time: 2^n masks, O(n) work to build each subset.
vector<vector<int>> powerSet(const vector<int>& nums) {
int n = nums.size();
vector<vector<int>> result;
for (int mask = 0; mask < (1 << n); mask++) {
vector<int> subset;
for (int i = 0; i < n; i++) {
if (mask & (1 << i)) { // is bit i set in this mask?
subset.push_back(nums[i]);
}
}
result.push_back(subset);
}
return result;
}
// Bitmask DP building block you'll reuse starting Week 13: iterate over every
// SUBMASK of a given mask -- useful for "assign these items to groups" DP.
void forEachSubmask(int mask) {
for (int sub = mask; sub > 0; sub = (sub - 1) & mask) {
// process sub -- a proper subset (or all) of mask's set bits
}
// process 0 separately if the empty submask counts
}
Bitmasking only stays practical while 2ᵃ fits comfortably in an
int or long long and finishes within your time limit — in
practice that means n up to roughly 20–22 for an O(2ᵃ) enumeration.
Beyond that, you need the backtracking-with-pruning approach Week 8 covers instead.
"Which subset of items have I used so far?" is exactly what a bitmask encodes in a single integer, which is why problems like the Traveling Salesman DP or "assign tasks to workers" use dp[mask][i] arrays. Being fluent with masks now means that state design will feel natural rather than mysterious when it shows up in Week 13's dynamic programming.
5. Hands-on Exercise
Build a bitmask toolkit and solve "Two Single Numbers"
Combine this week's XOR trick, Kernighan's algorithm, and masking helpers into one small library, then use it to solve a problem that needs all of them together.
Requirements:
- Implement
isSet,setBit,clearBitandtoggleBitexactly as shown in Section 1, with a smallmainthat prints a few results to confirm correctness. - Implement
countSetBitsusing Brian Kernighan's algorithm, and verify it against__builtin_popcountfor at least 20 random integers. - Implement
vector<int> twoSingleNumbers(const vector<int>& nums)for an array where every value appears exactly twice except two values that appear once each — return those two values in either order. - Inside
twoSingleNumbers, XOR the whole array to getxorAll = a ^ b, isolate its lowest set bit withxorAll & (-xorAll), and use that bit to splitnumsinto two groups whose separate XORs give youaandb. - Write a small test harness with at least 3 hand-built arrays (including one where
aandbdiffer only in the highest bit) that confirms your function returns the correct pair every time. - State the time and space complexity of
twoSingleNumbersin a comment above the function.
Use long long when computing xorAll & (-xorAll) if you're testing negative numbers — two's-complement negation of INT_MIN overflows a 32-bit int. For the split step, remember you only need one bit that differs between a and b, not all of them.
6. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why does XOR-ing every element of an array in which each value appears exactly twice, except one, always leave that one value as the result?
Why does XOR-ing every element of an array in which each value appears exactly twice, except one, always leave that one value as the result?
XOR is commutative and associative, so the order you combine values in doesn't matter — every pair of equal values can be grouped together and cancels to 0 because x ^ x == 0. Zero is also XOR's identity element (x ^ 0 == x), so once every pair has cancelled, all that remains is the single unpaired value XOR-ed with 0, which is itself.
Q2
Explain why n & (n - 1) always clears exactly the lowest set bit of n.
Explain why n & (n - 1) always clears exactly the lowest set bit of n.
Subtracting 1 from n flips the lowest set bit to 0 and flips every trailing zero below it to 1, while leaving all higher bits unchanged. ANDing that with the original n keeps the unchanged higher bits, but the lowest set bit is now 0 in one operand so it becomes 0 in the result, and the trailing positions were already 0 in n, so the net effect is exactly that one bit being cleared.
Q3
What's the time and space complexity of enumerating every subset of an n-element set via bitmasking, and for roughly what range of n is that practical?
What's the time and space complexity of enumerating every subset of an n-element set via bitmasking, and for roughly what range of n is that practical?
There are 2ᵃ masks, and building each subset from a mask costs O(n), so total time is O(2ᵃ × n); space is O(n) per subset, or O(2ᵃ × n) if every subset is stored at once. Because 2ᵃ grows exponentially, this stays practical only up to roughly n = 20–22 on typical time limits — beyond that you need pruning or a fundamentally different approach.
Q4
In the "two single numbers" problem, why must the two lone values end up in different groups when you split the array by one set bit of a ^ b?
In the "two single numbers" problem, why must the two lone values end up in different groups when you split the array by one set bit of a ^ b?
A bit is set in a ^ b exactly where a and b differ, so any chosen set bit is guaranteed to be 1 in one of them and 0 in the other — that difference is what routes them into opposite groups. Every other value in the array appears twice and both copies are identical, so both copies always land in the same group and still cancel out via XOR within that group, leaving only a in one group's XOR and only b in the other's.