Опубликован: 02.02.2011 | Уровень: для всех | Доступ: свободно
Лекция 46:

Алгоритмы на графах. Алгоритмы нахождения кратчайшего пути

< Лекция 45 || Лекция 46: 123 || Лекция 47 >

Алгоритм Флойда

Рассматриваемый алгоритм иногда называют алгоритмом Флойда-Уоршелла. Алгоритм Флойда-Уоршелла является алгоритмом на графах, который разработан в 1962 году Робертом Флойдом и Стивеном Уоршеллом. Он служит для нахождения кратчайших путей между всеми парами вершин графа.

Метод Флойда непосредственно основывается на том факте, что в графе с положительными весами ребер всякий неэлементарный (содержащий более 1 ребра) кратчайший путь состоит из других кратчайших путей.

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

В алгоритме Флойда используется матрица A размером nxn, в которой вычисляются длины кратчайших путей. Элемент A[i,j] равен расстоянию от вершины i к вершине j, которое имеет конечное значение, если существует ребро (i,j), и равен бесконечности в противном случае.

Алгоритм Флойда

Основная идея алгоритма. Пусть есть три вершины i, j, k и заданы расстояния между ними. Если выполняется неравенство A[i,k]+A[k,j]<A[i,j], то целесообразно заменить путь i->j путем i->k->j. Такая замена выполняется систематически в процессе выполнения данного алгоритма.

Шаг 0. Определяем начальную матрицу расстояния A0 и матрицу последовательности вершин S0. Каждый диагональный элемент обеих матриц равен 0, таким образом, показывая, что эти элементы в вычислениях не участвуют. Полагаем k = 1.

Основной шаг k. Задаем строку k и столбец k как ведущую строку и ведущий столбец. Рассматриваем возможность применения замены описанной выше, ко всем элементам A[i,j] матрицы Ak-1. Если выполняется неравенство A[i,k]+A[k,j]<A[i,j], (i\ne k, j\ne k, i\ne j), тогда выполняем следующие действия:

  1. создаем матрицу Ak путем замены в матрице Ak-1 элемента A[i,j] на сумму A[i,k]+A[k,j] ;
  2. создаем матрицу Sk путем замены в матрице Sk-1 элемента S[i,j] на k. Полагаем k = k + 1 и повторяем шаг k.

Таким образом, алгоритм Флойда делает n итераций, после i -й итерации матрица А будет содержать длины кратчайших путей между любыми двумя парами вершин при условии, что эти пути проходят через вершины от первой до i -й. На каждой итерации перебираются все пары вершин и путь между ними сокращается при помощи i -й вершины ( рис. 45.2).

Демонстрация алгоритма Флойда

Рис. 45.2. Демонстрация алгоритма Флойда
//Описание функции алгоритма Флойда
void Floyd(int n, int **Graph, int **ShortestPath){
  int i, j, k;
  int Max_Sum = 0;
  for ( i = 0 ; i < n ; i++ )
    for ( j = 0 ; j < n ; j++ )
      Max_Sum += ShortestPath[i][j];
  for ( i = 0 ; i < n ; i++ )
    for ( j = 0 ; j < n ; j++ )
      if ( ShortestPath[i][j] == 0 && i != j ) 
        ShortestPath[i][j] = Max_Sum;
  for ( k = 0 ; k < n; k++ )
    for ( i = 0 ; i < n; i++ )
      for ( j = 0 ; j < n ; j++ )
        if ((ShortestPath[i][k] + ShortestPath[k][j]) < 
             ShortestPath[i][j])
          ShortestPath[i][j] = ShortestPath[i][k] + 
            ShortestPath[k][j];
}

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

Если граф представлен матрицей смежности, то время выполнения этого алгоритма имеет порядок O(n3), поскольку в нем присутствуют вложенные друг в друга три цикла.

Переборные алгоритмы

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

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

Постановка задачи.

Лабиринт, состоящий из проходимых и непроходимых клеток, задан матрицей A размером mxn. Элемент матрицы A[i,j]=0, если клетка (i,j) проходима. В противном случае A[i,j]=\infty.

Требуется найти длину кратчайшего пути из клетки (1, 1) в клетку (m, n).

Фактически дана матрица смежности (только в ней нули заменены бесконечностями, а единицы – нулями). Лабиринт представляет собой граф.

Вершинами дерева вариантов в данной задаче являются пути, начинающиеся в клетке (1, 1). Ребра – показывают ход конструирования этих путей и соединяют два пути длины k и k+1, где второй путь получается из первого добавлением к пути еще одного хода.

Перебор с возвратом

Данный метод основан на методе поиска в глубину. Перебор с возвратом считают методом проб и ошибок ("попробуем сходить в эту сторону: не получится – вернемся и попробуем в другую"). Так как перебор вариантов осуществляется методом поиска в глубину, то целесообразно во время работы алгоритма хранить текущий путь в дереве. Этот путь представляет собой стек Way.

Также необходим массив Dist, размерность которого соответствует количеству вершин графа, хранящий для каждой вершины расстояние от нее до исходной вершины.

Пусть текущей является некоторая клетка (в начале работы алгоритма – клетка (1, 1) ). Если для текущей клетки есть клетка-сосед Neighbor, отсутствующая в Way, в которую на этом пути еще не ходили, то добавляем Neighbor в Way и текущей клетке присваиваем Neighbor, иначе извлечь из Way.

Приведенное выше описание дает четко понять, почему этот метод называется перебором с возвратом. Возврату здесь соответствует операция "извлечь из Way ", которая уменьшает длину Way на 1.

Перебор заканчивается, когда Way пуст и делается попытка возврата назад. В этой ситуации возвращаться уже некуда ( рис. 45.3).

Way является текущим путем, но в процессе работы необходимо хранить и оптимальный путь OptimalWay.

Усовершенствование алгоритма можно произвести следующим образом: не позволять, чтобы длина Way была больше или равна длине OptimalWay. В этом случае, если и будет найден какой-то вариант, он заведомо не будет оптимальным. Такое усовершенствование в общем случае означает, что как только текущий путь станет заведомо неоптимальным, надо вернуться назад. Данное улучшение алгоритма позволяет во многих случаях сильно сократить перебор.

Демонстрация алгоритма перебора с возвратом

Рис. 45.3. Демонстрация алгоритма перебора с возвратом
/*Описание функции переборного алгоритма методом поиска в глубину */
void Backtracking(int n, int m, int **Maze){
  int Begin, End, Current;
  Begin = (n - 1) * m;
  End = m - 1;
  int *Way, *OptimalWay;
  int LengthWay, LengthOptimalWay;
  Way = new int[n*m];
  OptimalWay = new int[n*m];
  LengthWay = 0;
  LengthOptimalWay = m*n;
  for (int i = 0 ; i < n*m ; i++ )
    Way[i] = OptimalWay[i] = -1;
  int *Dist;
  Dist = new int[n*m];
  for (int i = 0 ; i < n ; i++ )
    for (int j = 0 ; j < m ; j++ )
      Dist[i * m + j] = ( Maze[i][j] == 0 ? 0 : -1 );
  Way[LengthWay++] = Current = Begin;
  while ( LengthWay > 0 ){
    if(Current == End){
      if (LengthWay < LengthOptimalWay){
        for (int i = 0 ; i < LengthWay ; i++ )
          OptimalWay[i] = Way[i];
        LengthOptimalWay = LengthWay;
      }
      if (LengthWay > 0) Way[--LengthWay] = -1;
      Current = Way[LengthWay-1];
    }
    else{
      int Neighbor = -1;
      if ((Current/m - 1) >= 0 && !Insert(Way, Current - m) &&
        (Dist[Current - m] == 0 || Dist[Current - m] > LengthWay)
        && Dist[Current] < LengthOptimalWay)
          Neighbor = Current - m;
      else 
        if ((Current%m - 1) >= 0 && !Insert(Way,Current - 1)&&
          (Dist[Current - 1]== 0 || Dist[Current - 1] > LengthWay)
          && Dist[Current] < LengthOptimalWay )
            Neighbor = Current - 1;
        else 
          if ((Current%m + 1) < m && !Insert(Way,Current + 1) &&
           (Dist[Current + 1]== 0 || Dist[Current + 1] > LengthWay)
          && Dist[Current] < LengthOptimalWay )
            Neighbor = Current + 1;
         else 
           if ((Current/m + 1) < n && !Insert(Way,Current + m) &&
            (Dist[Current + m]== 0 || Dist[Current + m] > LengthWay)
           && Dist[Current] < LengthOptimalWay )
             Neighbor = Current + m;
      if ( Neighbor != -1 ){
        Way[LengthWay++] = Neighbor;
        Dist[Neighbor] = Dist[Current] + 1;
        Current = Neighbor;
      }
      else {
        if (LengthWay > 0) Way[--LengthWay] = -1;
        Current = Way[LengthWay-1];
      }
    }
  }
  if ( LengthOptimalWay < n*m ) 
    cout << endl << "Yes. Length way=" << LengthOptimalWay<< endl;
  else cout << endl << "No" << endl;
}
Листинг .

Волновой алгоритм

Этот переборный алгоритм, который основан на поиске в ширину, состоит из двух этапов:

  1. распространение волны;
  2. обратный ход.

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

Демонстрация волнового алгоритма

Рис. 45.4. Демонстрация волнового алгоритма

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

Ключевые термины

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

Алгоритм Флойда – это алгоритм поиска кратчайшего пути между любыми двумя вершинами графа.

Волновой алгоритм – это переборный алгоритм, который основан на поиске в ширину и состоит из двух этапов: распространение волны и обратный ход.

Кратчайший путь – это путь в графе, то есть последовательность вершин и ребер, инцидентных двум соседним вершинам, и его длина.

Переборный алгоритм – это алгоритм обхода графа, основанный на последовательном переборе возможных путей.

Краткие итоги

  1. Нахождение кратчайшего пути на сегодняшний день является актуальной задачей
  2. К наиболее эффективным алгоритмам нахождения кратчайшего пути в графах относятся алгоритм Дейкстры, алгоритм Флойда и переборные алгоритмы. Эти алгоритмы эффективны при достаточно небольших количествах вершин.
  3. В реализации алгоритма Дейкстры строится множество вершин, для которых кратчайшие пути от начальной вершины уже известны. Следующие шаги основаны на добавлении к имеющемуся множеству по одной вершине с сохранением длин оптимальных путей.
  4. Сложность алгоритма Дейкстры зависит от способа нахождения вершины, а также способа хранения множества непосещенных вершин и способа обновления длин.
  5. Метод Флойда основывается на факте, что в графе с положительными весами ребер всякий неэлементарный кратчайший путь состоит из других кратчайших путей.
  6. Если граф представлен матрицей смежности, то время выполнения алгоритма Флойда имеет порядок O(n3).
  7. Переборные алгоритмы являются алгоритмами поиска оптимального решения.
  8. Волновой алгоритм является переборным алгоритмом, который основан на поиске в ширину и состоит из двух этапов: распространение волны и обратный ход.
  9. Перебор методом поиска в ширину, по сравнению с перебором с возвратом, требует больше вспомогательной памяти для хранения информации, однако, он работает быстрее, так как исключается посещение одной и той же вершины более чем один раз.
< Лекция 45 || Лекция 46: 123 || Лекция 47 >
Денис Курбатов
Денис Курбатов
Владислав Нагорный
Владислав Нагорный

Подскажите, пожалуйста, планируете ли вы возобновление программ высшего образования? Если да, есть ли какие-то примерные сроки?

Спасибо!

Андрей Садовщиков
Андрей Садовщиков
Россия