Перейти до основного вмісту

Пошук компонент зв'язності у графі

Дано неорієнтований граф GG з nn вершинами та mm ребрами. Нам потрібно знайти в ньому всі компоненти зв'язності, тобто кілька груп вершин таких, що в межах однієї групи кожну вершину можна досягти з будь-якої іншої, а між різними групами жодного шляху не існує.

Коли підходить цей алгоритм?
  • Граф неорієнтований, і потрібно розбити вершини на групи взаємно досяжних? (якщо граф орієнтований і потрібна взаємна досяжність в обидва боки → компоненти сильної зв'язності)
  • Граф статичний і обхід (DFS/BFS) робиться один раз? (якщо компоненти змінюються при поступовому додаванні ребер → Система неперетинних множин)

Алгоритм розв'язання задачі

  • Щоб розв'язати цю задачу, ми можемо використати пошук у глибину (DFS) або пошук у ширину (BFS).

  • Насправді ми будемо виконувати серію запусків DFS: перший запуск розпочнеться з першої вершини, і всі вершини першої компоненти зв'язності будуть обійдені (знайдені). Потім ми знаходимо першу невідвідану вершину серед решти вершин і запускаємо з неї пошук у глибину, знаходячи таким чином другу компоненту зв'язності. І так далі, доки всі вершини не будуть відвідані.

  • Загальна асимптотична складність цього алгоритму — O(n+m)O(n + m): насправді цей алгоритм не запуститься на одній і тій самій вершині двічі, а отже, кожне ребро буде розглянуто рівно двічі (з одного кінця та з іншого).

Реалізація

int n;
vector<vector<int>> adj;
vector<bool> used;
vector<int> comp;

void dfs(int v) {
used[v] = true;
comp.push_back(v);
for (int u : adj[v]) {
if (!used[u])
dfs(u);
}
}

void find_comps() {
used.assign(n, false);
for (int v = 0; v < n; ++v) {
if (!used[v]) {
comp.clear();
dfs(v);
cout << "Component:" ;
for (int u : comp)
cout << ' ' << u;
cout << endl ;
}
}
}
  • Найважливіша функція, яку ми використовуємо, — це find_comps(), що знаходить і виводить компоненти зв'язності графа.

  • Граф зберігається у вигляді списку суміжності, тобто adj[v] містить список вершин, до яких є ребра з вершини v.

  • Вектор comp містить список вершин поточної компоненти зв'язності.

Ітеративна реалізація коду

Глибоко рекурсивні функції загалом погані. Кожен окремий рекурсивний виклик потребує трохи пам'яті у стеку, а за замовчуванням програми мають лише обмежений обсяг стекового простору. Тож коли ви виконуєте рекурсивний DFS на зв'язному графі з мільйонами вершин, ви можете натрапити на переповнення стека.

Рекурсивну програму завжди можна перетворити на ітеративну, вручну підтримуючи структуру даних «стек». Оскільки ця структура даних розміщується в купі, переповнення стека не станеться.

int n;
vector<vector<int>> adj;
vector<bool> used;
vector<int> comp;

void dfs(int v) {
stack<int> st;
st.push(v);

while (!st.empty()) {
int curr = st.top();
st.pop();
if (!used[curr]) {
used[curr] = true;
comp.push_back(curr);
for (int i = adj[curr].size() - 1; i >= 0; i--) {
st.push(adj[curr][i]);
}
}
}
}

void find_comps() {
used.assign(n, false);
for (int v = 0; v < n ; ++v) {
if (!used[v]) {
comp.clear();
dfs(v);
cout << "Component:" ;
for (int u : comp)
cout << ' ' << u;
cout << endl ;
}
}
}

Задачі для практики

Відеоматеріали