「HNOI2010」城市建设(线段树分治,LCT/Kruskal)
文章目录
【注意】最后更新于 February 27, 2020,文中内容可能已过时,请谨慎使用。
题目描述
给你一张边带权的无向连通图,多次修改,每次修改一条边的边权,每次修改完后求最小生成树的边权之和。
点数 $2\cdot 10^4$, 边数和修改数 $5\cdot 10^4$,时限 3s。
简要做法
LCT 做法
把每条边按两次修改之间以及头尾分成若干段,然后按出现时间线段树分治,LCT 维护最小生成树。
时间复杂度 $O(n+(m+q)\log q\log n)$ ,常数有点大,听说很难卡到满分,自己没写。
Kruskal 做法
还是线段树分治,但尝试用 Kruskal 算法求最小生成树。
然而,Kruskal 算法要求边权从小到大加入,直接线段树分治无法满足这个要求。
(注:为了严谨,你可以认为下文中在比较大小时给第 $i$ ($1\le i\le m$) 条边的边权加上了 $2^{-i}$,这样保证了 MST 是唯一的。当然,在代码中无需如此。)
可以观察到,随着分治区间越来越小,修改的边也越来越少,图中大多数的边是不会被修改的。正因如此,可以猜想,大多数边在或不在 MST 中是固定的,而我们需要关注的,只有那些因会修改的边的边权变化而既可能在 MST 中又可能不在 MST 中的边。
如果我们有一张图 $G=(V, E)$,其中 $E'\subseteq E$ 是以后会被修改的边,那么,如何求出那些无论 $E'$ 中的边边权如何变化都一定在 MST 中/一定不在 MST 中的边呢?
- 将 $E'$ 中的边边权设为正无穷,求 MST,此时不在 $E'$ 中且不在 MST 中的边,无论 $E'$ 中的边边权如何变化,都不在 MST 中,下文中称这些边为“一类边”。
- 将 $E'$ 中的边边权设为 $0$,求 MST,此时不在 $E'$ 中且在 MST 中的边,无论 $E'$ 中的边边权如何变化,都在 MST 中,下文中称这些边为“二类边”。
那么,在继续分治下去的过程中,我们就只需关注 $E'$ 中的边以及是一类边而不是二类边的边。
由于一类边最多有 $|V|-1$ 条,而二类边至少有 $|V|-1-|E'|$ 条,所以是一类边而不是二类边的边最多有 $|E'|$ 条,于是,向下分治时需要关注的边就不超过 $2|E'|$ 条。
虽然只需关注这些边,但二类边依然要在分治过程中保留(计算 MST 时并查集中要保留,最后边权要计入答案)。边权计入答案很好办,而保留在并查集中可以通过可回退并查集来实现。
一层分治中每个分治内会被修改的边加起来是 $O(m+q)$ 条,并查集要可回退,只能按秩合并。所以总时间复杂度是 $O(n+(m+q)\log(m+q)+(m+q)\log q\log n)$ 。
时间复杂度和 LCT 做法差不多(你愿意的话你可以说时间复杂度更大),但跑得飞快。
有个实现上的小细节:将 $E'$ 中的边边权设为无穷大和 $0$ 时不需要真的修改再排序,最后加入(其实可以直接不加入)/最先加入即可,而其它边的排序可以在主函数中完成,而在分治过程中保持原序。这样时间复杂度的瓶颈上就只有并查集而没有排序了(当然我说的是 (m+q)logqlogn 这个瓶颈,不是 (m+q)log(m+q) 这个瓶颈)。
Kruskal 做法参考代码
#include <iostream>
#include <cstdio>
#include <vector>
#include <stack>
#include <algorithm>
using namespace std;
typedef long long ll;
struct Edge
{
int u, v, w, l, r;
bool operator<(const Edge &b) const
{
return w < b.w;
}
bool change(int left, int right)
{
return (l > left && l <= right) || (r >= left && r < right);
}
};
struct UFS
{
vector<int> f, siz;
struct Change
{
int x, y;
};
stack<Change> c;
void init(int n)
{
f.resize(n + 1);
siz.resize(n + 1, 1);
for (int i = 0; i <= n; ++i) f[i] = i;
}
int find(int x)
{
return x == f[x] ? x : find(f[x]);
}
bool merge(int x, int y)
{
x = find(x);
y = find(y);
if (x == y) return false;
if (siz[x] < siz[y]) swap(x, y);
c.push({x, y});
siz[x] += siz[y];
f[y] = x;
return true;
}
void rollback(int x)
{
while ((int)c.size() > x)
{
f[c.top().y] = c.top().y;
siz[c.top().x] -= siz[c.top().y];
c.pop();
}
}
int status() const { return c.size(); }
} ufs;
vector<ll> ans;
void solve(int l, int r, vector<Edge> &e, ll delta = 0)
{
if (l == r)
{
ans[l] = delta;
int ver = ufs.status();
for (int i = 0; i < (int)e.size(); ++i) if (ufs.merge(e[i].u, e[i].v)) ans[l] += e[i].w;
ufs.rollback(ver);
return;
}
vector<bool> ininf(e.size(), false), inzero(e.size(), false);
int ver = ufs.status();
for (int i = 0; i < (int)e.size(); ++i) if (!e[i].change(l, r)) ininf[i] = ufs.merge(e[i].u, e[i].v);
ufs.rollback(ver);
for (int i = 0; i < (int)e.size(); ++i) if (e[i].change(l, r)) ufs.merge(e[i].u, e[i].v);
for (int i = 0; i < (int)e.size(); ++i)
{
if (!e[i].change(l, r) && ufs.merge(e[i].u, e[i].v))
{
inzero[i] = true;
delta += e[i].w;
}
}
int mid = (l + r) >> 1;
vector<Edge> le, re;
for (int i = 0; i < (int)e.size(); ++i)
{
if (e[i].change(l, r) || (ininf[i] && !inzero[i]))
{
if (e[i].l <= mid) le.push_back(e[i]);
if (e[i].r > mid) re.push_back(e[i]);
}
}
ufs.rollback(ver);
for (int i = 0; i < (int)e.size(); ++i) if (inzero[i]) ufs.merge(e[i].u, e[i].v);
solve(l, mid, le, delta);
solve(mid + 1, r, re, delta);
ufs.rollback(ver);
}
int main()
{
int n, m, q;
scanf("%d%d%d", &n, &m, &q);
vector<Edge> e;
vector<int> u(m), v(m), w(m), last(m, 1);
for (int i = 0; i < m; ++i) scanf("%d%d%d", &u[i], &v[i], &w[i]);
for (int i = 1; i <= q; ++i)
{
int p, x;
scanf("%d%d", &p, &x);
--p;
if (i > 1) e.push_back({u[p], v[p], w[p], last[p], i - 1});
last[p] = i;
w[p] = x;
}
for (int i = 0; i < m; ++i) e.push_back({u[i], v[i], w[i], last[i], q});
sort(e.begin(), e.end());
ans.resize(q + 1);
ufs.init(n);
solve(1, q, e);
for (int i = 1; i <= q; ++i) printf("%lld\n", ans[i]);
return 0;
}
评论正在加载中...如果评论较长时间无法加载,你可以 搜索对应的 issue 或者 新建一个 issue 。