Главная страница Случайная страница КАТЕГОРИИ: АвтомобилиАстрономияБиологияГеографияДом и садДругие языкиДругоеИнформатикаИсторияКультураЛитератураЛогикаМатематикаМедицинаМеталлургияМеханикаОбразованиеОхрана трудаПедагогикаПолитикаПравоПсихологияРелигияРиторикаСоциологияСпортСтроительствоТехнологияТуризмФизикаФилософияФинансыХимияЧерчениеЭкологияЭкономикаЭлектроника |
Глава 18. Поиск на графе
public: CC(const Graph & G): G(G), ccnt(O), id(G.V(), -1) { for (int v = 0; v < G.V(); v++) if (id[v] == -1) { ccR(v); ccnt++; } } int count() const { return cent; } bool connect (int s, int t) const { return id[s] == id[t]; } }; Какими преимуществами решение задачи определения связности графа, использующее поиск в глубину и реализованное программой 18.4, обладает перед подходом, использующим алгоритм объединения-поиска, который рассматривался в главе 1 применительно к решению задачи определения связности графа, в случае, когда граф задан списком ребер? Теоретически поиск в глубину обладает большим быстродействием, чем алгоритм объединения-поиска, поскольку он обеспечивает постоянное время выполнения, в то время как алгоритм объединения-поиска не способен на это; на практике, однако, эта разница незначительна. В конечном итоге, алгоритм объединения-поиска выполняется быстрее, поскольку для него не надо строить полное представление графа. Что еще важнее, алгоритм объединения-поиска работает в оперативном режиме (мы можем проверить, имеется ли связь между двумя какими-либо вершины за время, близкое к постоянному), в то время как решение, использующее поиск в глубину, должно выполнить предварительную обработку, чтобы ответить на запрос о связности за постоянное время. В силу этих причин, мы предпочитаем применять алгоритм объединения-поиска, например, когда определение связности графа является нашей единственной целью или когда многочисленные запросы перемешиваются с операциями вставки ребер, в то же время мы можем посчитать решение с использованием поиска в глубину более подходящим применительно к АТД графа, поскольку оно с большей эффективностью использует существующую инфраструктуру. Ни тот, ни другой подход не способен работать эффективно в условиях смеси большого числа вставок ребер, удалений ребер и запросов определения связности; оба подхода требуют отдельных поисков в глубину для вычисления пути. Эти рассуждения показывают, с какими осложнениями приходится сталкиваться при анализе алгоритмов на графах; подробное их исследование проводится в разделе 18.9. Программа 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)); Часть 5. Алгоритмы на графах 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.5 реализует класс, использующий поиск в глубину и осуществляющей поиск пути, который использует все ребра графа в точности два раза - по одному в каждом направлении (см. раздел 17.7). Этот путь соответствует исследованию Тремо, по условиям которого мы разматываем нить, куда бы ни шли. Мы не применяем освещения перекрестках, зато проверяем, есть ли нить в коридоре (следовательно, мы должны проходить по коридорам, ведущим к перекресткам, на которых уже были), и сначала подготавливаем возможность перемещаться взад и вперед по каждой связи назад (первый раз, когда мы сталкиваемся с тем или иным обратным ребром), после чего игнорируем связи вниз (при втором выходе на каждое обратное ребро). Можно также игнорировать связи вниз (при первой встрече) и перемещаться взад и вперед по связям вниз (вторая встреча) (см. упражнение 18.25). РИСУНОК 18.14. ДВУХПРОХОДНЫЙ ЭЙЛЕРОВ ЦИКЛ Поиск в глубину предоставляет возможность исследовать любой лабиринт, проходя коридор в каждом направлении. Мы вносим изменения в исследование Тремо, которое заключается в том, что мы разматываем нить, куда бы ни шли, и проходим взад и вперед по коридорам, в которых нет нити и которые ведут к посещенным перекресткам. На этом рисунке показан порядок обхода, который отличается от изображенного на рисунках 18.2 и 18.3 главным образом тем, что мы можем найти такой путь обхода, который не пересекает сам себя. Такое упорядочение может иметь место, например, когда при построении списков смежных вершин для представления графа ребра подвергались обработке в каком-то другом порядке; либо когда могли быть внесены изменения непосредственно в поиск в глубину с таким расчетом, чтобы было принято во внимание геометрическое расположение узлов (см. упражнение 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 (игнорируя при этом заштрихованные вершины, которые означают, что мы встречаемся с данным ребром второй раз), при этом мы распечатываем имя соответствующей вершины, просматриваем в рекурсивном режиме поддеревья, затем снова печатаем имя этой вершины. Глава 18. Поиск на графе
Остовный лес. На заданном связном графе с V вершинами требуется найти множество из V— 1 ребер, соединяющее эти вершины. Если в графе имеются С связных компонент, необходимо найти остовный лес (с V-C ребрами). Мы уже ознакомились с классом DFS, который решает эту задачу: см. программу 18.3. Поиск вершин. Сколько вершин находится в той же компоненте, что и заданная вершина? Мы легко можем решить эту проблему, начав поиск в глубину с заданной вершины и подсчитывая количество помеченных вершин. В насыщенном графе мы можем существенно ускорить этот процесс, остановив поиск в глубину после пометки V вершин - в этой точке мы уже знаем, что никакое ребро не приведет нас в вершину, которой мы еще не видели, поэтому мы игнорируем остальные ребра. Подобное усовершенствование, по-видимому, позволит нам посетить все вершины за время, пропорциональное V log V, но не E(см. раздел 18.8). Раскраска в два цвета, двухдольные графы, нечетные циклы. Существует ли способ покрасить каждую вершину в два цвета таким образом, что ни одно из ребер не соединяет вершины одного и того же цвета? Принадлежит ли заданный граф к категории двудольных (см. раздел 17.1)? Содержит ли заданный граф цикл нечетной длины? Все эти три задачи эквивалентны: первые две суть различные названия одной и той же задачи; любой граф, содержащий нечетный цикл, не допускает описанную выше раскраску в два цвета, в то же время программа 18.6 показывает, что любой граф, в котором нет нечетных циклов, может быть раскрашен в два цвета. Указанная программа представляет собой реализацию функции АТД, использующую поиск в глубину, которая проверяет, является ли заданный граф двудольным, допускает ли он окраску в два цвета, содержит ли он нечетные циклы. Эту рекурсивную функцию можно рассматривать как схему доказательства методом индукции того факта, что программа может раскрасить в два цвета любой граф, свободный от нечетных циклов (или обнаружить в графе нечетный цикл как доказательство того, что граф, не свободный от нечетный циклов, не может быть раскрашен в два цвета). Чтобы раскрасить граф в два цвета, когда процесс раскраски на- РИСУНОК 18.15. РАСКРАСКА ДЕРЕВА DFS В ДВА ЦВЕТА Чтобы раскрасить граф в два цвета, мы меняем цвет в процессе движения вниз по дереву DFS, затем проводим проверку обратных ребер на несовместимость. В дереве, изображенном в верхней части рисунка, дерево DFS графа, взятого в качестве примера и представленного на рис. 18.9, обратные ребра 5-4 и 7-0 показывают, что рассматриваемый граф не допускает окраски в два цвета ввиду наличия циклов нечетной длины 4-3-5-4 и 0-2-6-4-7-0. В дереве в нижней части рисунка, представляющем собой дерево DFS двудольного графа, изображенного на рис. 17.5, таких несоответствий не существует; в рассматриваемом случае окраска в два цвета изображена с применением штриховки. Часть 5. Алгоритмы на графах чинается с того, что заданную вершину v раскрашивают в заданный цвет, все вершины, смежные с v, закрашиваются в другой цвет, отличный от цвета вершины v. Этот процесс эквивалентен раскраске соседних уровней дерева DFS по мере того, как мы спускаемся вниз, проверяя обратные ребра на соответствие цветов (см. рис. 18.15). Любое обратное ребро, соединяющее вершины одного цвета, являются доказательством наличия в рассматриваемом графе нечетных циклов. Программа 18.6. Раскраска графа в два цвета (двудольные графы)_______________ Конструктор рассматриваемого класса DFS устанавливает переменной ОК значение 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(), -l) { 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]; } }; Эти типовые примеры служат иллюстрацией способов, с помощью которых поиск в глубину может дать представление о структуре графа. Они также показывают, что мы можем изучить важнейшие свойства графа посредством его просмотра за линейное время, в процессе которого мы исследуем каждое ребро дважды, по разу в каждом направлении. Далее мы рассмотрим пример, который показывает, как поиск в глубину используется с целью обнаружения более сложных свойств структуры графа за линейное время.
|