Алгоритм D*
Алгоритм D* — алгоритм поиска кратчайшего пути во взвешенном ориентированном графе, где структура графа неизвестна заранее или постоянно подвергается изменению. Разработан Свеном Кёнигом и Максимом Лихачевым в 2002 году.
Алгоритм LPA*
Постановка задачи
Дан взвешенный ориентированный граф . Даны вершины: стартовая вершина и конечная вершина . Требуется после каждого изменения графа уметь вычислять функцию для каждой известной вершины
Описание
Функция будет возвращать наименьшую стоимость пути из вершины в . Её значение для алгоритма будет почти аналогичным значению в алгоритме A*, за исключением того, что в данном алгоритме наc интересуют только -значения известных вершин на данной итерации.
Будем поддерживать для каждой вершины два вида смежных с ней вершин:
- Обозначим множество как множество вершин, исходящих из вершины .
- Обозначим множество как множество вершин, входящих в вершину .
Функция будет возвращать стоимость ребра . При этом будет тогда и только тогда, когда ребра не существует.
| Определение: |
| Будем называть rhs-значением (англ. right-hand side value) такую функцию , которая будет возвращать потенциальное минимальное расстояние от до по следующим правилам:
Так как rhs-значение использует минимальное значение из минимальных расстояний от до вершин, входящих в данную вершину , это будет нам давать информацию об оценочном расстоянии от до . |
| Определение: |
| Вершина называется насыщенной (англ. locally consistent), если |
| Определение: |
| Вершина называется переполненной (англ. locally overconsistent), если |
| Определение: |
| Вершина называется ненасыщенной (англ. locally underconsistent), если |
Очевидно, что если все вершины насыщены, то мы можем найти расстояние от стартовой вершины до любой. Такой граф будем называть устойчивым (насыщенным).
Эвристическая функция теперь должна быть неотрицательная и выполнять неравенство треугольника, т.е. и для всех и
| Определение: |
Будем называть ключом вершины такую функцию , которая возвращает вектор из 2-ух значений , .
|
Если в конце поиска пути , то мы не смогли найти путь от до на текущей итерации. Но после следующего изменения графа путь вполне может найтись.
Псевдокод
Основная функция, описывающая алгоритм
function main():
initialize()
while true
computeShortestPath()
// В данный момент мы знаем кратчайший путь из f в t.
Ждем каких-либо изменений графа.
for всех ориентированных ребер (u, v) с измененными весами:
Обновляем результат функции c(u, v)
updateVertex(v)
Функция инициализации исходного графа устанавливает для всех вершин кроме стартовой вершины значения и равными бесконечности. Для стартовой . Очевидно, что минимальное расстояние от стартовой вершины до самой себя должно быть равным 0, но . Это сделано для того, чтобы стартовая вершина была ненасыщенной и имела право попасть в приоритетную очередь.
function initialize(): // Заведем приоритетную очередь U, в которую будем помещать вершины. // Сортировка будет производиться по функции key(s). U = for s V rhs(s) = g(s) = rhs(f) = 0 U.insert(f, calcKey(f))
Функция . Возвращаемые значения сортируются в лексографическом порядке, то есть сначала сортируется по , потом по
function calcKey(s): return [min(g(s), rhs(s)) + h(s, t), min(g(s), rhs(s))]
Обновляет данные вершины в соответствие с данными выше определениями. Также поддерживает инвариант того, что в очереди U лежат только ненасыщенные вершины.
function updateVertex(u): if u f if u U U.remove(u) if g(u) rhs(u) U.insert(u, calcKey(u))
Функция несколько раз перерасчитывает значение у ненасыщенных вершин в неубывающем порядке их ключей. Такой перерасчет значения будем называть расширением вершины.
function computeShortestPath(): while U.topKey() < calcKey(t) or rhs(t) g(t) u = U.pop() if g(u) > rhs(u) g(u) = rhs(u) for Succ(u) UpdateVertex(s) else g(u) = for s Succ(u) updateVertex(s)
Асимптотика
| Теорема (О монотонности изменения ключей): |
В течение выполнения функции ComputeShortestPath вершины, взятые из очереди, монотонно не убывают. |
| Доказательство: |
| Доказательство [1] |
| Теорема (О необратимой насыщенности): |
Если в функции ComputeShortestPath была взята переполненная вершина, то на следующей итерации она станет насыщенной. |
| Доказательство: |
| Доказательство [2] |
| Теорема: |
После выполнения функции ComputeShortestPath можно восстановить путь из в . Для этого, начиная с вершины , нужно постоянно передвигаться к такой вершине , входящей в , чтобы было минимальным, до тех пора, пока не будет достигнута вершина . |
| Доказательство: |
| Доказательство [3] |
Таким образом мы описали алгоритм LPA*. Он вычисляет длину кратчайшего пути между вершинами и , используя при этом данные из предыдущих итераций. Очевидно, что в худшем случае (а именно когда все ребра вокруг текущей вершины изменили свой вес) алгоритм будет работать как последовательные вызовы алгоритма А* за . Улучшим эту оценку с помощью алгоритма D* lite.
Примечание: на практике же такой подход тоже имеет место на плотных графах (или матрицах), так как в среднем дает оценку .
Алгоритм D* (Первая версия)
Пока что был описан только алгоритм LPA*. Он находит кратчайшее расстояние между начальной и конечной вершинами при любом изменении данного графа. Его первоначальный поиск полностью совпадает с алгоритмом A*, но последующие итерации способны использовать информацию из предыдущих поисков.
Постановка задачи
Дан взвешенный ориентированный граф . Даны вершины и . Требуется в процессе движения по кратчайшему пути в графе обновлять значения функции при поступлении новой информации о графе .
Теперь на основе LPA* опишем алгоритм D*, который способен определять расстояние между текущей вершиной , в которой, допустим, находится способный к сканированию местности "робот", и конечной вершиной при каждом изменении графа в то время, как наш "робот" движется вдоль найденного пути.
Описание
Опишем первую версию алгоритма D*. Так как при движении по кратчайшему пути путь может только сокращаться и происходит изменение только стартовой вершины, то можно применить идею из алгоритма LPA*.
Примечание: Большинство функций переходят в данный алгоритм без изменений, поэтому опишем только измененные части.
Для начала мы поменяем направление поиска в графе.
Теперь функция g(s) хранит минимальное известное расстояние от до . Свойства остаются прежними.
Эвристическая функция теперь должна быть неотрицательная и обратно-устойчивая, т.е. и для всех и . Очевидно, что при движении робота изменяется, поэтому данные свойства должны выполняться для всех .
Дополнительное условие выхода также меняется, т.е. при путь не найден на данной итерации. Иначе путь найден и "робот" может проследовать по нему.
Примечание: Так же следует отметить, что функция Initialize не обязана инициализировать абсолютно все вершины перед стартом алгоритма. Это важно, так как на практике число вершин может быть огромным, и только немногие будут пройдены роботом в процессе движения. Так же это дает возможность добавления/удаления ребер без потери устойчивости всех подграфов данного графа.
Псевдокод
При такой постановке задачи псевдокод не сильно меняется, но функция Main все-таки претерпевает значительные изменения.
function calcKey(s): return [min(g(s), rhs(s)) + h(f,s), min(g(s), rhs(s))]
function initialize(): U = for s V rhs(s) = g(s) = rhs(t) = 0 U.insert(t, calcKey(t))
function UpdateVertex(u): if u t rhs(u) = if u U U.remove(u) if g(u) rhs(u) U.insert(u, calcKey(u))
function ComputeShortestPath(): while U.topKey() < calcKey(f) or rhs(f) g(f) u = U.pop() if (g(u) > rhs(u)) g(u) = rhs(u) for s Pred(u) updateVertex(s) else g(u) = for Pred(u) {u} updateVertex(s)
function main(): initialize() computeShortestPath() while // if (g(f) = ) тогда путь на данной итерации не найден. = такая вершина s', что Передвинулись вдоль найденного пути и изменили вершину Сканируем роботом какие-либо изменения в графе или убеждаемся, что граф остается прежним. if граф изменился for всех ориентированных ребер с измененными весами: Обновляем результат функции updateVertex(u) for U U.update(s, CalcKey(s)) computeShortestPath()
Асимптотика
| Теорема (Свен Кёниг, Об устойчивой насыщенности вершин): |
Функция ComputeShortestPath в данной версии алгоритма расширяет вершину максимум 2 раза, а именно 1 раз, если вершина ненасыщена, и максимум 1 раз, если она переполнена. |
| Доказательство: |
| Доказательство [4] |
Алгоритм D* (Вторая версия)
Описание
В первой версии алгоритма была серьезная проблема в том, что для каждой вершины в приоритетной очереди нужно было обновлять ключ суммарно за . Это дорогая операция, так как очередь может содержать огромное число вершин. Воспользуемся оригинальным методом поиска и изменим основной цикл, чтобы избежать постоянного перестроения очереди .
Теперь эвристическая функция должна поддерживать неравенство треугольника для всех вершин , т.е. . Так же должно выполняться свойство , где - стоимость перехода по кратчайшему пути из в , при этом и не должны быть обязательно смежными. Такие свойства не противоречат свойствами из первой версии, а лишь усиливают их.
Допустим, что после того, как робот продвинется вдоль найденного пути на предыдущих итерациях, из вершины в , он обнаружит изменения в графе. Первая компонента ключей может уменьшится максимум на (по определению ключа). Вторая компонента не зависит от функции h. Аналогично первой версии алгоритма, мы должны уменьшить первую компоненту ключа у всех вершин в очереди U. Очевидно, что будет одинаковым для всех вершин из U. Порядок в очереди не изменится, если произвести уменьшение. Следовательно уменьшение можно отложить, тем самым очередь не придется перестраивать на каждой итерации. Так же исходя из нового определения функции , её значение будет всегда меньше, чем разность первых компонент ключей у соседних по приоритету вершин. Таким образом мы можем добавлять h(s,s') ко всем у ключей вершин из U.
Будем называть ключевым модификатором. В нем мы и будет хранить сумму , которые нужно добавить ко всем вершинам из U.
Псевдокод
function calcKey(s):
return [min(g(s), rhs(s)) + h(f, s) + , min(g(s), rhs(s))]
function initialize(): U = for s V rhs(s) = g(s) = rhs(t) = 0 U.insert(t, CalcKey(t))
function updateVertex(u): if u t rhs(u) = if u U U.remove(u) if g(u) rhs(u) U.insert(u, calcKey(u))
function computeShortestPath(): while U.topKey() < calcKey(f) or rhs(f) g(f) = U.topKey() u = U.pop() if < calcKey(u) U.insert(u, calcKey(u)) if (g(u) > rhs(u)) g(u) = rhs(u) for Pred(u) updateVertex(s) else g(u) = for Pred(u) updateVertex(s)
function main(): initialize() computeShortestPath() while f t // if (g(f) = ) тогда путь на данной итерации не найден. = такая вершина s', что Передвинулись вдоль найденного пути и изменили вершину . Сканируем роботом какие-либо изменения в графе или убеждаемся, что граф остается прежним. if граф изменился for всех ориентированных ребер (u, v) с измененными весами: Обновляем результат функции c(u, v) updateVertex(u) computeShortestPath()
Асимптотика
С помощью введения ключевого модификатора и отложенного обновления ключей вершин получилось убрать из итерации алгоритма операций, которые тратились на обновление очереди . Очевидно, что на основе теорем, приведенных выше, алгоритм использовал операций. Итак, нам удалось уменьшить константу в 2 раза, что дает существенный рост производительности на практических задачах.
Пример работы
|
|
| Итерации в функции ComputeShortestPath на исходном графе. | Итерации в функции ComputeShortestPath после изменения графа. (Второй вызов функции) |

