Skip to content
Ficon's Paper
Go back

CF2222F

题意:给定一张图,现在要选一些点,问让这些点连通的代价最小是多少。

dis(u,v)=MEX(road(u,v))dis(u, v)=MEX(road(u, v))

在新图上,两个点之间有权值为 ww 的连边,当且仅当在旧图上存在一条路径 u,vu, v,使得路径的 MEX 值为 ww

如果我们知道所有的 disdis,那么这就是一个最小生成树问题。

最小生成树的逻辑是从小到大枚举可能的边,如果两个点不连通,那么就加入这条边让两个点连通。

MEX 的范围是 [0,m][0, m],因为最多只有 mm 条边,如果要产生 m+1m + 1 的 MEX,就需要至少 m+1m + 1 条边。

枚举 w:0mw:0\to m,找是否存在一条权值为 ww 的路径。

如果存在这样一条边,说明连接 u,vu, v 的所有的边的值都不等于 ww,此时路径的权值一定小于等于 ww,而因为我们是从小到大枚举的,如果 MEX 比 ww 更小的话,那么在之前这条边就已经加过,u,vu, v 已经连通,所以一定等于 ww

我们每次优先向左递归,这样就保证了最小生成树的贪心。

本质上: 线段树分治的前序遍历(先左后右),天然保证了我们传给并查集的 MEX 权值是严格单调递增的。这完美替代了常规 Kruskal 算法中对边权排序的步骤。

也就是说,我们从小到大枚举 ww,对于每个 ww,把权值等于 ww 边删掉,剩下的边加入到并查集中,此时可以连通的两个点的权值就一定是 ww

暴力的做法就是枚举 ww,每次暴力新建一个并查集。

考虑线段树二分+可撤销并查集。

[0,m][0, m] 区间开始递归,每次优先递归左边,然后将值在右半部分的边加入到并查集。

缺少左半部分,此时的 MEX 为 l,在合并的过程中,我们需要知道两个连通块中是否各自包含一个新图上的点,如果是,那么就将其添加到最小生成树中,合并两个集合。

所以需要为并查集维护一个代表元,初始时,只有新图中的点代表元是它本身,其它点的代表元是 0.

合并时,更新父节点的代表元(如果没有)。如果合并前两个连通块都有各自的代表元,说明这两个代表元之间可以连一条权值为 l 的边,但假如在最小生成树上这两个点已经连通了,这说明这两个代表元之间可能在之前通过更小的 MEX 连通了,所以就无需添加到最小生成树当中。

问题来了,所有合并都已完成时,并查集内的点两两都是可以连一条长度为 l 的边的。合并的过程中,一个连通块内部可能不止一个在新图中的点,这样做对吗?

想象这样一种情况,对于每条边都相同的图,建一颗最小生成树,那肯定是怎么建都可以,有很多种情况,连哪条边都一样。

所以各只选一个点当代表元没问题,两个连通块想要变得连通,选任意一条边即可。

我们实际上维护了一个动态的边集,递归到 [l,r][l, r] 时,图是由 [0,l][0, l][r+1,m][r + 1, m] 的边组成的,递归到底时,所有的答案都已经处理完成。

我们利用回滚操作,利用了所有的中间状态,从而优化了时间复杂度。

对于这样一个线段树形状的结构,按照原来暴力的思路,除了值为 0 以外所有的边的集合,实际上就在所有左端点为 0 的 Node 上被动态处理了,从顶层开始,每次往并查集中添加 n / 2, n / 4, … 范围的边,最终边集变成了和暴力一样的值域大小为 [1,2n][1, 2^n] 的所有边。

时间复杂度:

只有启发式合并的并查集,深度在 logn\log n 级别,单次操作的复杂度是 logn\log n

分治树的深度是 logm\log m,每一层中每条边都要添加/删除一次,于是总复杂度为 mlogmlognm\log m logn

Tricks:

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int INF = 1e9 + 7;
#define fi first
#define se second
using pii = pair<int, int>;
using pli = pair<ll, int>;
using pil = pair<int, ll>;
using pll = pair<ll, ll>;

class DSU {
  vector<int> fa, sz, repr;

public:
  void init(int n) {
    fa.resize(n + 1);
    sz.assign(n + 1, 1);
    iota(fa.begin(), fa.end(), 0);
  }
  void init(int n, vector<bool> &inC) {
    fa.resize(n + 1);
    sz.assign(n + 1, 1);
    repr.assign(n + 1, 0);
    for (int i = 1; i <= n; i++) {
      if (inC[i])
        repr[i] = i;
    }
    iota(fa.begin(), fa.end(), 0);
  }
  int get(int x) { return sz[find(x)]; }
  int find(int u) { return fa[u] == u ? u : find(fa[u]); }
  bool unite(int x, int y) {
    x = find(x), y = find(y);
    if (x == y)
      return false;
    if (sz[y] > sz[x])
      swap(x, y);
    fa[y] = x;
    sz[x] += sz[y];
    return true;
  }
  pair<int, int> unite(int x, int y, stack<array<int, 2>> &s) {
    x = find(x), y = find(y);
    if (x == y)
      return {-1, -1};
    if (sz[y] > sz[x])
      swap(x, y);
    int &u = repr[x], &v = repr[y];
    s.push({y, u});
    fa[y] = x;
    sz[x] += sz[y];
    if (u && v)
      return {u, v};
    if (!u)
      u = v;
    return {-1, -1};
  }
  void roll_back(int time, stack<array<int, 2>> &s) {
    while (s.size() > time) {
      auto [y, pr] = s.top();
      s.pop();
      int x = fa[y];
      repr[x] = pr;
      sz[x] -= sz[y];
      fa[y] = y;
    }
  }
} dsu, mst;

void solve() {
  int n, m, q, color = 0, c = -1;
  cin >> n >> m >> q;
  vector<vector<pii>> e(m + 1, vector<pii>());
  vector<bool> inC(n + 1);
  for (int i = 0, u, v, w; i < m; i++) {
    cin >> u >> v >> w;
    e[w].emplace_back(u, v);
  }
  for (int i = 0, x; i < q; i++) {
    cin >> x;
    if (c == -1)
      c = x;
    if (!inC[x]) {
      inC[x] = true;
      color++;
    }
  }
  dsu.init(n, inC), mst.init(n);
  ll ans = 0;
  stack<array<int, 2>> s;
  auto add = [&](int u, int v, int mex) {
    auto [up, vp] = dsu.unite(u, v, s);
    if (up != -1 && vp != -1) {
      if (mst.unite(up, vp))
        ans += mex;
    }
  };
  function<void(int, int)> dfs = [&](int l, int r) {
    if (l == r) {
      return;
    }
    int mid = (l + r) >> 1;
    auto cnt = s.size();
    for (int i = mid + 1; i <= r; i++) {
      for (auto [u, v] : e[i]) {
        add(u, v, l);
      }
    }
    dfs(l, mid);
    dsu.roll_back(cnt, s);
    for (int i = l; i <= mid; i++) {
      for (auto [u, v] : e[i]) {
        add(u, v, mid + 1);
      }
    }
    dfs(mid + 1, r);
    dsu.roll_back(cnt, s);
  };
  dfs(0, m);
  if (mst.get(c) != color)
    cout << -1 << endl;
  else
    cout << ans << endl;
}

signed main() {
  ios::sync_with_stdio(false);
  cin.tie(nullptr);
  int T;
  cin >> T;
  while (T--)
    solve();
  return 0;
}

dsu 负责维护局部连通性,在分治中不断更新和回滚。mst 负责维护最小生成树并累加答案。


Share this post on:

Previous Post
P5787 线段树分治
Next Post
CF1765I