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

Фарбування ребер дерева

Це доволі поширена задача. Дано дерево GG з NN вершинами. Є два типи запитів: перший — пофарбувати ребро, другий — запитати кількість пофарбованих ребер між двома вершинами.

Тут ми опишемо доволі простий розв'язок (з використанням дерева відрізків), який відповідатиме на кожен запит за час O(logN)O(\log N). Крок попередньої обробки займе час O(N)O(N).

Коли підходить цей алгоритм?
  • Запити стосуються ребер на шляху між двома вершинами дерева з оновленнями (пофарбувати ребро / порахувати пофарбовані на шляху)?
  • Дерево статичне за структурою (змінюються лише кольори ребер), тож вистачає ейлерового обходу + дерева відрізків?
  • Запит зведений до пари «предок — нащадок» через LCA? (для загальніших запитів на шляхах з оновленнями значень → важко-легке розбиття (HLD))

Алгоритм

Спершу нам потрібно знайти LCA, щоб звести кожен запит другого типу (i,j)(i,j) до двох запитів (l,i)(l,i) та (l,j)(l,j), де ll — це LCA вершин ii та jj. Відповіддю на запит (i,j)(i,j) буде сума обох підзапитів. Обидва ці запити мають особливу структуру: перша вершина є предком другої. До кінця статті ми говоритимемо лише про такий особливий вид запитів.

Почнімо з опису кроку попередньої обробки. Запустимо пошук у глибину з кореня дерева й запишемо ейлерів обхід цього пошуку в глибину (кожну вершину додаємо до списку, коли обхід відвідує її вперше, а також щоразу, коли ми повертаємося з одного з її нащадків). Той самий прийом можна використати під час попередньої обробки для LCA.

Цей список міститиме кожне ребро (у тому сенсі, що якщо ii та jj — кінці ребра, то у списку знайдеться місце, де ii та jj є сусідами), причому воно з'явиться рівно двічі: у прямому напрямку (від ii до jj, де вершина ii ближча до кореня, ніж вершина jj) та у зворотному напрямку (від jj до ii).

Для цих ребер ми побудуємо два списки. Перший зберігатиме кольори всіх ребер у прямому напрямку, а другий — кольори всіх ребер у зворотному напрямку. Ми використовуватимемо 11, якщо ребро пофарбоване, і 00 — інакше. Над цими двома списками ми побудуємо по дереву відрізків (на суму з одиничною модифікацією); назвемо їх T1T1 та T2T2.

Відповімо на запит виду (i,j)(i,j), де ii є предком jj. Нам потрібно визначити, скільки ребер пофарбовано на шляху між ii та jj. Знайдімо ii та jj в ейлеровому обході вперше; нехай це будуть позиції pp та qq (це можна зробити за O(1)O(1), якщо обчислити ці позиції заздалегідь під час попередньої обробки). Тоді відповіддю на запит буде сума T1[p..q1]T1[p..q-1] мінус сума T2[p..q1]T2[p..q-1].

Чому? Розгляньмо відрізок [p;q][p;q] в ейлеровому обході. Він містить усі ребра потрібного нам шляху від ii до jj, але також містить набір ребер, що лежать на інших шляхах із ii. Однак між потрібними нам ребрами та рештою ребер є одна велика відмінність: потрібні нам ребра будуть перелічені лише раз — у прямому напрямку, а всі інші ребра з'являються двічі: один раз у прямому й один раз у зворотному напрямку. Отже, різниця T1[p..q1]T2[p..q1]T1[p..q-1] - T2[p..q-1] дасть нам правильну відповідь (мінус одиниця потрібна, бо інакше ми захопимо зайве ребро, що виходить із вершини jj). Запит суми в дереві відрізків виконується за O(logN)O(\log N).

Відповідь на запит першого типу (фарбування ребра) ще простіша — нам потрібно лише оновити T1T1 та T2T2, а саме виконати одну модифікацію елемента, що відповідає нашому ребру (знайти ребро у списку, знову ж таки, можна за O(1)O(1), якщо виконати цей пошук під час попередньої обробки). Одна модифікація в дереві відрізків виконується за O(logN)O(\log N).

Реалізація

Ось повна реалізація розв'язку, включно з обчисленням LCA:

const int INF = 1000 * 1000 * 1000;

typedef vector<vector<int>> graph;

vector<int> dfs_list;
vector<int> edges_list;
vector<int> h;

void dfs(int v, const graph& g, const graph& edge_ids, int cur_h = 1) {
h[v] = cur_h;
dfs_list.push_back(v);
for (size_t i = 0; i < g[v].size(); ++i) {
if (h[g[v][i]] == -1) {
edges_list.push_back(edge_ids[v][i]);
dfs(g[v][i], g, edge_ids, cur_h + 1);
edges_list.push_back(edge_ids[v][i]);
dfs_list.push_back(v);
}
}
}

vector<int> lca_tree;
vector<int> first;

void lca_tree_build(int i, int l, int r) {
if (l == r) {
lca_tree[i] = dfs_list[l];
} else {
int m = (l + r) >> 1;
lca_tree_build(i + i, l, m);
lca_tree_build(i + i + 1, m + 1, r);
int lt = lca_tree[i + i], rt = lca_tree[i + i + 1];
lca_tree[i] = h[lt] < h[rt] ? lt : rt;
}
}

void lca_prepare(int n) {
lca_tree.assign(dfs_list.size() * 8, -1);
lca_tree_build(1, 0, (int)dfs_list.size() - 1);

first.assign(n, -1);
for (int i = 0; i < (int)dfs_list.size(); ++i) {
int v = dfs_list[i];
if (first[v] == -1)
first[v] = i;
}
}

int lca_tree_query(int i, int tl, int tr, int l, int r) {
if (tl == l && tr == r)
return lca_tree[i];
int m = (tl + tr) >> 1;
if (r <= m)
return lca_tree_query(i + i, tl, m, l, r);
if (l > m)
return lca_tree_query(i + i + 1, m + 1, tr, l, r);
int lt = lca_tree_query(i + i, tl, m, l, m);
int rt = lca_tree_query(i + i + 1, m + 1, tr, m + 1, r);
return h[lt] < h[rt] ? lt : rt;
}

int lca(int a, int b) {
if (first[a] > first[b])
swap(a, b);
return lca_tree_query(1, 0, (int)dfs_list.size() - 1, first[a], first[b]);
}

vector<int> first1, first2;
vector<char> edge_used;
vector<int> tree1, tree2;

void query_prepare(int n) {
first1.resize(n - 1, -1);
first2.resize(n - 1, -1);
for (int i = 0; i < (int)edges_list.size(); ++i) {
int j = edges_list[i];
if (first1[j] == -1)
first1[j] = i;
else
first2[j] = i;
}

edge_used.resize(n - 1);
tree1.resize(edges_list.size() * 8);
tree2.resize(edges_list.size() * 8);
}

void sum_tree_update(vector<int>& tree, int i, int l, int r, int j, int delta) {
tree[i] += delta;
if (l < r) {
int m = (l + r) >> 1;
if (j <= m)
sum_tree_update(tree, i + i, l, m, j, delta);
else
sum_tree_update(tree, i + i + 1, m + 1, r, j, delta);
}
}

int sum_tree_query(const vector<int>& tree, int i, int tl, int tr, int l, int r) {
if (l > r || tl > tr)
return 0;
if (tl == l && tr == r)
return tree[i];
int m = (tl + tr) >> 1;
if (r <= m)
return sum_tree_query(tree, i + i, tl, m, l, r);
if (l > m)
return sum_tree_query(tree, i + i + 1, m + 1, tr, l, r);
return sum_tree_query(tree, i + i, tl, m, l, m) +
sum_tree_query(tree, i + i + 1, m + 1, tr, m + 1, r);
}

int query(int v1, int v2) {
return sum_tree_query(tree1, 1, 0, (int)edges_list.size() - 1, first[v1], first[v2] - 1) -
sum_tree_query(tree2, 1, 0, (int)edges_list.size() - 1, first[v1], first[v2] - 1);
}

int main() {
// зчитування графа
int n;
scanf("%d", &n);
graph g(n), edge_ids(n);
for (int i = 0; i < n - 1; ++i) {
int v1, v2;
scanf("%d%d", &v1, &v2);
--v1, --v2;
g[v1].push_back(v2);
g[v2].push_back(v1);
edge_ids[v1].push_back(i);
edge_ids[v2].push_back(i);
}

h.assign(n, -1);
dfs(0, g, edge_ids);
lca_prepare(n);
query_prepare(n);

for (;;) {
if () {
// запит на фарбування ребра x;
// якщо start = true, то ребро фарбується, інакше фарбування
// знімається
edge_used[x] = start;
sum_tree_update(tree1, 1, 0, (int)edges_list.size() - 1, first1[x],
start ? 1 : -1);
sum_tree_update(tree2, 1, 0, (int)edges_list.size() - 1, first2[x],
start ? 1 : -1);
} else {
// запит кількості пофарбованих ребер на шляху між v1 та v2
int l = lca(v1, v2);
int result = query(l, v1) + query(l, v2);
// result - відповідь на запит
}
}
}