cphb/luku10.tex

555 lines
14 KiB
TeX

\chapter{Bit manipulation}
A computer internally manipulates data
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 for manipulating them.
It turns out that there are many uses for
bit operations in the implementation of algorithms.
\section{Bit representation}
\index{bit representation}
The \key{bit representation} of a number
indicates which powers of two form the number.
For example, the bit representation of the number 43
is 101011 because
$43 = 2^5 + 2^3 + 2^1 + 2^0$ where
bits 0, 1, 3 and 5 from the right are ones,
and all other bits are zeros.
The length of a bit representation of a number
in a computer is static, and depends on the
data type chosen.
For example, the \texttt{int} type in C++ is
usually a 32-bit type, and an \texttt{int} number
consists of 32 bits.
In this case, 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}.
The first bit of a signed number is the sign
($+$ or $-$), and we can represent numbers
$-2^{n-1} \ldots 2^{n-1}-1$ using $n$ bits.
In an unsigned number, in turn,
all bits belong to the number and we
can represent numbers $0 \ldots 2^n-1$ using $n$ bits.
In an signed bit representation,
the first bit of a nonnegative number is 0,
and the first bit of a negative number is 1.
\key{Two's complement} is used which means that
the opposite number of a number can be calculated
by first inversing all the bits in the number,
and then increasing the number by one.
For example, the representation of $-43$
as an \texttt{int} number is as follows:
\[11111111111111111111111111010101\]
The connection between signed and unsigned numbers
is that the representations of a signed
number $-x$ and an unsigned number $2^n-x$
are equal.
Thus, the above representation corresponds to
the unsigned number $2^{32}-43$.
In C++, the numbers are signed as default,
but we can create unsigned numbers by
using the keyword \texttt{unsigned}.
For example, in the code
\begin{lstlisting}
int x = -43;
unsigned int y = x;
cout << x << "\n"; // -43
cout << y << "\n"; // 4294967253
\end{lstlisting}
the signed number
$x=-43$ becomes the unsigned number $y=2^{32}-43$.
If a number becomes too large or too small for the
bit representation chosen, it will overflow.
In practice, 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, in the code
\begin{lstlisting}
int x = 2147483647
cout << x << "\n"; // 2147483647
x++;
cout << x << "\n"; // -2147483648
\end{lstlisting}
we increase $2^{31}-1$ by one to get $-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 bit 1 in positions where both the numbers
$x$ and $y$ have bit 1.
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.
\subsubsection{Or operation}
\index{or operation}
The \key{or} operation $x$ | $y$ produces a number
that has bit 1 in positions where at least one
of the numbers
$x$ and $y$ have bit 1.
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 bit 1 in positions where exactly one
of the numbers
$x$ and $y$ have bit 1.
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 inversed.
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$ produces a number
where the bits of $x$ have been moved $k$ steps to
the left by adding $k$ zero bits to the number.
The right bit shift $x > > k$ produces a number
where the bits of $x$ have been moved $k$ steps
to the right by removing $k$ last bits from the number.
For example, $14 < < 2 = 56$
because $14$ equals 1110,
and it becomes $56$ that equals 111000.
Correspondingly, $49 > > 3 = 6$
because $49$ equals 110001,
and it becomes $6$ that equals 110.
Note that the left bit shift $x < < k$
corresponds to multiplying $x$ by $2^k$,
and the right bit shift $x > > k$
corresponds to dividing $x$ by $2^k$
rounding downwards.
\subsubsection{Bit manipulation}
The bits in a number are indexed from the right
to the left beginning from zero.
A number of the form $1 < < k$ contains a one bit
in position $k$, and all other bits are zero,
so we can manipulate single bits of numbers
using these numbers.
The $k$th bit in $x$ is one if
$x$ \& $(1 < < k) = (1 < < k)$.
The formula $x$ | $(1 < < k)$
sets the $k$th bit of $x$ to one,
the formula
$x$ \& \textasciitilde $(1 < < k)$
sets the $k$th bit of $x$ to zero,
and the formula
$x$ $\XOR$ $(1 < < k)$
inverses 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)$, in turn,
inverses 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 contains the following
functions for bit manipulation:
\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 following code shows how to use the functions:
\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 functions support \texttt{int} numbers,
but there are also \texttt{long long} versions
of the functions
available with the prefix \texttt{ll}.
\section{Joukon bittiesitys}
Joukon $\{0,1,2,\ldots,n-1\}$
jokaista osajoukkoa
vastaa $n$-bittinen luku,
jossa ykkösbitit ilmaisevat,
mitkä alkiot ovat mukana osajoukossa.
Esimerkiksi joukkoa $\{1,3,4,8\}$
vastaa bittiesitys 100011010 eli luku
$2^8+2^4+2^3+2^1=282$.
Joukon bittiesitys vie vähän muistia,
koska tieto kunkin alkion kuulumisesta
osajoukkoon vie vain yhden bitin tilaa.
Lisäksi bittimuodossa tallennettua joukkoa
on tehokasta käsitellä bittioperaatioilla.
\subsubsection{Joukon käsittely}
Seuraavan koodin muuttuja $x$
sisältää joukon $\{0,1,2,\ldots,31\}$
osajoukon.
Koodi lisää luvut 1, 3, 4 ja 8
joukkoon ja tulostaa
joukon sisällön.
\begin{lstlisting}
// x on tyhjä joukko
int x = 0;
// lisätään luvut 1, 3, 4 ja 8 joukkoon
x |= (1<<1);
x |= (1<<3);
x |= (1<<4);
x |= (1<<8);
// tulostetaan joukon sisältö
for (int i = 0; i < 32; i++) {
if (x&(1<<i)) cout << i << " ";
}
cout << "\n";
\end{lstlisting}
Koodin tulostus on seuraava:
\begin{lstlisting}
1 3 4 8
\end{lstlisting}
Kun joukko on tallennettu bittiesityksenä,
niin joukko-operaatiot voi toteuttaa
tehokkaasti bittioperaatioiden avulla:
\begin{itemize}
\item $a$ \& $b$ on joukkojen $a$ ja $b$ leikkaus $a \cap b$
(tämä sisältää alkiot,
jotka ovat kummassakin joukossa)
\item $a$ | $b$ on joukkojen $a$ ja $b$ yhdiste $a \cup b$
(tämä sisältää alkiot,
jotka ovat ainakin toisessa joukossa)
\item $a$ \& (\textasciitilde$b$) on joukkojen $a$ ja $b$ erotus
$a \setminus b$ (tämä sisältää alkiot,
jotka ovat joukossa $a$ mutta eivät joukossa $b$)
\end{itemize}
Seuraava koodi muodostaa
joukkojen $\{1,3,4,8\}$ ja $\{3,6,8,9\}$ yhdisteen:
\begin{lstlisting}
// joukko {1,3,4,8}
int x = (1<<1)+(1<<3)+(1<<4)+(1<<8);
// joukko {3,6,8,9}
int y = (1<<3)+(1<<6)+(1<<8)+(1<<9);
// joukkojen yhdiste
int z = x|y;
// tulostetaan yhdisteen sisältö
for (int i = 0; i < 32; i++) {
if (z&(1<<i)) cout << i << " ";
}
cout << "\n";
\end{lstlisting}
Koodin tulostus on seuraava:
\begin{lstlisting}
1 3 4 6 8 9
\end{lstlisting}
\subsubsection{Osajoukkojen läpikäynti}
Seuraava koodi käy läpi joukon $\{0,1,\ldots,n-1\}$ osajoukot:
\begin{lstlisting}
for (int b = 0; b < (1<<n); b++) {
// osajoukon b käsittely
}
\end{lstlisting}
Seuraava koodi käy läpi
osajoukot, joissa on $k$ alkiota:
\begin{lstlisting}
for (int b = 0; b < (1<<n); b++) {
if (__builtin_popcount(b) == k) {
// osajoukon b käsittely
}
}
\end{lstlisting}
Seuraava koodi käy läpi joukon $x$ osajoukot:
\begin{lstlisting}
int b = 0;
do {
// osajoukon b käsittely
} while (b=(b-x)&x);
\end{lstlisting}
Esimerkiksi jos $x$ esittää joukkoa $\{2,5,7\}$,
niin koodi käy läpi osajoukot
$\emptyset$, $\{2\}$, $\{5\}$, $\{7\}$,
$\{2,5\}$, $\{2,7\}$, $\{5,7\}$ ja $\{2,5,7\}$.
\section{Dynaaminen ohjelmointi}
\subsubsection{Permutaatioista osajoukoiksi}
Dynaamisen ohjelmoinnin avulla on usein mahdollista
muuttaa permutaatioiden läpikäynti osajoukkojen läpikäynniksi.
Tällöin dynaamisen ohjelmoinnin tilana on
joukon osajoukko sekä mahdollisesti muuta tietoa.
Tekniikan hyötynä on,
että $n$-alkioisen joukon permutaatioiden määrä $n!$
on selvästi suurempi kuin osajoukkojen määrä $2^n$.
Esimerkiksi jos $n=20$, niin $n!=2432902008176640000$,
kun taas $2^n=1048576$.
Niinpä tietyillä $n$:n arvoilla permutaatioita ei ehdi
käydä läpi mutta osajoukot ehtii käydä läpi.
Lasketaan esimerkkinä, monessako
joukon $\{0,1,\ldots,n-1\}$
permutaatiossa ei ole
missään kohdassa kahta peräkkäistä lukua.
Esimerkiksi tapauksessa $n=4$ ratkaisuja on kaksi:
\begin{itemize}
\item $(1,3,0,2)$
\item $(2,0,3,1)$
\end{itemize}
Merkitään $f(x,k)$:llä,
monellako tavalla osajoukon
$x$ luvut voi järjestää niin,
että viimeinen luku on $k$ ja missään kohdassa
ei ole kahta peräkkäistä lukua.
Esimerkiksi $f(\{0,1,3\},1)=1$,
koska voidaan muodostaa permutaatio $(0,3,1)$,
ja $f(\{0,1,3\},3)=0$, koska 0 ja 1 eivät
voi olla peräkkäin alussa.
Funktion $f$ avulla ratkaisu tehtävään
on summa
\[ \sum_{i=0}^{n-1} f(\{0,1,\ldots,n-1\},i). \]
\noindent
Dynaamisen ohjelmoinnin tilat voi
tallentaa seuraavasti:
\begin{lstlisting}
long long d[1<<n][n];
\end{lstlisting}
\noindent
Perustapauksena $f(\{k\},k)=1$ kaikilla $k$:n arvoilla:
\begin{lstlisting}
for (int i = 0; i < n; i++) d[1<<i][i] = 1;
\end{lstlisting}
\noindent
Tämän jälkeen muut funktion arvot
saa laskettua seuraavasti:
\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}
\noindent
Muuttujassa $b$ on osajoukon bittiesitys,
ja osajoukon luvuista muodostettu
permutaatio on muotoa $(\ldots,j,i)$.
Vaatimukset ovat, että lukujen $i$ ja $j$
etäisyyden tulee olla yli 1
ja lukujen tulee olla osajoukossa $b$.
Lopuksi ratkaisujen määrän saa laskettua näin
muuttujaan $s$:
\begin{lstlisting}
long long s = 0;
for (int i = 0; i < n; i++) {
s += d[(1<<n)-1][i];
}
\end{lstlisting}
\subsubsection{Osajoukkojen summat}
Oletetaan sitten, että jokaista
joukon $\{0,1,\ldots,n-1\}$
osajoukkoa $x$ vastaa arvo $c(x)$ ja
tehtävänä on laskea kullekin
osajoukolle $x$ summa
\[s(x)=\sum_{y \subset x} c(y) \]
eli bittimuodossa ilmaistuna
\[s(x)=\sum_{y \& x = y} c(y). \]
Seuraavassa on esimerkki funktioiden arvoista,
kun $n=3$:
\begin{center}
\begin{tabular}{rrr}
$x$ & $c(x)$ & $s(x)$ \\
\hline
000 & 2 & 2 \\
001 & 0 & 2 \\
010 & 1 & 3 \\
011 & 3 & 6 \\
100 & 0 & 2 \\
101 & 4 & 6 \\
110 & 2 & 5 \\
111 & 0 & 12 \\
\end{tabular}
\end{center}
Esimerkiksi $s(110)=c(000)+c(010)+c(100)+c(110)=5$.
Tehtävä on mahdollista ratkaista ajassa $O(2^n n)$
laskemalla arvoja funktiolle $f(x,k)$:
mikä on lukujen $c(y)$ summa, missä $x$:stä saa $y$:n
muuttamalla millä tahansa tavalla bittien $0,1,\ldots,k$
joukossa ykkösbittejä nollabiteiksi.
Tämän funktion avulla ilmaistuna $s(x)=f(x,n-1)$.
Funktion pohjatapaukset ovat:
\begin{equation*}
f(x,0) = \begin{cases}
c(x) & \textrm{jos $x$:n bitti 0 on 0}\\
c(x)+c(x \XOR 1) & \textrm{jos $x$:n bitti 0 on 1}\\
\end{cases}
\end{equation*}
Suuremmille $k$:n arvoille pätee seuraava rekursio:
\begin{equation*}
f(x,k) = \begin{cases}
f(x,k-1) & \textrm{jos $x$:n bitti $k$ on 0}\\
f(x,k-1)+f(x \XOR (1 < < k),k-1) & \textrm{jos $x$:n bitti $k$ on 1}\\
\end{cases}
\end{equation*}
Niinpä funktion arvot voi laskea seuraavasti
dynaamisella ohjelmoinnilla.
Koodi olettaa, että taulukko \texttt{c} sisältää
funktion $c$ arvot ja muodostaa taulukon \texttt{s},
jossa on funktion $s$ arvot.
\begin{lstlisting}
for (int x = 0; x < (1<<n); x++) {
f[x][0] = c[x];
if (x&1) f[x][0] += c[x^1];
}
for (int k = 1; k < n; k++) {
for (int x = 0; x < (1<<n); x++) {
f[x][k] = f[x][k-1];
if (b&(1<<k)) f[x][k] += f[x^(1<<k)][k-1];
}
if (k == n-1) s[x] = f[x][k];
}
\end{lstlisting}
Itse asiassa saman laskennan voi toteuttaa lyhyemmin
seuraavasti niin, että tulokset lasketaan
suoraan taulukkoon \texttt{s}:
\begin{lstlisting}
for (int x = 0; x < (1<<n); x++) s[x] = c[x];
for (int k = 0; k < n; k++) {
for (int x = 0; x < (1<<n); x++) {
if (x&(1<<k)) s[x] += s[x^(1<<k)];
}
}
\end{lstlisting}