题解:P13548 [OOI 2022] Air Reform

· · 题解

这真是神仙题了。爆肝我 8days,看来还是太菜了。

大概转化一下题意:就是说给你一张无向连通图,你需要建它的一个补图,其中补图两点的边权就是在原图中两点最小瓶颈路的最大边权,你需要求出对于原图的每条边 (u,v),求两点在补图中最小瓶颈路的最大边权。

对于最小瓶颈路的最大边权,这个东西显然可以用 Kruskal 重构树解决,求一个 LCA 即可。
所以算法瓶颈在于怎么快速建出补图的重构树。

考虑暴力,注意到在建原图的重构树时是从小到大枚举边权建新节点的,所以我们考虑在每次建新节点的时候枚举左右两个子树集合,若两点在原图中没有边且在补图中还未连通,则连一条边。
可以得到 21pts 的高分。 ::::warning[21pts]

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define pii pair<int,int>
const int maxn = 3e5 + 10;
int T, g, n, m, q, cnt;
struct Node {
    int u, v, w;
    bool operator<(const Node &ppl)const & {
        return w < ppl.w;
    }
} e[maxn], _[maxn];
int val[maxn], f[maxn][21], dep[maxn];
vector<int>son[maxn];
map<pii, int>mp;
struct dsu {
    int fa[maxn];
    void init(int n) {
        for (int i = 1; i <= n; i++) fa[i] = i;
    }
    int find(int x) {
        return fa[x] == x ? x : fa[x] = find(fa[x]);
    }
} B1, B2;
int LCA(int x, int y) {
    if (dep[x] < dep[y]) swap(x, y);
    int d = dep[x] - dep[y];
    for (int i = 20; i >= 0; i--) if (d >> i & 1) x = f[x][i];
    if (x == y) return x;
    for (int i = 20; i >= 0; i--) if (f[x][i] != f[y][i]) x = f[x][i], y = f[y][i];
    return f[x][0];
}
void init() {
    mp.clear();
    for (int i = 1; i <= n; i++) son[i].clear();
}
signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    cin >> T >> g;
    while (T--) {
        init();
        cin >> n >> m;
        for (int i = 1, u, v, w; i <= m; i++) {
            cin >> u >> v >> w;
            mp[ {u, v}] = mp[ {v, u}] = 1;
            _[i] = e[i] = {u, v, w};
        }
        sort(e + 1, e + m + 1);
        for (int i = 1; i <= n; i++) son[i].push_back(i);
        B1.init(n), B2.init(n);
        int _cnt = n;
        cnt = n;
        for (int i = 1; i <= m; i++) {
            int fu = B1.find(e[i].u), fv = B1.find(e[i].v), w = e[i].w;
            if (fu == fv) continue;
            _cnt++;
            B1.fa[_cnt] = B1.fa[fu] = B1.fa[fv] = _cnt;
            for (auto x : son[fu]) for (auto y : son[fv]) {//暴力枚举左右子树 
                    if (mp[ {x, y}]) continue;
                    int fx = B2.find(x), fy = B2.find(y);
                    if (fx == fy) continue;
                    val[++cnt] = w;
                    f[fx][0] = f[fy][0] = cnt;
                    B2.fa[cnt] = B2.fa[fx] = B2.fa[fy] = cnt;
                }
            son[_cnt].clear();
            for (auto x : son[fu]) son[_cnt].push_back(x);
            for (auto x : son[fv]) son[_cnt].push_back(x);
            son[fu].clear(), son[fv].clear();
        }
        for (int k = 1; k <= 20; k++) for (int i = 1; i <= cnt; i++) f[i][k] = f[f[i][k - 1]][k - 1];
        dep[cnt] = 1;
        for (int i = cnt - 1; i >= 1; i--) dep[i] = dep[f[i][0]] + 1;
        for (int i = 1; i <= m; i++) {
            int u = _[i].u, v = _[i].v;//查原图的边 
            cout << val[LCA(u, v)] << " ";
        }
        cout << "\n";
    }
    return 0;
}

:::: 考虑优化。

为了方便表示,对于当前节点 u,不妨设其左右子树分别含 id_lid_r 个联通块,st_x 表示第 x 个连通块中的元素个数。

考虑如何合并 u 左右子树的连通块。

不妨先枚举左右子树的连通块 id_lid_r,用 idxidy 表示,再枚举每个连通块中间的元素 st_{idx}st_{idy},用 xy 表示,若两点在原图中没有边,且在补图中尚未连通,则将两点相连(到这里和暴力是一样的)。

这样会存在一个问题,若左子树 $id_l$ 内有两个连通块 $idx$ 和 $idz$ 内均有节点能和 $idy$ 连通块内的节点相连,则会导致这三个连通块合并成一个大的连通块。 但我们若将 $st_{idy}$ 和 $st_{idx}$ 联通后直接将其加入 $st_{idx}$,就会导致建出的树不是标准的重构树,或是建不出一个完整的树。 所以当我们处理完 $st_{idx}$ 内的所有节点后,还要将 $st_{idx}$ 加到 $id_r$ 里去。 然后在做一个启发式合并,复杂度就会变成优秀的 $O(n\log^2n)$。 正确性显然,考虑证明时间复杂度。 - 若 $st_{idx}$ 和 $st_{idy}$ 存在点对使得两连通块连通。显然最多只会连通 $n$ 次(在补图中判连通还要乘一个 $\alpha(n)$),每一次联通后把两个块启发式合并,这一部分复杂度是 $O(\alpha(n)\log n)$ 的。 - 若不能连通,则枚举完 $st_{idy}$ 后,会将 $st_{idx}$ 加到 $id_r$ 里,注意到这里也是用启发式合并的,一个点最多会被加入 $\log n$ 次。 同时每次查一下在原图是否有边也是 $\log n$ 的。 所以建补图重构树的复杂度是 $O(m\log^2n+m\alpha(n)\log n)$。 其余部分复杂度显然没有建补图的复杂度大,直接忽视即可。 ::::success[100pts] ```cpp #include <bits/stdc++.h> using namespace std; #define int long long const int maxn = 4e5 + 10; int T, g, n, m; int ans[maxn]; set<int> E[maxn], id[maxn], st[maxn]; struct Edge { int u, v, w; bool operator<(const Edge &ppl) const { return w < ppl.w; } } e[maxn], _[maxn]; namespace Kurskal_Tree { struct dsu { int fa[maxn]; void init(int n) { for (int i = 1; i <= n; i++) fa[i] = i; } int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); } } B1, B2; struct Tree2 { int cnt, val[maxn], f[maxn][30], dep[maxn]; void add(int u, int v, int w) { int fu = B2.find(u), fv = B2.find(v); if (fu == fv) return; val[++cnt] = w; f[fu][0] = f[fv][0] = cnt; B2.fa[fu] = B2.fa[fv] = B2.fa[cnt] = cnt; } void init() { dep[cnt] = 1; for (int i = cnt; i >= 1; i--) dep[i] = dep[f[i][0]] + 1; for (int k = 1; k <= 20; k++) for (int i = 1; i <= cnt; i++) f[i][k] = f[f[i][k - 1]][k - 1]; } int LCA(int x, int y) { if (dep[x] < dep[y]) swap(x, y); int d = dep[x] - dep[y]; for (int i = 20; i >= 0; i--) if (d >> i & 1) x = f[x][i]; if (x == y) return x; for (int i = 20; i >= 0; i--) if (f[x][i] != f[y][i]) x = f[x][i], y = f[y][i]; return f[x][0]; } } T2; struct Tree1 { int cnt, sz[maxn]; void init() { cnt = n, T2.cnt = n; sort(e + 1, e + m + 1); B1.init(n), B2.init(n); for (int i = 1; i <= n; i++) sz[i] = 1, id[i].insert(i), st[i].insert(i); for (int i = 1; i <= m; i++) { int fu = B1.find(e[i].u), fv = B1.find(e[i].v), w = e[i].w; if (fu == fv) continue; ++cnt; B1.fa[fu] = B1.fa[fv] = B1.fa[cnt] = cnt; int u = cnt, l = fu, r = fv; sz[u] = sz[l] + sz[r]; if (sz[l] > sz[r]) swap(l, r);//启发式合并 for (auto idx : id[l]) { auto it = id[r].begin(); while (it != id[r].end()) { auto idy = *it; bool flag = 0; for (auto x : st[idx]) { for (auto y : st[idy]) if (E[x].find(y) == E[x].end()) { flag = 1; T2.add(x, y, w);//能连通直接连 break; } if (flag) break; } if (flag) { if (st[idx].size() < st[idy].size()) swap(st[idx], st[idy]);//启发式合并 for (auto y : st[idy]) st[idx].insert(y); st[idy].clear(); it = id[r].erase(it); } else ++it; } id[r].insert(idx); } id[u] = id[r];//所有连通块都加到id[r]里了,直接赋值给id[u]即可 } } } T1; } using namespace Kurskal_Tree; void init() { for (int i = 1; i <= n * 2; i++) st[i].clear(), E[i].clear(), id[i].clear(); } signed main() { ios::sync_with_stdio(0); cin.tie(0), cout.tie(0); cin >> T >> g; while (T--) { cin >> n >> m; init(); for (int i = 1, u, v, w; i <= m; i++) { cin >> u >> v >> w; _[i] = e[i] = {u, v, w}; E[u].insert(v), E[v].insert(u); } T1.init(), T2.init(); for (int i = 1; i <= m; i++) { int u = _[i].u, v = _[i].v; cout << T2.val[T2.LCA(u, v)] << " "; } cout << "\n"; } return 0; } ``` ::::