Опубликован: 05.01.2015 | Доступ: свободный | Студентов: 2177 / 0 | Длительность: 63:16:00
Лекция 19:

Орграфы и DAG-графы

Программа 19.3. Алгоритм Уоршелла

Конструктор класса TC вычисляет транзитивное замыкание графа G в приватном члене данных T, чтобы клиентские программы могли использовать объекты TC для проверки, достижима ли заданная вершина орграфа из любой другой вершины. Конструктор инициализирует T копией графа G, добавляет петли, и затем использует алгоритм Уоршелла. Класс tcGraph должен содержать реализацию проверки существования ребра edge.

  template <class tcGraph, class Graph>
    class TC
      { tcGraph T;
      public:
        TC(const Graph &G) : T(G)
        { for (int s = 0; s < T.V(); s++)
          T.insert(Edge(s, s));
          for (int i = 0; i < T.V(); i++)
            for (int s = 0; s < T.V(); s++)
              if (T.edge(s, i))
                for (int t = 0; t < T.V(); t++)
                  if (T.edge(i, t))
                    T.insert(Edge(s, t));
        }
      bool reachable(int s, int t) const
        { return T.edge(s, t); }
      } ;
      

Было бы неплохо иметь более эффективные решения, особенно для разреженных графов — к примеру, сократить как время, так и объем памяти, необходимые для предварительной обработки, поскольку оба эти фактора делают алгоритм Уоршелла неподъемным при обработке крупных разреженных орграфов.

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

Мы будем использовать термин абстрактное транзитивное замыкание (abstract transitive closure) для обозначения АТД, который позволяет клиентам выполнять проверки после предварительной обработки графа, как в программе 19.3. В этом контексте необходимо оценивать алгоритмы не только по затратам на вычисление транзитивного замыкания (стоимость предварительной обработки), но и по объему памяти и времени ответа на запросы. То есть мы предлагаем следующую формулировку леммы 19.7:

Лемма 19.8. Можно обеспечить проверки достижимости в заданном орграфе (абстрактное транзитивное замыкание) за постоянное время, используя на предварительную обработку объем памяти, пропорциональный V2, и время, пропорциональное V3.

Доказательство. Эта лемма непосредственно следует из базовых рабочих характеристик алгоритма Уоршелла. $\blacksquare$

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

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

Сначала мы рассмотрим взаимосвязь между транзитивным замыканием и задачей определения кратчайших путей для всех пар вершин (all-pairs shortest-paths). Для орграфов задача заключается в том, чтобы для каждой пары вершин найти ориентированный путь с минимальным количеством ребер.

Для заданного орграфа мы инициализируем целочисленную матрицу A размером

  V х V: элемент A[s][t] содержит 1, если в орграфе существует ребро из s в t, 
или сигнальное значение V, если такого ребра нет. 
Эту задачу выполняет следующий код:
    for (i = 0; i < V; i++)
      for (s = 0; s < V; s++)
        for (t = 0; t < V; t++)
          if (A[s][i] + A[i][t] < A[s][t])
            A[s][t] = A[s][i] + A[i][t];
      

Данный код отличается от алгоритма Уоршелла, приведенного непосредственно перед леммой 19.7, только оператором if во внутреннем цикле. В самом деле, в соответствующей абстрактной форме эти вычисления эквивалентны (см. упражнения 19.55 и 19.56). Несложно преобразовать доказательство леммы 19.7 в прямое доказательство того, что этот метод делает то, что нужно. Данный метод является частным случаем алгоритма Флойда (Floid) поиска кратчайших путей во взвешенных графах (см. "Кратчайшие пути" ). Решение для ориентированных графов на основе поиска в ширину, рассмотренное в "Поиск на графе" , также может (после соответствующей модификации) отыскивать кратчайшие пути в орграфах. Кратчайшие пути будут рассматриваться в "Кратчайшие пути" , поэтому мы отложим детальное сравнение рабочих характеристик до этой главы.

Далее. Как мы видели, задача транзитивного замыкания также тесно связана с задачей перемножения булевых матриц. Рассмотренные выше базовые алгоритмы решения обеих задач на основе аналогичной вычислительной схемы требуют времени, пропорционального V3. Умножение булевых матриц является сложной вычислительной задачей: известны алгоритмы, асимптотически более быстрые, чем простые методы, однако получаемая выгода обычно не оправдывает усилий на их реализацию. Этот факт имеет большое значение в данном контексте, поскольку мы могли бы воспользоваться быстрым алгоритмом умножения булевых матриц для разработки быстрого алгоритма транзитивного замыкания (медленнее алгоритма умножения лишь в lgV раз), используя метод многократного возведения в квадрат, показанный на рис. 19.15. И наоборот, можно оценить нижнюю границу сложности вычисления транзитивного замыкания.

Лемма 19.9. Алгоритм транзитивного замыкания можно использовать для вычисления произведения двух булевых матриц с разницей времени вычисления не более чем в постоянное количество раз.

Доказательство. Пусть даны булевы матрицы A и B размером V х V. Построим следующую матрицу размером 3Vх 3V:


            $$\left(\begin{array}{ccc}
            I &A &0\\
            0 &I &B\\
            0 &0 &I
            \end{array}\right)$$

Здесь 0 означает нулевую матрицу размером V х V, все элементы которой равны 0, а I — единичную матрицу размером V х V, все элементы которой равны 0, за исключением элементов, стоящих на главной диагонали, которые равны 1. Будем рассматривать эту матрицу как матрицу смежности орграфа, и вычислим его транзитивное замыкание повторными возведениями в квадрат. Для этого потребуется лишь одно действие:


            $$\left(\begin{array}{ccc}
            I &A &0\\
            0 &I &B\\
            0 &0 &I
            \end{array}\right)^{2}=\left(\begin{array}{ccc}
            I &A &A*B\\
            0 &I &B\\
            0 &0 &I
            \end{array}\right)$$

Матрица в правой части этого равенства является транзитивным замыканием, поскольку последующие умножения дают эту же матрицу. Однако в верхнем правом углу этой матрицы содержится произведение A * B. Любой алгоритм вычисления транзитивного замыкания можно применить для перемножения булевых матриц с теми же затратами (в пределах постоянного множителя). $\blacksquare$

Важность этой леммы определяется убежденностью экспертов в сложности задачи умножения булевых матриц: математики десятилетиями пытаются точно оценить ее сложность, но решения пока нет; наилучшие известные результаты говорят, что время выполнения умножения должно быть пропорционально примерно V2,5(см. раздел ссылок). Но если мы сможем найти линейное по времени решение задачи транзитивного замыкания (т.е. пропорциональное V2 ), то мы получим и линейное решение задачи перемножения булевых матриц. Подобная зависимость между задачами называется сведением (reduction): мы говорим, что задача перемножения булевых матриц сводится (reduce) к задаче транзитивного замыкания (см. раздел 21.6 "Кратчайшие пути" и часть 8). На самом деле приведенное выше доказательство показывает, что умножение булевых матриц сводится к нахождению в орграфе путей длиной 2.

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

Лемма 19.10. Алгоритм DFS позволяет отвечать за постоянное время на запросы относительно абстрактного транзитивного замыкания орграфа, используя на предварительную обработку (вычисление транзитивного замыкания) объем памяти, пропорциональный V2, и время, пропорциональное V(E + V).

Доказательство. Как было сказано в предыдущем разделе, поиск в глубину по представлению списками смежности позволяет найти все вершины, достижимые из исходной, за время, пропорциональное E (лемма 19.5 и рис. 19.11). И если выполнить поиск в глубину V раз, считая каждую вершину исходной, то можно вычислить множество вершин, достижимых из каждой вершины, т.е. транзитивное замыкание, за время, пропорциональное V (E + V). Это же рассуждение верно для любого обобщенного поиска, выполняющегося за линейное время (см. "Поиск на графе" и упражнение 19.66). $\blacksquare$

Программа 19.4 содержит реализацию алгоритма транзитивного замыкания на основе алгоритма поиска. Этот класс реализует тот же интерфейс, что и программа 19.3. Результат работы программы на орграфе с рис. 19.1 показан первым деревом каждого леса на рис. 19.11.

В случае разреженных орграфов этот подход, основанный на поиске, удобнее всего. Например, если E пропорционально V, то программа 19.4 вычисляет транзитивное замыкание за время, пропорциональное V2. Но как она это делает, учитывая сведение к умножению булевых матриц? Ответ таков: данный алгоритм транзитивного замыкания действительно является оптимальным способом умножения определенных типов булевых матриц (с количеством ненулевых элементов O(V)). Нижняя граница показывает, что не стоит надеяться найти алгоритм транзитивного замыкания, который выполняется за время, пропорциональное V2, для всех орграфов — однако это не исключает, что можно найти подобные алгоритмы, которые работают быстрее на некоторых классах орграфов.

Программа 19.4. Вычисление транзитивного замыкания на основе поиска в глубину

Данный класс DFS реализует тот же интерфейс, что и программа 19.3. Он вычисляет транзитивное замыкание T, выполняя поиск в глубину для каждой вершины графа G и вычисляя множество узлов, достижимых из этой вершины. Каждый вызов рекурсивной функции добавляет ребро из начальной вершины и выполняет рекурсивные вызовы, чтобы заполнить соответствующую строку в матрице транзитивного замыкания. Эта матрица используется также для пометки посещенных вершин при работе поиска в глубину, поэтому необходимо, чтобы класс Graph поддерживал проверку существования ребер.

  template <class Graph>
  class tc
    { Graph T; const Graph &G;
      void tcR(int v, int w)
        { T.insert(Edge(v, w));
          typename Graph::adjIterator A(G, w);
          for (int t = A.beg(); !A.end(); t = A.nxt())
            if (!T.edge(v, t)) tcR(v, t);
        }
    public:
      tc(const Graph &G) : G(G), T(G.V(), true)
        { for (int v = 0; v < G.V(); v++) tcR(v, v); }
      bool reachable(int v, int w)
        { return T.edge(v, w); }
    };
      

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

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

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

Таблица 19.1. Эмпирическое сравнение алгоритмов транзитивного замыкания
Разреженные (10V ребер) Насыщенные (250 вершин)
V W W* A L E W W* A L
25 0 0 1 0 5000 289 203 177 23
50 3 1 2 1 10000 300 214 184 38
125 35 24 23 4 25000 309 226 200 97
250 275 181 178 13 50000 315 232 218 337
500 2222 1438 1481 54 100000 326 246 235 784
Обозначения:
W Алгоритм Уоршелла (раздел 19.3)
W* Усовершенствованный алгоритм Уоршелла (программа 19.3)
A DFS по матрице смежности (программы 19.4 и 17.7)
L DFS по спискам смежности (программа 17.9)

В данной таблице приведены значения времени выполнения различных алгоритмов вычисления транзитивного замыкания для случайных орграфов, как насыщенных, так и разреженных. Для всех алгоритмов, кроме DFS по спискам смежности, при удвоении V время выполнения возрастает в 8 раз — это подтверждает, что время пропорционально V3. DFS по спискам смежности требует для своего выполнения время, пропорциональное VE. Поэтому время выполнения этого алгоритма возрастает в примерно в 4 раза при удвоении и V, и E (разреженные графы), и примерно в 2 раза при удвоении E (насыщенные графы) — кроме случаев снижения производительности при обходе сильно насыщенных графов.

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

Если матрица смежности симметрична, она эквивалентна неориентированному графу, и тогда поиск транзитивного замыкания эквивалентен поиску связных компонентов: транзитивное замыкание представляет собой объединение полных графов для вершин в связных компонентах (см. упражнение 19.48). Алгоритмы определения связности, представленные в "Поиск на графе" , эквивалентны вычислению абстрактного транзитивного замыкания для симметричных орграфов (неориентированных графов), используют объем памяти, пропорциональный V, и также способны отвечать на запросы о достижимости за постоянное время. Возможно ли такое в случае орграфов общего вида? Для каких видов орграфов можно вычислить транзитивное замыкание за линейное время? Для ответа на эти вопросы нам нужно подробнее ознакомиться со структурой орграфов, и в первую очередь — со структурой DAG-графов.

Упражнения

19.46. Как выглядит транзитивное замыкание орграфа, который состоит лишь из направленного цикла с V вершинами?

19.47. Сколько ребер содержит транзитивное замыкание орграфа, который состоит лишь из простого ориентированного пути с V вершинами?

19.48. Приведите транзитивное замыкание неориентированного графа

3-71-47-80-55-23-82-90-64-92-66-4.

19.49. Покажите, как можно построить орграф с V вершинами и E ребрами, такой, что количество ребер в транзитивном замыкании пропорционально t, для любого t от E до V2. Как обычно, предполагается, что E > V.

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

19.51. Представьте в стиле рис. 19.15 процесс вычисления транзитивного замыкания орграфа

3-71-47-80-55-23-82-90-64-92-66-4

многократным возведением в квадрат.

19.52. Представьте в стиле рис. 19.16 процесс вычисления транзитивного замыкания орграфа

3-71-47-80-55-23-82-90-64-92-66-4 с помощью алгоритма Уоршелла.

19.53. Опишите семейство разреженных орграфов, для которых усовершенствованный вариант алгоритма Уоршелла для вычисления транзитивного замыкания (программа 19.3) выполняется за время, пропорциональное VE.

19.54. Найдите разреженный орграф, для которого усовершенствованный вариант алгоритма Уоршелла для вычисления транзитивного замыкания (программа 19.3) выполняется за время, пропорциональное V3.

19.55. Разработайте базовый класс для порождения производных классов, которые реализуют как алгоритм Уоршелла, так и алгоритм Флойда. (Это упражнение — вариант упражнения 19.56 для тех, кто лучше знаком с абстрактными типами данных, чем с абстрактной алгеброй).

19.56. Воспользуйтесь аппаратом абстрактной алгебры для разработки обобщенного алгоритма, который заключает в себе как алгоритм Уоршелла, так и алгоритм Флойда. (Это упражнение — вариант упражнения 19.55 для тех, кто лучше ознакомлен с абстрактной алгеброй, чем с абстрактными типами данных).

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

19.58. Является ли произведение двух симметричных булевых матриц симметричным? Обоснуйте ваш ответ.

19.59. Добавьте в программы 19.3 и 19.4 общедоступную функцию-член, которая позволит клиентам использовать объекты tc для определения количества ребер в транзитивном замыкании.

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

19.61. Добавьте в программы 19.3 и 19.4 общедоступную функцию-член, которая возвращает вектор, индексированный именами вершин, который позволяет определить вершины, достижимые из данной вершины.

19.62. Эмпирически определите количество ребер в транзитивном замыкании различных видов орграфов (см. упражнения 19.11—19.18).

19.63. Рассмотрите представление графа битовой матрицей, описанное в упражнении 17.23. Какой из методов можно ускорить с его помощью в B раз (где B есть количество битов в слове вашего компьютера) — алгоритм Уоршелла или алгоритм на основе поиска в глубину? Проверьте ответ, разработав соответствующую программную реализацию.

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

19.65. Реализуйте алгоритм абстрактного транзитивного замыкания для разреженных графов, который использует объем памяти, пропорциональный T, и может отвечать на запросы о достижимости за постоянное время после предварительной обработки за время, пропорциональное VE + T, где T — количество ребер в транзитивном замыкании. Указание. Воспользуйтесь динамическим хешированием.

19.66. Напишите версию программы 19.4, которая основана на обобщенном поиске на графе (см. "Поиск на графе" ), и эмпирически определите, влияет ли выбор алгоритма поиска на графе на ее производительность.

Бактыгуль Асаинова
Бактыгуль Асаинова

Здравствуйте прошла курсы на тему Алгоритмы С++. Но не пришел сертификат и не доступен.Где и как можно его скаачат?

Александра Боброва
Александра Боброва

Я прошла все лекции на 100%.

Но в https://www.intuit.ru/intuituser/study/diplomas ничего нет.

Что делать? Как получить сертификат?