Вспомним обычный поиск в глубину. Он рекурсивно обходит весь граф, и посещает каждую вершину ровно один раз. Посещенные вершины при этом отмечаются в массиве used.
Давайте его немного модифицируем, а именно, давайте сохраним в какой-нибудь массив, в каком порядке мы посещали вершины. Есть два способа это сделать - посчитать время входа DFS-а в каждую вершину и время выхода.
Давайте назовем массивы
Как их заполнить: давайте заведем таймер, отвечающий за время на текущий момент программы и будем обновлять информацию, когда заходим/выходим из вершины:
int timer = 0;
void dfs (int v) {
tin[v] = timer++;
for (int i = 0; i < g[v].size(); ++i) {
if (!used[g[v][i]]) {
dfs(g[v][i]);
}
}
tout[v] = timer++;
}
Время входа или выхода пригождается во многих алгоритмах, давайте разберем несколько.
Задача о топологической сортировке графа звучит так:
Дан ориентированный граф, нужно расставить его вершины в массиве так, чтобы все ребра шли вправо по массиву.
Эта же задача очень часто встречается в реальной жизни. Например, у вас есть много зависимостей вида “в библиотеке А используется библиотека B”, и вам нужно загрузить все библиотеки в таком порядке, чтобы никогда не загружать A до B.
Или, например, у вас есть список зависимых дел - “чтобы сделать А, надо сначала сделать B”, и вам нужно понять, в каком порядке делать все дела.
Во-первых, сразу заметим, что граф с циклом топологически отсортировать не получится - как ни располагай цикл в массиве, все время идти вправо по ребрам цикла не получится, потому что в итоге должен оказаться в самой левой вершине цикла.
Во-вторых, верно обратное! Если цикла нет, то его обязательно можно топологически отсортировать. Попытаемся придумать как, заодно придумаем алгоритм.
Заметим, что вершину, из которой не ведет ни одно ребро, можно всегда поставить последней. И такая вершина всегда есть в графе без циклов (если всегда есть внешнее ребро, то есть цикл). Из этого сразу следует доказательство: просто будем класть в массив вершину, из которой ничего не ведет и убирать ее из графа. Массив потом надо будет развернуть.
Алгоритм же сразу получается, если внимательно посмотреть на время выхода вершин. Вершина, из которой мы вышли в DFS-е первой - как раз та, из которой ничего не выходит. Ведь если из нее есть ребро, то только в уже посещенную, но мы не могли выйти из нее, ведь мы берем самую первую из которой мы вышли. Значит, это одна из серых вершин, в котороые мы вошли, и не вышли, и это цикл.
Из этого следует, что достаточно просто брать вершины в порядке выхода из DFS, то есть в конце DFS, например, просто класть эту вершину в конец массива с ответом. Ну и этот массив надо удет перевернуть, чтобы все ребра шли вправо, а не влево.
Мы только что научились топологически сортировать ациклические графы. А что же делать с циклическими графами? Ведь в них тоже хочется найти какую-то структуру.
Для этого обычно вводят такое понятие как “сильная связность”. В ориентированных графах две вершины связаны сильно, если существует путь из одной в другую и наоборот. Проще говоря, они обе лежат на каком-то цикле.
Понятно, что такое отношение транзитивно: если А и B сильно связны, и B и C сильно связны, то A и C тоже сильно связны. Поэтому все вершины распадаются на такие компоненты сильной связности - такое разбиение вершин, что внутри одной компоненты все вершины сильно связаны, а между вершинами разных компонент сильно связности нет.
Самый простой пример сильно-связной компоненты - это цикл. Но это может быть и полный граф, и любое сложное пересечение нескольких циклов.
Часто рассматривают граф самих компонент сильной связности. На катинке выше их шесть, и между ними остаются ребра из изначально графа. Очевидно, что такой граф уже удет ациклических: иначе компоненты на цикле нужно было бы объединить в одну.
Задача о конденсации графа звучит так:
Дан ориентированный граф
, множество вершин которого и множество рёбер — . Конденсацией назовем сжатие каждой компоненты сильной связности в одну вершину. Каждой вершине графа конденсации соответствует компонента сильной связности графа , а ориентированное ребро между двумя вершинами и графа конденсации проводится, если найдётся пара вершин , между которыми существовало ребро в исходном графе, т.е. .
Необходимо найти, какие вершины лежат в каждой компоненте сильной связности и построить граф конденсации.
Теорема. Запустим DFS. Пусть
При доказательстве возникают два принципиально различных случая в зависимости от того, в какую из компонент первой зайдёт обход в глубину$:
Первой была достигнута компонента
Обратный случай рассматривается проще, из
Из этого и следует первая часть решения - давайте отсортируем вершины по убыванию времени выхода (будто топсорт, но на циклическом графе). Рассмотрим компоненту сильной связности первой вершины, назовем ее
Так что второй шаг такой - разворачиваем ребра, запускаем DFS в таком порядке, ищем компоненты связности как в обычном графе. Попутно можно строить и граф конденсации.
vector<int> g[N], gr[N];
bool used[N];
vector<int> order, component;
void dfs1(int v) {
used[v] = true;
for (int i = 0; i < g[v].size(); i++) {
if (!used[g[v][i]]) {
dfs1(g[v][i]);
}
}
order.push_back (v);
}
void dfs2(int v) {
used[v] = true;
component.push_back(v);
for (int i = 0; i < gr[v].size(); i++) {
if (!used[gr[v][i]]) {
dfs2(gr[v][i]);
}
}
}
int main() {
for (;;) {
g[a].push_back(b);
gr[b].push_back(a);
}
for (int i = 0; i < n; i++){
if (!used[i]) {
dfs1(i);
}
}
for (int i = 0; i < n; i++) {
used[i] = 0;
}
reverse(order.begin(), order.end());
for (int i = 0; i < n; i++) {
int v = order[i];
if (!used[v]) {
dfs2 (v);
}
}
}
Еще раз вкратце:
Сортируем вершины в порядке убывания времени выхода.
Проходимся по массиву вершин в том порядке на равернутом графе и запускаем на нем DFS, выделяя компоненты сильной связности.
Вспомним, что такое конъюнкция (логическое И, обозначается знаком * или
Давайте теперь возьмем конъюнкцию дизъюнктов, а именно И от ИЛИ от переменных (или НЕ переменных). Например, такое выражение:
(a | b) & (!c | d) & (!a | !b) // (А ИЛИ B) И (НЕ C ИЛИ D) И (НЕ A ИЛИ НЕ B)
Задача SAT заключается в том, чтобы найти такие значения переменных, при которых это выражение становится истинным, ии сказать, что таких нет. Заметьте, что у нас в каждой скобке в этом примере ровно две переменные, в таком случае задача называется 2-SAT, и именно ее мы и хотим решить. Для примера выше решением является
Причем тут графы? А вот часто в разных математических задачах надо внезапно ввести граф и он тут же помогает решить задачу.
Приведем сначала выоажение к другому виду - импликативному. Заметим, что выражение вида a | b эквивалентно
Построим граф импликаций: для каждой переменной в графе будет по две вершины, обозначим их через
Теперь заметим, что если для какой-то переменной
Для того, чтобы данная задача 2-SAT имела решение, необходимо и достаточно, чтобы для любой переменной
Пусть решение существует и нам надо его найти.
Заметим, что, несмотря на то, что решение существует, для некоторых переменных может выполняться, что из
Утверждается следующее. Пусть
Докажем, что при таком выборе значений мы не придём к противоречию. Пусть, для определённости, выбрана вершина
Во-первых, докажем, что из
Во-вторых, докажем, что из любой вершины
Итак, мы построили алгоритм, который находит искомые значения переменных в предположении, что для любой переменной
Корректной раскраской графа в два цвета назывется такая раскраска, что никакое ребро не соединяет две вершины одного цвета. Графы, которые можно так раскрасить, называют еще двудольными.
Пусть первая вершина графа принадлежит первой доли, тогда все вершины, для которых есть ребро в первую, должны принадлежить второй и так далее, но тогда заметим, что если в какой-то момент мы хотем покрасить уже покращенную вершину не в ее цвет, то граф не двудольный, проверить это можно например с помощью дфса : запустим дфс от каждой еще не посешенной вершины.
int n;
vector<int> g[N];
vector<bool> part(n, -1);
void dfs(int u, int color) {
if (part[u] != color && part[u] != -1) {
cout << "NO";
exit(0);
}
if (part[u] == color) {
return;
}
for (int i = 0; i < g.size(); i++) {
dfs(g[u][i], 1 - color);
}
}