Лекция 7

Рисование планарного графа. Сложность рекурсивных алгоритмов. Умножение матриц (над кольцом и булевых). Нахождение пары ближайших точек на плоскости.

7.1 Рисование планарного графа

Планарным называется граф, который можно нарисовать на плоскости без самопересечений. Будем рассматривать неориентированные графы.

Ниже мы представим алгоритм, который работает с двусвязным графом, то есть с графом, в котором не существует вершины, удаление которой ведет к потере связности. (Заметим, что если граф не двусвязен, то в нем имеется некоторая вершина v, удаление которой приводит к разбиению графа на (V_{1}, E_{1}) и (V_{2}, E_{2}); тогда можно нарисовать подграфы, порожденные^{1} V_{1}∪{v} и V_{2}∪{v}, по отдельности и соединить полученные рисунки; чтобы отправить точку соединения на границу области, занимаемой графом, рисуем граф на сфере, после чего �раскрываем� сферу, проделав дырку рядом с вершиной.) Алгоритм будет рисовать граф на плоскости, если граф — планарный.

Определение 7.1. Пусть дан граф G = (V, E), из которого уже нарисовано некоторое подмножество вершин W ⊆ V. В этом разделе окрестностью множества вершин S ⊆ V \ W будем называть множество Γ(S) = {v ∈ W | ∃e ∈ E : e = {v, s}, s ∈ S}.

1Подграфом графа (V, E), порожденным подмножеством вершин V′ ⊆ V, называется граф (V′, E′), где E′ = {{x, y} ∈ E | x ∈ V′ ∧ y ∈ V′}.

На каждом шаге работы нашего алгоритма плоскость будет разбита уже нарисованными частями графа на клетки (таким образом, каждая из клеток K будет ограничена нарисованной границей ∂K — некоторым циклом исходного графа). Сам же граф будет уменьшаться и распадаться на компоненты связности (мы будем выкидывать уже нарисованные вершины). Очевидно, одна компонента может быть нарисована только целиком в одной клетке. Компонента совместна с клеткой, если все нарисованные вершины, бывшие в ее окрестности в исходном графе, принадлежат границе этой клетки.

Алгоритм 7.1.

1. Взять какой-нибудь цикл, нарисовать его и выкинуть из графа (у нас получилось две клетки).

2. Если в оставшемся графе существует компонента, совместная лишь с одной клеткой, то взять путь в этой компоненте, соединяющий (вместе с двумя соответствующими ребрами исходного графа) две вершины границы этой клетки, и нарисовать его в клетке (удалив путь из компоненты и разбив компоненту на несколько, если она развалилась).

3. Если каждая компонента согласована с несколькими клетками, то взять любую компоненту и вставить путь в клетку (удалив путь и т. д.) аналогично шагу 2.

4. Если еще не весь граф нарисован, вернуться к шагу 2.

Заметим, что шаги 1 и 2 алгоритма являются вынужденными. Поэтому его корректность вытекает из следующей леммы.

Лемма 7.1. Пусть в какой-то момент что-то уже нарисовано и алгоритм находится в шаге 3. Компонента C согласована с клетками K_{1} и K_{2}. Если C можно вложить в K_{1} (и успешно дорисовать граф до конца), то ее можно вложить и в K_{2} (и успешно дорисовать).

Доказательство. Рассмотрим правильный рисунок (всего графа), в котором C вложена в K_{1}. Построим правильный рисунок (всего графа), в котором C вложена в K_{2}. В дальнейшем под клетками и компонентами понимаются те клетки и компоненты, которые имелись в рассматриваемый момент времени.

Поменяем местами все компоненты, согласованные и с K_{1}, и с K_{2} (назовем такие компоненты активными): те, что в исходном рисунке были нарисованы внутри K_{1}, отправим в K_{2}, и наоборот. Покажем, что их по-прежнему можно нарисовать без самопересечений.

Действительно, две активные компоненты всегда можно �развести� на плоскости: ведь прежде они были разведены. Так что конфликт может возникнуть только между активной и неактивной компонентами. Перебором случаев проверим, что таких конфликтов также не должно возникнуть: разобьем границы клеток K_{1} и K_{2} на участки, принадлежащие только K_{1}, только K_{2}, либо им вместе; разберем случаи, когда конфликтующий путь начинается на одном из участков, а заканчивается на другом.

Если окрестность неактивной компоненты целиком содержится в пути (в нарисованной части графа), внутренние (не первая и не последняя) вершины которого содержатся в ∂K_{i} \ ∂K_{3−i}, то конфликта возникнуть не может (эти внутренние вершины не могут входить в окрестность активной компоненты — ведь она согласована с обеими клетками!). Если же окрестность не содержится в таком пути, легко видеть (нарисуйте!), что неактивная компонента в рассматриваемый момент времени была согласована только с одной клеткой, а это противоречит условию шага 3.

Упражнение 7.1. Важное упражнение на понимание: найдите, где используется двусвязность графа.

Замечание 7.1. Существует алгоритм, позволяющий нарисовать планарный граф отрезками прямых за линейное число операций.

7.2 Сложность рекурсивных алгоритмов

Предположим, что алгоритм действует по схеме �разделяй и властвуй�, т.е. сводит задачу к нескольким таким же задачам меньшего размера и решает их. Тогда время его работы можно оценить при помощи следующей теоремы (аналогично можно оценить и занимаемую память).

Теорема 7.1. Пусть функция T задана соотношениями

T (1) = 1,
T (n) ≤ aT (⌈n/c⌉) + bn^{d} при n>1,

где a, b, c, d ≥ 0 — константы. Тогда • если a < c^{d}, то T (n) = O(n^{d});

• если a = c^{d}, то T (n) = O(n^{d} log n);

• если a > c^{d}, то T (n) = O(n^{log_{c} a}).

Замечание 7.2. При использовании этой леммы n может быть любым параметром задачи, а не только размером входа.

Доказательство. Оценим T (n) для n вида c^{k}; результат для других n будет простым следствием.

Раскрыв рекуррентное соотношение, получим

T (n) ≤ bn^{d} + aT (n/c) ≤ bn^{d} + ab(n/c)^{d} + T (n/c^{2}) ≤ … ≤ bn^{d} ∑_{i=0}^{k} (\frac{a}{c^{d}} )^{i} .

В случае a < c^{d} эта ∑_{i=0}^{k} ограничена ∑_{i=0}^{+∞}, а та, в свою очередь, константой. В случае a = c^{d} имеем сумму из k = log_{c} n единиц. Если же a > c^{d}, вычислим сумму как сумму геометрической прогрессии.

Наконец, для произвольного n

T (n) ≤ T_{∗}(c^{⌈log_{c} n⌉}) = O(T_{∗}(n)),

где T_{∗} — наша оценка (с конкретной константой вместо O(…)).

7.3 Умножение матриц

Задача: вычислить произведение C матриц A и B размера n×n над произвольным кольцом, используя лишь операции кольца. Будем подсчитывать количество этих операций. Как обычно, n можно считать степенью двойки.

Очевидный способ. Поделим эти матрицы на четыре части, пополам

по вертикали и горизонтали: например, A = (\begin{matrix}A_{11} & A_{12} \\ A_{21} & A_{22} \end{matrix}). Каждая из

матриц разбиения будет иметь размерность \frac{n}{2} × \frac{n}{2}. Сведем перемножение матриц размера n × n к перемножению матриц размера \frac{n}{2} × \frac{n}{2}:

C_{11} = A_{11}B_{11} + A_{12}B_{21},
C_{12} = A_{11}B_{12} + A_{12}B_{22},
C_{21} = A_{21}B_{11} + A_{22}B_{21},
C_{22} = A_{21}B_{12} + A_{22}B_{22}.

Далее каждую из матриц A_{ij}, B_{ij} опять поделим на четыре равные части, и так далее, пока не сведем перемножение матриц к операциям перемножения элементов кольца.

Подсчитаем количество T (n) операций с элементами матриц, выполняемых таким алгоритмом:

T (n) = 8T (\frac{n}{2}) + cn^{2}, где c — некоторая константа.

По теореме 7.1, T (n) = O(n^{3}).

Алгоритм Штрассена. Опять рассмотрим такое же разбиение матриц и введем новые матрицы

M_{1} = (A_{12} − A_{22})(B_{21} + B_{22}),
M_{2} = (A_{11} + A_{22})(B_{11} + B_{22}),
M_{3} = (A_{11} − A_{21})(B_{11} + B_{12}),
M_{4} = (A_{11} + A_{12})B_{22},
M_{5} = A_{11}(B_{12} − B_{22}),
M_{6} = A_{22}(B_{21} − B_{11}),
M_{7} = (A_{21} + A_{22})B_{11}.

Тогда C_{ij} можно выразить через M_{kl}:

C_{11} = M_{1} + M_{2} − M_{4} + M_{6},
C_{12} = M_{4} + M_{5},
C_{21} = M_{6} + M_{7},
C_{22} = M_{2} − M_{3} + M_{5} − M_{7}.

Подсчитаем количество T (n) операций с элементами матриц, выполняемых таким алгоритмом:

T (n) = 7T (\frac{n}{2}) + cn^{2}, где c — некоторая константа.

По теореме 7.1, T (n) = O(n^{log_{2} 7}). Поскольку log_{2} 7 ≈ 2.80735, этот алгоритм лучше предыдущего и лучше тривиального алгоритма (через вычисление каждого элемента матрицы C по определению произведения матриц).

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

Упражнение 7.2. Где мы воспользовались принадлежностью кольцу элементов матриц?

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

7.4 Умножение булевых матриц

Произведение (конъюнкция) булевых матриц (их элементами могут быть T (истина) и F (ложь)) определяется точно так же, как и произведение обычных матриц, но в качестве умножения элементов выступает конъюнкция ∧, а в качестве сложения — дизъюнкция ∨. Мы не можем использовать наш быстрый алгоритм для перемножения булевых матриц, так как T и F с операциями ∨ и ∧ не образуют кольца.

Пример 7.1. Пример перемножения булевых матриц:

(\begin{matrix}T & F \\ T & F \end{matrix} ) ∧ (\begin{matrix}F & T \\ T & T \end{matrix} ) = (\begin{matrix}F & T \\ F & T \end{matrix} ) .

Теорема 7.2. Умножение булевых матриц можно выполнить за O(n^{log 7}) арифметических операций по модулю n + 1.

Доказательство. Чтобы воспользоваться нашим быстрым алгоритмом, будем вместо булевых операций ∨ и ∧ использовать операции сложения и умножения в кольце ℤ_{n+1}, где n – размер матрицы. Легко показать, что элемент произведения, вычисленного таким образом, отличен от нуля тогда и только тогда, когда соответствующий элемент произведения булевых матриц истинен.

7.5 Проверка результата алгоритма умножения матриц

Итак, мы знаем уже несколько алгоритмов умножения матриц, но у нас нет хорошего способа проверки таких алгоритмов (и реализующих их программ). Рассмотрим вероятностный алгоритм, который даст нам возможность проверять результат умножения матриц над полем быстрее, чем вычислять произведение.

Возьмем случайный вектор r, т.е. вектор, составленный из битов, принимающих значения 0 или 1 с вероятностью \frac{1}{2} независимо друг от друга. У нас уже есть результат перемножения n × n матриц A и B — матрица C, полученная при помощи алгоритма, в правильности которого мы не уверены. Будем проверять равенство

A · B = C. (7.1)

Домножим обе части справа на случайный вектор r. Вместо (7.1) проверим новое равенство

(AB) · r = C · r

и выдадим ответ, соответствующий результату этой проверки. На такую проверку уйдет лишь O(n^{2}) операций с элементами матриц — это меньше, чем в алгоритме Штрассена (и в любом другом известном алгоритме для умножения матриц). Докажем, что этот алгоритм действительно проверяет результат перемножения.

Теорема 7.3. ∀ матриц A, B, C

a) AB = C ⇒ алгоритм проверки не ошибается,

b) AB ≠ C ⇒ алгоритм ошибается с вероятностью не более \frac{1}{2}.

Доказательство. Пункт a) очевиден, рассмотрим пункт b). Известно, что (AB − C)r ≠ 0. В каком случае алгоритм ошибется? Если скажет, что AB = C, то есть если (AB − C)r = 0. Возьмем строчку матрицы X = AB − C, не равную 0 (она есть, поскольку AB ≠ C). Пусть x_{kl} — ненулевой элемент этой строчки. Тогда произведение k-ой строки на r выглядит так:

∑_{i∈{1,2,...,l̂,...}} x_{ki}r_{i} + x_{kl}r_{l} = 0, где x_{kl} ≠ 0. (7.2)

Обозначим

c := −\frac{1}{x_{kl}} ∑_{i∈\{1,2,...,l̂,...\}} x_{ki}r_{i}.

С какой вероятностью r_{l} = c? С вероятностью выбрать бит r_{l} равным биту c, то есть с вероятностью \frac{1}{2}. Следовательно, алгоритм ошибается с вероятностью не более \frac{1}{2}.

Такой метод проверки называется методом отпечатков пальцев (fingerprinting).

Итак, наш алгоритм правильно решает задачу (т.е. говорит, что данная ему программа верно вычисляет произведение A и B), если AB = C, и ошибается (говорит �верно�, хотя на самом деле �неверно�) с вероятностью не более \frac{1}{2}, если AB ≠ C. Алгоритмы такого типа называются вероятностными алгоритмами с односторонней ограниченной вероятностью ошибки (one-sided bounded error). Какова реальная польза от такого алгоритма? На первый взгляд, вероятность ошибки велика. Но если этот алгоритм повторить 100 раз (100 — это лишь константа!), то вероятность ошибки станет \frac{1}{2^{100}} , а это уже меньше вероятности отказа вычислительной техники.

Замечание 7.4. Мы не пользовались тем, что матрицы — квадратные.

7.6 Нахождение пары ближайших точек на плоскости

Задача: на плоскости заданы координаты n ≥ 2 точек (x_{i}, y_{i}). Найти две различные точки из числа заданных, находящиеся на минимально возможном расстоянии (и определить это расстояние).

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

Разделим наше множество точек на два приблизительно равных по мощности: первые ⌈n/2⌉ элементов списка X и оставшиеся. (Сделать это, используя наши списки, просто — получатся такие же пары списков, только в два раза короче.) Назовем эти множества S_{1} и S_{2}; имеется значение x_{0} первой координаты, которое разделяет элементы этих множеств. Рекурсивно применим наш алгоритм к S_{1} и к S_{2} — тем самым, найдем ближайшие пары точек для каждого из этих множеств.

Пусть наименьшее из полученных расстояний — δ. Для завершения вычислений нам остается проверить случай, когда ближайшая пара состоит из одной точки множества S_{1} и одной точки множества S_{2}. Если это так, расстояние между ними менее δ, а значит, обе они находятся в вертикальной полосе с координатами от x_{0}−δ до x_{0}+δ (множество таких точек легко выделить при помощи списка X).

Проверим расстояния от каждой из полученных точек до следующих семи точек в списке Y. Заметим, что этого достаточно: искомая пара точек находится внутри прямоугольника высоты δ, выделенного из нашей вертикальной полосы. Этот прямоугольник состоит из двух квадратов со стороной δ, в каждом из них может быть не более четырех точек, иначе в соответствующем множестве S_{i} были бы точки, расстояние между которыми было бы меньше δ (разделим этот квадрат на четыре одинаковых квадратика — в каждом из них может быть только одна точка).

Рекуррентное неравенство для количества операций, совершаемых нашим алгоритмом, очевидно, T (n) ≤ 2T (⌈n/2⌉) + O(n). По теореме 7.1, T (n) = O(n log n), и столько же операций используется на построение исходных списков (поскольку их надо отсортировать). (Заметим, что на перебор всех пар точек понадобилось бы Ω(n^{2}) операций.)