生成树 III
2025-10-13

mst,以及 mst related


Boruvka

  • 用途:\(O(m\log n)\) 求 mst。
  • 过程:

    • 考虑和 Kruskal、Prim 类似的孤立点 \(\to\) 加边 \(\to\) 树。
    • 每一轮算法,对于当前的每个连通块,找到其连向其它连通块的边中最小的一条,并在这一轮后加入边集(用并查集维护连通块合并)。
    • 重复执行该算法,最终找到 mst。
    图源 OI Wiki
    图源 OI Wiki
  • 复杂度:每一轮中,每个连通块都会被合并一次,故一轮之后现存连通块数量最坏情况下变为原来的一半。也即共执行 \(\log n\) 轮。复杂度 \(O(m\log n)\)
  • 优势:对于特殊边权的完全图 / 稠密图,边权并不由输入直接给定,而是由顶点计算得到,此时若可以依据性质直接找到连通块的最小边,则复杂度降低至 \(O(n\log n)\) 之类非常优秀的级别(注意并查集的 log 和 Boruvka 本身的 log 是平行的)。

    『依据性质找到最小的边』的一个 Bonus:见 KDT + Boruvka 做法的 平面欧几里得最小生成树


A - Jumping Around

https://www.luogu.com.cn/problem/CF1550F

  • 本题就是『特殊边权完全图』的体现。定义任意两个点之间的边权为两个点相互跳需要的最小的 \(k=||x_0-x_1|-d|\),那么只需求瓶颈路。
  • 做 Boruvka 即可。考虑怎么求某个连通块的最小边,预处理出每个位置(包括空位)前 / 后第一个和自己颜色不同的非空位,每次 check \(i\pm d\)

    • 若该位置存在和当前位置不同颜色的点:连边。
    • 否则,若该位置和当前位置颜色相同:转到刚刚求出的前驱、后继,选更小者连边。
    • 否则该位为空。前驱、后继则转化为上面两种情况。
  • 和双指针做法不同,似乎并不支持离散化,只能做到 \(O(V\log n)\)

#include <bits/stdc++.h>
const int V = 1e6;
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
    std::freopen(".in", "r", stdin);
    std::freopen(".out", "w", stdout);
    const auto stime = std::chrono::steady_clock::now();
#endif
    int n, q, s, d;
    std::cin >> n >> q >> s >> d;
    std::vector<int> a(n + 1), tab(V + 1);
    for (int i = 1; i <= n; ++i)
        std::cin >> a[i], tab[a[i]] = i;
    std::vector<std::pair<int, int> > to(n + 1);
    std::vector<std::vector<std::pair<int, int> > > g(n + 1);
    std::vector<int> u(V + 1), pre(V + 1), nex(V + 1), mn(n + 1), f(n + 1), pos;
    std::iota(f.begin() + 1, f.end(), 1);
    std::function<int(int)> find = [&](int x) {
        return x == f[x] ? x : f[x] = find(f[x]);
    };
    for (int tot = n; tot != 1; ) {
        pos.clear();
        for (int i = 1; i <= n; ++i) {
            if (f[i] == i) {
                mn[i] = 0x3f3f3f3f, to[i] = { 0, 0 };
                pos.push_back(i);
            }
            u[a[i]] = find(i);
            if (u[a[i - 1]] == u[a[i]])
                pre[a[i]] = pre[a[i - 1]];
            else
                pre[a[i]] = a[i - 1];
        }
        for (int i = 1; i <= V; ++i)
            if (!u[i])
                pre[i] = u[i - 1] ? i - 1 : pre[i - 1];
        nex[a[n]] = V + 1;
        for (int i = n - 1; i; --i)
            if (u[a[i + 1]] == u[a[i]])
                nex[a[i]] = nex[a[i + 1]];
            else
                nex[a[i]] = a[i + 1];
        nex[V] = V + 1;
        for (int i = V - 1; i; --i)
            if (!u[i])
                nex[i] = u[i + 1] ? i + 1 : nex[i + 1];
        for (int i = 1; i <= n; ++i) {
            int x = a[i], now = 0x3f3f3f3f, pos = 0;
            auto upd = [&](int x1) {
                if (std::abs(std::abs(x - x1) - d) < now)
                    now = std::abs(std::abs(x - x1) - d), pos = x1;
                return;
            };
            auto trans = [&](int x1) {
                if (pre[x1])
                    upd(pre[x1]);
                if (nex[x1] != V + 1)
                    upd(nex[x1]);
                return;
            };
            std::function<void(int)> work = [&](int x1) {
                if (u[x1] && u[x1] != u[x])
                    upd(x1);
                else if (u[x1])
                    trans(x1);
                else {
                    if (pre[x1])
                        work(pre[x1]);
                    if (nex[x1] != V + 1)
                        work(nex[x1]);
                }
                return;
            };
            work(std::max(1, x - d)), work(std::min(V, x + d));
            if (now < mn[u[x]])
                mn[u[x]] = now, to[u[x]] = { i, tab[pos] };
        }
        for (auto i : pos) {
            int j = u[a[to[i].second]], x = to[i].first, y = to[i].second;
            if (find(i) != find(j)) {
                --tot;
                f[find(i)] = find(j);
                g[x].emplace_back(y, mn[i]), g[y].emplace_back(x, mn[i]);
            }
        }
    }
    std::vector<int> res(n + 1);
    std::function<void(int, int)> DFS = [&](int x, int fa) {
        for (auto [i, w] : g[x])
            if (i != fa) {
                res[i] = std::max(res[x], w);
                DFS(i, x);
            }
        return;
    };
    DFS(s, -1);
    for (int x, k; q--; ) {
        std::cin >> x >> k;
        std::cout << ((res[x] <= k) ? "Yes" : "No") << '\n';
    }
#ifndef ONLINE_JUDGE
    std::cerr << std::fixed << std::setprecision(6) << std::chrono::duration<double> (std::chrono::steady_clock::now() - stime).count() << "s\n";
#endif
    return 0;
}

B - Parametric MST

https://www.luogu.com.cn/problem/CF1656F

  • 可以猜到能够把答案写出来,但是发现太抽象了以至于不太好模拟。
  • 首先需要猜到题目给的 \(w\) 是可以继续化的,注意到 \(a_i\cdot a_j+t(a_i+a_j)\) 可以写成 \((a_j+t)\cdot a_i+t\cdot a_i\) 的一次函数形式,一个很重要的思路是放弃模拟 mst 转而从点出发贪心
  • \(t\) 会影响的是斜率 \(a_j+t\),所以应该根据 \(a_j+t\) 的正负情况选择贪心策略

    \(a_j+t\le 0\) 时,使 \(a_j\)\(a_{\max}\) 连边,否则向 \(a_{\min}\) 连边。

    这其实是 Boruvka 第一轮的过程;此时只剩至多两个连通块,连边方式也就讨论一下 \(a_1+t\)\(a_n+t\) 的正负,然后很显然了。

  • \(t\) 足够小时,所有点都会连向 \(a_{\max}\);反之,当 \(t\) 足够大时,所有点都会连向 \(a_{\min}\)

    对于第一种情况,mst 的权为 \(a_{\max}\cdot (s - a_{\max}) + t\cdot ((n-1)\cdot a_{\max}+s-a_{\max})\);第二种情况,mst 的权为 \(a_{\min}\cdot (s - a_{\min}) + t\cdot ((n-1)\cdot a_{\min}+s-a_{\min})\),check \(t\) 的系数即可判 INF

  • 通过上面一种情况,可以注意到 \(t\in[-a_{\max},-a_{\min}]\),进一步需要猜到 \(t\) 取某个 \(-a_i\)。由于连的边是很已知的,可以发现当 \(t\) 夹在两个连续的 \(-a\) 之间时,mst 的连边情况是不变的。

    显然此时 mst 的权是一个关于 \(t\) 的一次函数,故 check 两端点即可。排序即可快速 check。

    由于此时第二轮 Boruvka 要连的边一定是 \((a_1,a_n)\),连上即可。

#include <bits/stdc++.h>
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
    std::freopen(".in", "r", stdin);
    std::freopen(".out", "w", stdout);
    const auto stime = std::chrono::steady_clock::now();
#endif
    int T;
    for (std::cin >> T; T--; ) {
        int n;
        std::cin >> n;
        std::vector<long long> a(n + 1), s(n + 1);
        for (int i = 1; i <= n; ++i)
            std::cin >> a[i];
        std::sort(a.begin() + 1, a.end());
        std::partial_sum(a.begin() + 1, a.end(), s.begin() + 1);
        if ((n - 1) * a[n] + s[n - 1] < 0ll || (n - 1) * a[1] + s[n] - a[1] > 0ll)
            std::cout << "INF\n";
        else {
            long long res = -1e18;
            for (int i = 1; i <= n; ++i) {
                auto t = -a[i], sl = s[i] - a[1], sr = s[n - 1] - s[i];
                res = std::max(res, a[n] * sl + a[1] * sr + t * ((i - 1) * a[n] + (n - 1 - i) * a[1] + sl + sr) + a[1] * a[n] + t * (a[1] + a[n]));
            }
            std::cout << res << '\n';
        }
    }
#ifndef ONLINE_JUDGE
    std::cerr << std::fixed << std::setprecision(6) << std::chrono::duration<double> (std::chrono::steady_clock::now() - stime).count() << "s\n";
#endif
    return 0;
}

C - Star MST

https://www.luogu.com.cn/problem/CF1657E

  • 模拟 Kruskal 的过程,在边权 \(w\) 处出现 \((1,x)\),那么允许在 \(\ge w\) 处出现其他与 \(x\) 相关的边。
  • 会有一个比较暴力的想法,设 \(f_{i,j,l}\) 表示已经分配完边权 \(i\),共分配了 \(j\) 条与 \(1\) 相关的边,还剩下 \(l\) 条空闲边可以放,会有一个 \(O(k\cdot n^4)\) 的转移。
  • 考虑优化。容易注意到没必要存 \(l\) 而是可以当场完成分配,但这样就导致 \(i\) 维的限制转化为『恰好』,需要额外枚举一轮。总之可以得到:

    \[ f_{i,j}=\sum_{d_i=1}^{i}\sum_{d_j=1}^{j} C_{n-(j-d_j)}^{d_j}\times f_{i-d_i,j-d_j}\times (k-i+1)^{C_{d_j}^2+d_j\times (j-d_j)} \]

    此时复杂度为 \(O(k^2n^2\log k)\),依然不够看;注意到交换求和顺序可以前缀和优化,故:

    \[ f_{j, i}=\sum_{d_j=1}^{j} C_{n-(j-d_j)}^{d_j}\times (k-i+1)^{C_{d_j}^2+d_j\times (j-d_j)}\times \sum_{d_i=1}^{i}f_{j - d_j,i-d_i} \]

    复杂度 \(O(kn^2\log k)\)\(\log\) 来源于快速幂。当然可以考虑预处理优化掉 log,whatever.

#include <bits/stdc++.h>
const int mod = 998244353;
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
    std::freopen(".in", "r", stdin);
    std::freopen(".out", "w", stdout);
    const auto stime = std::chrono::steady_clock::now();
#endif
    int n, k;
    std::cin >> n >> k;
    std::vector<std::vector<long long> > C(n + 1, std::vector<long long> (n + 1));
    for (int i = 0; i <= n; ++i) {
        C[i][0] = 1ll;
        for (int j = 1; j <= i; ++j)
            C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % mod;
    }
    auto qkp = [&](long long x, int y) {
        auto res = 1ll;
        for (; y; (x *= x) %= mod, y >>= 1)
            if (y & 1)
                (res *= x) %= mod;
        return res;
    };
    std::vector<std::vector<long long> > s(n, std::vector<long long> (k + 1));
    for (int i = 0; i <= k; ++i)
        s[0][i] = 1ll;
    for (int j = 1; j < n; ++j)
        for (int i = 1; i <= k; ++i) {
            for (int dj = 1; dj <= j; ++dj) 
                s[j][i] += C[n - (j - dj) - 1][dj] * qkp(k - i + 1, (C[dj][2] + dj * (j - dj)) % mod) % mod * s[j - dj][i - 1] % mod;
            (s[j][i] += s[j][i - 1]) %= mod;
        }
    std::cout << s[n - 1][k] << '\n';
#ifndef ONLINE_JUDGE
    std::cerr << std::fixed << std::setprecision(6) << std::chrono::duration<double> (std::chrono::steady_clock::now() - stime).count() << "s\n";
#endif
    return 0;
}

D - Smooth Sailing (Hard Version)

https://www.luogu.com.cn/problem/CF1920F2

  • 实在十分熟悉,之前在哪道 CF 题遇到过这个 trick,然而没什么回忆线索,遂放弃

    一个连通块被包围的充要条件:从连通块边缘任意一个点向任意方向发一条射线,垂直经过路径奇数次。

    垂直:即切线不切换奇偶状态。

    这个东西其实来源于计算几何,但是我的数学水平只停留在听说过计算几何四个字的水平,故不深究了。

    这种网格图上的图论问题,可以往欧拉定理(\(v-e+f=2\)),计算几何考虑,

  • 怎么在保证这一点的前提下求 mst 呢?答案是丢到状态里。

    找一个最靠左的点往左边引一条射线并标记沿路经过的点。令 \((x, y, 0/1)\) 表示走到 \((x, y)\),经过关键点偶数次 / 奇数次。边权啥的也没什么好说的,点权前移即可。

  • 询问的转化反而不太好想,其实就是问 \((x,y,0)\)\((x,y,1)\) 的瓶颈路。

    巧得有些不太自然,然而并没找到合理的解释。

  • 询问建个 Kruskal 重构树找 LCA 即可,复杂度 \(O(nm\log nm)\)

#include <bits/stdc++.h>
const int dir[][2] = {{ -1, 0 }, { 1, 0 }, { 0, -1 }, { 0, 1 }};
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
    std::freopen(".in", "r", stdin);
    std::freopen(".out", "w", stdout);
    const auto stime = std::chrono::steady_clock::now();
#endif
    int n, m, q;
    std::cin >> n >> m >> q;
    int cnt = 2 * n * m;
    std::vector<int> W(1);
    std::vector<std::vector<int> > g(2 * n * m + 1);
    auto fun = [&](int i, int j, int k) {
        return k * n * m + (i - 1) * m + j;
    };
    {
        int px = 0, py = m + 1;
        std::vector<std::vector<char> > a(n + 1, std::vector<char> (m + 1));
        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= m; ++j) {
                std::cin >> a[i][j];
                if (a[i][j] == '#' && j < py)
                    px = i, py = j;
            }
        std::vector<std::vector<int> > w(n + 1, std::vector<int> (m + 1, 0x3f3f3f3f));
        {
            std::queue<std::pair<int, int> > q;
            for (int i = 1; i <= n; ++i)
                for (int j = 1; j <= m; ++j)
                    if (a[i][j] == 'v')
                        q.emplace(i, j), w[i][j] = 0;
            for (; !q.empty(); ) {
                auto [x, y] = q.front();
                q.pop();
                for (auto [fx, fy] : dir) {
                    int nx = x + fx, ny = y + fy;
                    if (nx >= 1 && nx <= n && ny >= 1 && ny <= m && w[nx][ny] == 0x3f3f3f3f) {
                        w[nx][ny] = w[x][y] + 1;
                        q.emplace(nx, ny);
                    }
                }
            }
        }
        struct edge { int u, v, w; };
        std::vector<edge> e;
        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= m; ++j)
                if (a[i][j] != '#') {
                    if (j != m && a[i][j + 1] != '#') {
                        e.push_back({ fun(i, j, 0), fun(i, j + 1, 0), std::min(w[i][j], w[i][j + 1]) });
                        e.push_back({ fun(i, j, 1), fun(i, j + 1, 1), std::min(w[i][j], w[i][j + 1]) });
                    }
                    if (i != n && a[i + 1][j] != '#') {
                        if (i + 1 == px && j <= py) {
                            e.push_back({ fun(i, j, 0), fun(i + 1, j, 1), std::min(w[i][j], w[i + 1][j]) });
                            e.push_back({ fun(i, j, 1), fun(i + 1, j, 0), std::min(w[i][j], w[i + 1][j]) });
                        }
                        else {
                            e.push_back({ fun(i, j, 0), fun(i + 1, j, 0), std::min(w[i][j], w[i + 1][j]) });
                            e.push_back({ fun(i, j, 1), fun(i + 1, j, 1), std::min(w[i][j], w[i + 1][j]) });
                        }
                    }
                }
        std::sort(e.begin(), e.end(), [&](edge &x, edge &y) { return x.w > y.w; });
        std::vector<int> f(2 * n * m + 1);
        std::iota(f.begin() + 1, f.end(), 1);
        std::function<int(int)> find = [&](int x) {
            return x == f[x] ? x : f[x] = find(f[x]);
        };
        for (auto [x, y, w] : e)
            if (find(x) != find(y)) {
                f.push_back(++cnt);
                W.push_back(w), g.emplace_back();
                g[cnt].push_back(find(x)), g[cnt].push_back(find(y));
                f[find(x)] = f[find(y)] = cnt;
            }
    }
    std::vector<int> fa(cnt + 1), siz(cnt + 1), dep(cnt + 1), son(cnt + 1), top(cnt + 1);
    std::function<void(int)> DFS = [&](int x) {
        siz[x] = 1;
        for (auto i : g[x]) {
            dep[i] = dep[x] + 1;
            DFS(i), fa[i] = x;
            siz[x] += siz[i];
            if (siz[i] > siz[son[x]])
                son[x] = i;
        }
        return;
    };
    dep[1] = 1, DFS(cnt);
    DFS = [&](int x) {
        if (son[x])
            top[son[x]] = top[x], DFS(son[x]);
        for (auto i : g[x])
            if (i != son[x])
                top[i] = i, DFS(i);
        return;
    };
    top[cnt] = cnt, DFS(cnt);
    auto ask = [&](int x, int y) {
        for (; top[x] != top[y]; x = fa[top[x]])
            if (dep[top[x]] < dep[top[y]])
                std::swap(x, y);
        return dep[x] < dep[y] ? x : y;
    };
    for (int x, y; q--; ) {
        std::cin >> x >> y;
        std::cout << W[ask(fun(x, y, 0), fun(x, y, 1)) - 2 * n * m] << '\n';
    }
#ifndef ONLINE_JUDGE
    std::cerr << std::fixed << std::setprecision(6) << std::chrono::duration<double> (std::chrono::steady_clock::now() - stime).count() << "s\n";
#endif
    return 0;
}

E - Turtle and Intersected Segments

https://www.luogu.com.cn/problem/CF1981E

  • 考虑绝对值的几何意义,很容易发现有的边注定是无效的:

    如图中三条线段形成一个环,权值如图,排除掉权值最大的边,发现只会连权值相邻的边
    如图中三条线段形成一个环,权值如图,排除掉权值最大的边,发现只会连权值相邻的边
  • 换句话说,对于数轴上的这个点,仅需把覆盖之的所有线段序列\(a\) 排序,连接相邻者即可。怎么更快地实现这个过程呢?
  • 发现只需要在这个线段序列变化的时候连新的边,故用 multiset 维护这个序列,扫一遍,在加入新线段时连接它和前驱后继即可(结合前文论述发现删除的时候不用管)。

#include <bits/stdc++.h>
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
    std::freopen(".in", "r", stdin);
    std::freopen(".out", "w", stdout);
    const auto stime = std::chrono::steady_clock::now();
#endif
    int T;
    for (std::cin >> T; T--; ) {
        int n;
        std::cin >> n;
        struct node { int l, r, v; };
        struct edge { int u, v, w; };
        std::vector<edge> e;
        std::vector<node> a(n + 1);
        for (int i = 1; i <= n; ++i)
            std::cin >> a[i].l >> a[i].r >> a[i].v;
        std::sort(a.begin() + 1, a.end(), [&](node &x, node &y) { return x.l < y.l; });
        std::set<std::tuple<int, int, int> > t;
        std::set<std::pair<int, int> > now;
        for (int i = 1; i <= n; ++i) {
            for (; !t.empty();) {
                auto [r, v, id] = *t.begin();
                if (r >= a[i].l)
                    break;
                now.erase(std::make_pair(v, id));
                t.erase(t.begin());
            }
            auto p = now.lower_bound(std::make_pair(a[i].v, i));
            if (p != now.end())
                e.push_back({ i, p->second, p->first - a[i].v });
            if (p != now.begin()) {
                --p;
                e.push_back({ i, p->second, a[i].v - p->first });
            }
            t.emplace(a[i].r, a[i].v, i), now.emplace(a[i].v, i);
        }
        std::sort(e.begin(), e.end(), [&](edge &x, edge &y) { return x.w < y.w; });
        auto res = 0ll;
        std::vector<int> f(n + 1);
        std::iota(f.begin() + 1, f.end(), 1);
        std::function<int(int)> find = [&](int x) {
            return x == f[x] ? x : f[x] = find(f[x]);
        };
        int cntm = 0;
        for (auto [u, v, w] : e)
            if (find(u) != find(v)) {
                f[find(u)] = find(v);
                res += w, ++cntm;
            }
        if (cntm == n - 1)
            std::cout << res << '\n';
        else
            std::cout << -1 << '\n';
    }
#ifndef ONLINE_JUDGE
    std::cerr << std::fixed << std::setprecision(6) << std::chrono::duration<double> (std::chrono::steady_clock::now() - stime).count() << "s\n";
#endif
    return 0;
}

F - Digital Village (Extreme Version)

https://www.luogu.com.cn/problem/CF2021E3

  • 为啥会想到 DP(。)前面不都是神秘建图题吗(。)
  • 会发现待求即为重构树上 LCA 的权值,但题设中的 Key 是设置了服务器的结点,二者不匹配。如果直接设 \(f_{x,j}\) 表示 \(x\) 子树内设了 \(j\) 个服务器,感觉不太可行。

    当然也可能是因为我没做过重构树上的 DP。Whatever.

  • 实际上需要结合重构树的性质考虑。发现一个子树内部不能消化,当且仅当


言论