APIO2020 交換城市
阿新 • • 發佈:2020-10-14
我是真的不穩定的垃圾選手。
對於一張圖來說,兩個人能滿足題面關係等價於這張圖不是鏈,很好證明,如果有度數 \(> 2\) 的點,讓一個人跑到一個度數 \(= 1\) 的地方就可以了。
如果離線就可以什麼啟發式合併 + 並查集做到一個 \(\log\)。就是合併兩個點 \(x, y\),動態維護每個聯通塊是不是鏈。
- 連線兩個已經在同一個聯通塊的點變成環就不是鏈了
- 若 \(x, y\) 之前所在聯通塊不是鏈總共也不是鏈了
- 若 \(x, y\) 其中有一個是不是端點總共也不是鏈了。
- 如果 \(x\) 所在聯通塊大小 \(> 1\) 那麼 \(x\) 就不是端點了,\(y\) 同理。
然後這是強制線上的。。可持久化並查集,詢問的時候要二分,每次 check 還是兩個 \(\log\)
#include <vector> #include <algorithm> #include <iostream> using namespace std; const int MAXN = 100005, MAXM = 200005; int n, m, arr[MAXM], tot; struct E{ int u, v, w; bool operator < (const E &b) const { return w < b.w; } } e[MAXM]; struct T{ int l, r, v; }; struct Array{ int rt[MAXM], idx; T t[MAXN * 22]; void build(int &p, int l, int r, int o) { p = ++idx; if (l == r) { t[p].v = o == -1 ? r : o; return ; } int mid = (l + r) >> 1; build(t[p].l, l, mid, o); build(t[p].r, mid + 1, r, o); } void update(int &p, int q, int l, int r, int x, int o) { t[p = ++idx] = t[q]; if (l == r) { t[p].v = o; return; } int mid = (l + r) >> 1; if (x <= mid) update(t[p].l, t[q].l, l, mid, x, o); else update(t[p].r, t[q].r, mid + 1, r, x, o); } int query(int p, int l, int r, int x) { if (l == r) return t[p].v; int mid = (l + r) >> 1; if (x <= mid) return query(t[p].l, l, mid, x); else return query(t[p].r, mid + 1, r, x); } void inline set(int x, int k, int i) { update(rt[i], rt[i], 0, n - 1, x, k); } int inline get(int x, int i) { return query(rt[i], 0, n - 1, x); } } p, f, d, sz; int find(int x, int i) { int fa = f.get(x, i); return x == fa ? x : find(fa, i); } void inline merge(int x, int y, int i) { int a = find(x, i), b = find(y, i); if (a == b) { p.set(a, 1, i); } else { int sa = sz.get(a, i), sb = sz.get(b, i); if (sa > sb) swap(sa, sb), swap(a, b), swap(x, y); f.set(a, b, i); int dx = d.get(x, i), dy = d.get(y, i); if (p.get(a, i) || !dx || !dy) p.set(b, 1, i); if (dx && sa > 1) d.set(x, 0, i); if (dy && sb > 1) d.set(y, 0, i); sz.set(b, sa + sb, i); } } void init(int N, int M, std::vector<int> U, std::vector<int> V, std::vector<int> W) { n = N, m = M; for (int i = 0; i < m; i++) { arr[++tot] = W[i]; e[i] = (E) { U[i], V[i], W[i] }; } sort(arr + 1, arr + 1 + tot); tot = unique(arr + 1, arr + 1 + tot) - arr - 1; sort(e, e + m); f.build(f.rt[0], 0, n - 1, -1), sz.build(sz.rt[0], 0, n - 1, 1); p.build(p.rt[0], 0, n - 1, 0), d.build(d.rt[0], 0, n - 1, 1); for (int i = 1, j = 0; i <= tot; i++) { p.rt[i] = p.rt[i - 1], f.rt[i] = f.rt[i - 1]; d.rt[i] = d.rt[i - 1], sz.rt[i] = sz.rt[i - 1]; while (j < m && e[j].w <= arr[i]) { merge(e[j].u, e[j].v, i); ++j; } } } bool inline check(int X, int Y, int i) { int a = find(X, i), b = find(Y, i); return a == b && p.get(a, i); } int getMinimumFuelCapacity(int X, int Y) { if (!check(X, Y, tot)) return -1; int l = 1, r = tot; while (l < r) { int mid = (l + r) >> 1; if (check(X, Y, mid)) r = mid; else l = mid + 1; } return arr[r]; }
然後就去無恥的看題解,然後去學了一下 Kruscal 重構樹。
我們不妨設計一個特殊的 Kruscal 重構樹,使得對於 \(x, y\) 的詢問,答案就是他們的 LCA ,即他們的 LCA 是合併過程中讓他們從鏈變成不是鏈的那條關鍵邊!
那麼,設計一種類似於普通 Kruscal 最小重構樹的方式,只不過改動如下:
- 對於合併還是鏈的聯通塊,先不要在重構樹上連邊。
- 當一條鏈 \(\Rightarrow\) 不是鏈的時候,把這條邊的新點是鏈裡所有點兩兩的關鍵邊,所以把這個新點作為這條鏈上所有點的父親就行了!這樣的意義是這些點兩兩關鍵邊就是這個邊。
發現這麼構造仍然滿足大根堆性質。
用啟發式合併儲存當前聯通塊的所有點,每個原始點只會連一次,即連向他所在聯通塊轉變的關鍵邊新點,所以複雜度是 \(O(n \log n)\)
正確性:在 LCA 這個子樹中,所有邊都是 \(\le w_{LCA}\) 的,LCA 的子樹形成的結構不是鏈,因為只有不是鏈才會連邊,所以必然 LCA 的權值是可以的。然後證明 \(ans \ge w_{LCA}\)。\(ans < w_{LCA}\) 顯然是不行的,因為他們要用這條關鍵邊。所以 \(ans = w_{LCA}\)。
所以每次詢問求個 LCA 就行了。
時間複雜度 \(O((n + q) \log n)\)
我程式碼偷了一下懶,即只在鏈的時候維護各種資訊,已經不是鏈了,除了 \(f\) 陣列剩下就不用維護。
#include "swap.h"
#include <algorithm>
#include <vector>
#include <iostream>
using namespace std;
const int N = 100005, M = 200005, L = 19;
int n, m, f[N << 1], fa[N << 1][L], dep[N << 1], cnt, sz[N], a[N << 1];
bool ch[N << 1], d[N];
vector<int> g[N << 1];
vector<int> c[N << 1];
struct E {
int u, v, w;
bool operator<(const E &b) const { return w < b.w; }
} e[M];
int find(int x) { return x == f[x] ? x : f[x] = find(f[x]); }
void dfs(int u) {
for (int i = 1; i < L && fa[u][i - 1]; i++) fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (int i = 0; i < g[u].size(); i++) {
int v = g[u][i];
fa[v][0] = u, dep[v] = dep[u] + 1;
dfs(v);
}
}
int lca(int x, int y) {
if (dep[x] < dep[y])
swap(x, y);
for (int i = L - 1; ~i; i--)
if (dep[x] - (1 << i) >= dep[y])
x = fa[x][i];
if (x == y)
return x;
for (int i = L - 1; ~i; i--)
if (fa[x][i] != fa[y][i])
x = fa[x][i], y = fa[y][i];
return fa[x][0];
}
void inline merge(int u, int v, int w) {
int x = find(u), y = find(v);
if (x == y) {
if (ch[x]) {
a[++cnt] = w;
for (int i = 0; i < sz[x]; i++) g[cnt].push_back(c[x][i]);
f[x] = cnt, ch[cnt] = false;
}
} else {
if (sz[x] > sz[y])
swap(x, y), swap(u, v);
if (!ch[x] || !ch[y] || !d[u] || !d[v]) {
a[++cnt] = w;
if (ch[y])
for (int i = 0; i < sz[y]; i++) g[cnt].push_back(c[y][i]);
else
g[cnt].push_back(y);
if (ch[x])
for (int i = 0; i < sz[x]; i++) g[cnt].push_back(c[x][i]);
else
g[cnt].push_back(x);
f[x] = f[y] = cnt;
} else {
if (sz[x] > 1)
d[u] = false;
if (sz[y] > 1)
d[v] = false;
f[x] = y, sz[y] += sz[x];
for (int i = 0; i < sz[x]; i++) c[y].push_back(c[x][i]);
}
}
}
void init(int N, int M, std::vector<int> U, std::vector<int> V, std::vector<int> W) {
n = cnt = N, m = M;
for (int i = 1; i <= m; i++) e[i] = (E){ U[i - 1] + 1, V[i - 1] + 1, W[i - 1] };
sort(e + 1, e + 1 + m);
for (int i = 1; i < 2 * n; i++) {
f[i] = i;
if (i <= n)
sz[i] = 1, c[i].push_back(i), ch[i] = true, d[i] = true;
}
for (int i = 1; i <= m; i++) merge(e[i].u, e[i].v, e[i].w);
if (cnt > n)
dfs(cnt);
}
int getMinimumFuelCapacity(int X, int Y) {
++X, ++Y;
if (ch[find(X)])
return -1;
return a[lca(X, Y)];
}