\documentclass[12pt,fleqn,a4paper]{book}
%
% Packages used:
%
\usepackage[koi8-r]{inputenc}
\usepackage[russian,english]{babel}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{amsfonts}
\usepackage{amsthm}
\usepackage{url}
\usepackage{fullpage}
\makeatletter
\def\@listI {\leftmargin\leftmarginii
              \labelwidth\leftmarginii
              \advance\labelwidth-\labelsep
              \topsep0pt
              \parsep0pt
              \itemsep0pt}
\def\@listii {\leftmargin\leftmarginii
              \labelwidth\leftmarginii
              \advance\labelwidth-\labelsep
              \topsep0pt
              \parsep0pt
              \itemsep0pt}
\def\@listiii{\leftmargin\leftmarginiii
              \labelwidth\leftmarginiii
              \advance\labelwidth-\labelsep
              \topsep0pt
              \parsep0pt
              \partopsep0pt
              \itemsep0pt}
\def\@listiv {\leftmargin\leftmarginiv
              \labelwidth\leftmarginiv
              \advance\labelwidth-\labelsep}
\def\@listv  {\leftmargin\leftmarginv
              \labelwidth\leftmarginv
              \advance\labelwidth-\labelsep}
\def\@listvi {\leftmargin\leftmarginvi
              \labelwidth\leftmarginvi
              \advance\labelwidth-\labelsep}
\makeatother
%
% Common customization:
%
\pagestyle{headings}
\newtheorem{theorem}{Теорема}[chapter]
\newtheorem{lemma}{Лемма}[chapter]
\newtheorem{proposition}{Утверждение}[chapter]
\newtheorem{fact}{Факт}[chapter]
\theoremstyle{definition}
\newtheorem{problem}{Задача}[chapter]
\newtheorem{exercise}{Упражнение}[chapter]
\newtheorem{example}{Пример}[chapter]
\newtheorem{definition}{Определение}[chapter]
\newtheorem{remark}{Замечание}[chapter]
\newtheorem{algorithm}{Алгоритм}[chapter]
\newcommand{\marginnote}[1]{} %{\marginpar{\raggedright\ \fbox{\small #1}}}
\def\gap{\medskip\centerline{\fbox{\Huge\bfseries{ПРОБЕЛ В КОНСПЕКТЕ.}}}\medskip}
\advance\headheight by 7pt
\def\headsep{15mm}
\newcommand{\lecture}[2]{%
\def\rightmark{\fbox{\parbox{125mm}{\lecturername: \coursetitle%
\hfil\phantom{.}}}}
\def\leftmark{\fbox{\parbox{125mm}{Лекция {#1}. {#2}%
\hfil\phantom{.}}}}
\renewcommand\chaptername{Лекция}\renewcommand\thechapter{#1}\chapter{{#2}}
\thispagestyle{headings}}
%
% Things to customize for the course are here:
%
\newcommand\lecturername{Э. А. Гирш}
\newcommand\coursetitle{``Информатика'' (отд. информатики, 1 курс)}
%
% Now, this particular lecture definitions:
%
%
% The document
%
\begin{document}
\selectlanguage{russian}
%
% Lecture title
%
\lecture{6}{Сложность рекурсивных алгоритмов.
Умножение матриц (над кольцом и булевых).
Простой рекурсивный алгоритм для умножения целых чисел.
Нахождение пары ближайших точек на плоскости}
%
% The lecture
%
%-----------------------------------------------------------
\section{Сложность рекурсивных алгоритмов}\marginnote{15}
Предположим, что алгоритм действует по схеме <<разделяй и властвуй>>,
т.е. сводит задачу к нескольким таким же задачам меньшего размера
и решает их. Тогда время его работы можно оценить при помощи
следующей теоремы (аналогично можно оценить и занимаемую память).

\begin{theorem}\label{th:recursion}
Пусть функция $T$ задана соотношениями
\[T(1)=1,\] 
\[T(n)\le aT(\lceil n/c\rceil)+bn^d\mbox{ при }$n>1$,\]
где $a,b,c,d\ge0$ --- константы.
Тогда\\[-7pt]
\begin{itemize} 
\item если $a<c^d$, то $T(n)=O(n^d)$;
\item если $a=c^d$, то $T(n)=O(n^d\log n)$;
\item если $a>c^d$, то $T(n)=O(n^{\log_c a})$.
\end{itemize}
\end{theorem}
\begin{remark}
При использовании этой теоремы $n$ может быть любым параметром задачи,
а не только размером входа.
\end{remark}
\begin{proof}
Оценим $T(N)$ для $N$ вида $c^k$; 
результат для $n$, не являющихся степенями $c$, будет простым следствием.

Раскрыв рекуррентное соотношение, получим
\begin{eqnarray}
T(N)&\le&
bN^d+aT(N/c)\le
bN^d+ab(N/c)^d+T(N/c^2)\le\ldots\nonumber\\
&\le& bN^d\sum_{i=0}^k \left(\frac{a}{c^d}\right)^i.\label{eq:power}
\end{eqnarray}

В случае $a<c^d$ эта $\sum_{i=0}^k$ ограничена $\sum_{i=0}^{+\infty}$,
а та, в свою очередь, константой. В случае $a=c^d$ имеем сумму из
$k=\log_c N$ единиц.
Если же $a>c^d$, вычислим сумму как сумму геометрической прогрессии.

\medskip
Наконец, покажем, что теорема верна и для произвольного $n$.
Пусть $N=c^{\lceil \log_c n\rceil}$, для таких $N$ мы теорему уже доказали.
Тогда
\[
T(n)\le^{?} T_*(N) = O( T_*(n) ),
\]
где $T_*$ --- наша оценка (с конкретной константой вместо $O(\ldots)$).
Неравенство $\le^{?}$ выполняется, так как каждый член более громоздко
выглядящей суммы
$$
T(n) \le
bn^d + ab\lceil n/c\rceil^d + a^2 b (\lceil\lceil n/c\rceil /c\rceil)^d+\ldots
$$
мажорируется соответствующим членом оцененной нами суммы~(\ref{eq:power}),
которая в <<раскрытом>> виде выглядит как
$$
T(N) \le 
bN^d + ab(N/c)^d + a^2b(N/c^2)^d+\ldots\ .%+ a^kb(N/c^k)^d
$$
\end{proof}

%-----------------------------------------------------------
\section{Умножение матриц}\marginnote{20}
Задача: вычислить произведение $\mathbf{C}$ 
матриц $\mathbf{A}$ и $\mathbf{B}$ размера $n\times n$ над произвольным кольцом,
используя лишь операции кольца. Будем подсчитывать количество
этих операций. Как обычно, $n$ можно
считать степенью двойки (говоря формально, можно перемножить
чуть б\'ольшие матрицы; при написании реальной программы
это, конечно, необязательно).

\paragraph{Очевидный способ.} 
Поделим эти матрицы на четыре части, пополам по вертикали и горизонтали:
например,
$        \bf{A} = \begin{pmatrix}
                                \bf A_{11} & \bf A_{12} \\
                                \bf A_{21} & \bf A_{22}
                \end{pmatrix}
$.
Каждая из матриц разбиения будет иметь размерность 
$\frac{n}{2}\times\frac{n}{2}$.   
Сведем перемножение матриц размера $n\times n$ 
к перемножению матриц размера $\frac{n}{2}\times\frac{n}{2}$:
\begin{eqnarray*}
        {\bf C}_{11}&=&{\bf A}_{11}{\bf B}_{11} + {\bf A}_{12}{\bf B}_{21},\\ 
        {\bf C}_{12}&=&{\bf A}_{11}{\bf B}_{12} + {\bf A}_{12}{\bf B}_{22},\\
        {\bf C}_{21}&=&{\bf A}_{21}{\bf B}_{11} + {\bf A}_{22}{\bf B}_{21},\\
        {\bf C}_{22}&=&{\bf A}_{21}{\bf B}_{12} + {\bf A}_{22}{\bf B}_{22}.
\end{eqnarray*}   
Далее каждую из матриц ${\bf A}_{ij}$, ${\bf B}_{ij}$ 
опять поделим на четыре равные части,
и так далее, пока не сведем перемножение матриц к операциям 
перемножения элементов кольца. 

Подсчитаем количество $T(n)$ операций с элементами матриц,
выполняемых таким алгоритмом:
\[
 T(n) = 8 T\left(\frac{n}{2}\right) + cn^2, 
 \quad \text{где $c$ --- некоторая константа}.   
\]
По теореме~\ref{th:recursion}, $T(n)=O(n^3)$.

\paragraph{Алгоритм Штрассена.}
Опять рассмотрим такое же разбиение матриц и введем новые матрицы
\begin{eqnarray*}
{\bf M}_{1}&=&({\bf A}_{12} - {\bf A}_{22})({\bf B}_{21} + {\bf B}_{22}),\\
{\bf M}_{2}&=&({\bf A}_{11} + {\bf A}_{22})({\bf B}_{11} + {\bf B}_{22}),\\
{\bf M}_{3}&=&({\bf A}_{11} - {\bf A}_{21})({\bf B}_{11} + {\bf B}_{12}),\\
{\bf M}_{4}&=&({\bf A}_{11} + {\bf A}_{12}){\bf B}_{22},\\
{\bf M}_{5}&=&{\bf A}_{11}({\bf B}_{12} - {\bf B}_{22}),\\
{\bf M}_{6}&=&{\bf A}_{22}({\bf B}_{21} - {\bf B}_{11}),\\
{\bf M}_{7}&=&({\bf A}_{21} + {\bf A}_{22}){\bf B}_{11}.
\end{eqnarray*}     

Тогда ${\bf C}_{ij}$ можно выразить через ${\bf M}_{kl}$:
\begin{eqnarray*}
{\bf C}_{11}&=&{\bf M}_1+{\bf M}_2-{\bf M}_4+{\bf M}_6,\\
{\bf C}_{12}&=&{\bf M}_4+{\bf M}_5,\\
{\bf C}_{21}&=&{\bf M}_6+{\bf M}_7,\\
{\bf C}_{22}&=&{\bf M}_2-{\bf M}_3+{\bf M}_5-{\bf M}_7.
\end{eqnarray*}

Подсчитаем количество $T(n)$ операций с элементами матриц,
выполняемых таким алгоритмом:
\[
 T(n) = 7 T\left(\frac{n}{2}\right) + cn^2, 
 \quad \text{где $c$ --- некоторая константа}.   
\]
По теореме \ref{th:recursion}, 
$T(n)=O(n^{\log_27})$. Поскольку $\log_27\approx 2.80735$, 
этот алгоритм лучше предыдущего 
и лучше тривиального алгоритма (через вычисление каждого элемента 
матрицы $C$ по определению произведения матриц). 

Как можно проверить, что алгоритм действительно находит произведение
матриц? Этот алгоритм прост, и убедиться в его правильности можно простой 
подстановкой. Далее мы научимся проверять произвольный алгоритм для этой
задачи и даже
программу, написанную на его основе, быстрее и лучше. 

\begin{exercise}
Где мы воспользовались принадлежностью \emph{кольцу} элементов матриц?
\end{exercise}

\begin{remark}
К умножению можно свести и обращение матриц (конечно, невырожденных 
и, к тому же, над полем). Для этого понадобится разложить матрицу в произведение
матриц специального вида (нижнетреугольную, верхнетреугольную
и матрицу перестановки). Если кому-то понадобится реализовать этот алгоритм,
можно прочесть в книге
Ахо, Хопкрофта и Ульмана или Кормена, Лейзерсона и Ривеста.
\end{remark}

\begin{exercise}
Мы выяснили количество операций над элементами кольца.
За какое время можно перемножить на РАМ матрицы, состоящие из целых чисел?
(Подсказка: за какое время Вы можете перемножить целые числа на РАМ?)
\end{exercise}

\section{Умножение булевых матриц}\marginnote{10}
Произведение (конъюнкция) булевых матриц (их
элементами могут быть $T$ (истина) и $F$ (ложь)) определяется точно так же,
как и произведение обычных матриц, но в качестве умножения элементов
выступает конъюнкция $\land$, а в качестве сложения --- дизъюнкция $\vee$.
Мы не можем использовать наш быстрый алгоритм для перемножения булевых матриц, 
так как $T$ и $F$ 
с операциями $\lor$ и $\land$ не образуют кольца.

\begin{example}
Пример перемножения булевых матриц:
%, для которого не работает 
%представленный быстрый алгоритм:

$$
        \begin{pmatrix} T & F \\
                        T & F
        \end{pmatrix}   \wedge
        \begin{pmatrix}
                        F & T \\
                        T & T
        \end{pmatrix} =
        \begin{pmatrix}
                        F & T \\
                        F & T
        \end{pmatrix}.
$$
\end{example}

\begin{theorem}
Умножение булевых матриц можно выполнить за $O(n^{\log7})$ арифметических
операций по модулю $n+1$.
\end{theorem}
\begin{proof}
Чтобы воспользоваться нашим быстрым алгоритмом, будем вместо
булевых операций $\lor$ и $\land$ использовать операции
сложения и умножения в кольце $\mathbb{Z}_{n+1}$, где $n$ -- размер матрицы.
Легко показать, что элемент произведения, вычисленного таким образом,
отличен от нуля тогда и только тогда, когда соответствующий
элемент произведения булевых матриц истинен.
\end{proof}

\begin{problem}
Сформулируйте и докажите верхнюю оценку \emph{времени} работы этого алгоритма на РАМ.
Обратите внимание, что Вам предстоит производить операции по модулю $n+1$.
\end{problem}

%\section{Сортировка на четырех лентах}
%Разобьем исходный файл пополам на две ленты (последовательно считывая
%элементы, будем нечетные записывать на первую ленту, а четные --- на вторую).
%%произвольным образом,
%%сортировать не надо).
%Далее будем из двух лент, состоящих из отсортированных блоков 
%по $i$ элементов, составлять две ленты, состоящие из отсортированных
%блоков по $2i$ элементов (\emph{распространенный прием}: 
%для простоты будем считать, что количество элементов
%является степенью двойки $2^k$, --- в противном случае время работы вырастет
%заведомо не более, чем в константу раз, поскольку размер входа
%вырастет не более, чем в константу раз, даже если его округлить до степени двойки
%в большую сторону).
%
%Делается это так:
%читаем поэлементно блоки с обеих лент (назовем эти ленты A и B), 
%пишем блок удвоенной длины на одну ленту (назовем ее C)
%(а следующий --- на другую, назовем ее D); 
%при этом каждый раз на ленту C мы пишем наименьший элемент $v$ 
%из двух считанных (с ленты A и с ленты B) 
%и читаем следующий элемент
%с той ленты, с которой взяли $v$ (если текущий блок на ней еще не закончился).
%\begin{lemma}
%Если ленты A, B длины $2^{k-1}$
%состояли из блоков, отсортированных по $i$ элементов,
%то после этой операции ленты C, D будут состоять из блоков, отсортированных
%по $2i$ элементов (и по-прежнему будут иметь длину $2^{k-1}$). 
%Эта процедура займет $O(n)$ операций считывания/записи
%элементов и $O(1)$ ячеек оперативной памяти.
%\end{lemma}
%
%После этого ленты (A,B) и (C,D) меняются местами
%(читаем C и D, пишем на A и B). Очевидно, за $t$ итераций
%ленты, отсортированные по $1$ элементу, превратятся в ленты,
%отсортированные по $2^t$ элементов. Таким образом, мы доказали
%следующую теорему.
%
%\begin{theorem}
%Приведенный алгоритм сортирует исходный файл за $O(n\log n)$
%обращений к файлам.
%\end{theorem}


\section{Простой рекурсивный алгоритм для умножения чисел}

Сложение $n$-битовых чисел можно осуществить за $O(n)$
(во-первых, у нас имеется такая инструкция RAM-машины;
во-вторых, ясно, что это действительно можно реализовать физически).
Рассмотрим умножение $n$-битовых чисел
(пусть $n$ --- степень двойки; можно считать так в любом случае,
так как иначе время работы увеличится не более, чем в константу раз).
Умножение <<в столбик>> даст, очевидно, время $\Omega(n^2)$.
Хотелось бы построить более быстрый алгоритм.

Почему мы не включили операцию умножения в RAM-машину?
Именно потому, что непонятно, как физически реализовать ее так,
чтобы она работала время $O(n)$.

Начнем с простого алгоритма, время работы которого составляет 
$O(n^{\log_23})$. В дальнейшем мы изучим алгоритм, который
умножает числа за время $O(n\cdot\log n\cdot \log\log n)$
(он, кстати, будет использовать наш простой рекурсивный алгоритм как подпрограмму).

Имеем два $n$-битовых числа $a$ и $b$.
Разделим их в битовом представлении на $n/2$-битовые $a_1,a_2$ и $b_1,b_2$
(соответственно), а затем перемножим эти числа рекурсивно:
\begin{eqnarray*}
 a&=&a_1\cdot 2^{n/2}+a_2,\\
 b&=&b_1\cdot 2^{n/2}+b_2,\\
 a\cdot b&=&a_1b_1\cdot 2^n+(a_1 b_2+a_2 b_1)\cdot 2^{n/2}+a_2 b_2;
\end{eqnarray*}
средний коэффициент можно вычислить, используя лишь одно умножение
и остальные два коэффициента:
\[
 a_1 b_2+a_2 b_1=(a_1+a_2)(b_1+b_2)-a_1b_1-a_2b_2.
\]
Тем самым получаем, что нам достаточно трех умножений
$n/2$-битовых чисел (ибо умножение на степень двойки --- по существу,
не умножение), т.е. рекуррентное уравнение для времени работы ---
\[
T(n)\le 3T(n/2)+c n.
\]
Тогда по теореме~\ref{th:recursion}
$T(n)=O(n^{\log_23})$.
\begin{remark}
Нюанс: строго говоря, мы перемножали не $n/2$-битные,
а $(n/2+1)$-битные числа $x=a_1+a_2$ и $y=b_1+b_2$,
но одно к другому сводится за линейное время.
Действительно, пусть $x=2A+x'$, $y=2B+y'$,
где $A$ и $B$ --- $n/2$-битные числа, $x'$ и $y'$ --- биты.
Тогда $xy=4AB+2Ay'+2Bx'+x'y'$.
<<Сложным>> умножением здесь является только $A\cdot B$;
остальные <<умножения>> реализуются за линейное время,
поскольку это умножения на степени двойки или 0.
\end{remark}

\section{Нахождение пары ближайших точек на плоскости}\marginnote{20}
Задача: на плоскости заданы координаты $n\ge2$ точек $(x_i,y_i)$.
Найти две различные точки из числа заданных, 
находящиеся на минимально возможном расстоянии
(и определить это расстояние).

Решение: построим рекурсивный алгоритм.
Нам понадобится два упорядоченных двунаправленных
списка номеров наших точек:
список $X$ будет упорядочен по возрастанию первой координаты,
список $Y$ --- по возрастанию второй координаты.
Будет также полезно, если в элементах первого списка
будут храниться ссылки на соответствующие тем же точкам
места второго списка.

Разделим наше множество точек 
на два приблизительно равных по мощности:
первые $\lceil n/2\rceil$ элементов списка $X$ и оставшиеся.
(Сделать это, используя наши списки, просто --- получатся
такие же пары списков, только в два раза короче.)
Назовем эти множества $S_1$ и $S_2$;
имеется значение $x_0$ первой координаты, которое
разделяет элементы этих множеств.
Рекурсивно применим наш алгоритм к $S_1$ и к $S_2$ --- тем самым, 
найдем ближайшие пары точек для каждого из этих множеств.

Пусть наименьшее из полученных расстояний --- $\delta$.
Для завершения вычислений нам остается проверить случай,
когда ближайшая пара состоит из одной точки множества $S_1$
и одной точки множества $S_2$.
Если это так, расстояние между ними менее $\delta$, 
а значит, обе они находятся в вертикальной полосе 
с координатами от $x_0-\delta$ до $x_0+\delta$
(множество таких точек легко выделить при помощи списка $X$).

Проверим расстояния от каждой из
полученных точек до следующих семи точек в списке $Y$.
Заметим, что этого достаточно: искомая пара точек находится
внутри прямоугольника высоты $\delta$,
выделенного из нашей вертикальной полосы.
Этот прямоугольник состоит из двух квадратов со стороной $\delta$,
в каждом из них может быть не более четырех точек, иначе
в соответствующем множестве $S_i$ были бы точки,
расстояние между которыми было бы меньше $\delta$ (разделим
этот квадрат на четыре одинаковых квадратика --- в каждом из
них может быть только одна точка).

Рекуррентное неравенство для количества операций,
совершаемых рекурсивной частью нашего алгоритма,
очевидно, $T(n)\le 2T(\lceil n/2\rceil)+O(n)$.
По теореме~\ref{th:recursion}, $T(n)=O(n\log n)$, 
и столько же операций используется
на построение исходных списков (поскольку их надо отсортировать:
это можно сделать, \emph{например}, построив $B^+$-дерево\footnote{В
дальнейшем мы изучим и более разумные способы сортировки.}).
(Заметим, что на перебор всех пар точек понадобилось бы
$\Omega(n^2)$ операций.)
\end{document}


