cphb/chapter27.tex

422 lines
13 KiB
TeX
Raw Normal View History

2016-12-28 23:54:51 +01:00
\chapter{Square root algorithms}
2017-01-25 22:13:05 +01:00
\index{square root algorithm}
2016-12-28 23:54:51 +01:00
2017-01-25 22:13:05 +01:00
A \key{square root algorithm} is an algorithm
that has a square root in its time complexity.
A square root can be seen as a ''poor man's logarithm'':
the complexity $O(\sqrt n)$ is better than $O(n)$
but worse than $O(\log n)$.
2017-02-18 17:46:37 +01:00
In any case, many square root algorithms are fast in practice
2017-01-25 22:13:05 +01:00
and have small constant factors.
2016-12-28 23:54:51 +01:00
2017-02-11 20:24:28 +01:00
As an example, let us consider the problem of
creating a data structure that supports
2017-02-18 17:46:37 +01:00
two operations on an array:
2017-02-11 20:24:28 +01:00
modifying an element at a given position
and calculating the sum of elements in the given range.
2016-12-28 23:54:51 +01:00
2017-01-25 22:13:05 +01:00
We have previously solved the problem using
2017-02-18 17:46:37 +01:00
a binary indexed tree and segment tree,
2017-01-25 22:13:05 +01:00
that support both operations in $O(\log n)$ time.
However, now we will solve the problem
in another way using a square root structure
2017-02-11 20:24:28 +01:00
that allows us to modify elements in $O(1)$ time
and calculate sums in $O(\sqrt n)$ time.
2016-12-28 23:54:51 +01:00
2017-01-26 22:12:30 +01:00
The idea is to divide the array into blocks
of size $\sqrt n$ so that each block contains
the sum of elements inside the block.
2017-02-11 20:24:28 +01:00
For example, an array of 16 elements will be
divided into blocks of 4 elements as follows:
2016-12-28 23:54:51 +01:00
\begin{center}
\begin{tikzpicture}[scale=0.7]
\draw (0,0) grid (16,1);
\draw (0,1) rectangle (4,2);
\draw (4,1) rectangle (8,2);
\draw (8,1) rectangle (12,2);
\draw (12,1) rectangle (16,2);
\node at (0.5, 0.5) {5};
\node at (1.5, 0.5) {8};
\node at (2.5, 0.5) {6};
\node at (3.5, 0.5) {3};
\node at (4.5, 0.5) {2};
\node at (5.5, 0.5) {7};
\node at (6.5, 0.5) {2};
\node at (7.5, 0.5) {6};
\node at (8.5, 0.5) {7};
\node at (9.5, 0.5) {1};
\node at (10.5, 0.5) {7};
\node at (11.5, 0.5) {5};
\node at (12.5, 0.5) {6};
\node at (13.5, 0.5) {2};
\node at (14.5, 0.5) {3};
\node at (15.5, 0.5) {2};
\node at (2, 1.5) {21};
\node at (6, 1.5) {17};
\node at (10, 1.5) {20};
\node at (14, 1.5) {13};
\end{tikzpicture}
\end{center}
2017-02-11 20:24:28 +01:00
Using this structure,
it is easy to modify the array,
2017-02-18 17:46:37 +01:00
because it is only needed to update
the sum of a single block
2017-02-11 20:24:28 +01:00
after each modification,
which can be done in $O(1)$ time.
For example, the following picture shows
how the value of an element and
the sum of the corresponding block change:
2016-12-28 23:54:51 +01:00
\begin{center}
\begin{tikzpicture}[scale=0.7]
\fill[color=lightgray] (5,0) rectangle (6,1);
\draw (0,0) grid (16,1);
\fill[color=lightgray] (4,1) rectangle (8,2);
\draw (0,1) rectangle (4,2);
\draw (4,1) rectangle (8,2);
\draw (8,1) rectangle (12,2);
\draw (12,1) rectangle (16,2);
\node at (0.5, 0.5) {5};
\node at (1.5, 0.5) {8};
\node at (2.5, 0.5) {6};
\node at (3.5, 0.5) {3};
\node at (4.5, 0.5) {2};
\node at (5.5, 0.5) {5};
\node at (6.5, 0.5) {2};
\node at (7.5, 0.5) {6};
\node at (8.5, 0.5) {7};
\node at (9.5, 0.5) {1};
\node at (10.5, 0.5) {7};
\node at (11.5, 0.5) {5};
\node at (12.5, 0.5) {6};
\node at (13.5, 0.5) {2};
\node at (14.5, 0.5) {3};
\node at (15.5, 0.5) {2};
\node at (2, 1.5) {21};
\node at (6, 1.5) {15};
\node at (10, 1.5) {20};
\node at (14, 1.5) {13};
\end{tikzpicture}
\end{center}
2017-02-11 20:24:28 +01:00
Calculating the sum of elements in a range is
a bit more difficult.
It turns out that we can always divide
the range into three parts such that
the sum consists of values of single elements
and sums of blocks between them:
2016-12-28 23:54:51 +01:00
\begin{center}
\begin{tikzpicture}[scale=0.7]
\fill[color=lightgray] (3,0) rectangle (4,1);
\fill[color=lightgray] (12,0) rectangle (13,1);
\fill[color=lightgray] (13,0) rectangle (14,1);
\draw (0,0) grid (16,1);
\fill[color=lightgray] (4,1) rectangle (8,2);
\fill[color=lightgray] (8,1) rectangle (12,2);
\draw (0,1) rectangle (4,2);
\draw (4,1) rectangle (8,2);
\draw (8,1) rectangle (12,2);
\draw (12,1) rectangle (16,2);
\node at (0.5, 0.5) {5};
\node at (1.5, 0.5) {8};
\node at (2.5, 0.5) {6};
\node at (3.5, 0.5) {3};
\node at (4.5, 0.5) {2};
\node at (5.5, 0.5) {5};
\node at (6.5, 0.5) {2};
\node at (7.5, 0.5) {6};
\node at (8.5, 0.5) {7};
\node at (9.5, 0.5) {1};
\node at (10.5, 0.5) {7};
\node at (11.5, 0.5) {5};
\node at (12.5, 0.5) {6};
\node at (13.5, 0.5) {2};
\node at (14.5, 0.5) {3};
\node at (15.5, 0.5) {2};
\node at (2, 1.5) {21};
\node at (6, 1.5) {15};
\node at (10, 1.5) {20};
\node at (14, 1.5) {13};
\draw [decoration={brace}, decorate, line width=0.5mm] (14,-0.25) -- (3,-0.25);
\end{tikzpicture}
\end{center}
2017-02-11 20:24:28 +01:00
Since the number of single elements is $O(\sqrt n)$
and also the number of blocks is $O(\sqrt n)$,
the time complexity of the sum query is $O(\sqrt n)$.
Thus, the parameter $\sqrt n$ balances two things:
the array is divided into $\sqrt n$ blocks,
each of which contains $\sqrt n$ elements.
2016-12-28 23:54:51 +01:00
2017-02-11 20:24:28 +01:00
In practice, it is not needed to use the
2017-02-18 17:46:37 +01:00
exact value of $\sqrt n$ as a parameter, but it may be better to
2017-01-25 22:13:05 +01:00
use parameters $k$ and $n/k$ where $k$ is
2017-02-18 17:46:37 +01:00
different from $\sqrt n$.
2017-02-11 20:24:28 +01:00
The optimal parameter depends on the problem and input.
For example, if an algorithm often goes
through the blocks but rarely inspects
single elements inside the blocks,
it may be a good idea to divide the array into
2017-01-26 22:12:30 +01:00
$k < \sqrt n$ blocks, each of which contains $n/k > \sqrt n$
2017-01-25 22:13:05 +01:00
elements.
\section{Batch processing}
\index{batch processing}
2017-02-11 20:24:28 +01:00
Sometimes the operations of an algorithm
2017-02-18 17:46:37 +01:00
can be divided into batches,
each of which can be processed separately.
2017-02-11 20:24:28 +01:00
Some precalculation is done
between the batches
in order to process the future operations more efficiently.
If there are $O(\sqrt n)$ batches of size $O(\sqrt n)$,
this results in a square root algorithm.
As an example, let us consider a problem
2017-01-25 22:13:05 +01:00
where a grid of size $k \times k$
2017-02-18 17:46:37 +01:00
initially consists of white squares
2017-02-11 20:24:28 +01:00
and our task is to perform $n$ operations,
2017-01-25 22:13:05 +01:00
each of which is one of the following:
2016-12-28 23:54:51 +01:00
\begin{itemize}
\item
2017-01-25 22:13:05 +01:00
paint square $(y,x)$ black
2016-12-28 23:54:51 +01:00
\item
2017-01-25 22:13:05 +01:00
find the nearest black square to
square $(y,x)$ where the distance
between squares $(y_1,x_1)$ and $(y_2,x_2)$
is $|y_1-y_2|+|x_1-x_2|$
2016-12-28 23:54:51 +01:00
\end{itemize}
2017-02-11 20:24:28 +01:00
We can solve the problem by dividing
the operations into
2017-01-25 22:13:05 +01:00
$O(\sqrt n)$ batches, each of which consists
of $O(\sqrt n)$ operations.
At the beginning of each batch,
2017-02-18 17:46:37 +01:00
we calculate for each square of the grid
2017-01-25 22:13:05 +01:00
the smallest distance to a black square.
This can be done in $O(k^2)$ time using breadth-first search.
When processing a batch, we maintain a list of squares
that have been painted black in the current batch.
2017-02-11 20:24:28 +01:00
The list contains $O(\sqrt n)$ elements,
because there are $O(\sqrt n)$ operations in each batch.
2017-02-18 17:46:37 +01:00
Now, the distance from a square to the nearest black
2017-01-25 22:13:05 +01:00
square is either the precalculated distance or the distance
2017-02-18 17:46:37 +01:00
to a square that appears in the list.
2017-01-25 22:13:05 +01:00
The algorithm works in
$O((k^2+n) \sqrt n)$ time.
2017-02-11 20:24:28 +01:00
First, there are $O(\sqrt n)$ breadth-first searches
and each search takes $O(k^2)$ time.
Second, the total number of
squares processed during the algorithm
is $O(n)$, and at each square,
we go through a list of $O(\sqrt n)$ squares.
2017-01-25 22:13:05 +01:00
If the algorithm would perform a breadth-first search
2017-02-11 20:24:28 +01:00
at each operation, the time complexity would be
2017-01-25 22:13:05 +01:00
$O(k^2 n)$.
And if the algorithm would go through all painted
squares at each operation,
2017-02-11 20:24:28 +01:00
the time complexity would be $O(n^2)$.
Thus, the time complexity of the square root algorithm
is a combination of these time complexities,
2017-02-18 17:46:37 +01:00
but in addition, a factor of $n$ is replaced by $\sqrt n$.
2017-02-11 20:24:28 +01:00
\section{Subalgorithms}
Some square root algorithms consists of
subalgorithms that are specialized for different
input parameters.
Typically, there are two subalgorithms:
one algorithm is efficient when
some parameter is smaller than $\sqrt n$,
and another algorithm is efficient
when the parameter is larger than $\sqrt n$.
As an example, let us consider a problem where
we are given a tree of $n$ nodes,
2017-01-25 22:13:05 +01:00
each with some color. Our task is to find two nodes
2017-02-11 20:24:28 +01:00
that have the same color and whose distance
is as large as possible.
2017-01-25 22:13:05 +01:00
2017-02-11 20:24:28 +01:00
For example, in the following tree,
the maximum distance is 4 between
the red nodes 3 and 4:
2017-01-25 22:13:05 +01:00
2017-02-11 20:24:28 +01:00
\begin{center}
\begin{tikzpicture}[scale=0.9]
\node[draw, circle, fill=green!40] (1) at (1,3) {$2$};
\node[draw, circle, fill=red!40] (2) at (4,3) {$3$};
\node[draw, circle, fill=red!40] (3) at (1,1) {$5$};
\node[draw, circle, fill=blue!40] (4) at (4,1) {$6$};
\node[draw, circle, fill=red!40] (5) at (-2,1) {$4$};
\node[draw, circle, fill=blue!40] (6) at (-2,3) {$1$};
\path[draw,thick,-] (1) -- (2);
\path[draw,thick,-] (1) -- (3);
\path[draw,thick,-] (3) -- (4);
\path[draw,thick,-] (3) -- (6);
\path[draw,thick,-] (5) -- (6);
\end{tikzpicture}
\end{center}
2017-01-25 22:13:05 +01:00
2017-02-11 20:24:28 +01:00
The problem can be solved by going through
all colors and calculating
2017-02-18 17:46:37 +01:00
the maximum distance between two nodes
separately for each color.
2017-02-11 20:24:28 +01:00
Assume that the current color is $x$ and
there are $c$ nodes whose color is $x$.
There are two subalgorithms
that are specialized for small and large
values of $c$:
\emph{Case 1}: $c \le \sqrt n$.
2017-01-25 22:13:05 +01:00
If the number of nodes is small,
we go through all pairs of nodes whose
color is $x$ and select the pair that
has the maximum distance.
2017-02-11 20:24:28 +01:00
For each node, it is needed to calculate the distance
2017-02-11 20:27:03 +01:00
to $O(\sqrt n)$ other nodes (see Chapter 18.3),
2017-01-25 22:13:05 +01:00
so the total time needed for processing all
2017-02-11 20:24:28 +01:00
nodes is $O(n \sqrt n)$.
2017-01-25 22:13:05 +01:00
2017-02-11 20:24:28 +01:00
\emph{Case 2}: $c > \sqrt n$.
2017-01-25 22:13:05 +01:00
If the number of nodes is large,
2017-02-11 20:24:28 +01:00
we go through the whole tree
2017-01-25 22:13:05 +01:00
and calculate the maximum distance between
two nodes with color $x$.
The time complexity of the tree traversal is $O(n)$,
and this will be done at most $O(\sqrt n)$ times,
2017-02-11 20:24:28 +01:00
so the total time needed is $O(n \sqrt n)$.
2017-01-25 22:13:05 +01:00
The time complexity of the algorithm is $O(n \sqrt n)$,
2017-02-18 17:46:37 +01:00
because both cases take a total of $O(n \sqrt n)$ time.
2017-01-25 22:13:05 +01:00
\section{Mo's algorithm}
\index{Mo's algorithm}
2017-02-22 20:53:11 +01:00
\key{Mo's algorithm}\footnote{According to \cite{cod15}, this algorithm
is named after Mo Tao, a Chinese competitive programmer, but
the technique has appeared earlier in the literature.} can be used in many problems
2017-01-26 22:12:30 +01:00
that require processing range queries in
2017-01-25 22:13:05 +01:00
a \emph{static} array.
2017-01-26 22:12:30 +01:00
Before processing the queries, the algorithm
sorts them in a special order which guarantees
2017-02-11 20:24:28 +01:00
that the algorithm works efficiently.
2017-01-26 22:12:30 +01:00
At each moment in the algorithm, there is an active
2017-02-11 20:24:28 +01:00
range and the algorithm maintains the answer
to a query related to that range.
The algorithm processes the queries one by one,
2017-02-18 17:46:37 +01:00
and always moves the endpoints of the
2017-02-11 20:24:28 +01:00
active range by inserting and removing elements.
2017-01-25 22:13:05 +01:00
The time complexity of the algorithm is
2017-02-18 17:46:37 +01:00
$O(n \sqrt n f(n))$ when the array contains
$n$ elements, there are $n$ queries
2017-01-26 22:12:30 +01:00
and each insertion and removal of an element
takes $O(f(n))$ time.
2017-02-11 20:24:28 +01:00
The trick in Mo's algorithm is the order
in which the queries are processed:
The array is divided into blocks of $O(\sqrt n)$
2017-01-26 22:12:30 +01:00
elements, and the queries are sorted primarily by
2017-02-11 20:24:28 +01:00
the number of the block that contains the first element
in the range, and secondarily by the position of the
last element in the range.
2017-01-25 22:13:05 +01:00
It turns out that using this order, the algorithm
2017-01-26 22:12:30 +01:00
only performs $O(n \sqrt n)$ operations,
2017-02-11 20:24:28 +01:00
because the left endpoint of the range moves
2017-01-26 22:12:30 +01:00
$n$ times $O(\sqrt n)$ steps,
2017-02-11 20:24:28 +01:00
and the right endpoint of the range moves
$\sqrt n$ times $O(n)$ steps. Thus, both
endpoints move a total of $O(n \sqrt n)$ steps during the algorithm.
2017-01-25 22:13:05 +01:00
\subsubsection*{Example}
2017-02-11 20:24:28 +01:00
As an example, consider a problem
where we are given a set of queries,
each of them corresponding to a range in an array,
and our task is to calculate for each query
2017-02-18 17:46:37 +01:00
the number of \emph{distinct} elements in the range.
2017-01-25 22:13:05 +01:00
In Mo's algorithm, the queries are always sorted
2017-02-11 20:24:28 +01:00
in the same way, but it depends on the problem
how the answer to the query is maintained.
2017-01-25 22:13:05 +01:00
In this problem, we can maintain an array
\texttt{c} where $\texttt{c}[x]$
2017-02-18 17:46:37 +01:00
indicates the number of times an element $x$
2017-02-11 20:24:28 +01:00
occurs in the active range.
2017-01-25 22:13:05 +01:00
2017-02-11 20:24:28 +01:00
When we move from one query to another query,
the active range changes.
For example, if the current range is
2016-12-28 23:54:51 +01:00
\begin{center}
\begin{tikzpicture}[scale=0.7]
\fill[color=lightgray] (1,0) rectangle (5,1);
\draw (0,0) grid (9,1);
\node at (0.5, 0.5) {4};
\node at (1.5, 0.5) {2};
\node at (2.5, 0.5) {5};
\node at (3.5, 0.5) {4};
\node at (4.5, 0.5) {2};
\node at (5.5, 0.5) {4};
\node at (6.5, 0.5) {3};
\node at (7.5, 0.5) {3};
\node at (8.5, 0.5) {4};
\end{tikzpicture}
\end{center}
2017-02-11 20:24:28 +01:00
and the next range is
2016-12-28 23:54:51 +01:00
\begin{center}
\begin{tikzpicture}[scale=0.7]
\fill[color=lightgray] (2,0) rectangle (7,1);
\draw (0,0) grid (9,1);
\node at (0.5, 0.5) {4};
\node at (1.5, 0.5) {2};
\node at (2.5, 0.5) {5};
\node at (3.5, 0.5) {4};
\node at (4.5, 0.5) {2};
\node at (5.5, 0.5) {4};
\node at (6.5, 0.5) {3};
\node at (7.5, 0.5) {3};
\node at (8.5, 0.5) {4};
\end{tikzpicture}
\end{center}
2017-01-25 22:13:05 +01:00
there will be three steps:
2017-04-17 11:18:29 +02:00
the left endpoint moves one step to the right,
2017-02-11 20:24:28 +01:00
and the right endpoint moves two steps to the right.
2017-01-25 22:13:05 +01:00
2017-02-11 20:38:13 +01:00
After each step, the array \texttt{c}
needs to be updated.
2017-02-11 20:31:56 +01:00
After adding an element $x$,
we increase the value of
2017-02-18 17:46:37 +01:00
$\texttt{c}[x]$ by one,
2017-02-11 20:31:56 +01:00
and if $\texttt{c}[x]=1$ after this,
we also increase the answer to the query by one.
2017-02-11 20:38:13 +01:00
Similarly, after removing an element $x$,
2017-02-11 20:31:56 +01:00
we decrease the value of
2017-02-18 17:46:37 +01:00
$\texttt{c}[x]$ by one,
2017-02-11 20:31:56 +01:00
and if $\texttt{c}[x]=0$ after this,
we also decrease the answer to the query by one.
2017-01-25 22:13:05 +01:00
In this problem, the time needed to perform
each step is $O(1)$, so the total time complexity
2017-01-26 22:12:30 +01:00
of the algorithm is $O(n \sqrt n)$.