|
|
Строка 6: |
Строка 6: |
| В графе могут быть кратные рёбра и петли. | | В графе могут быть кратные рёбра и петли. |
| }} | | }} |
− | погодь
| |
| | | |
| == Решение упрощённой задачи == | | == Решение упрощённой задачи == |
Строка 40: |
Строка 39: |
| | | |
| == Время работы == | | == Время работы == |
− | Каждое из <tex>O(m)</tex> рёбер записывается в <tex>O(\log m)</tex> вершин дерева отрезков. Поэтому операций <tex>\mathrm{union}</tex> в СНМ будет <tex>O(m \log m)</tex>. Каждая выполняется за <tex>O(\log n)</tex> (СНМ с ранговой эвристикой). Откаты не влияют на время работы.
| |
− |
| |
− | Можно считать, что <tex>n = O(\log m)</tex>, так как в запросах используется не более <tex>2m</tex> вершин.
| |
− |
| |
− | Время работы: <tex>O(m \log m \log n) = O(m \log^2 m)</tex>.
| |
| | | |
| == Реализация на C++ == | | == Реализация на C++ == |
− | '''#include''' <bits/stdc++.h>
| |
− |
| |
− | '''using''' '''namespace''' std;
| |
− | '''typedef''' pair < '''int''' , '''int''' > ipair;
| |
− | '''const''' '''int''' N = 100321;
| |
− |
| |
− | <font color="green">// СНМ</font>
| |
− | '''int''' dsuP[N], dsuR[N];
| |
− | <font color="green">// В этот массив записываются все изменения СНМ, чтобы их можно откатить</font>
| |
− | <font color="green">// При изменении какого-то значения в СНМ в hist записывается пара < указатель, старое значение ></font>
| |
− | vector < pair < '''int'''*, '''int''' > > hist;
| |
− |
| |
− | <font color="green">// Для элемента из СНМ возвращает корень дерева, в котором он находится</font>
| |
− | '''int''' dsuRoot('''int''' v)
| |
− | {
| |
− | '''while''' (dsuP[v] != -1)
| |
− | v = dsuP[v];
| |
− | '''return''' v;
| |
− | }
| |
− |
| |
− | <font color="green">// Объединяет два множества. Используется ранговая эвристика.</font>
| |
− | <font color="green">// При любом изменении содержимого массивов dsuP и dsuR</font>
| |
− | <font color="green">// в hist записывается адрес и старое значение</font>
| |
− | '''void''' dsuMerge('''int''' a, '''int''' b)
| |
− | {
| |
− | a = dsuRoot(a);
| |
− | b = dsuRoot(b);
| |
− | '''if''' (a == b)
| |
− | '''return''';
| |
− | '''if''' (dsuR[a] > dsuR[b])
| |
− | {
| |
− | hist.emplace_back(&dsuP[b], dsuP[b]);
| |
− | dsuP[b] = a;
| |
− | } '''else''' '''if''' (dsuR[a] < dsuR[b])
| |
− | {
| |
− | hist.emplace_back(&dsuP[a], dsuP[a]);
| |
− | dsuP[a] = b;
| |
− | } '''else'''
| |
− | {
| |
− | hist.emplace_back(&dsuP[a], dsuP[a]);
| |
− | hist.emplace_back(&dsuR[b], dsuR[b]);
| |
− | dsuP[a] = b;
| |
− | ++dsuR[b];
| |
− | }
| |
− | }
| |
− |
| |
− | '''struct''' Query
| |
− | {
| |
− | '''int''' t, u, v;
| |
− | bool answer;
| |
− | };
| |
− | '''int''' n, m;
| |
− | Query q[N];
| |
− |
| |
− | <font color="green">// Дерево отрезков, в каждой вершине которого хранится список рёбер</font>
| |
− | vector < ipair > t[N*4];
| |
− |
| |
− | <font color="green">// Эта функция добавляет ребро на отрезок</font>
| |
− | <font color="green">// [l r] - отрезок, на который добавляется ребро</font>
| |
− | <font color="green">// uv - ребро, c - текущая вершина дерева отрезков,</font>
| |
− | <font color="green">// [cl cr] - отрезок текущей вершины дерева отрезков</font>
| |
− | '''void''' addEdge('''int''' l, '''int''' r, ipair uv, '''int''' c, '''int''' cl, '''int''' cr)
| |
− | {
| |
− | '''if''' (l > cr || r < cl)
| |
− | '''return''';
| |
− | '''if''' (l <= cl && cr <= r)
| |
− | {
| |
− | t[c].push_back(uv);
| |
− | '''return''';
| |
− | }
| |
− | '''int''' mid = (cl + cr) / 2;
| |
− | addEdge(l, r, uv, c*2+1, cl, mid);
| |
− | addEdge(l, r, uv, c*2+2, mid+1, cr);
| |
− | }
| |
− |
| |
− | <font color="green">// Обход дерева отрезков в глубину</font>
| |
− | '''void''' go('''int''' c, '''int''' cl, '''int''' cr)
| |
− | {
| |
− | '''int''' startSize = hist.size();
| |
− | <font color="green">// Добавляем рёбра при входе в вершину</font>
| |
− | '''for''' (ipair uv : t[c])
| |
− | dsuMerge(uv.first, uv.second);
| |
| | | |
− | '''if''' (cl == cr)
| |
− | {
| |
− | <font color="green">// Если эта вершина - лист, то отвечаем на запрос</font>
| |
− | '''if''' (q[cl].t == 3)
| |
− | q[cl].answer = (dsuRoot(q[cl].u) == dsuRoot(q[cl].v));
| |
− | } '''else''' {
| |
− | '''int''' mid = (cl + cr) / 2;
| |
− | go(c*2+1, cl, mid);
| |
− | go(c*2+2, mid+1, cr);
| |
− | }
| |
− |
| |
− | <font color="green">// Откатываем изменения СНМ</font>
| |
− | '''while''' (('''int''')hist.size() > startSize)
| |
− | {
| |
− | *hist.back().first = hist.back().second;
| |
− | hist.pop_back();
| |
− | }
| |
− | }
| |
− |
| |
− | '''int''' main()
| |
− | {
| |
− | ios::sync_with_stdio('''false''');
| |
− | <font color="green">// Формат входных данных:</font>
| |
− | <font color="green">// n и m, затем в m строках запросы: по три числа t, u, v</font>
| |
− | <font color="green">// t - тип (1 - добавить ребро, 2 - удалить, 3 - принадлежат ли одной компоненте)</font>
| |
− | <font color="green">// Нумерация вершин с нуля</font>
| |
− | cin >> n >> m;
| |
− | '''for''' ('''int''' i = 0; i < n; ++i) <font color="green">// Инициализация СНМ</font>
| |
− | dsuP[i] = -1;
| |
− |
| |
− | <font color="green">// В этом массиве для каждого ещё не удалённого ребра хранится</font>
| |
− | <font color="green">// на каком запросе оно было создано</font>
| |
− | set < pair < ipair, '''int''' > > edges;
| |
− | '''for''' ('''int''' i = 0; i < m; ++i)
| |
− | {
| |
− | cin >> q[i].t >> q[i].u >> q[i].v;
| |
− | <font color="green">// Поскольку рёбра неориентированные, u v должно означать то же самое, что и v u</font>
| |
− | '''if''' (q[i].u > q[i].v) swap(q[i].u, q[i].v);
| |
− | <font color="green">// При добавлении ребра кладём его в set</font>
| |
− | '''if''' (q[i].t == 1)
| |
− | edges.emplace(ipair(q[i].u, q[i].v), i);
| |
− | <font color="green">// При удалении ребра берём из set время его добавления - так мы узнаём отрезок заросов,</font>
| |
− | <font color="green">// на котором оно существует. Если есть несколько одинаковых рёбер, можно брать любое.</font>
| |
− | '''else''' '''if''' (q[i].t == 2)
| |
− | {
| |
− | '''auto''' iter = edges.lower_bound(make_pair(ipair(q[i].u, q[i].v), 0));
| |
− | addEdge(iter->second, i, iter->first, 0, 0, m - 1);
| |
− | edges.erase(iter);
| |
− | }
| |
− | }
| |
− | <font color="green">// Обрабатываем рёбра, которые не были удалены</font>
| |
− | '''for''' ('''auto''' e : edges)
| |
− | addEdge(e.second, m - 1, e.first, 0, 0, m - 1);
| |
− |
| |
− | <font color="green">// Запускаем dfs по дереву отрезков</font>
| |
− | go(0, 0, m - 1);
| |
− | <font color="green">// Выводим ответ.</font>
| |
− | <font color="green">// При обходе дерева отрезков запросы обрабатываются в том же порядке, в котором они даны,</font>
| |
− | <font color="green">// поэтому ответ можно выводить прямо в go без заполнения answer</font>
| |
− | '''for''' ('''int''' i = 0; i < m; ++i)
| |
− | '''if''' (q[i].t == 3)
| |
− | {
| |
− | '''if''' (q[i].answer)
| |
− | cout << "YES\n";
| |
− | '''else'''
| |
− | cout << "NO\n";
| |
− | }
| |
− |
| |
− | '''return''' 0;
| |
− | }
| |
− |
| |
| == См. также == | | == См. также == |
| * [[СНМ (реализация с помощью леса корневых деревьев)|Система непересекающихся множеств]] | | * [[СНМ (реализация с помощью леса корневых деревьев)|Система непересекающихся множеств]] |
Задача: |
Имеется неориентированный граф из [math]n[/math] вершин, изначально не содержащий рёбер. Требуется обработать [math]m[/math] запросов трёх типов:
- добавить ребро между вершинами [math]u[/math] и [math]v[/math],
- удалить ребро между вершинами [math]u[/math] и [math]v[/math],
- проверить, лежат ли вершины [math]u[/math] и [math]v[/math] в одной компоненте связности.
В графе могут быть кратные рёбра и петли. |
Решение упрощённой задачи
Если нет удалений рёбер, задачу можно решить при помощи системы непересекающихся множеств. Каждая компонента связности — одно множество в СНМ, и при добавлении рёбер они объединяются.
Время работы такого решения: [math]O(m \cdot \alpha (n))[/math], где [math]\alpha[/math] — обратная функция Аккермана.
Алгоритм
Построение дерева отрезков
Рассмотрим массив запросов. Каждое ребро в графе существует на некотором отрезке запросов: начиная с запроса добавления и заканчивая запросом удаления (либо концом запросов, если ребро не было удалено). Для каждого ребра можно найти этот отрезок, пройдя по массиву запросов и запоминая, когда какое ребро было добавлено.
Пусть есть [math]k[/math] рёбер, [math]i[/math]-е соединяет вершины [math]v_i[/math] и [math]u_i[/math], было добавлено запросом [math]L_i[/math] и удалено запросом [math]R_i[/math].
Построим на массиве запросов дерево отрезков, в каждой его вершине будем хранить список пар. [math]i[/math]-е рёбро графа нужно добавить на отрезок [math][L_i,R_i][/math]. Это делается аналогично тому, как в дереве отрезков происходит добавление на отрезке (процесс описан в статье "Несогласованные поддеревья. Реализация массового обновления"), но без [math]push[/math]: нужно спуститься по дереву от корня и записать пару [math]u_i,v_i[/math] в вершины дерева отрезков.
Теперь чтобы узнать, какие рёбра существуют во время выполнения [math]i[/math]-го запроса, достаточно посмотреть на путь от корня дерева отрезков до листа, который соответствует этому запросу — рёбра, записанные в вершинах этого пути, существуют во время выполнения запроса.
Ответы на запросы
Обойдём дерево отрезков в глубину, начиная с корня. Будем поддерживать граф, состоящий из рёбер, которые содержатся на пути от текущей вершины дерева отрезков до корня. При входе в вершину добавим в граф рёбра, записанные в этой вершине. При выходе из вершины нужно откатить граф к состоянию, которое было при входе. Когда мы добираемся до листа, в граф уже добавлены все рёбра, которые существуют во время выполнения соответствующего запроса, и только они. Поэтому если этот лист соответствует запросу третьего типа, его следует выполнить и сохранить ответ.
Для поддержания такого графа и ответа на запросы будем использовать систему непересекающихся множеств. При добавлении рёбер в граф объединим соответствующие множества в СНМ. Откатывание состояния СНМ описано ниже.
СНМ с откатами
Для того, чтобы иметь возможность откатывать состояние СНМ, нужно при каждом изменении любого значения в СНМ записывать в специальный массив, что именно изменилось и какое было предыдущее значение. Это можно реализовать как массив пар (указатель, значение).
Чтобы откатить состояние СНМ, пройдём по этому массиву в обратном порядке и присвоим старые значения обратно. Для лучшего понимания ознакомьтесь с приведённой ниже реализацией.
Нужно заметить, что эвристику сжатия путей в этом случае применять не следует. Эта эвристика улучшает асимптотическое время работы, но это время работы не истинное, а амортизированное. Из-за наличия откатов к предыдущим состояниям эта эвристика не даст выигрыша. СНМ с ранговой эвристикой же работает за [math]O(\log n)[/math] на запрос истинно.
Запоминание изменений и откаты не влияют на время работы, если оно истинное, а не амортизированное. Действительно: пусть в СНМ произошло [math]r[/math] изменений. Каждое из них будет один раз занесено в массив и один раз отменено. Значит, запись в массив и откаты работают за [math]\Theta(r)[/math]. Но и сами изменения заняли [math]\Theta(r)[/math] времени, значит, откаты не увеличили асимптотическое время работы.
Вместо описанного способа откатывания состояния СНМ можно использовать персистентный СНМ, но этот вариант сложнее и имеет меньшую эффективность.
Время работы
Реализация на C++
См. также