[EGOI 2025] Wind Turbines / 风力涡轮机

[EGOI 2025] Wind Turbines / 风力涡轮机

题目描述

Anna 负责为北海新建的一个海上风电场设计电缆布线。该风电场有 \(N\) 台风力发电机,编号为 \(0, 1, \ldots, N-1\)。她的目标是确保所有发电机都能以最低的成本与陆地(岸边)连通。

Anna 拥有 \(M\) 条可选的连接,每条连接都连接两台风力发电机,并有一个特定的费用。此外,附近的城市同意承担将一段连续编号区间 \([\ell, r]\) 内的发电机直接接入岸边的费用。也就是说,区间内的每台发电机 \(t\)\(\ell \leq t \leq r\))都可以免费直接接入岸边。如果所有可选连接都建成,则任意两台发电机之间都可以互相到达。这意味着只要有一台发电机接入岸边,就可以通过某些连接让所有发电机的电力都输送到岸边。当然,如果有更多发电机直接接入岸边,可能可以进一步降低总成本。注意,免费连接是唯一能直接连到岸边的方式。

Anna 的任务是选择一部分可选连接,使得它们的总费用最小,并保证每台发电机都能通过某些路径与岸边连通(可以经过其他发电机)。

为了让 Anna 做出明智的决策,城市方提供了 \(Q\) 种不同的区间 \([\ell, r]\) 方案。城市方希望 Anna 计算在每种方案下的最小总费用。

题解报告

整体思路

原问题等价于:给定一个无向连通图,每次询问指定一个连续编号区间 \([L,R]\),区间内的所有点可以免费连接到“岸边”,我们需要选出若干条边使所有点都与岸边连通,并最小化所选边的总费用。

不难发现,这相当于先求出整张图的最小生成树(MST),总费用为 \(sum\)。对于一次询问 \([L,R]\),所有 两个端点都在区间内 的 MST 边都可以被删去(因为区间内的点已经通过免费连接彼此连通,不再需要这些边来维持与岸边的连接)。因此答案为
[
\text{答案} = sum - \sum_{\substack{e\in \text{MST} \ e\text{ 的两端点}\in[L,R]}} w_e .
]

于是问题转化为:多次询问,求所有两端点均落在给定区间 \([L,R]\) 内的 MST 边的权值和。

Kruskal 重构树

由于询问是离线的,我们可以利用 Kruskal 重构树 来刻画 MST 的结构。

  • 初始时每个点自成一个集合。
  • 按边权从小到大遍历所有边。对于一条连接 \(u,v\)、权值为 \(c\) 的边,若 \(u,v\) 不在同一集合,则新建一个节点 \(t\),将 \(t\) 作为 \(u\)\(v\) 所在集合的父节点,并令 \(val[t]=c\)。这一过程将 MST 的边权转化成了重构树上的点权。
  • 最终的生成树总费用即为求和过程中累加的 \(sum\)

重构树具有如下性质:

  • 原来的 \(N\) 个点是叶子节点。
  • 两个叶子 \(u,v\) 在 MST 上的路径最大边权,恰好等于它们在重构树上的 LCA 的点权 \(val[\mathrm{LCA}(u,v)]\)

一条 MST 边(对应重构树上的一个点 \(x\))可以被删去,当且仅当它的 两个端点叶子 均出现在免费区间 \([L,R]\) 内。换句话说,对于节点 \(x\),若存在两个叶子 \(u,v\) 满足 \(u,v\in[L,R]\)\(\mathrm{LCA}(u,v)=x\),则这条边的权值 \(val[x]\) 可以被节省。

支配对与 DSU on tree

直接枚举所有叶子对是不可行的。我们采用 支配对 的思想来压缩需要考虑的叶子对。

对于重构树上的一个节点 \(x\),假设它的子树中包含了若干个叶子。我们把叶子按照编号排序,那么对于任意一个叶子,与其“相邻”的叶子对足以决定所有可能的 LCA 为 \(x\) 的情况。具体地,若我们在 \(x\) 的子树中维护一个有序的叶子集合,则对于每个轻儿子的叶子,只需将其与前驱、后继配对,就可以覆盖所有可能被某个区间完全包含的叶子对。这些配对就是所谓的“支配对”。

实现时,对重构树进行 轻重链剖分,然后采用 DSU on tree(启发式合并)生成支配对:

  • 维护一个全局 set,存放当前子树中所有叶子的编号。
  • 递归处理节点 \(x\):先处理轻儿子(不保留信息),再处理重儿子(保留信息)。
  • 对于每个轻儿子的子树,逐个取出其中的叶子 \(u\),在 set 中查找 \(u\) 的前驱 \(pre\) 和后继 \(nxt\),分别记录支配对 \((pre,u)\)\((u,nxt)\),它们的 LCA 就是 \(x\)。之后将轻儿子子树的所有叶子插入 set
  • 重儿子的信息保留,轻儿子的信息用完即删(通过清空 set 撤销)。

这样我们得到了形如 \((l, r, c)\) 的一系列支配对,表示如果一个区间完全包含 \([l,r]\),则权值为 \(c = val[\mathrm{LCA}(l,r)]\) 的边可以被删去。

离线扫描线处理

现在问题变为:有若干个区间 \([l,r]\),每个区间带有一个权值 \(c\)。对于每次询问 \([L,R]\),求所有满足 \(L\le l\)\(r\le R\) 的区间权值和。

这可以很自然地使用 扫描线 + 树状数组 离线解决:

  • 将支配对按右端点 \(r\) 分组,将询问也按右端点 \(R\) 分组。
  • 从左到右扫描右端点 \(r=1\dots N\)
    • 对于当前 \(r\) 上的所有支配对 \((l,c)\),我们尝试更新 \(l\) 位置上的权值。因为对于同一个 LCA(即同一个 \(c\)),如果存在新的左端点 \(l' \ge l\),那么原左端点 \(l\) 的贡献就不再必要(能覆盖 \([l,r]\) 的询问必然也能覆盖 \([l',r]\))。于是我们维护数组 col[c] 表示当前权值 \(c\) 对应的最大左端点,若新的 \(l\) 更大,则从树状数组中删去旧贡献,并添加新贡献。
    • 对于所有右端点为 \(r\) 的询问 \([L,r]\),答案即为树状数组上区间 \([L,r]\) 的和(即所有 \(l\ge L\)\(r\le R\) 的权值和)。
  • 最终第 \(i\) 次询问的答案为 \(sum - \text{query}(L,R)\)

由于 Kruskal 重构树最多有 \(2N-1\) 个点,支配对的总数是 \(O(N\log N)\) 级别,整体复杂度有保障。

参考代码

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5 + 10;
int n, m, q, fa[N], tot, val[N], sum = 0, dfn[N], cnt = 0, id[N], siz[N], son[N], ed[N], tr[N], col[N], ans[N];
vector <int> e[N]; set <int> s; vector <pair <int, int>> p[N]; vector <pair <int, int>> ask[N];
struct line {int u, v, c; } l[N];
int find(int u) {return fa[u] == u ? u : fa[u] = find(fa[u]);}
void dfs(int u) {
    dfn[u] = ++cnt, id[cnt] = u, siz[u] = 1;
    for (auto v : e[u]) dfs(v), siz[u] += siz[v], (siz[son[u]] < siz[v]) ? son[u] = v : 0;
    ed[u] = cnt;
}
int lowbit(int x) {return x & -x;}
void add(int x, int v) {if (!x) return; for (int i = x; i <= n; i += lowbit(i)) tr[i] += v;}
int query(int x) {int ret = 0; for (int i = x; i >= 1; i -= lowbit(i)) ret += tr[i]; return ret;}
void findcls(int x, int lca) {
    auto it = s.lower_bound(x); 
    if (it != s.end()) p[*it].emplace_back(x, lca);
    if (it != s.begin()) p[x].emplace_back(*prev(it), lca);
}
void dsu(int u, bool keep) {
    for (auto v : e[u]) if (v != son[u]) dsu(v, 0);
    if (son[u]) dsu(son[u], 1);
    for (auto v : e[u]) 
        if (v != son[u]) {
            for (int i = dfn[v]; i <= ed[v]; i++) findcls(id[i], u);
            for (int i = dfn[v]; i <= ed[v]; i++) s.insert(id[i]);
        }
    if (!keep) for (int i = dfn[u]; i <= ed[u]; i++) s.erase(id[i]);
    else if (u <= n) s.insert(u);
}
signed main() {
    cin.tie(nullptr) -> ios::sync_with_stdio(0);
    cin >> n >> m >> q; tot = n;
    for (int i = 1; i <= m; i++) cin >> l[i].u >> l[i].v >> l[i].c, l[i].u++, l[i].v++;
    for (int i = 1; i <= n * 2; i++) fa[i] = i;
    sort(l + 1, l + m + 1, [&] (const line x, const line y) { return x.c < y.c; });
    for (int i = 1; i <= m; i++) {
        auto [u, v, c] = l[i]; u = find(u), v = find(v); if (u == v) continue;
        fa[u] = fa[v] = ++tot, val[tot] = c, e[tot].emplace_back(u), e[tot].emplace_back(v), sum += c;
    }
    dfs(tot); dsu(tot, 0);
    for (int l, r, i = 1; i <= q; i++) cin >> l >> r, l++, r++, ask[r].emplace_back(l, i);
    for (int r = 1; r <= n; r++) {
        for (auto [l, c] : p[r]) 
            if (l > col[c]) add(col[c], -val[c]), col[c] = l, add(col[c], val[c]);
        for (auto [l, id] : ask[r]) ans[id] = query(r) - query(l - 1);
    }
    for (int i = 1; i <= q; i++) cout << sum - ans[i] << '\n';
    return 0;
}
posted @ 2026-05-08 13:22  Aojun  阅读(8)  评论(0)    收藏  举报