Рассмотрим базовую задачу.
Условие:
Дан неориентированный граф G, имеющий N вершин и M ребер. Чтобы все рассмотренные подходы имели практических смысл, ограничим N<=1000.
Необходимо найти количество компонент связности данного графа.
Формат входных данных: В первой строке входного файла находятся N и M, разделенные пробелом. Далее идет M строк, содержащих пару вершин, между которыми находится ребро. Вершины нумеруются с 1.
Формат выходных данный: В единственной строке выдать количество компонент связности графа.
Пример:
input.txt
15 11
1 2
2 3
2 11
10 11
11 12
11 15
4 12
12 13
8 14
7 14
5 6
output.txt
4
Компонента связности неориентированного графа является подграф, в котором для любой пары вершин v и u существует путь. Между двумя различными компонентами связности не существует пути.
Задача стара, как мир. Но тем не менее сегодня покажу несколько подходов по ее решению.
1. Поиск в глубину(DFS)
Самое классическое решение. Даже комментировать особо нечего.
- const int SIZE = 1e3 + 10;
- vector<int> adj[SIZE];
- bool usd[SIZE];
- ...
- void dfs(int cur) {
- usd[cur] = true;
- for (int i=0;i<adj[cur].size();++i) {
- int nxt = adj[cur][i];
- if (!usd[nxt])
- dfs(nxt);
- }
- }
- int connected_components_amount_dfs() {
- int cnt = 0;
- for (int i=1; i<=n; ++i) {
- if (!usd[i]) {
- dfs(i);
- ++cnt;
- }
- }
- return cnt;
- }
* This source code was highlighted with Source Code Highlighter.
Граф храним в виде списка смежности(строка 2). Общая сложность решения $latex O(N + M)$.
Решение
2. DSU подобная структура(ленивый подход)
Будем делать конденсацию компоненты в одну вершину. Идея следующая: будем последовательно обрабатывать ребра. Каждая компонента связности будет представлена одной своей вершиной(титульная). При этом неважно какой. По ходу обработки ребер титульная вершина компонент может меняться.
В итоге для нахождения количества компонент связности нужно найти количество титульных вершин.
- const int SIZE = 1e3 + 10;
- int p[SIZE];
- bool usd[SIZE];
- ...
- int connected_components_amount_dsu() {
-
- scanf("%d %d", &n, &m);
-
- for (int i=1;i<=n;++i) {
- p[i] = i;
- }
-
- for (int i=0;i<m;++i) {
- scanf("%d %d", &f, &s);
- int par = p[f];
- for (int j=1;j<=n;++j) {
- if (p[j] == par)
- p[j] = p[s];
- }
- }
- for (int i=1;i<=n;++i)
- usd[p[i]] = true;
- int cnt = 0;
- for (int i=1;i<=n;++i) {
- if (usd[i]) ++cnt;
- }
- return cnt;
- }
* This source code was highlighted with Source Code Highlighter.
Заметим, что сам граф непосредственно вообще никак не хранится. Общая сложность $latex O(M*N)$
Решение
3. Система непересекающихся множеств (DSU)
Реализацию, представленную выше можно существенно усовершенствовать. При добавлении нового ребра будем “мерджить меньшее подмножество к большему” + сжимать пути. У emaxx’а рассматривается доказательство того, что ассимптотика обработки одного ребра равна $latex O(\alpha (N))$, где $latex \alpha (N)$ – обратная функция Аккермана.
- const int SIZE = 1e3 + 10;
-
- int p[SIZE];
- int size[SIZE];
-
- int par(int x) {
- return p[x] == x ? x : p[x] = par(p[x]);
- }
- int connected_components_amount_dsu() {
-
- scanf("%d %d", &n, &m);
-
- for (int i=1;i<=n;++i) {
- p[i] = i;
- size[i] = 1;
- }
-
- for (int i=0;i<m;++i) {
- scanf("%d %d", &f, &s);
- f = par(f); s = par(s);
- if (f != s) {
- if (size[f] > size[s])
- swap(f,s);
- p[f] = s;
- size[s] += size[f];
- }
- }
- bool usd[SIZE];
- memset(usd, 0, sizeof(usd));
- for (int i=1;i<=n;++i)
- usd[par(i)] = true;
- int cnt = 0;
- for (int i=1;i<=n;++i) {
- if (usd[i]) ++cnt;
- }
-
- return cnt;
- }
* This source code was highlighted with Source Code Highlighter.
Как и прежде сам граф не хранится в виде матрицы смежности. Общая сложность $latex O(M * \alpha (N))$
4. Алгоритм Флойда.
Этот подход для самых ленивых, правда он требует хранить граф явно в виде матрицы смежности.
Запускаем алгоритм Флойда и строим матрицу достижимости для каждой пары вершин. В результате если первоначально adj[u][v] указывало на наличие ребра между u и v, то после работы алгоритма adj[u][v] будет указывать на наличие пути между u и v. После чего можно написать DFS двумя вложенными циклами.
- const int SIZE = 1e3 + 10;
- int adj[SIZE][SIZE];
- bool usd[SIZE];
- ...
- int connected_components_amount_floyd() {
-
- for (int k=1;k<=n;++k){
- for (int i=1;i<=n;++i){
- for (int j=1;j<=n;++j){
- if (adj[i][k] + adj[k][j] == 2)
- adj[i][j] = 1;
- }
- }
- }
-
- int cnt = 0;
- for (int i=1;i<=n;++i) {
- if (!usd[i]) {
- ++cnt;
- usd[i] = true;
- for (int j=1;j<=n;++j) {
- if (adj[i][j] && !usd[j])
- usd[j] = true;
- }
- }
- }
- return cnt;
- }
* This source code was highlighted with Source Code Highlighter.
Алгоритм ужасно долгий, но зато пишется довольно просто. Общая сложность $latex O({ N }^{ 3 }) $ Решение
Собственно пока и все. Мы рассмотрели 3 принципиально разных подхода. На маленьких ограничениях можно выбрать тот из них, что больше по душе.