cphb/chapter10.tex

545 lines
15 KiB
TeX

\chapter{Bit manipulation}
All data in computer programs is internally stored as bits,
i.e., as numbers 0 and 1.
In this chapter, we will learn how integers
are represented as bits, and how bit operations
can be used to manipulate them.
It turns out that there are many uses for
bit operations in algorithm programming.
\section{Bit representation}
\index{bit representation}
Every nonnegative integer can be represented as a sum
\[c_k 2^k + \ldots + c_2 2^2 + c_1 2^1 + c_0 2^0,\]
where each coefficient $c_i$ is either 0 or 1.
The bit representation of such a number is
$c_k \cdots c_2 c_1 c_0$.
For example, the number 43 corresponds to the sum
\[1 \cdot 2^5 + 0 \cdot 2^4 + 1 \cdot 2^3 + 0 \cdot 2^2 + 1 \cdot 2^1 + 1 \cdot 2^0,\]
so the bit representation of the number is 101011.
In programming, the length of the bit representation
depends on the data type of the number.
For example, in C++ the type \texttt{int} is
usually a 32-bit type and an \texttt{int} number
consists of 32 bits.
Thus, the bit representation of 43
as an \texttt{int} number is as follows:
\[00000000000000000000000000101011\]
The bit representation of a number is either
\key{signed} or \key{unsigned}.
Usually a signed representation is used,
which means that both negative and positive
numbers can be represented.
A signed number of $n$ bits can contain any
integer between $2^{n-1}$ and $2^{n-1}-1$.
For example, the \texttt{int} type in C++ is
a signed type, and it can contain any
integer between $2^{31}$ and $2^{31}-1$.
The first bit in a signed representation
is the sign of the number (0 for nonnegative numbers
and 1 for negative numbers), and
the remaining $n-1$ bits contain the magnitude of the number.
\key{Two's complement} is used, which means that the
opposite number of a number is calculated by first
inverting all the bits in the number,
and then increasing the number by one.
For example, the bit representation of $-43$
as an \texttt{int} number is as follows:
\[11111111111111111111111111010101\]
In a signed representation, only nonnegative
numbers can be used, but the upper bound of the numbers is larger.
A signed number of $n$ bits can contain any
integer between $0$ and $2^n-1$.
For example, the \texttt{unsigned int} type in C++
can contain any integer between $0$ and $2^{32}-1$.
There is a connection between signed and unsigned
representations:
a number $-x$ in a signed representation
equals the number $2^n-x$ in an unsigned representation.
For example, the following code shows that
the signed number $x=-43$ equals the unsigned
number $y=2^{32}-43$:
\begin{lstlisting}
int x = -43;
unsigned int y = x;
cout << x << "\n"; // -43
cout << y << "\n"; // 4294967253
\end{lstlisting}
If a number is larger than the upper bound
of the bit representation, the number will overflow.
In a signed representation,
the next number after $2^{n-1}-1$ is $-2^{n-1}$,
and in an unsigned representation,
the next number after $2^{n-1}$ is $0$.
For example, consider the following code:
\begin{lstlisting}
int x = 2147483647
cout << x << "\n"; // 2147483647
x++;
cout << x << "\n"; // -2147483648
\end{lstlisting}
Initially, the value of $x$ is $2^{31}-1$.
This is the largest number that can be stored
in an \texttt{int} variable,
so the next number after $2^{31}-1$ is $-2^{31}$.
\section{Bit operations}
\newcommand\XOR{\mathbin{\char`\^}}
\subsubsection{And operation}
\index{and operation}
The \key{and} operation $x$ \& $y$ produces a number
that has one bits in positions where both
$x$ and $y$ have one bits.
For example, $22$ \& $26$ = 18, because
\begin{center}
\begin{tabular}{rrr}
& 10110 & (22)\\
\& & 11010 & (26) \\
\hline
= & 10010 & (18) \\
\end{tabular}
\end{center}
Using the and operation, we can check if a number
$x$ is even because
$x$ \& $1$ = 0 if $x$ is even, and
$x$ \& $1$ = 1 if $x$ is odd.
More generally, $x$ is divisible by $2^k$
exactly when $x$ \& $(2^k-1)$ = 0.
\subsubsection{Or operation}
\index{or operation}
The \key{or} operation $x$ | $y$ produces a number
that has one bits in positions where at least one
of $x$ and $y$ have one bits.
For example, $22$ | $26$ = 30, because
\begin{center}
\begin{tabular}{rrr}
& 10110 & (22)\\
| & 11010 & (26) \\
\hline
= & 11110 & (30) \\
\end{tabular}
\end{center}
\subsubsection{Xor operation}
\index{xor operation}
The \key{xor} operation $x$ $\XOR$ $y$ produces a number
that has one bits in positions where exactly one
of $x$ and $y$ have one bits.
For example, $22$ $\XOR$ $26$ = 12, because
\begin{center}
\begin{tabular}{rrr}
& 10110 & (22)\\
$\XOR$ & 11010 & (26) \\
\hline
= & 01100 & (12) \\
\end{tabular}
\end{center}
\subsubsection{Not operation}
\index{not operation}
The \key{not} operation \textasciitilde$x$
produces a number where all the bits of $x$
have been inverted.
The formula \textasciitilde$x = -x-1$ holds,
for example, \textasciitilde$29 = -30$.
The result of the not operation at the bit level
depends on the length of the bit representation,
because the operation changes all bits.
For example, if the numbers are 32-bit
\texttt{int} numbers, the result is as follows:
\begin{center}
\begin{tabular}{rrrr}
$x$ & = & 29 & 00000000000000000000000000011101 \\
\textasciitilde$x$ & = & $-30$ & 11111111111111111111111111100010 \\
\end{tabular}
\end{center}
\subsubsection{Bit shifts}
\index{bit shift}
The left bit shift $x < < k$ appends $k$
zero bits to the number,
and the right bit shift $x > > k$
removes the $k$ last bits from the number.
For example, $14 < < 2 = 56$,
because $14$ equals 1110
and $56$ equals 111000.
Similarly, $49 > > 3 = 6$,
because $49$ equals 110001
and $6$ equals 110.
Note that $x < < k$
corresponds to multiplying $x$ by $2^k$,
and $x > > k$
corresponds to dividing $x$ by $2^k$
rounded down to an integer.
\subsubsection{Applications}
A number of the form $1 < < k$ has a one bit
in position $k$ and all other bits are zero,
so we can use such numbers to access single bits of numbers.
For example, the $k$th bit of a number is one
exactly when $x$ \& $(1 < < k)$ is not zero.
The following code prints the bit representation
of an \texttt{int} number $x$:
\begin{lstlisting}
for (int i = 31; i >= 0; i--) {
if (x&(1<<i)) cout << "1";
else cout << "0";
}
\end{lstlisting}
It is also possible to modify single bits
of numbers using the above idea.
For example, the expression $x$ | $(1 < < k)$
sets the $k$th bit of $x$ to one,
the expression
$x$ \& \textasciitilde $(1 < < k)$
sets the $k$th bit of $x$ to zero,
and the expression
$x$ $\XOR$ $(1 < < k)$
inverts the $k$th bit of $x$.
The formula $x$ \& $(x-1)$ sets the last
one bit of $x$ to zero,
and the formula $x$ \& $-x$ sets all the
one bits to zero, except for the last one bit.
The formula $x$ | $(x-1)$
inverts all the bits after the last one bit.
Also note that a positive number $x$ is
of the form $2^k$ if $x$ \& $(x-1) = 0$.
\subsubsection*{Additional functions}
The g++ compiler provides the following
functions for counting bits:
\begin{itemize}
\item
$\texttt{\_\_builtin\_clz}(x)$:
the number of zeros at the beginning of the number
\item
$\texttt{\_\_builtin\_ctz}(x)$:
the number of zeros at the end of the number
\item
$\texttt{\_\_builtin\_popcount}(x)$:
the number of ones in the number
\item
$\texttt{\_\_builtin\_parity}(x)$:
the parity (even or odd) of the number of ones
\end{itemize}
\begin{samepage}
The functions can be used as follows:
\begin{lstlisting}
int x = 5328; // 00000000000000000001010011010000
cout << __builtin_clz(x) << "\n"; // 19
cout << __builtin_ctz(x) << "\n"; // 4
cout << __builtin_popcount(x) << "\n"; // 5
cout << __builtin_parity(x) << "\n"; // 1
\end{lstlisting}
\end{samepage}
The above functions support \texttt{int} numbers,
but there are also \texttt{long long} functions
available with the suffix \texttt{ll}.
\section{Representing sets}
Each subset of a set $\{0,1,2,\ldots,n-1\}$
corresponds to an $n$ bit number
where the one bits indicate which elements
are included in the subset.
For example, the set $\{1,3,4,8\}$
corresponds to the number $2^8+2^4+2^3+2^1=282$,
whose bit representation is 100011010.
The benefit in using the bit representation
is that the information whether an element belongs
to the set requires only one bit of memory.
In addition, set operations can be efficiently
implemented as bit operations.
\subsubsection{Set implementation}
In the following code, $x$
contains a subset of $\{0,1,2,\ldots,31\}$.
The code adds the elements 1, 3, 4 and 8
to the set and then prints the elements.
\begin{lstlisting}
// x is an empty set
int x = 0;
// add elements 1, 3, 4 and 8 to the set
x |= (1<<1);
x |= (1<<3);
x |= (1<<4);
x |= (1<<8);
// print the elements in the set
for (int i = 0; i < 32; i++) {
if (x&(1<<i)) cout << i << " ";
}
cout << "\n";
\end{lstlisting}
The output of the code is as follows:
\begin{lstlisting}
1 3 4 8
\end{lstlisting}
\subsubsection{Set operations}
Set operations can be implemented as follows:
\begin{itemize}
\item $a$ \& $b$ is the intersection $a \cap b$ of $a$ and $b$
\item $a$ | $b$ is the union $a \cup b$ of $a$ and $b$
\item \textasciitilde$a$ is the complement $\bar a$ of $a$
\item $a$ \& (\textasciitilde$b$) is the difference
$a \setminus b$ of $a$ and $b$
\end{itemize}
For example, the following code constructs the union
of $\{1,3,4,8\}$ and $\{3,6,8,9\}$:
\begin{lstlisting}
// set {1,3,4,8}
int x = (1<<1)+(1<<3)+(1<<4)+(1<<8);
// set {3,6,8,9}
int y = (1<<3)+(1<<6)+(1<<8)+(1<<9);
// union of the sets
int z = x|y;
// print the elements in the union
for (int i = 0; i < 32; i++) {
if (z&(1<<i)) cout << i << " ";
}
cout << "\n";
\end{lstlisting}
The output of the code is as follows:
\begin{lstlisting}
1 3 4 6 8 9
\end{lstlisting}
\subsubsection{Iterating through subsets}
The following code goes through
the subsets of $\{0,1,\ldots,n-1\}$:
\begin{lstlisting}
for (int b = 0; b < (1<<n); b++) {
// process subset b
}
\end{lstlisting}
The following code goes through
the subsets with exactly $k$ elements:
\begin{lstlisting}
for (int b = 0; b < (1<<n); b++) {
if (__builtin_popcount(b) == k) {
// process subset b
}
}
\end{lstlisting}
The following code goes through the subsets
of a set $x$:
\begin{lstlisting}
int b = 0;
do {
// process subset b
} while (b=(b-x)&x);
\end{lstlisting}
\section{Dynamic programming}
\subsubsection{From permutations to subsets}
Using dynamic programming, it is often possible
to change an iteration over permutations into
an iteration over subsets, so that
the dynamic programming state
contains a subset of a set and possibly
some additional information.
The benefit in this is that
$n!$, the number of permutations of an $n$ element set,
is much larger than $2^n$, the number of subsets
of the same set.
For example, if $n=20$, then
$n! \approx 2.4 \cdot 10^{18}$ and $2^n \approx 10^6$.
Hence, for certain values of $n$,
we can efficiently go through subsets but not through permutations.
As an example, consider the problem of
calculating the number of
permutations of a set $\{0,1,\ldots,n-1\}$,
where the difference between any two consecutive
elements is larger than one.
For example, when $n=4$, there are two such permutations:
$(1,3,0,2)$ and $(2,0,3,1)$.
Let $f(x,k)$ denote the number of valid permutations
of a subset $x$ where the last element is $k$ and
the difference between any two consecutive
elements is larger than one.
For example, $f(\{0,1,3\},1)=1$,
because there is a permutation $(0,3,1)$,
and $f(\{0,1,3\},3)=0$, because 0 and 1
cannot be next to each other.
Using $f$, the answer to the problem equals
\[ \sum_{i=0}^{n-1} f(\{0,1,\ldots,n-1\},i), \]
because the permutation has to contain all
elements $\{0,1,\ldots,n-1\}$ and the last
element can be any element.
The dynamic programming values can be stored as follows:
\begin{lstlisting}
int d[1<<n][n];
\end{lstlisting}
First, $f(\{k\},k)=1$ for all values of $k$:
\begin{lstlisting}
for (int i = 0; i < n; i++) d[1<<i][i] = 1;
\end{lstlisting}
Then, the other values can be calculated
as follows:
\begin{lstlisting}
for (int b = 0; b < (1<<n); b++) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (abs(i-j) > 1 && (b&(1<<i)) && (b&(1<<j))) {
d[b][i] += d[b^(1<<i)][j];
}
}
}
}
\end{lstlisting}
In the above code,
the variable $b$ goes through all subsets and each
permutation is of the form $(\ldots,j,i)$,
where the difference between $i$ and $j$ is
larger than one and $i$ and $j$ belong to $b$.
Finally, the number of solutions can be
calculated as follows:
\begin{lstlisting}
int s = 0;
for (int i = 0; i < n; i++) {
s += d[(1<<n)-1][i];
}
\end{lstlisting}
\subsubsection{Counting subsets}
Our last problem in this chapter is as follows:
We are given a collection $C$ that consists of $m$ sets,
and our task is to determine for each set
the number of sets in $C$ that are its subsets.
For example, consider the following collection:
\[C = \{\{0\}, \{0,2\}, \{1,4\}, \{0,1,4\}, \{1,4,5\}\}\]
For any set $x$ in $C$,
let $f(x)$ denote the number of sets (including $x$) in $C$
that are subsets of $x$.
For example, $f(\{0,1,4\})=3$, because the
sets $\{0\}$, $\{1,4\}$ and $\{0,1,4\}$ are
subsets of $\{0,1,4\}$.
Using this notation, our task is to calculate the value of $f(x)$
for every set $x$ in the collection.
We will assume that each set is
a subset of $\{0,1,\ldots,n-1\}$.
Thus, the collection can contain at most
$2^n$ sets.
A straightforward way to solve the problem
is to go through all pairs of sets in the collection.
However, a more efficient solution is possible
using dynamic programming.
Let $c(x,k)$ denote the number of sets in
$C$ that equal a set $x$
if we are allowed to remove any subset of
$\{0,1,\ldots,k\}$ from $x$.
For example, in the above collection,
$c(\{0,1,4\},1)=2$,
where the corresponding sets are
$\{1,4\}$ and $\{0,1,4\}$.
It turns out that we can calculate all
values of $c(x,k)$ in $O(2^n n)$ time.
This solves our problem, because
\[f(x)=c(x,n-1).\]
The base cases for the function are:
\begin{equation*}
c(x,-1) = \begin{cases}
0 & \textrm{if $x \notin C$}\\
1 & \textrm{if $x \in C$}\\
\end{cases}
\end{equation*}
For larger values of $k$, the following recursion holds:
\begin{equation*}
c(x,k) = \begin{cases}
c(x,k-1) & \textrm{if $k \notin x$}\\
c(x,k-1)+c(x \setminus \{k\},k-1) & \textrm{if $k \in x$}\\
\end{cases}
\end{equation*}
We can conveniently implement the algorithm by representing
the sets using bits.
Assume that there is an array
\begin{lstlisting}
int d[1<<n];
\end{lstlisting}
that is initialized so that $d[x]=1$ if $x$ belongs to $C$
and otherwise $d[x]=0$.
We can now implement the algorithm as follows:
\begin{lstlisting}
for (int k = 0; k < n; k++) {
for (int b = 0; b < (1<<n); b++) {
if (b&(1<<k)) d[b] += d[b^(1<<k)];
}
}
\end{lstlisting}
The above code is based on the recursive definition
of $c$. As a special trick, the code only uses
the array $d$ to calculate all values of the function.
Finally, for each set $x$ in $C$, $f(x)=d[x]$.