Week 5: Bit Manipulation

Week 4 built pattern-matching skills on top of the byte-level view of a string that rolling hashes exposed. This week goes one level lower still — into the individual bits behind every int — because the XOR trick, Brian Kernighan's algorithm, and bitmask subset enumeration are exactly the tools that turn an O(2²) subset search from Week 8's backtracking into a tight, allocation-free loop, and they resurface again as a compact DP state starting Week 13. By the end of this week you'll read a bitwise expression as fluently as an arithmetic one.

Module 4 of 17 Week 5 of 20 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Use bitwise operators and masks to read, set, clear and toggle individual bits
  • Apply the XOR trick family to solve single-number and missing-number problems in O(n) time and O(1) space
  • Enumerate every subset of a small set with a bitmask, and count set bits with Brian Kernighan's algorithm

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.

masks.cpp
// 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.

Watch operator precedence

&, | 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.

xor_tricks.cpp
#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.

XOR swap is a trap, not a trick worth using

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.

kernighan.cpp
// 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.

The same trick checks "is this a power of two?"

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.

bitmask_subsets.cpp
#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.

Bitmasks make great DP state

"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

Hands-on

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:

  1. Implement isSet, setBit, clearBit and toggleBit exactly as shown in Section 1, with a small main that prints a few results to confirm correctness.
  2. Implement countSetBits using Brian Kernighan's algorithm, and verify it against __builtin_popcount for at least 20 random integers.
  3. 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.
  4. Inside twoSingleNumbers, XOR the whole array to get xorAll = a ^ b, isolate its lowest set bit with xorAll & (-xorAll), and use that bit to split nums into two groups whose separate XORs give you a and b.
  5. Write a small test harness with at least 3 hand-built arrays (including one where a and b differ only in the highest bit) that confirms your function returns the correct pair every time.
  6. State the time and space complexity of twoSingleNumbers in a comment above the function.
Hint

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?

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.

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?

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?

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.