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

Друге за вагою мінімальне кістякове дерево

Мінімальне кістякове дерево TT — це дерево заданого графа GG, яке з'єднує всі вершини графа й має найменшу суму ваг усіх ребер серед усіх можливих кістякових дерев. Друге за вагою мінімальне кістякове дерево TT' — це кістякове дерево, яке має другу за величиною найменшу суму ваг усіх ребер серед усіх можливих кістякових дерев графа GG.

Коли підходить цей алгоритм?
  • Потрібне саме друге за вагою кістякове дерево (а не мінімальне)? (якщо потрібне просто мінімальне → Крускал)
  • Уже вмієш будувати MST і готовий зробити заміну одного ребра?
  • Для швидкого варіанта через цикли доступний LCA з двійковим підйомом для пошуку найважчого ребра на шляху?

Спостереження

Нехай TT — мінімальне кістякове дерево графа GG. Можна помітити, що друге за вагою мінімальне кістякове дерево відрізняється від TT заміною лише одного ребра. (Доведення цього твердження дивіться в задачі 23-1 тут).

Отже, нам потрібно знайти ребро enewe_{new}, якого немає в TT, і замінити ним ребро з TT (нехай це буде eolde_{old}) так, щоб новий граф T=(T{enew}){eold}T' = (T \cup \{e_{new}\}) \setminus \{e_{old}\} був кістяковим деревом, а різниця ваг (eneweolde_{new} - e_{old}) була мінімальною.

Використання алгоритму Краскала

Ми можемо спочатку знайти MST за допомогою алгоритму Краскала, а потім просто спробувати вилучити з нього одне ребро й замінити його іншим.

  1. Сортуємо ребра за O(ElogE)O(E \log E), потім знаходимо MST за допомогою алгоритму Краскала за O(E)O(E).
  2. Для кожного ребра в MST (у ньому буде V1V-1 ребро) тимчасово виключаємо його зі списку ребер, щоб його не можна було обрати.
  3. Потім знову намагаємося знайти MST за O(E)O(E), використовуючи решту ребер.
  4. Робимо це для всіх ребер MST і беремо найкраще з усіх.

Зауваження: на кроці 3 нам не потрібно сортувати ребра знову.

Отже, загальна часова складність становитиме O(ElogV+E+VE)O(E \log V + E + V E) = O(VE)O(V E).

Зведення до задачі про найнижчого спільного предка (LCA)

У попередньому підході ми перебирали всі можливості вилучення одного ребра MST. Тут ми зробимо все навпаки. Ми спробуємо додати кожне ребро, якого ще немає в MST.

  1. Сортуємо ребра за O(ElogE)O(E \log E), потім знаходимо MST за допомогою алгоритму Краскала за O(E)O(E).
  2. Для кожного ребра ee, якого ще немає в MST, тимчасово додаємо його до MST, утворюючи цикл. Цикл проходитиме через LCA.
  3. Знаходимо ребро kk з максимальною вагою в циклі, яке не дорівнює ee, рухаючись по батьках вершин ребра ee вгору до LCA.
  4. Тимчасово вилучаємо kk, утворюючи нове кістякове дерево.
  5. Обчислюємо різницю ваг δ=weight(e)weight(k)\delta = weight(e) - weight(k) і запам'ятовуємо її разом зі зміненим ребром.
  6. Повторюємо крок 2 для всіх інших ребер і повертаємо кістякове дерево з найменшою різницею ваг порівняно з MST.

Часова складність алгоритму залежить від того, як ми обчислюємо значення kk — ребра з максимальною вагою на кроці 2 цього алгоритму. Один зі способів ефективно обчислити їх за O(ElogV)O(E \log V) — звести задачу до задачі про найнижчого спільного предка (LCA).

Ми виконаємо попередню обробку для LCA, підвісивши MST за корінь, а також обчислимо максимальні ваги ребер для кожної вершини на шляхах до її предків. Це можна зробити за допомогою двійкового підйому для LCA.

Підсумкова часова складність цього підходу — O(ElogV)O(E \log V).

Наприклад:

MST Second best MST
На зображенні зліва — MST, а справа — друге за вагою MST.

У заданому графі припустимо, що ми підвісили MST за синю вершину вгорі, а потім запускаємо наш алгоритм, починаючи з вибору ребер, яких немає в MST. Нехай першим обраним ребром буде ребро (u,v)(u, v) з вагою 36. Додавання цього ребра до дерева утворює цикл 36 - 7 - 2 - 34.

Тепер ми знайдемо ребро з максимальною вагою в цьому циклі, знайшовши LCA(u,v)=p\text{LCA}(u, v) = p. Ми обчислюємо ребро з максимальною вагою на шляхах від uu до pp та від vv до pp. Зауваження: у деяких випадках LCA(u,v)\text{LCA}(u, v) може також дорівнювати uu або vv. У цьому прикладі ми отримаємо ребро з вагою 34 як ребро з максимальною вагою в циклі. Вилучивши це ребро, ми отримуємо нове кістякове дерево, що має різницю ваг лише 2.

Зробивши це також з усіма іншими ребрами, які не є частиною початкового MST, ми бачимо, що це кістякове дерево було також другим за вагою кістяковим деревом загалом. Вибір ребра з вагою 14 збільшить вагу дерева на 7, вибір ребра з вагою 27 збільшує її на 14, вибір ребра з вагою 28 збільшує її на 21, а вибір ребра з вагою 39 збільшить вагу дерева на 5.

Реалізація

struct edge {
int s, e, w, id;
bool operator<(const struct edge& other) { return w < other.w; }
};
typedef struct edge Edge;

const int N = 2e5 + 5;
long long res = 0, ans = 1e18;
int n, m, a, b, w, id, l = 21;
vector<Edge> edges;
vector<int> h(N, 0), parent(N, -1), size(N, 0), present(N, 0);
vector<vector<pair<int, int>>> adj(N), dp(N, vector<pair<int, int>>(l));
vector<vector<int>> up(N, vector<int>(l, -1));

pair<int, int> combine(pair<int, int> a, pair<int, int> b) {
vector<int> v = {a.first, a.second, b.first, b.second};
int topTwo = -3, topOne = -2;
for (int c : v) {
if (c > topOne) {
topTwo = topOne;
topOne = c;
} else if (c > topTwo && c < topOne) {
topTwo = c;
}
}
return {topOne, topTwo};
}

void dfs(int u, int par, int d) {
h[u] = 1 + h[par];
up[u][0] = par;
dp[u][0] = {d, -1};
for (auto v : adj[u]) {
if (v.first != par) {
dfs(v.first, u, v.second);
}
}
}

pair<int, int> lca(int u, int v) {
pair<int, int> ans = {-2, -3};
if (h[u] < h[v]) {
swap(u, v);
}
for (int i = l - 1; i >= 0; i--) {
if (h[u] - h[v] >= (1 << i)) {
ans = combine(ans, dp[u][i]);
u = up[u][i];
}
}
if (u == v) {
return ans;
}
for (int i = l - 1; i >= 0; i--) {
if (up[u][i] != -1 && up[v][i] != -1 && up[u][i] != up[v][i]) {
ans = combine(ans, combine(dp[u][i], dp[v][i]));
u = up[u][i];
v = up[v][i];
}
}
ans = combine(ans, combine(dp[u][0], dp[v][0]));
return ans;
}

int main(void) {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
parent[i] = i;
size[i] = 1;
}
for (int i = 1; i <= m; i++) {
cin >> a >> b >> w; // нумерація з 1
edges.push_back({a, b, w, i - 1});
}
sort(edges.begin(), edges.end());
for (int i = 0; i <= m - 1; i++) {
a = edges[i].s;
b = edges[i].e;
w = edges[i].w;
id = edges[i].id;
if (unite_set(a, b)) {
adj[a].emplace_back(b, w);
adj[b].emplace_back(a, w);
present[id] = 1;
res += w;
}
}
dfs(1, 0, 0);
for (int i = 1; i <= l - 1; i++) {
for (int j = 1; j <= n; ++j) {
if (up[j][i - 1] != -1) {
int v = up[j][i - 1];
up[j][i] = up[v][i - 1];
dp[j][i] = combine(dp[j][i - 1], dp[v][i - 1]);
}
}
}
for (int i = 0; i <= m - 1; i++) {
id = edges[i].id;
w = edges[i].w;
if (!present[id]) {
auto rem = lca(edges[i].s, edges[i].e);
if (rem.first != w) {
if (ans > res + w - rem.first) {
ans = res + w - rem.first;
}
} else if (rem.second != -1) {
if (ans > res + w - rem.second) {
ans = res + w - rem.second;
}
}
}
}
cout << ans << "\n";
return 0;
}

Джерела

  1. Competitive Programming-3, by Steven Halim
  2. web.mit.edu

Задачі