Bit operations are unary and binary operations on the binary representations of integers. They are divided into two categories: bitwise operations and shifts. Bit operations are among the most basic operations in a CPU, and are often quite fast.
Integers and bit sequences
See also: integer types, complement representation
A fixed-length sequence consisting only of 0 and 1 is called a bit sequence. The leftmost bit is the most significant bit, and the rightmost bit is the least significant bit.
Computers use bit sequences to represent integers in a certain range. A bit sequence of length has possible forms, so it can correspond one-to-one with integers. These correspondences fall into two categories: signed and unsigned. Signed means that the corresponding integers include negative numbers; unsigned means that all corresponding integers are nonnegative.
-
For an unsigned correspondence, we can directly use the binary representation of an integer as the bit sequence, padding the high bits with
0if it is too short.Under an unsigned correspondence, a bit sequence of length can represent integers in .
-
For a signed correspondence, there are two representation rules: ones' complement and two's complement.
For nonnegative integers, the representation rule is the same as for the unsigned case. For negative integers, invert each bit of the bit sequence corresponding to its opposite (changing
0to1and1to0); the result is called the ones' complement. Convert the ones' complement to an integer under the unsigned correspondence, add one, and convert it back to a bit sequence under the unsigned correspondence. Discard any part exceeding the original bit-sequence length; the new sequence is called the two's complement.Under the ones' complement correspondence, a bit sequence of length can represent integers in .
Under the two's complement correspondence, a bit sequence of length can represent integers in .
For example, for -bit sequences:
| Bit sequence | Unsigned integer | Signed integer (ones' complement) | Signed integer (two's complement) |
|---|---|---|---|
000 | |||
001 | |||
010 | |||
011 | |||
100 | |||
101 | |||
110 | |||
111 |
The main problem with ones' complement is that it contains , a “negative number” that does not actually exist, so in general we use only two's complement. When representing signed integers, the sign is determined solely by the most significant bit of the bit sequence, so this bit is called the sign bit.
Converting a bit sequence to an integer is also easy: for a nonnegative number, no special operation is needed; for ones' complement, invert the bits to obtain the corresponding opposite number; for two's complement, invert the bits and add one to obtain the corresponding opposite number.
Bitwise operations
A bitwise operation applies a Boolean function independently to each bit of a bit sequence. Formally, for a Boolean function , a bitwise operation is a function of the form
where is the length of the bit sequence. Again, we generally study only unary and binary bitwise operations. Unless otherwise specified, the bitwise operations below are limited to these two cases.
In general, we regard bitwise NOT, bitwise AND, bitwise OR, and bitwise XOR as the basic bitwise operations; all other bitwise operations can be composed from them.
| Bitwise operation | Mathematical notation | Corresponding Boolean function | C++ operator | Explanation |
|---|---|---|---|---|
| Bitwise NOT | ~ | becomes , and becomes | ||
| Bitwise AND | & | It is only when both corresponding bits are | ||
| Bitwise OR | | | It is when at least one of the two corresponding bits is | ||
| Bitwise XOR | 、 | ^ | It is only when the two corresponding bits differ |
Warning
Be careful to distinguish bitwise operations from Boolean functions.
For example:
- ,
- ,
- ,
- .
Since the four bitwise operations above operate independently on each bit, they directly inherit the properties of their corresponding Boolean functions.
For convenience, when the bit-sequence length is known, we can also apply bitwise operations directly to integers, for example:
Assume . We can also express bitwise operations using sums:
When there is no ambiguity, “bitwise” is omitted below.
Shifts
See also: C++ bitwise operators.
A shift is a binary operation that “moves a bit sequence left or right bit by bit.” Its first argument is a bit sequence, and its second argument is generally a nonnegative integer. Moving left is called a left shift, and moving right is called a right shift. According to how the positions vacated by the shift are filled, shift operations can be divided into arithmetic shifts, logical shifts, and rotations. Specifically:
- Logical shifts fill the empty positions with 0;
- arithmetic right shifts fill the empty positions with the sign bit, while arithmetic left shifts are the same as logical left shifts;
- rotations fill the empty positions with the bits that overflow.
For example, for the -bit sequence 10 01 01 10:
| Operation | Result |
|---|---|
| Arithmetic left shift by bits | 01 01 10 00 |
| Arithmetic right shift by bits | 11 10 01 01 |
| Logical left shift by bits | 01 01 10 00 |
| Logical right shift by bits | 00 10 01 01 |
| Rotate left by bits | 01 01 10 10 |
| Rotate right by bits | 10 10 01 01 |
In C++, a << b denotes a left shift and a >> b denotes a right shift; see C++ bitwise operators for the specific shift rules.
A rotation can be implemented with the following code:
Implementation
// From <https://stackoverflow.com/a/776523/224132>
#include <climits>
#include <cstdint>
uint32_t rotl32(uint32_t value, unsigned int count) {
const unsigned int mask = CHAR_BIT * sizeof(value) - 1;
count &= mask;
return (value << count) | (value >> (-count & mask));
}
uint32_t rotr32(uint32_t value, unsigned int count) {
const unsigned int mask = CHAR_BIT * sizeof(value) - 1;
count &= mask;
return (value >> count) | (value << (-count & mask));
}
Applications of bit operations
Bit operations generally have three uses:
- Perform certain operations efficiently in place of less efficient methods. See compiler optimizations #strength reduction.
- Represent sets (often used in state-compression DP).
- The problem itself requires bit operations.
Note that replacing other operations with bit operations often does not bring much optimization, and may instead make the code more complex, so it should be considered carefully.
Applications involving powers of 2
Since bit operations target binary representations, many applications involving integral powers of 2 follow naturally.
Multiplying (dividing) a number by a nonnegative integral power of 2:
C++
int mulPowerOfTwo(int n, int m) { // Compute n*(2^m)
return n << m;
}
int divPowerOfTwo(int n, int m) { // Compute n/(2^m)
return n >> m;
}
Python
def mulPowerOfTwo(n, m): # Compute n*(2^m)
return n << m
def divPowerOfTwo(n, m): # Compute n/(2^m)
return n >> m
Warning
The division we usually write rounds toward , whereas the right shift here rounds down (note the difference). The two methods are equivalent when the number is greater than or equal to , but differ when it is less than : for example, -1 / 2 has value , while -1 >> 1 has value .
Absolute value
On some machines, this is more efficient than n > 0 ? n : -n.
C++
int Abs(int n) {
return (n ^ (n >> 31)) - (n >> 31);
/* n>>31 gives the sign of n: 0 when n is positive and -1 when n is negative
For positive n, n^0=n and the number is unchanged; for negative n, n^(-1)
computes the complements of n and -1, then applies XOR;
the result changes the sign of n and is one less than its absolute value; subtracting -1 gives the absolute value */
}
Python
def Abs(n):
return (n ^ (n >> 31)) - (n >> 31)
"""
n >> 31 obtains the sign of n: it is 0 when n is positive and -1 when n is negative
If n is positive, n ^ 0 = n and the value is unchanged; if n is negative, compute n ^ (-1)
Compute the two's-complement representations of n and -1, then XOR them.
This flips the sign and yields |n| - 1; subtracting -1 then gives the absolute value
"""
Maximum/minimum of two numbers
On some machines, this is more efficient than a > b ? a : b.
C++
// If a >= b, (a - b) >> 31 is 0; otherwise it is -1
int max(int a, int b) { return (b & ((a - b) >> 31)) | (a & (~(a - b) >> 31)); }
int min(int a, int b) { return (a & ((a - b) >> 31)) | (b & (~(a - b) >> 31)); }
Python
# If a >= b, (a - b) >> 31 is 0; otherwise, it is -1
def max(a, b):
return b & ((a - b) >> 31) | a & (~(a - b) >> 31)
def min(a, b):
return a & ((a - b) >> 31) | b & (~(a - b) >> 31)
Determining whether two nonzero numbers have the same sign
C++
bool isSameSign(int x, int y) { // The case involving 0 is exceptional
return (x ^ y) >= 0;
}
Python
# The case involving 0 is an exception
def isSameSign(x, y):
return (x ^ y) >= 0
Swapping two numbers
This method has limitations
This method can only be used to swap two integers, so its scope is limited.
For general swapping, it is recommended to call std::swap from the algorithm library directly.
void swap(int& a, int& b) { a ^= b ^= a ^= b; }
Manipulating the binary bits of a number
Get a particular bit of a number's binary representation:
C++
// Get bit b of a, with the least significant bit numbered 0
int getBit(int a, int b) { return (a >> b) & 1; }
Python
# Get bit b of a, with the least significant bit numbered 0
def getBit(a, b):
return (a >> b) & 1
Set a particular bit of a number's binary representation to :
C++
// Set bit b to 0, with the least significant bit numbered 0
int unsetBit(int a, int b) { return a & ~(1 << b); }
Python
# Set bit b to 0, with the least significant bit numbered 0
def unsetBit(a, b):
return a & ~(1 << b)
Set a particular bit of a number's binary representation to :
C++
// Set bit b to 1, with the least significant bit numbered 0
int setBit(int a, int b) { return a | (1 << b); }
Python
# Set bit b to 1, with the least significant bit numbered 0
def setBit(a, b):
return a | (1 << b)
Invert a particular bit of a number's binary representation:
C++
// Invert bit b of a, with the least significant bit numbered 0
int flapBit(int a, int b) { return a ^ (1 << b); }
Python
# Invert bit b of a, with the least significant bit numbered 0
def flapBit(a, b):
return a ^ (1 << b)
These operations are equivalent to treating a -bit integer variable as a Boolean array of length .
Hamming weight
The Hamming weight is the number of symbols in a string that differ from the zero symbol (defined in the character set being used). For a binary number, its Hamming weight is the number of s (that is, popcount).
The Hamming weight of a number can be computed by looping: repeatedly remove the last bit of the number in binary (that is, right-shift by ), maintain an answer variable, and update the answer according to whether the least significant bit is during the division.
The code is as follows:
// Compute the Hamming weight of x
int popcount(int x) {
int cnt = 0;
while (x) {
cnt += x & 1;
x >>= 1;
}
return cnt;
}
The Hamming weight of a number can also be computed using the lowbit operation: repeatedly subtract its lowbit1 until the number becomes .
The code is as follows:
// Compute the Hamming weight of x
int popcount(int x) {
int cnt = 0;
while (x) {
cnt++;
x -= x & -x;
}
return cnt;
}
Constructing a permutation with increasing Hamming weights
In state-compression DP, enumerating in increasing order of popcount can sometimes avoid enumerating states repeatedly. This is one important use of constructing a permutation with increasing Hamming weights.
Let us investigate how to construct such a permutation in time.
We know that the smallest integer with Hamming weight is . If we can construct, in constant time, a successor with the same Hamming weight as an integer, then by enumerating Hamming weights and repeatedly finding the next number starting from , we can construct in time the required permutation of .
The successor of a number with the same Hamming weight can be found as follows, using as an example:
-
Move the rightmost of one position to the left. If it cannot move, move the to its left, and so on, obtaining .
-
In the resulting , move all the s from the original position of the last moved through the least significant bit to the far right. The last moved was originally in the third position, so the last three bits should become , giving .
This process can be optimized with bit operations:
int t = x + (x & -x);
x = t | ((((t & -t) / (x & -x)) >> 1) - 1);
- In the first step, we add
lowbitto . In binary, this is equivalent to replacing the rightmost consecutive block of s in with a immediately to its left. For the binary number mentioned above, adding itslowbitgives . This obtains the first half of the answer. - We next fill the s in the rest of the answer. The
lowbitof is the position after moving the leftmost of the rightmost consecutive block of s in , while thelowbitof is the position of the rightmost of in that block. For , , , and . - The division operation that follows is the hardest part of this bit operation to understand, but also the most crucial. Let the highest-position in the rightmost consecutive block of s of the original number be at position (positions start at ), and the lowest-position at position . Then the
lowbitof equals1 << (r+1), and thelowbitof equals1 << l;(((t&-t)/(x&-x))>>1)gives(1<<(r+1))/(1<<l)/2 = (1<<r)/(1<<l) = 1<<(r-l), which in binary is a followed by zeros. The number of zeros is exactly the number of consecutive s minus . In our example, . Subtracting yields the low bits to fill; ORing this with the original number gives the answer.
Therefore, the complete code for enumerating in increasing Hamming-weight order is:
for (int i = 0; (1 << i) - 1 <= n; i++) {
for (int x = (1 << i) - 1, t; x <= n; t = x + (x & -x),
x = x ? (t | ((((t & -t) / (x & -x)) >> 1) - 1)) : (n + 1)) {
// Write the operation to be completed
}
}
The special case must be handled, because has no successor with the same Hamming weight.
Related classes and functions in C++
GCC built-in functions
GCC also has several built-in functions for bit operations:
int __builtin_ffs(int x): returns the position of the last in the binary representation of , with positions numbered from (the least significant bit is numbered ). It returns when is .int __builtin_clz(unsigned int x): returns the number of leading s in the binary representation of . The result is undefined when is .int __builtin_ctz(unsigned int x): returns the number of consecutive trailing s in the binary representation of . The result is undefined when is .int __builtin_clrsb(int x): when the sign bit of is , returns the number of leading s in the binary representation of minus one; otherwise, it returns the number of leading s of minus one.int __builtin_popcount(unsigned int x): returns the number of s in the binary representation of .int __builtin_parity(unsigned int x): determines whether the number of s in the binary representation of is odd or even.
Each of these functions can have l or ll appended to its name (for example, __builtin_popcountll) to change the parameter type to (unsigned)long or (unsigned)long long; the return type remains int.
For example, we sometimes want to find the base-2 logarithm of a number. Ignoring the special case of 0, this is equivalent to the number of bits in its binary representation minus -1. The number of bits in the binary representation of an N-bit integer n can be expressed as N - __builtin_clz(n), so N - 1 - __builtin_clz(n) gives the base-2 logarithm of n.
Since these are built-in functions, they have been highly optimized by the compiler and run very quickly (some need only a single instruction).
More bits
If the bit sequence to be operated on is very long, use std::bitset.
Recommended problems
References and notes
- Bit Twiddling Hacks
- Bit Operation Builtins (Using the GNU Compiler Collection (GCC))
- Bitwise operation
Footnotes
-
The first in a number's binary representation when read from low to high, together with the zeros after it; for example, the
lowbitof is . See Fenwick tree. ↩