Kruskal 重构树
Kruskal 重构树
- 第一类Kruskal重构树
这一类的Kruskal重构树是基于最小生成树的Kruskal算法衍生的一个数据结构。
回想一下最小生成树Kruskal算法:按照边权从小到大枚举所有边,若当前的边 两端点不连通就将其连起来。重构树的构建只是最后一步有区别:若当前的边 两端点不连通,则新建一个节点 ,将 所在的连通块的根节点的父亲赋为 ,并且将节点 的点权值赋为边 的权值。容易发现,若原图有 个节点,那么Kruskal重构树共有 个节点。
for (const Edge &E: Graph)
if (findRoot(E.u) != findRoot(E.v)) {
newRoot = new Node();
newRoot.nodeValue = E.edgeValue;
newGraph.addEdge(newRoot, findRoot(E.u));
newGraph.addEdge(newRoot, findRoot(E.v));
makeComponent(newRoot);
Component[findRoot(E.u)].father = Component[newRoot];
Component[findRoot(E.v)].father = Component[newRoot];
}
Kruskal重构树有一些很好用的性质:
- Kruskal重构树是二叉树。当按照边权从小到大或从大到小建立Kruskal重构树时,它是一个堆。
- 原来图中的所有节点现在是Kruskal重构树上的叶节点。
- 设Kruskal重构树是大根堆 (即按照边权从小到大顺序建立)。给定原图上的节点 和一个值 ,在重构树上找到深度最小的节点 满足 是 的祖先且 (其中 表示虚拟节点 的点权,即虚拟节点 对应边的边权),那么 子树内的所有叶子节点集合便是原图中节点 在只经过边权不超过 的边能到达的所有节点的集合。当Kruskal重构树是小根堆时亦同理。
从性质3可以知道,若题目限制只经过边权不超过/不小于某个值的边,那么通常可以使用Kruskal重构树。
例题 1 Peaks
Kruskal重构树的裸题。按边权从小到大建立Kruskal重构树后,问题转化为询问一个子树内叶节点中权值第 大。转换为 序后就是静态区间第 大,可以使用主席树。总时间复杂度为 。
例题 2 Life is a Game
容易注意到,对于每一次询问,所有经过的节点必定组成一个连通块,而且所有经过的边必定是原图最小生成树上的包含该连通块的边集。
基于这个结论,可以按边权从小到大建立Kruskal重构树。虽然对于一次询问在增广连通块的同时,经过的边权限制也会改变,但最后该询问所有经过的节点的集合必定是Kruskal重构树上某一棵子树的叶节点集合。于是每一次询问在重构树上从叶子节点开始暴力跳父亲,直到找到第一条不能通过的边 (设其为 则有 子树内叶节点权值和加上初始值小于虚拟节点 所对应边的边权)。这样做的正确性显然,只不过时间复杂度为 。
考虑优化,发现上述跳父亲的过程可以使用树上倍增优化。时间复杂度为 ,可以通过。
参考代码
#include <bits/stdc++.h>
using namespace std;
static constexpr int Maxn = 2e5 + 5, LOG = 19;
static constexpr int64_t inf = 0x3f3f3f3f3f3f3f3f;
int n, m, q, nc;
int64_t a[Maxn];
struct Edge {
int u, v;
int64_t w;
Edge() = default;
Edge(int u, int v, int64_t w) : u(u), v(v), w(w) { }
friend bool operator < (const Edge &lhs, const Edge &rhs) {
return lhs.w < rhs.w;
}
} e[Maxn];
int fa[Maxn];
int fnd(int x) {
return fa[x] == x ? x : fa[x] = fnd(fa[x]);
} // fnd
vector<int> g[Maxn];
int64_t b[Maxn], sa[Maxn];
int64_t c[LOG][Maxn];
int par[LOG][Maxn];
void dfs(int u, int fa) {
sa[u] = (u > n ? 0 : a[u]); par[0][u] = fa;
for (int j = 1; j < LOG; ++j) par[j][u] = par[j - 1][par[j - 1][u]];
for (const int &v: g[u]) dfs(v, u), sa[u] += sa[v];
c[0][u] = (u == nc ? -inf : sa[u] - b[fa]);
} // dfs
int main(void) {
scanf("%d%d%d", &n, &m, &q);
for (int i = 1; i <= n; ++i)
scanf("%lld", &a[i]);
for (int i = 1; i <= m; ++i)
scanf("%d%d%lld", &e[i].u, &e[i].v, &e[i].w);
sort(e + 1, e + m + 1);
for (int i = 1; i <= n; ++i) fa[i] = i;
nc = n;
for (int i = 1; i <= m; ++i) {
int u = e[i].u, v = e[i].v;
int64_t w = e[i].w;
int fu = fnd(u), fv = fnd(v);
if (fu != fv) {
++nc, fa[nc] = nc;
b[nc] = w;
fa[fu] = nc, fa[fv] = nc;
g[nc].push_back(fu);
g[nc].push_back(fv);
}
}
memset(c, inf, sizeof(c));
dfs(nc, 0);
for (int j = 1; j < LOG; ++j) for (int i = 1; i <= nc; ++i)
c[j][i] = min(c[j - 1][i], c[j - 1][par[j - 1][i]]);
while (q--) {
int x;
int64_t w;
scanf("%d%lld", &x, &w);
if (b[par[0][x]] > w + a[x]) {
printf("%lld\n", w + a[x]);
} else {
int u = x;
for (int j = LOG - 1; j >= 0; --j)
if (w + c[j][u] >= 0) u = par[j][u];
printf("%lld\n", sa[u] + w);
}
}
exit(EXIT_SUCCESS);
} // main
首先要知道Kruskal重构树不仅可以解决边权相关的问题,也可以解决点权相关的问题。具体措施很简单,将一条边 拆成两条边 和 即可;或者也可以将边 的权值赋为 ,其中 是某种 类运算符。
原题等价于给定两点 ,从 点出发只能经过点权 的点,从 点出发只能经过点权 的点,问是否存在某个点 ,使得从 出发都能到达点 。于是我们分别按照边权从小到大/从大到小建出Kruskal重构树 (其中 是大根堆, 是小根堆)。那么现在询问的就是 和 的两棵子树内是否有公共的叶节点。转换为 序就是一个静态二维数点的问题,主席树即可。总时间复杂度为 。
参考代码
#include "werewolf.h"
#include <bits/stdc++.h>
using namespace std;
static constexpr int Maxn = 4e5 + 5, Maxm = 8e5 + 5;
static constexpr int LOG = 20;
int n, N, m, q;
struct Edge {
int u, v, w;
Edge() = default;
Edge(int u, int v, int w) : u(u), v(v), w(w) { }
};
struct kruskal_tree {
typedef int weight_t;
vector<vector<int>> g;
vector<vector<int>> par;
vector<int> dfn, siz;
vector<weight_t> ew;
kruskal_tree() = default;
int size(void) const { return n * 2 - 1; }
int root(void) const { return n * 2 - 2; }
template<typename F>
void build(vector<Edge> e, F ecmp) {
sort(e.begin(), e.end(), ecmp);
fa.resize(size()); iota(fa.begin(), fa.end(), 0);
g.assign(size(), vector<int>());
ew.resize(size());
int cn = n;
for (int i = 0; i < (int)e.size(); ++i) {
int u = e[i].u, v = e[i].v;
weight_t w = e[i].w;
if ((u = fnd(u)) != (v = fnd(v))) {
g[cn].push_back(u), g[cn].push_back(v);
fa[u] = cn, fa[v] = cn, ew[cn] = w; ++cn;
}
}
assert(cn == size());
par.assign(LOG, vector<int>(size()));
dfn.resize(size()); siz.resize(size());
dfn_index = 0, initialize(root(), -1);
} // kruskal_tree::build
template<typename F>
int get(int x, weight_t W, F wcmp) {
for (int j = LOG - 1; j >= 0; --j)
if (par[j][x] != -1 && wcmp(ew[par[j][x]], W))
x = par[j][x];
return x;
} // kruskal_tree::get
private:
vector<int> fa;
int dfn_index;
int fnd(int x) {
return fa[x] == x ? x : fa[x] = fnd(fa[x]);
} // kruskal_tree::fnd
void initialize(int u, int fa) {
par[0][u] = fa, dfn[u] = dfn_index++, siz[u] = 1;
for (int j = 1; j < LOG; ++j)
par[j][u] = (par[j - 1][u] == -1 ? -1 : par[j - 1][par[j - 1][u]]);
for (const int &v: g[u])
initialize(v, u), siz[u] += siz[v];
} // kruskal_tree::initialize
};
kruskal_tree gmin, gmax;
set<pair<int, int>> points;
struct node {
int ls, rs, s;
} tr[Maxn * LOG * 2];
int root[Maxn], tot;
inline int clone(int p) { tr[++tot] = tr[p]; return tot; }
inline void pushup(int p) {
tr[p].s = tr[tr[p].ls].s + tr[tr[p].rs].s;
} // pushup
void modify(int &p, int l, int r, int x) {
p = clone(p);
if (l + 1 == r) return ++tr[p].s, void();
int mid = (l + r) >> 1;
if (x < mid) modify(tr[p].ls, l, mid, x);
else modify(tr[p].rs, mid, r, x);
pushup(p);
} // modify
int ask(int pl, int pr, int l, int r, int L, int R) {
if (!pr || L >= r || l >= R) return 0;
if (L <= l && r <= R) return tr[pr].s - tr[pl].s;
int mid = (l + r) >> 1;
return ask(tr[pl].ls, tr[pr].ls, l, mid, L, R) + ask(tr[pl].rs, tr[pr].rs, mid, r, L, R);
} // ask
int query(int xl, int xr, int yl, int yr) {
return ask(xl == 0 ? 0 : root[xl - 1], root[xr - 1], 0, N, yl, yr) != 0;
} // query
void build(void) {
vector<vector<int>> all(N);
for (const auto &[x, y]: points) all[x].push_back(y);
for (int i = 0; i < N; ++i) {
for (const auto &j: all[i])
modify(root[i], 0, N, j);
root[i + 1] = clone(root[i]);
}
} // build
std::vector<int> check_validity(int nN, std::vector<int> X, std::vector<int> Y,
std::vector<int> S, std::vector<int> E,
std::vector<int> L, std::vector<int> R) {
n = nN, m = (int)X.size(), q = (int)S.size();
N = n * 2 - 1;
vector<Edge> Emin(m), Emax(m);
for (int i = 0; i < m; ++i) {
Emin[i] = Edge(X[i], Y[i], min(X[i], Y[i]));
Emax[i] = Edge(X[i], Y[i], max(X[i], Y[i]));
}
gmin.build(Emax, [&](const Edge &lhs, const Edge &rhs)->bool { return lhs.w < rhs.w; });
gmax.build(Emin, [&](const Edge &lhs, const Edge &rhs)->bool { return lhs.w > rhs.w; });
for (int i = 0; i < n; ++i) points.insert({gmin.dfn[i], gmax.dfn[i]});
build();
vector<int> Ans(q);
for (int i = 0; i < q; ++i) {
int s = gmax.get(S[i], L[i] - 1, greater<kruskal_tree::weight_t>());
int e = gmin.get(E[i], R[i] + 1, less<kruskal_tree::weight_t>());
int lmax = gmax.dfn[s], rmax = gmax.dfn[s] + gmax.siz[s];
int lmin = gmin.dfn[e], rmin = gmin.dfn[e] + gmin.siz[e];
Ans[i] = query(lmin, rmin, lmax, rmax);
}
return Ans;
} // check_validity
习题 1 Labyrinth
习题 2 「NOI2018」归程
习题 3 「APIO2020」交换城市
- 第二类Kruskal重构树
这一类Kruskal重构树的本质上就相当于是树上的笛卡尔树。给定一棵树 ,其Kruskal重构树为 ,则 ,若设 ,那么 ,其中 为节点 的权值,且 为原树 上节点 到 的路径上的点集合。
这种Kruskal重构树在处理某些树上路径问题有奇效。
给定一棵树。
我们认为一条从 的简单路径是好的,当且仅当路径上的点中编号最小的是 ,最大的是 。
求出好的简单路径条数。
数据范围:。
观察到该计数问题与树上路径的最小值/最大值有关, 可以考虑按编号建立Kruskal重构树。
设树 分别满足: 为原树的路径 上编号最小的点; 为原树的路径 上编号最大的点。则原树上的路径 满足要求当且仅当树 中 是 的祖先 且 树 中 是 的祖先。转换为 序后就是一个偏序问题,可以用树状数组实现,复杂度为 。
参考代码
#include <bits/stdc++.h>
using namespace std;
template<typename _Tp> void chmin(_Tp &x, const _Tp &y) { x = min(x, y); }
template<typename _Tp> void chmax(_Tp &x, const _Tp &y) { x = max(x, y); }
static constexpr int Maxn = 2e6 + 5;
int n, par[Maxn];
int64_t ans;
int fa[Maxn];
int fnd(int x) { return fa[x] == x ? x : fa[x] = fnd(fa[x]); }
struct graph {
int head[Maxn], nxt[Maxn * 2], to[Maxn * 2], tot;
graph() = default;
void add_edge(int u, int v) {
to[++tot] = v, nxt[tot] = head[u]; head[u] = tot;
}
} g, g1, g2;
int dfn[Maxn], idfn[Maxn], dfnTime, dfned[Maxn];
void dfs1(int u) {
idfn[dfn[u] = ++dfnTime] = u;
for (int j = g2.head[u]; j; j = g2.nxt[j]) dfs1(g2.to[j]);
dfned[u] = dfnTime;
} // dfs1
int b[Maxn];
void upd(int x, int v) { for (; x <= n; x += x & -x) b[x] += v; }
int ask(int x) { int r = 0; for (; x; x -= x & -x) r += b[x]; return r; }
void dfs2(int u) {
ans += ask(dfned[u]) - ask(dfn[u] - 1);
upd(dfn[u], 1);
for (int j = g1.head[u]; j; j = g1.nxt[j]) dfs2(g1.to[j]);
upd(dfn[u], -1);
} // dfs2
int main(void) {
freopen("charity.in", "r", stdin);
freopen("charity.out", "w", stdout);
extern uint32_t readu32(void);
n = readu32();
for (int i = 1; i <= n; ++i) par[i] = readu32();
for (int i = 2; i <= n; ++i)
g.add_edge(par[i], i), g.add_edge(i, par[i]);
for (int i = 1; i <= n; ++i) fa[i] = i;
for (int u = 1; u <= n; ++u)
for (int j = g.head[u]; j; j = g.nxt[j])
if (g.to[j] <= u) g1.add_edge(u, fnd(g.to[j])), fa[fnd(g.to[j])] = u;
for (int i = 1; i <= n; ++i) fa[i] = i;
for (int u = n; u >= 1; --u)
for (int j = g.head[u]; j; j = g.nxt[j])
if (g.to[j] >= u) g2.add_edge(u, fnd(g.to[j])), fa[fnd(g.to[j])] = u;
dfs1(1), dfs2(n);
printf("%lld\n", ans);
exit(EXIT_SUCCESS);
} // main
// fast io
static const int _BUF_SIZE = 1 << 18;
static char _ibuf[_BUF_SIZE], *iS = _ibuf, *iT = _ibuf;
inline char getch(void) {
if (__builtin_expect(iS == iT, false))
iT = (iS = _ibuf) + fread(_ibuf, 1, _BUF_SIZE, stdin);
if (__builtin_expect(iS == iT, false)) return EOF;
else return *iS++;
} // getch
uint32_t readu32(void) {
register uint32_t x = 0;
register char ch = getch();
while (ch < '0' || ch > '9') ch = getch();
while (ch >= '0' && ch <= '9') ((x += (x << 2)) <<= 1) += (ch ^ '0'), ch = getch();
return x;
} // readu32
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!