Visibility graph и motion planning — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
(Построение visibility графа)
(Lee’s Algorithm. O(n ^ 2 \log n))
Строка 64: Строка 64:
 
Будем рассматривать точки слева направо, таким образом потребуется заметать только правую половину плоскости, так как ребра, которые должны идти в левую будут уже добавлены ранее.
 
Будем рассматривать точки слева направо, таким образом потребуется заметать только правую половину плоскости, так как ребра, которые должны идти в левую будут уже добавлены ранее.
  
Итак, первым делом вершины <tex> w \in V </tex> сортируются по углу между лучом <tex> vw </tex> и вертикальной полуосью, проходящей через <tex> v </tex> {{---}} луч <tex> l </tex>. Затем инициализируем статус отрезками, которые пересекают луч <tex> l </tex>. Далее начинается заметание плоскости. В порядке сортировки вершин для каждой из них выполняется проверка: видна ли вершина <tex> w </tex> из вершины <tex> v </tex>. Для этого нам достаточно проверить только пересечение заметающего луча с ближайшим отрезком в статусе. Если вершина видима, необходимо добавить её в список видимых вершин. И, наконец, вне зависимости от видимости вершины, необходимо изменить статус заметающего луча. Для этого для текущей вершины <tex> w </tex> необходимо удалить из статуса все рёбра (отрезки), которые заканчиваются в этой вершине (лежат слева от прямой <tex> vw </tex>) и добавить все рёбра (отрезки), которые в ней начинаются (лежат справа от прямой <tex> vw </tex>).
+
Изначально, пустим луч из рассматриваемой вершины вертикально вверх и добавим в статус все отрезки, которые он пересекает, по увеличению расстояния до них. Теперь будем рассматривать точки <tex> w \in V </tex> в порядке сортировки по углу между <tex> v </tex> и вертикальной полуосью <tex> l </tex>. При таком обходе проверка видимости вершины будет выполняться за <tex> O(1) </tex>, так как достаточно проверить пересечение с отрезком, первым в статусе. Действительно, если вершина не видна, то отрезок <tex> vw </tex> пересекает несколько отрезков, лежащих перед <tex> w </tex>, а значит и ближайший, то есть первый в статусе. В противном случае все пересекаемые лучом отрезки лежат за вершиной <tex> w </tex> и пересечения отрезка <tex> uw </tex> с ближайшим отрезком в статусе не будет. Вне зависимости от видимости вершины, необходимо изменить статус заметающего луча. Для этого необходимо удалить из статуса все отрезки, которые заканчиваются вершине <tex> w </tex> (лежат слева от прямой <tex> vw </tex>) и добавить все отрезки, которые в ней начинаются (лежат справа от прямой <tex> vw </tex>).
  
 
===== Псевдокод =====
 
===== Псевдокод =====
Строка 73: Строка 73:
 
   graph visibilityGraph(vertices)                    //изначально в графе только вершины
 
   graph visibilityGraph(vertices)                    //изначально в графе только вершины
 
   for Vertex v in vertices                          //для каждой вершины
 
   for Vertex v in vertices                          //для каждой вершины
       for Vertex w in getVisibleVertices(v)           //добавляем в граф все видимые из нее вершины
+
       for Vertex w in getVisibleVertices(v, segments) //добавляем в граф все видимые из нее вершины
 
         visibilityGraph.addEdge(v, w)
 
         visibilityGraph.addEdge(v, w)
 
   return visibilityGraph
 
   return visibilityGraph
Строка 79: Строка 79:
 
Здесь функция getVisibleVertices(<tex> v </tex>) возвращет все видимые из <tex> v </tex> вершины и выглядит так:
 
Здесь функция getVisibleVertices(<tex> v </tex>) возвращет все видимые из <tex> v </tex> вершины и выглядит так:
 
<pre>
 
<pre>
vector<Vertex> getVisibleVertices(vertex v)
+
vector<Vertex> getVisibleVertices(vertex v, set<segment> segments)
   vector<Vertex> answer, currentVertices
+
// Инициализируем статус
  currentVertices.add(all w in vertices : w.x >= v.x)   //рассматриваем только вершины правее v
+
   for segment s in segments
   sort vertexes by angle                               //в порядке увеличения угла с
+
      if intersection s and ray from v to up exists
                                                        //вертикальной прямой, проходящей через v
+
        status.add(s)
  heap<Segment> status
+
// Инициализируем множество вершин, которые нужно рассматривать
  status.add(all e in Edges : intersection(e, l) != null)  //инициализируем статус
+
  for point p in segments
 +
      if p.x >= v.x
 +
        currentVertices.add(p)
 +
   sort(currentVertices) by angle
 +
// Для каждой вершины проверяем, видима ли она и обновляем статус
 
   for w in currentVertices
 
   for w in currentVertices
       if intersection(vw, status.closest) == null        //если отрезок vw не пересекает ближайший
+
       if intersection vw and status.first not exists
                                                        //  отрезок в статусе
+
         answer.add(w)
         answer.add(w)                                   //  добавляем в ответ
+
       delete from status all edges ending in w
       delete from status all edges ending in w           //обновляем статус
 
 
       add in status all edges beginning in w
 
       add in status all edges beginning in w
 
   return answer
 
   return answer
 
</pre>
 
</pre>
Вершин у нас <tex> n </tex>, для каждой вершины мы выполяеем следующие шаги: сортировка (<tex> O(n \log n) </tex>), проверка пересечения (<tex> O(1) </tex>), обновление статуса (суммарно выполняется за <tex> O(n \log n) </tex>, так как мы имеем <tex> O(n) </tex> ребер и каждое ребро мы добавляем и удаляем один раз (вставка происходит за <tex> O(\log n) </tex>, удаление можно сделать за <tex> O(1) </tex>, например, для каждой вершины сохраняя позицию входящих в нее отрезков). Итого получаеем <tex> O(n) * O(n \log n) = O(n^2 \log n) </tex>. Что и требовалось доказать.
+
В качестве статуса нужно использовать структуру данных, позволяющую добавлять и удалять из нее отрезки за <tex> O(\log n) </tex> и извлекать минимум за <tex> O(1) </tex> или <tex> O(\log n) </tex>. В этом случае достигается асимптотика <tex> O(n^2 \log n) </tex>, так как для каждой из <tex> n </tex> точек выполняется сортировка(<tex> O(n \log n) </tex>), обновление статуса (суммарно <tex> O(n \log n) </tex>, так как каждый отрезок добавляется и удаляется из статуса только один раз) и запросы ближайшего отрезка (<tex> O(\log n) </tex> или <tex> O(1) </tex> на точку, то есть <tex> O(n \log n) </tex> или <tex> O(n) </tex>).  
 
|
 
|
 
[[Файл:Zamrefr1.png|250px|thumb|right|Обновление статуса заметающего луча]]
 
[[Файл:Zamrefr1.png|250px|thumb|right|Обновление статуса заметающего луча]]

Версия 17:38, 6 февраля 2015

Visibility graph

Путь с препятствиями через трапецоидную карту
Такой путь не самый короткий

Рассмотрим задачу нахождения пути от точки [math] S [/math] до [math] T [/math] с препятствиями. Для начала рассмотрим движение материальной точки, случай, когда размером и формой движимого объекта пренебречь нельзя, будет позднее.

Обычно эта задача решается с помощью трапецоидной карты, по которой строится граф, ребра которого соединяют центры трапедоидов и вершины [math] S [/math] и [math] T [/math] с серединами вертикальных сторон трапецоидов. В этом графе любым алгоритмом поиска кратчайших путей (например, алгоритмом Дейкстры или A*) находится путь от [math] S [/math] до [math] T [/math].

Данный алгоритм работает за [math] O(n \log n) [/math] и за линейное количество памяти и идеально подходит для нахождения какого-нибудь пути между конечными вершинами. Но иногда нужно найти кратчайший путь, и этот алгоритм не подходит, хоть и дает хорошее приближение.

На сегодняшний день точные решения в лучшем случае работают за [math] O(n^2) [/math] времени и памяти (здесь и далее [math] n [/math] — количество всех вершин).

Теперь рассмотрим точное решение с помощью построения графа видимости. После его построения, как и в случае с трапецоидной картой, путь ищется стандартными алгоритмами.

Для простоты рассуждений вершины [math] S [/math] и [math] T [/math] будем считать вершинами полигонов.

Лемма (О кратчайшем пути):
Любой кратчайший путь между двумя вершинами с полигональными препятствиями представляет собой ломаную, вершины которой — вершины полигонов.
Доказательство:
[math]\triangleright[/math]
Short cut
Пусть кратчайший путь — не ломаная. В таком случае, на пути существует такая точка [math] p [/math], которая не принадлежит ни одному прямому отрезку. Это означает, что существует [math]\epsilon[/math]-окрестность точки [math] p [/math], в которую не попадает ни одно препятствие (случай, когда точка попала на ребро рассматривается аналогично). В таком случае, подпуть, который находится внутри [math]\epsilon[/math]-окрестности, по неравенству треугольника может быть сокращён по хорде, соединяющий точки пересечения границы [math]\epsilon[/math]-окрестности с путем. Раз часть пути может быть уменьшена, значит и весь путь может быть уменьшен, а значит исходное предположение некорректно.
[math]\triangleleft[/math]
Определение:
Говорят, что вершина [math] u [/math] видна (англ. mutually visible) из [math] v [/math], если отрезок [math] uv [/math] не пересекает ни одного препятствия.


Определение:
visibility graph — граф, вершины которого — вершины полигонов. Между вершинами [math] u [/math] и [math] v [/math] существует ребро, если из [math] u [/math] видна [math] v [/math].


В худшем случае в таком графе может быть [math] O(n^2) [/math] ребер. Однако по некоторым ребрам кратчайший путь точно не пройдет, и такие ребра из графа можно удалить.

Лемма:
Удаляем [math] BD [/math]
Если существуют вершины [math] A, B, C [/math] одного препятствия и вершина [math] D [/math] такая, что поворот [math] DBA [/math] не совпадает с поворотом [math] DBC [/math], то ребро [math] DB [/math] не принадлежит кратчайшему пути и его можно удалить из графа. (См. поясняющую картинку справа)
Доказательство:
[math]\triangleright[/math]
Не удаляем [math] BS [/math]
Путь проходящий через ребро [math] BD [/math] будет длиннее, чем через соседей точки [math] B [/math], так как по неравенству треугольника [math] AB + BD \lt AD [/math]
[math]\triangleleft[/math]

По доказанным леммам любое ребро кратчайшего пути содержится в графе, таким образом для нахождения кратчайшего пути осталось найти кратчайший путь в этом графе от [math] S [/math] до [math] T [/math]. Рассмотрим алгоритмы построения графа видимости.

Построение visibility графа

Наивный алгоритм. [math] O(n ^ 3) [/math]

Если делать наивно, т. е. для каждой пары вершин проверять можно ли добавить ли такое ребро(нет ли пересечений с полигонами), будет [math] O(n^3) [/math].

Lee’s Algorithm. [math] O(n ^ 2 \log n) [/math]

Заметание плоскости вращающимся лучом

Однако можно это сделать за [math] O(n ^ 2 \log n) [/math]. Идея алгоритма проста : для каждой вершины найдем видимые из нее вершины независимо. Если научиться делать это за [math] O(n \log n) [/math], задача решена, так как всего точек [math] n [/math].

Переформулируем задачу : дана точка [math] v [/math] и множество отрезков — ребер препятствий. Найти : множество концов отрезков, видимых из [math] v [/math].

Для решения этой задачи будем использовать заметающий луч с началом в точке [math] v [/math]. Его статусом будут отрезки, которые его пересекают, упорядоченные по возрастанию расстояния от точки [math] v [/math] до точки пересечения. Точками событий будут концы отрезков.

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

Изначально, пустим луч из рассматриваемой вершины вертикально вверх и добавим в статус все отрезки, которые он пересекает, по увеличению расстояния до них. Теперь будем рассматривать точки [math] w \in V [/math] в порядке сортировки по углу между [math] v [/math] и вертикальной полуосью [math] l [/math]. При таком обходе проверка видимости вершины будет выполняться за [math] O(1) [/math], так как достаточно проверить пересечение с отрезком, первым в статусе. Действительно, если вершина не видна, то отрезок [math] vw [/math] пересекает несколько отрезков, лежащих перед [math] w [/math], а значит и ближайший, то есть первый в статусе. В противном случае все пересекаемые лучом отрезки лежат за вершиной [math] w [/math] и пересечения отрезка [math] uw [/math] с ближайшим отрезком в статусе не будет. Вне зависимости от видимости вершины, необходимо изменить статус заметающего луча. Для этого необходимо удалить из статуса все отрезки, которые заканчиваются вершине [math] w [/math] (лежат слева от прямой [math] vw [/math]) и добавить все отрезки, которые в ней начинаются (лежат справа от прямой [math] vw [/math]).

Псевдокод
graph buildVisibilityGraph(Set<Segment> segments)
   Set<Vertex> vertices = getVertices(segments)       //получаем все вершины препятствий
   vertices.add(s, t)                                 //добавляем начальную и конечную вершину
   graph visibilityGraph(vertices)                    //изначально в графе только вершины
   for Vertex v in vertices                           //для каждой вершины
      for Vertex w in getVisibleVertices(v, segments) //добавляем в граф все видимые из нее вершины
         visibilityGraph.addEdge(v, w)
   return visibilityGraph

Здесь функция getVisibleVertices([math] v [/math]) возвращет все видимые из [math] v [/math] вершины и выглядит так:

vector<Vertex> getVisibleVertices(vertex v, set<segment> segments)
// Инициализируем статус
   for segment s in segments
      if intersection s and ray from v to up exists
         status.add(s)
// Инициализируем множество вершин, которые нужно рассматривать
   for point p in segments
      if p.x >= v.x
         currentVertices.add(p)
   sort(currentVertices) by angle
// Для каждой вершины проверяем, видима ли она и обновляем статус
   for w in currentVertices
      if intersection vw and status.first not exists
         answer.add(w)
      delete from status all edges ending in w
      add in status all edges beginning in w
   return answer

В качестве статуса нужно использовать структуру данных, позволяющую добавлять и удалять из нее отрезки за [math] O(\log n) [/math] и извлекать минимум за [math] O(1) [/math] или [math] O(\log n) [/math]. В этом случае достигается асимптотика [math] O(n^2 \log n) [/math], так как для каждой из [math] n [/math] точек выполняется сортировка([math] O(n \log n) [/math]), обновление статуса (суммарно [math] O(n \log n) [/math], так как каждый отрезок добавляется и удаляется из статуса только один раз) и запросы ближайшего отрезка ([math] O(\log n) [/math] или [math] O(1) [/math] на точку, то есть [math] O(n \log n) [/math] или [math] O(n) [/math]).

Обновление статуса заметающего луча
Обновление статуса заметающего луча
Обновление статуса заметающего луча

Overmars and Welzl’s Algorithm [math] O(n ^ 2) [/math]

visibility graph при помощи rotation tree

C помощью rotation tree можно достичь асимптотики [math] O(n^2) [/math].

Motion planning

Раздуваем препятствия
Ищем путь для точки

Тут мы двигаем не точку, а произвольный выпуклый полигон. Если мы его не можем вращать, просто считаем configuration space, т. е. "обводим" препятствия нашим полигоном (делаем сумму Минковского препятствий и полигона, сдвинутого в начало координат какой-нибудь точкой) и получаем другие препятствия, но зато теперь мы двигаем точку. А это мы уже научились делать выше.

Теперь рассмотрим случай, когда мы можем вращать полигон. Для начала построим трапецоидную карту, как будто мы не можем вращать полигон. Теперь будем вращать полигон на малый угол, пока он не сделает полный оборот вокруг своей оси, и для каждого угла сделаем трапецоидную карту. Теперь разместим(мысленно) все карты друг над другом. Таким образом получится, что поворот на малый угол — это движение вверх/вниз между слоями. Осталось попересекать соседние слои и добавить между ними ребра (помним что первый и последний слои одинаковы) и уже в этом графе найти путь.

При такой реализации в некоторых случаях у нас может возникнуть ошибка в повороте, так как в одной плоскости мы все можем делать точно: положения на соседних слоях могут допускаться, а повернуть мы не сможем. Это лечится в основном двумя способами: измельчением угла поворота и изначальным сглаживанием углов полигона — повращать полигон на [math] +\epsilon [/math] и [math] -\epsilon [/math] и объединить с исходным, получив новый полигон.

Так как эта задача достаточно ресурсоемка, мы рассматриваем только наличие пути, а не нахождение кратчайшего.

Источники

  • Mark de Berg, Otfried Cheong, Marc van Kreveld, Mark Overmars (2008), Computational Geometry: Algorithms and Applications (3rd edition), Springer-Verlag, ISBN 978-3-540-77973-5 Chapter 15 page 324-331
  • статья про visibility graphs
  • Хабр