Kruskal 重构树

Kruskal 重构树

  • 第一类Kruskal重构树

​ 这一类的Kruskal重构树是基于最小生成树的Kruskal算法衍生的一个数据结构。

​ 回想一下最小生成树Kruskal算法:按照边权从小到大枚举所有边,若当前的边 e(u,v) 两端点不连通就将其连起来。重构树的构建只是最后一步有区别:若当前的边 e(u,v) 两端点不连通,则新建一个节点 T,将 u,v 所在的连通块的根节点的父亲赋为 T,并且将节点 T 的点权值赋为边 e 的权值。容易发现,若原图有 n 个节点,那么Kruskal重构树共有 2n1 个节点。

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重构树有一些很好用的性质:

  1. Kruskal重构树是二叉树。当按照边权从小到大或从大到小建立Kruskal重构树时,它是一个堆。
  2. 原来图中的所有节点现在是Kruskal重构树上的叶节点。
  3. 设Kruskal重构树是大根堆 (即按照边权从小到大顺序建立)。给定原图上的节点 x 和一个值 c,在重构树上找到深度最小的节点 y 满足 yx 的祖先且 wyc (其中 wy 表示虚拟节点 y 的点权,即虚拟节点 y 对应边的边权),那么 y 子树内的所有叶子节点集合便是原图中节点 x 在只经过边权不超过 c 的边能到达的所有节点的集合。当Kruskal重构树是小根堆时亦同理。

​ 从性质3可以知道,若题目限制只经过边权不超过/不小于某个值的边,那么通常可以使用Kruskal重构树。

例题 1 Peaks

​ Kruskal重构树的裸题。按边权从小到大建立Kruskal重构树后,问题转化为询问一个子树内叶节点中权值第 k 大。转换为 dfn 序后就是静态区间第 k 大,可以使用主席树。总时间复杂度为 O(nlogn)

例题 2 Life is a Game

​ 容易注意到,对于每一次询问,所有经过的节点必定组成一个连通块,而且所有经过的边必定是原图最小生成树上的包含该连通块的边集。

​ 基于这个结论,可以按边权从小到大建立Kruskal重构树。虽然对于一次询问在增广连通块的同时,经过的边权限制也会改变,但最后该询问所有经过的节点的集合必定是Kruskal重构树上某一棵子树的叶节点集合。于是每一次询问在重构树上从叶子节点开始暴力跳父亲,直到找到第一条不能通过的边 (设其为 vu 则有 u 子树内叶节点权值和加上初始值小于虚拟节点 v 所对应边的边权)。这样做的正确性显然,只不过时间复杂度为 O(n2)

​ 考虑优化,发现上述跳父亲的过程可以使用树上倍增优化。时间复杂度为 O(nlogn),可以通过。

参考代码

#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

例题 3 [IOI2018] werewolf 狼人

​ 首先要知道Kruskal重构树不仅可以解决边权相关的问题,也可以解决点权相关的问题。具体措施很简单,将一条边 e(u,v) 拆成两条边 e1(u,,wu)e2(,v,wv) 即可;或者也可以将边 e(u,v) 的权值赋为 op(wu,wv),其中 op 是某种 min/max 类运算符。

​ 原题等价于给定两点 S,T,从 S 点出发只能经过点权 L 的点,从 T 点出发只能经过点权 R 的点,问是否存在某个点 X,使得从 S,T 出发都能到达点 X。于是我们分别按照边权从小到大/从大到小建出Kruskal重构树 Kmin,Kmax (其中 Kmin 是大根堆,Kmax 是小根堆)。那么现在询问的就是 KminKmax 的两棵子树内是否有公共的叶节点。转换为 dfn 序就是一个静态二维数点的问题,主席树即可。总时间复杂度为 O(nlogn)

参考代码
#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重构树的本质上就相当于是树上的笛卡尔树。给定一棵树 T,其Kruskal重构树为 Kmax,则 uv,若设 z=lcaKmax(u,v),那么 az=maxxpath(u,v){ax},其中 ax 为节点 x 的权值,且 path(u,v) 为原树 T 上节点 uv 的路径上的点集合。

​ 这种Kruskal重构树在处理某些树上路径问题有奇效。

例题 1 NOIP2021模拟赛57 C. 超级加倍

给定一棵树。

我们认为一条从 xy 的简单路径是好的,当且仅当路径上的点中编号最小的是 x,最大的是 y

求出好的简单路径条数。

数据范围:n2×106

​ 观察到该计数问题与树上路径的最小值/最大值有关, 可以考虑按编号建立Kruskal重构树。

​ 设树 T1,T2 分别满足:lcaT1(x,y) 为原树的路径 xy 上编号最小的点;lcaT2(x,y) 为原树的路径 xy 上编号最大的点。则原树上的路径 xy 满足要求当且仅当树 T1xy 的祖先 且 树 T2yx 的祖先。转换为 dfn 序后就是一个偏序问题,可以用树状数组实现,复杂度为 O(nlogn)

参考代码
#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
posted @   cutx64  阅读(4)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示