Участник:Kabanov — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
м
(Kd-tree)
Строка 1: Строка 1:
Есть множество точек на плоскости. Нужно найти две самые удалённые из них.
+
[[Файл:Kd-tree.png | 400px | right]]
  
[[Статические выпуклые оболочки: Джарвис, Грэхем, Эндрю, Чен, QuickHull|Найдём выпуклую оболочку]] исходного множества и получим более простую задачу: найти две наиболее удалённые вершины в выпуклом многоугольнике. Сделать это можно за линейное время с помощью метода, который называется '''вращающиеся калиперы''' (англ. ''rotating calipers'').
+
'''K-d дерево''' (short for k-dimensional tree) {{---}} статическая структура данных для хранения точек в <tex>k</tex>-мерном пространстве. Позволяет отвечать на запрос, какие точки лежат в данном прямоугольнике.
 +
Строится это дерево следующим образом: разобьём все точки вертикальной прямой так, чтобы слева (нестрого) и справа (строго) от неё было примерно поровну точек (для  этого посчитаем медиану первых координат). Получим подмножества для левого и правого ребёнка. Далее построим для этих подмножеств деревья, но разбивать будем уже не вертикальной, а горизонтальной прямой (для этого посчитаем медиану вторых координат). И так далее (будем считать, что <tex>k = 2</tex> (случай бОльших размерностей обрабатывается аналогично), поэтому на следующем уровне вновь будем разбивать вертикальными прямыми).
  
''Обоснование: Пусть диаметр с одной стороны содержит точку, которая не принадлежит выпуклой оболочке. Тогда продлим диаметр в эту сторону до пересечения с выпуклой оболочкой. Очевидно, он станет длиннее. А потом заметим, что диаметр станет еще больше, если сдвинуть его к одному из концов ребра, в которое мы уперлись. Конец.''
+
''Замечание: проблемы могут возникнуть, если много точек имеют одинаковую координату, тогда разбить примерно поровну не получится (почти все точки будут лежать на медиане и попадут в левую часть). Лучший способ борьбы с этим {{---}} не вспоминать о данной проблеме совсем. Но вообще с этим борются, используя composite numbers, то есть сравнивая ещё и по другой (другим) координате.''
  
== Постановка задачи ==
+
Реализовывать построение можно рекурсивно с помощью функции <tex>BuildKdTree(P, Depth)</tex>, принимающей множество точек и глубину. В зависимости от остатка при делении на размерность (при <tex> k = 2 </tex> от чётности размерности) сплитим множество на два подмножества и делаем рекурсивные вызовы. Для лучшего понимания приведём псевдокод:
Пусть <tex>P = (p_1, p_2, ... ,p_n)</tex> {{---}} выпуклый многоугольник, в котором порядок обхода вершин направлен против часовой стрелки, и никакие три последовательные точки не лежат на одной прямой. Найти пару чисел <tex>\langle i, j \rangle</tex>, такие, что <tex>d(p_i, p_j)</tex> максимально.
+
<code>
 +
BuildKdTree(P, Depth)
 +
//Input. A set of points P and the current depth Depth.
 +
//Output. The root of a kd-tree storing P.
 +
if P contains only one point
 +
    return a leaf storing this point
 +
else if depth is even
 +
    Split P into two subsets <tex>P_1</tex> and <tex>P_2</tex> with a vertical line <tex>l</tex> through the median x-coordinate of the points in P
 +
else
 +
    Split P into two subsets <tex>P_1</tex> and <tex>P_2</tex> with a horizontal line <tex>l</tex> through the median y-coordinate of the points in P.
 +
<tex>V_{left}</tex> <- BuildKdTree(<tex>P_1</tex>, Depth + 1)
 +
<tex>V_{right}</tex> <- BuildKdTree(<tex>P_2</tex>, Depth + 1)
 +
Create a node v storing <tex>l</tex>, make <tex>V_{left}</tex> the left child of v, and make <tex>V_{right}</tex> the right child of v.
 +
return v
 +
</code>
  
=== Опорные прямые ===
+
{{Лемма
{{Определение
+
|about=
|definition=
+
О времени построения
Прямая <tex>L</tex> называется '''опорной прямой''' (англ. ''line of support'') для многоугольника <tex>P</tex>, если его внутренность лежит по одну сторону от <tex>L</tex>, при этом <tex>L</tex> проходит хотя бы через одну из вершин <tex>P</tex>.
 
}}
 
{{Теорема
 
 
|statement=
 
|statement=
Пусть <tex>L_1</tex> и <tex>L_2</tex> {{---}} две параллельные опорные прямые фигуры <tex>\Phi</tex>, расстояние между которыми имеет максимальное значение. <tex>A_1</tex> и <tex>A_2</tex> {{---}} граничные точки фигуры <tex>\Phi</tex>, принадлежащие соответственно прямым <tex>L_1</tex> и <tex>L_2</tex>. Тогда отрезок <tex>A_1A_2</tex> перпендикулярен обеим прямым <tex>L_1</tex> и <tex>L_2</tex>.
+
Построение выполняется за <tex>O(n \log n)</tex>.
 
|proof=
 
|proof=
[[Файл:perpendicular.png|250px|right]]
+
Время построения обозначим <tex>T(n)</tex>. Поиск медианы можно сделать за линейное время, поэтому достаточно очевидно, что:
Предположим, что это не так. Тогда расстояние между прямыми <tex>L_1</tex> и <tex>L_2</tex> было бы меньше, чем отрезок <tex>A_1A_2</tex>, и тем более меньше, чем расстояние между двумя опорными прямыми <tex>L'_1</tex> и <tex>L'_2</tex> фигуры <tex>\Phi</tex>, перпендикулярными к отрезку <tex>A_1A_2</tex>, что противоречит условию.
+
 
 +
<tex>T(n) = O(1)</tex> if <tex>n = 1</tex>.
 +
 
 +
<tex>T(n) = O(n) + 2 \cdot T(n / 2)</tex>, otherwise.
 +
 
 +
Решением этого является <tex>T(n) = O(n \log n)</tex>.
 +
 
 +
Также стоит отметить, что можно и не искать медиану за линейное время, а просто посортить все точки в самом начале и дальше использовать это. В реализации попроще, асимптотика та же.
 
}}
 
}}
Так как <tex>A_1</tex> и <tex>A_2</tex> {{---}} какие угодно граничные точки фигуры <tex>\Phi</tex>, принадлежащие соответственно прямым <tex>L_1</tex> и <tex>L_2</tex>, то из перпендикулярности отрезка <tex>A_1A_2</tex> к прямым <tex>L_1</tex> и <tex>L_2</tex> следует, что ни одна из прямых <tex>L_1</tex>, <tex>L_2</tex> не может иметь с фигурой <tex>\Phi</tex> целый общий отрезок. Другими словами, каждая из этих прямых содержит единственную граничную точку фигуры <tex>\Phi</tex>.
 
  
{{Теорема
+
{{Лемма
 +
|about=
 +
О занимаемой памяти
 
|statement=
 
|statement=
Диаметр выпуклого многоугольника равен максимальному расстоянию между параллельными опорными прямыми.
+
K-d дерево требует <tex>O(n)</tex> памяти.
 
|proof=
 
|proof=
[[Файл:max_parallel.png|170px|right]]
+
Высота дерева, очевидно, логарифмическая, а листьев всего <tex>O(n)</tex>. Поэтому будет <tex>O(n)</tex> вершин, каждая занимает <tex>O(1)</tex> памяти.
Пусть <tex>\Phi</tex> {{---}} выпуклая фигура, <tex>L_1</tex> и <tex>L_2</tex> {{---}} параллельные опорные прямые, расстояние между которыми имеет наибольшее возможное значение <tex>d</tex>, <tex>A_1</tex> и <tex>A_2</tex> {{---}} общие точки фигуры <tex>\Phi</tex> и прямых <tex>L_1</tex> и <tex>L_2</tex> соответственно. По предыдущей теореме <tex>A_1A_2</tex> перпендикулярен к прямым <tex>L_1</tex>, <tex>L_2</tex>, следовательно, его длина равна <tex>d</tex>. Докажем, что расстояние между любыми двумя точками фигуры <tex>\Phi</tex> не преводходит <tex>d</tex>. Действительно, если <tex>B</tex> и <tex>C</tex> {{---}} какие-либо две точки фигуры <tex>\Phi</tex>, а <tex>m</tex> и <tex>n</tex> {{---}} опорные прямые, перпендикулярные к отрезку <tex>BC</tex>, то отрезок <tex>BC</tex> не превосходит расстояния между прямыми <tex>m</tex> и <tex>n</tex>, которое в свою очередь не превосходит <tex>d</tex>. Следовательно, длина <tex>BC</tex> не может быть больше <tex>d</tex>.
 
 
}}
 
}}
  
=== Алгоритм ===
+
By the way, если считать <tex>k</tex> константой, то и для случая большей размерности эти оценки будут такими же (доказывается аналогично).
Заметим, что параллельные опорные прямые можно провести не через любую пару точек.
 
  
{{Определение
+
== Запрос ==
|definition=
+
Пусть нам поступил какой-то прямоугольник <tex>R</tex>. Нужно вернуть все точки, которые в нём лежат. Будем это делать рекурсивно, получая на вход корень дерева и сам прямоугольник <tex>R</tex>. Обозначим область, соответствующую вершине <tex>v</tex>, как <tex>region(v)</tex>. Она будет прямоугольником, одна или более границ которого могут быть на бесконечности. <tex>region(v)</tex> можно явно хранить в узлах, записав при построении, или же считать при рекурсивном спуске. Если корень дерева является листом, то просто проверяем одну точку и при необходимости репортим её. Если нет, то смотрим пересекают ли регионы детей прямоугольник <tex>R</tex>. Если да, то запускаемся рекурсивно от такого ребёнка. При этом, если регион полностью содержится в <tex>R</tex>, то можно репортить сразу все точки из него. Тем самым мы, очевидно, вернём все нужные точки и только их. Чтобы стало совсем понятно, приведём псевдокод:
Точки, через которые можно провести параллельные опорные прямые, будем называть '''противолежащими''' (англ. ''antipodal'').
 
}}
 
  
Благодаря предыдущей теореме нам нужно рассмотреть только противолежащие точки. Задача в том, чтобы рассмотреть их, не перебирая все пары точек.
+
<code>
 +
SearchKdTree(v, R)
 +
//Input. The root of (a subtree of) a kd-tree, and a range R.
 +
//Output. All points at leaves below v that lie in the range.
 +
if v is a leaf
 +
    Report the point stored at v if it lies in R.
 +
else
 +
    if region(v.left) is fully contained in R
 +
      ReportSubtree(v.left)
 +
    else if region(v.left) intersects R
 +
      SearchKdTree(v.left, R)
 +
    if region(v.right) is fully contained in R
 +
      ReportSubtree(v.right)
 +
    else if region(v.right) intersects R
 +
      SearchKdTree(v.right, R)
 +
</code>
  
[[Файл:calipers.png|250px|right]]
+
Здесь <tex>ReportSubtree</tex> репортит все точки в поддереве.
Рассмотрим рисунок справа. <tex>L</tex> и <tex>M</tex> {{---}} опорные прямые, проходящие через вершины <tex>A</tex> и <tex>D</tex> соответственно. Значит, <tex>\langle A,D \rangle</tex> {{---}} противолежащая пара точек. Если начать вращать прямые против часовой стрелки вокруг данных точек, они будут оставаться опорными прямыми, пока одна из прямых не станет накладываться на сторону многоугольника. В нашем примере, при вращении <tex>M</tex> к позиции <tex>M'</tex>, <tex>M</tex> коснётся точки <tex>E</tex> раньше, чем <tex>L</tex> коснётся <tex>B</tex>, поэтому <tex>\langle A,E \rangle</tex> становится новой парой противолежащих точек.  
 
  
Теперь <tex>M'</tex> будет вращаться вокруг <tex>E</tex>, а <tex>L'</tex> продолжит вращаться вокруг <tex>A</tex>, и следующей парой противолежащих точек станет <tex>\langle B,E \rangle</tex>. Продолжая таким образом, мы сгенерируем все пары противолежащих точек, так как параллельные линии пройдут под всеми возможными углами. Определение новой пары противолежащих точек требует только одного сравнения углов, которое можно выполнить с помощью предиката поворота.
+
By the way, точно так же можно перечислять точки в любом множестве, ведь нигде не используется, что <tex>R</tex> {{---}} прямоугольник.
  
=== Асимптотика ===
 
 
{{Теорема
 
{{Теорема
 +
|about=
 +
О времени на запрос
 
|statement=
 
|statement=
Представленный выше алгоритм генерирует все пары противолежащих точек в многоугольнике <tex>P</tex>, состоящем из <tex>N</tex> вершин, за время <tex>O(N)</tex>.
+
Перечисление точек в прямоугольнике выполняется за <tex>O(\sqrt n + ans)</tex>, где <tex>ans</tex> {{---}} размер ответа.
 
|proof=
 
|proof=
Данный алгоритм можно реализовать следующим образом: хранить указатели на противолежащие вершины, и на каждой итерации алгоритма увеличивать либо один из данных указателей, либо сразу оба (когда обе прямые проходят через сторону многоугольника), и заканчивать работу, когда опорные прямые сделают полный круг. Таким образом, каждая из вершин будет посещена каждой из прямых не более двух раз.
+
Сперва заметим, что все <tex>ReportSubtree</tex> суммарно выполняются за <tex>O(ans)</tex>. Поэтому достаточно доказать оценку для числа рекурсивных вызовов. А рекурсивные вызовы выполняются только для тех вершин, регионы которых пересекают <tex>R</tex>, но не содержатся в нём. Такие регионы обязательно пересекают хотя бы одну (axis-parallel) сторону заданного прямоугольника. Оценим количество регионов, которые могут пересекаться произвольной вертикальной прямой. Для горизонтальной прямой это будет аналогично.
 +
 
 +
Обозначим максимально возможное количество регионов, пересекаемых какой-либо вертикальной прямой, в дереве для <tex>n</tex> точек, у которого первое разбиение делается вертикальной прямой, как <tex>Q(n)</tex>. Рассмотрим произвольную вертикальную прямую <tex>l</tex>. Она будет пересекать регион корня и какого-то одного из его детей (например, левого). При этом ни один из регионов в другом (правом) поддереве пересекать она не может. Левая половина разбита ещё на 2 части горизонтальной прямой, в каждой из них примерно <tex>n / 4</tex> вершин, и они хранятся в поддереве, у которого первое разбиение делается вертикальной прямой. Это даёт нам следующее соотношение:
 +
 
 +
<tex>Q(n) = O(1)</tex> if <tex>n = 1</tex>.
 +
 
 +
<tex>Q(n) = 2 + 2 \cdot Q(n / 4)</tex>, otherwise.
 +
 
 +
Нетрудно заметить, что <tex>Q(n) = O(\sqrt n)</tex> является решением. Принимая во внимание всё, что писалось выше, получаем требуемое.
 
}}
 
}}
 +
By the way, в общем случае время на запрос <tex>O(n^{1 - 1/k} + ans)</tex> из соотношения <tex>Q(n) = k + 2^{k - 1} \cdot Q(n / 2^k)</tex>.

Версия 14:07, 21 января 2015

Kd-tree.png

K-d дерево (short for k-dimensional tree) — статическая структура данных для хранения точек в [math]k[/math]-мерном пространстве. Позволяет отвечать на запрос, какие точки лежат в данном прямоугольнике. Строится это дерево следующим образом: разобьём все точки вертикальной прямой так, чтобы слева (нестрого) и справа (строго) от неё было примерно поровну точек (для этого посчитаем медиану первых координат). Получим подмножества для левого и правого ребёнка. Далее построим для этих подмножеств деревья, но разбивать будем уже не вертикальной, а горизонтальной прямой (для этого посчитаем медиану вторых координат). И так далее (будем считать, что [math]k = 2[/math] (случай бОльших размерностей обрабатывается аналогично), поэтому на следующем уровне вновь будем разбивать вертикальными прямыми).

Замечание: проблемы могут возникнуть, если много точек имеют одинаковую координату, тогда разбить примерно поровну не получится (почти все точки будут лежать на медиане и попадут в левую часть). Лучший способ борьбы с этим — не вспоминать о данной проблеме совсем. Но вообще с этим борются, используя composite numbers, то есть сравнивая ещё и по другой (другим) координате.

Реализовывать построение можно рекурсивно с помощью функции [math]BuildKdTree(P, Depth)[/math], принимающей множество точек и глубину. В зависимости от остатка при делении на размерность (при [math] k = 2 [/math] от чётности размерности) сплитим множество на два подмножества и делаем рекурсивные вызовы. Для лучшего понимания приведём псевдокод:

BuildKdTree(P, Depth)
//Input. A set of points P and the current depth Depth.
//Output. The root of a kd-tree storing P.
if P contains only one point
   return a leaf storing this point
else if depth is even
   Split P into two subsets [math]P_1[/math] and [math]P_2[/math] with a vertical line [math]l[/math] through the median x-coordinate of the points in P
else 
   Split P into two subsets [math]P_1[/math] and [math]P_2[/math] with a horizontal line [math]l[/math] through the median y-coordinate of the points in P. 
[math]V_{left}[/math] <- BuildKdTree([math]P_1[/math], Depth + 1)
[math]V_{right}[/math] <- BuildKdTree([math]P_2[/math], Depth + 1)
Create a node v storing [math]l[/math], make [math]V_{left}[/math] the left child of v, and make [math]V_{right}[/math] the right child of v.
return v 

Лемма (О времени построения):
Построение выполняется за [math]O(n \log n)[/math].
Доказательство:
[math]\triangleright[/math]

Время построения обозначим [math]T(n)[/math]. Поиск медианы можно сделать за линейное время, поэтому достаточно очевидно, что:

[math]T(n) = O(1)[/math] if [math]n = 1[/math].

[math]T(n) = O(n) + 2 \cdot T(n / 2)[/math], otherwise.

Решением этого является [math]T(n) = O(n \log n)[/math].

Также стоит отметить, что можно и не искать медиану за линейное время, а просто посортить все точки в самом начале и дальше использовать это. В реализации попроще, асимптотика та же.
[math]\triangleleft[/math]
Лемма (О занимаемой памяти):
K-d дерево требует [math]O(n)[/math] памяти.
Доказательство:
[math]\triangleright[/math]
Высота дерева, очевидно, логарифмическая, а листьев всего [math]O(n)[/math]. Поэтому будет [math]O(n)[/math] вершин, каждая занимает [math]O(1)[/math] памяти.
[math]\triangleleft[/math]

By the way, если считать [math]k[/math] константой, то и для случая большей размерности эти оценки будут такими же (доказывается аналогично).

Запрос

Пусть нам поступил какой-то прямоугольник [math]R[/math]. Нужно вернуть все точки, которые в нём лежат. Будем это делать рекурсивно, получая на вход корень дерева и сам прямоугольник [math]R[/math]. Обозначим область, соответствующую вершине [math]v[/math], как [math]region(v)[/math]. Она будет прямоугольником, одна или более границ которого могут быть на бесконечности. [math]region(v)[/math] можно явно хранить в узлах, записав при построении, или же считать при рекурсивном спуске. Если корень дерева является листом, то просто проверяем одну точку и при необходимости репортим её. Если нет, то смотрим пересекают ли регионы детей прямоугольник [math]R[/math]. Если да, то запускаемся рекурсивно от такого ребёнка. При этом, если регион полностью содержится в [math]R[/math], то можно репортить сразу все точки из него. Тем самым мы, очевидно, вернём все нужные точки и только их. Чтобы стало совсем понятно, приведём псевдокод:

SearchKdTree(v, R)
//Input. The root of (a subtree of) a kd-tree, and a range R.
//Output. All points at leaves below v that lie in the range.
if v is a leaf 
   Report the point stored at v if it lies in R.
else 
   if region(v.left) is fully contained in R
      ReportSubtree(v.left)
   else if region(v.left) intersects R
      SearchKdTree(v.left, R)
   if region(v.right) is fully contained in R
      ReportSubtree(v.right)
   else if region(v.right) intersects R
      SearchKdTree(v.right, R) 

Здесь [math]ReportSubtree[/math] репортит все точки в поддереве.

By the way, точно так же можно перечислять точки в любом множестве, ведь нигде не используется, что [math]R[/math] — прямоугольник.

Теорема (О времени на запрос):
Перечисление точек в прямоугольнике выполняется за [math]O(\sqrt n + ans)[/math], где [math]ans[/math] — размер ответа.
Доказательство:
[math]\triangleright[/math]

Сперва заметим, что все [math]ReportSubtree[/math] суммарно выполняются за [math]O(ans)[/math]. Поэтому достаточно доказать оценку для числа рекурсивных вызовов. А рекурсивные вызовы выполняются только для тех вершин, регионы которых пересекают [math]R[/math], но не содержатся в нём. Такие регионы обязательно пересекают хотя бы одну (axis-parallel) сторону заданного прямоугольника. Оценим количество регионов, которые могут пересекаться произвольной вертикальной прямой. Для горизонтальной прямой это будет аналогично.

Обозначим максимально возможное количество регионов, пересекаемых какой-либо вертикальной прямой, в дереве для [math]n[/math] точек, у которого первое разбиение делается вертикальной прямой, как [math]Q(n)[/math]. Рассмотрим произвольную вертикальную прямую [math]l[/math]. Она будет пересекать регион корня и какого-то одного из его детей (например, левого). При этом ни один из регионов в другом (правом) поддереве пересекать она не может. Левая половина разбита ещё на 2 части горизонтальной прямой, в каждой из них примерно [math]n / 4[/math] вершин, и они хранятся в поддереве, у которого первое разбиение делается вертикальной прямой. Это даёт нам следующее соотношение:

[math]Q(n) = O(1)[/math] if [math]n = 1[/math].

[math]Q(n) = 2 + 2 \cdot Q(n / 4)[/math], otherwise.

Нетрудно заметить, что [math]Q(n) = O(\sqrt n)[/math] является решением. Принимая во внимание всё, что писалось выше, получаем требуемое.
[math]\triangleleft[/math]

By the way, в общем случае время на запрос [math]O(n^{1 - 1/k} + ans)[/math] из соотношения [math]Q(n) = k + 2^{k - 1} \cdot Q(n / 2^k)[/math].