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

Поиск на графе

Алгоритмы DFS

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

Обнаружение циклов. Имеются ли в заданном графе циклы? (Является ли граф лесом?) Эта задача легко решается с помощью поиска в глубину, поскольку любое обратное ребро дерева DFS принадлежит циклу, состоящему из этого ребра и пути в дереве, соединяющего две вершины ребра (см. рис. 18.9). Таким образом, поиск в глубину позволяет непосредственно выявлять циклы: граф является ациклическим тогда и только тогда, когда во время выполнения поиска в глубину не встречаются обратные ссылки (или нисходящие!). Например, для проверки этого условия в программе 18.1 достаточно добавить в оператор if предложение else, в котором проверяется равенство t и v. Если имеет место равенство, это означает, что обнаружена родительская ссылка w-v (второе представление ребра v-w, которое привело нас в w). Если равенства нет, то w-t замыкает цикл в дереве DFS, состоящий из ребер от t до w. Более того, нет необходимости проверять все ребра: мы должны либо найти цикл, либо завершить поиск, не обнаружив его, прежде чем проверим V ребер, ведь любой граф с V или большим числом ребер должен содержать цикл. Следовательно, мы можем проверить, является ли рассматриваемый граф ациклическим, за время, пропорциональное V, в случае представления списками смежности, хотя если граф задан в виде матрицы смежности, может понадобиться время, пропорциональное V2 (чтобы найти ребра).

Простой путь. Существует ли путь в графе, который связывает две заданных вершины? В "Виды графов и их свойства" мы видели, что нетрудно построить класс DFS, который способен решить эту задачу за линейное время.

Простая связность. Как было сказано в разделе 18.3, алгоритм DFS позволяет за линейное время определить, является ли граф связным. Ведь выбранная нами стратегия основана на вызове функции поиска для каждого связного компонента. При проведении поиска в глубину граф является связным тогда и только тогда, когда функция поиска на графе вызывает рекурсивную функцию DFS только один раз (программа 18.2). Количество связных компонентов в графе равно как раз количеству вызовов рекурсивной функции из функции GRAPHsearch — значит, количество связных компонентов графа можно определить простым подсчетом таких вызовов.

Программа 18.4 содержит класс DFS для более общего случая. Он позволяет получать за постоянное время ответы на запросы, касающиеся связности, после этапа препроцес-сорной обработки в конструкторе, которая выполняется за линейное время. Порядок посещения вершин тот же, что и в программе 18.3. Рекурсивная функция в качестве второго аргумента принимает вершину, а не ребро, поскольку ей не нужно знать родительский узел. Каждому дереву леса DFS соответствует связный компонент графа, так что мы быстро можем определить, содержатся ли две вершины в одном и том же компоненте, включив в представление графа вектор, индексированный именами вершин, который заполняется при поиске в глубину и используется для ответов на запросы о связности. В рекурсивной функции DFS текущее значение счетчика компонентов присваивается элементу вектора, соответствующему каждой посещенной вершине. Тогда две вершины принадлежат одному компоненту графа тогда и только тогда, когда равны соответствующие им элементы этого вектора. Здесь данный вектор снова отображает структурные свойства графа, а не особенности представления графа или динамики поиска.

Программа 18.4. Связность графа

Конструктор CC вычисляет за линейное время количество связных компонентов заданного графа и сохраняет индекс компонента, которому принадлежит каждая вершина, в приватном векторе id, индексированном именами вершин. Клиенты могут использовать объект CC для определения за постоянное время количества связных компонентов (count) или для проверки (connect), является ли связанной какая-либо пара вершин.

  template <class Graph>
  class CC
    { const Graph &G;
      int ccnt;
      vector <int> id;
      void ccR(int w)
        { id[w] = ccnt;
          typename Graph::adjIterator A(G, w);
          for (int v = A.beg(); !A.end(); v = A.nxt())
            if (id[v] == -1) ccR(v);
        }
    public:
      CC(const Graph &G) : G(G), ccnt(0), id(G.V(), -1)
        { for (int v = 0; v < G.V(); v++)
          if (id[v] == -1) { ccR(v); ccnt+ + ; }
        }
      int count() const { return ccnt; }
      bool connect(int s, int t) const
        { return id[s] == id[t]; }
    };
      

Программа 18.4 типизирует базовый подход, который мы будем использовать при решении различных задач обработки графов. Мы разрабатываем класс, ориентированный на решение конкретной задачи, чтобы клиенты могли создавать объекты, решающие эту задачу. Как правило, мы затрачиваем некоторое время на предварительную обработку конструктором, который вычисляет приватные данные, описывающие нужные структурные свойства графа. Эти данные помогают обеспечить эффективную реализацию общедоступных функций для обработки запросов. В данном случае конструктор выполняет предварительную обработку с помощью поиска в глубину (за линейное время) и заполняет приватный член данных (вектор id, индексированный именами вершин), который позволяет отвечать на запросы о связности за постоянное время. В случаях других задач обработки графов конструкторы и функции обработки запросов могут затрачивать больше памяти и/или времени на предварительную обработку и на ответы на запросы. Как обычно, основное внимание мы уделяем минимизации этих затрат, хотя зачастую сделать это весьма непросто. Например, большая часть "Орграфы и DAG-графы" посвящена решению задачи связности для орграфов, для которых очень трудно добиться линейного времени на предварительную обработку и постоянного времени на обработку запросов о связности, как в программе 18.4.

Как соотносится определение связности графа на базе DFS, реализованное в программе 18.4, с алгоритмом объединения-поиска, который был рассмотрен в "Введение" , если граф задан списком ребер? Теоретически поиск в глубину работает быстрее, поскольку он, в отличие от объединения-поиска, гарантирует постоянное время выполнения, однако на практике это редко играет роль. В конечном итоге, алгоритм объединения-поиска выполняется быстрее, поскольку в нем не обязательно строится полное представление графа. Что еще важнее, алгоритм объединения-поиска работает в оперативном режиме (в любой момент мы можем проверить, связаны ли какие-либо две вершины, за почти постоянное время), а решение на базе DFS должно выполнить предварительную обработку, чтобы ответить на запрос о связности за постоянное время. Поэтому мы, например, предпочитаем алгоритм объединения-поиска, если требуется определить связность графа лишь один раз или при наличии множества запросов, но вперемешку с операциями вставки ребер. Однако решение на базе DFS будет более подходящим для АТД графа, поскольку оно эффективно использует существующую инфраструктуру. Ни тот, ни другой подход не способен работать эффективно в случае смеси большого количества вставок ребер, удалений ребер и запросов определения связности; оба подхода требуют отдельных поисков в глубину для вычисления пути. Эти рассуждения показывают, с какими трудностями приходится сталкиваться при анализе алгоритмов на графах; подробнее мы рассмотрим их в разделе 18.9.

Двухсторонний эйлеров цикл. Программа 18.5 представляет собой класс для поиска пути с помощью поиска в глубину, который использует все ребра графа в точности два раза — по одному в каждом направлении (см. "Виды графов и их свойства" ). Этот путь соответствует методу Тремо: мы разматываем нить там, куда мы идем, проверяем, есть ли нить в коридоре, а не включаем свет (поэтому приходится проходить по коридорам, ведущим к уже пройденным перекресткам), и вначале проходим туда и сюда по каждой обратной ссылке (при первой встрече с обратным ребром), после чего игнорируем нисходящие ссылки (при второй встрече каждого обратного ребра). Можно также игнорировать обратные ссылки (при первой встрече) и проходить назад и вперед по нисходящим ссылкам (при второй встрече) (см. упражнение 18.25 и рис. 18.14).

Программа 18.5. Двухсторонний эйлеров цикл

Этот класс DFS выводит каждое ребро дважды, по одному в каждом направлении, в порядке обхода двухстороннего эйлерова цикла. Мы проходим назад и вперед по обратным ребрам и игнорируем нисходящие ребра (см. текст). Этот класс порожден от базового класса SEARCH из программы 18.2.

  template <class Graph>
  class EULER : public SEARCH<Graph>
    { void searchC(Edge e)
        { int v = e.v, w = e.w;
          ord[w] = cnt+ + ;
          cout <<    << w;
          typename Graph::adjIterator A(G, w);
          for (int t = A.beg(); !A.end(); t = A.nxt())
            if (ord[t] == -1)
              searchC(Edge(w, t));
            else if (ord[t] < ord[v])
              cout << "-" << t << "-" << w;
          if (v != w) cout << "-" << v; else cout << endl;
        }
    public:
      EULER(const Graph &G) : SEARCH<Graph>(G)
        { search(); }
   };
      
 Двухсторонний эйлеров цикл

Рис. 18.14. Двухсторонний эйлеров цикл

Поиск в глубину позволяет исследовать любой лабиринт, проходя коридоры в обоих направлениях. Мы вносим изменения в метод Тремо: разматываем нить всюду, куда идем, и проходим туда и назад по коридорам, в которых нет нити и которые ведут к посещенным перекресткам. На этом рисунке показан порядок обхода, который отличается от изображенного на рисунках 18.2 и 18.3 — главным образом тем, что путь обхода можно нарисовать без пересечения себя. Такой порядок может быть, например, если при построении представления графа списками смежности ребра обрабатывались в каком-то другом порядке, либо при явном изменении алгоритма DFS, чтобы учесть геометрическое расположение узлов (см. упражнение 18.26). Двигаясь по нижнему пути из 0 через 2, 6, 4 и 7, мы пробуем пройти из 7 в 0 и возвращаемся назад,, поскольку ord[0] меньше ord[7]. Затем мы идем в 1, назад в 7, назад в 4, в 3, в 5, из 5 в 0 и назад, потом из 5 в 4 и назад, далее назад в 3, назад в 4, назад в 6, назад в 2 и назад в 0. Такой путь может быть получен с помощью прямого и обратного рекурсивного обхода дерева DFS (игнорируя заштрихованные вершины, которые означают вторую встречу с ребром), когда выводится имя соответствующей вершины, рекурсивно просматриваются поддеревья, затем снова выводится имя этой вершины.

Остовный лес. В заданном связном графе с V вершинами требуется найти множество из V— 1 ребер, соединяющих эти вершины. Если граф состоит из C связных компонентов, то нужно найти остовный лес (с V— C ребрами). Мы уже знаем класс DFS, который решает эту задачу — это программа 18.3.

Поиск вершин. Сколько вершин находится в том же компоненте, что и заданная вершина? Эту задачу можно легко решить, начав поиск в глубину с указанной вершины и подсчитывая количество помеченных вершин. В насыщенном графе этот процесс можно существенно ускорить, остановив поиск в глубину после пометки V вершин — в этот момент мы уже знаем, что никакое ребро не приведет нас в еще не помеченную вершину, поэтому остальные ребра можно игнорировать. Это усовершенствование позволит посетить все вершины за время, пропорциональное VlogV , а не E (см. раздел 18.8).

Раскраска двумя цветами, двудольные графы, нечетные циклы. Существует ли способ покрасить каждую вершину одним из двух цветов таким образом, чтобы ни одно из ребер не соединяло вершины одинакового цвета? Является ли данный граф двудольным (см. "Виды графов и их свойства" )? Содержит ли он цикл нечетной длины? Все эти три задачи эквивалентны: первые две — просто различные названия одной и той же задачи; любой граф, содержащий нечетный цикл, не допускает раскраску в два цвета, а программа 18.6 показывает, что любой граф, в котором нет нечетных циклов, может быть раскрашен двумя цветами. Эта программа представляет собой реализацию функции АТД на базе DFS, которая проверяет, является ли заданный граф двудольным, раскрашиваемым двумя цветами и не содержащим нечетные циклы. Эту рекурсивную функцию можно рассматривать как схему доказательства по индукции, что программа может раскрасить в два цвета любой граф без нечетных циклов (или обнаружить в графе нечетный цикл как доказательство того, что граф с нечетными циклами невозможно раскрасить двумя цветами). Чтобы раскрасить граф двумя цветами, нужно начать с раскраски вершины v одним цветом, а затем закрасить другим цветом все вершины, смежные с v. Этот процесс эквивалентен раскраске дерева DFS, спускаясь по уровням вниз и проверяя обратные ребра на соответствие цветов (см. рис. 18.15). Любое обратное ребро, соединяющее вершины одного цвета, является свидетельством наличия в графе нечетного цикла.

Программа 18.6. Раскраска графа в два цвета (двудольность)

Конструктор этого DFS-класса заносит в DK значение true тогда и только тогда, когда может заполнить значениями 0 и 1 вектор vc, индексированный именами вершин, так, что для каждого ребра v-w графа значения vc[v] и vc[w] различны.

  template <class Graph>
  class BI
    { const Graph &G;
      bool OK;
      vector <int> vc; 
      bool dfsR(int v, int c)
        { vc[v] = (c+1) % 2;
          typename Graph::adjIterator A(G, v);
          for (int t = A.beg(); !A.end(); t = A.nxt())
            if (vc[t] == -1)
              { if (!dfsR(t, vc[v])) return false; }
            else if (vc[t] != c)
              return false;
          return true;
        }
    public:
      BI(const Graph &G) : G(G), OK(true), vc(G.V(), -1)
        { for (int v = 0; v < G.V(); v++)
            if (vc[v] == -1)
              if (!dfsR(v, 0)) { OK = false; return; }
        }
      bool bipartite() const { return OK; }
      int color(int v) const { return vc[v]; }
    };
      
 Раскраска дерева DFS двумя цветами

Рис. 18.15. Раскраска дерева DFS двумя цветами

Чтобы раскрасить граф двумя цветами, мы меняем цвет при спуске по дереву DFS и проверяем обратные ребра на совместимость. В дереве DFS для графа с рис. 18.9, изображенном в верхней части рисунка, обратные ребра 5-4 и 7-0 показывают, что рассматриваемый граф невозможно закрасить двумя цветами из-за наличия циклов нечетной длины 4-3-5-4 и 0-2-6-4-7-0 . В дереве DFS для двудольного графа с рис. 17.5 (внизу), таких несоответствий нет; возможная раскраска показана штриховкой.

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

Упражнения

18.22. Реализуйте класс проверки наличия циклов на базе DFS, который выполняет в конструкторе предварительную обработку графа за время, пропорциональное V, и обеспечивает работу функций-членов, определяющих наличие в графе каких-либо циклов и выводящих найденные циклы.

18.23. Опишите семейство графов с V вершинами, в котором стандартный DFS по матрице смежности для обнаружения циклов выполняется за время, пропорциональное V2.

18.24. Реализуйте класс определения связности из программы 18.4, производный от класса поиска на графе наподобие программы 18.3.

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

18.26. Измените программу 18.5, чтобы она вычисляла двусторонний эйлеров цикл, который можно нарисовать без пересечений себя ни в одной вершине (как на рис. 18.14). Например, если бы поиск, представленный на рис. 18.14, прошел сначала по ребру 4-3, а уже потом по ребру 4-7, то цикл пересек бы сам себя. Нужно, чтобы алгоритм не допускал таких пересечений.

18.27. Разработайте версию программы 18.5, которая сортирует все ребра в порядке двухстороннего эйлерова цикла. Программа должна возвратить вектор ребер, который соответствует двухстороннему эйлерову циклу.

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

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

18.30. Большую часть графов невозможно раскрасить двумя цветами, и поиск в глубину обычно быстро обнаруживает это. Эмпирически определите количество ребер, просмотренных программой 18.6, для графов различных размеров и построенных по различным моделям (см. упражнения 17.64—17.76).

18.31. Докажите, что в каждом связном графе имеются вершины, удаление которых не нарушает связность графа, и напишите функцию DFS, которая обнаруживает такие вершины. Указание. Рассмотрите листья дерева DFS.

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

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

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

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

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

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

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