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

Видалення зі структури даних за O(T(n)logn)O(T(n)\log n)

Припустимо, у нас є структура даних, яка дозволяє додавати елементи за справжній O(T(n))O(T(n)). У цій статті ми опишемо прийом, який дозволяє виконувати видалення за O(T(n)logn)O(T(n)\log n) в офлайн-режимі.

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

Алгоритм

Кожен елемент існує у структурі даних протягом певних відрізків часу між додаваннями та видаленнями. Побудуймо дерево відрізків над запитами. Кожен відрізок, упродовж якого якийсь елемент «живий», розбивається на O(logn)O(\log n) вузлів дерева. Помістімо кожен запит, у якому ми хочемо щось дізнатися про структуру, у відповідний листок. Тепер, щоб обробити всі запити, ми запустимо DFS по дереву відрізків. Заходячи у вузол, ми додаватимемо всі елементи, що містяться в цьому вузлі. Потім ми йтимемо далі до дітей цього вузла або відповідатимемо на запити (якщо вузол є листком). Виходячи з вузла, ми маємо скасувати додавання. Зауважимо, що якщо ми змінюємо структуру за O(T(n))O(T(n)), то можемо відкотити зміни за O(T(n))O(T(n)), зберігаючи стек змін. Зауважимо, що відкати ламають амортизовану складність.

Зауваження

Ідея побудови дерева відрізків над відрізками, упродовж яких щось є «живим», може використовуватися не лише в задачах на структури даних. Див. деякі задачі нижче.

Реалізація

Ця реалізація стосується задачі про динамічну зв'язність. Вона вміє додавати ребра, видаляти ребра та підраховувати кількість компонент зв'язності.

struct dsu_save {
int v, rnkv, u, rnku;

dsu_save() {}

dsu_save(int _v, int _rnkv, int _u, int _rnku)
: v(_v), rnkv(_rnkv), u(_u), rnku(_rnku) {}
};

struct dsu_with_rollbacks {
vector<int> p, rnk;
int comps;
stack<dsu_save> op;

dsu_with_rollbacks() {}

dsu_with_rollbacks(int n) {
p.resize(n);
rnk.resize(n);
for (int i = 0; i < n; i++) {
p[i] = i;
rnk[i] = 0;
}
comps = n;
}

int find_set(int v) {
return (v == p[v]) ? v : find_set(p[v]);
}

bool unite(int v, int u) {
v = find_set(v);
u = find_set(u);
if (v == u)
return false;
comps--;
if (rnk[v] > rnk[u])
swap(v, u);
op.push(dsu_save(v, rnk[v], u, rnk[u]));
p[v] = u;
if (rnk[u] == rnk[v])
rnk[u]++;
return true;
}

void rollback() {
if (op.empty())
return;
dsu_save x = op.top();
op.pop();
comps++;
p[x.v] = x.v;
rnk[x.v] = x.rnkv;
p[x.u] = x.u;
rnk[x.u] = x.rnku;
}
};

struct query {
int v, u;
bool united;
query(int _v, int _u) : v(_v), u(_u) {
}
};

struct QueryTree {
vector<vector<query>> t;
dsu_with_rollbacks dsu;
int T;

QueryTree() {}

QueryTree(int _T, int n) : T(_T) {
dsu = dsu_with_rollbacks(n);
t.resize(4 * T + 4);
}

void add_to_tree(int v, int l, int r, int ul, int ur, query& q) {
if (ul > ur)
return;
if (l == ul && r == ur) {
t[v].push_back(q);
return;
}
int mid = (l + r) / 2;
add_to_tree(2 * v, l, mid, ul, min(ur, mid), q);
add_to_tree(2 * v + 1, mid + 1, r, max(ul, mid + 1), ur, q);
}

void add_query(query q, int l, int r) {
add_to_tree(1, 0, T - 1, l, r, q);
}

void dfs(int v, int l, int r, vector<int>& ans) {
for (query& q : t[v]) {
q.united = dsu.unite(q.v, q.u);
}
if (l == r)
ans[l] = dsu.comps;
else {
int mid = (l + r) / 2;
dfs(2 * v, l, mid, ans);
dfs(2 * v + 1, mid + 1, r, ans);
}
for (query q : t[v]) {
if (q.united)
dsu.rollback();
}
}

vector<int> solve() {
vector<int> ans(T);
dfs(1, 0, T - 1, ans);
return ans;
}
};

Задачі