跳转至

时光倒流⚓︎

时光倒流是一种处理动态连通性问题的技巧。它的核心思想是将操作顺序反转,通过从最后一个操作开始逆序处理操作,从而简化问题的复杂度。

航线规划

给定一张有 n 个节点和 m 条边的无向连通图。初始时图中包含所有边。现在有 q 次操作, 每次操作要么删除一条边, 要么询问两点间的关键边数量(uv 之间的边, 如果删除该边会使得 uv 不连通, 则该边为关键边)。请你回答所有询问。保证任意时刻图仍然连通。

Hint

由于删除边最后仍然连通,说明删除的边都是非关键边。因此可以将所有删除操作逆序处理, 相当于每次加入一条边。而加入边不会增加关键边的数量, 只会减少关键边的数量。因此可以使用树链剖分维护当前图的关键边数量。

注意删除操作完成后图仍可能有非关键边,这些边会对树边的关键性产生影响,因此在逆序处理时也需要将这些非关键边加入图中。

C++
#include <algorithm>
#include <functional>
#include <iostream>
#include <numeric>
#include <set>
#include <tuple>
#include <utility>
#include <vector>
using namespace std;

using PII  = pair<int, int>;
using TIII = tuple<int, int, int>;

struct segment_tree {
  vector<int> sum;          // 区间和
  vector<int> tag_set;      // 区间赋值懒标记
  vector<int> tag_set_val;  // 区间赋值懒标记值, 只有tag_set为true时该值才有意义

  explicit segment_tree(int n) : sum(n * 4), tag_set(n * 4), tag_set_val(n * 4) {}

  void push_up(int i) { sum[i] = sum[2 * i] + sum[2 * i + 1]; }

  // 构建线段树
  void build(int i, int left, int right) {
    if (left == right) {  // 叶子节点,进行初始化, 将dfn映射回节点编号
      sum[i] = 1;
      return;
    }
    int mid = left + ((right - left) / 2);
    build(2 * i, left, mid);
    build(2 * i + 1, mid + 1, right);
    push_up(i);
  }

  void lazy_set(int i, int val, int count) {
    sum[i]         = count * val;
    tag_set[i]     = 1;
    tag_set_val[i] = val;
  }

  // 向下传递懒标记
  void push_down(int i, int left_count, int right_count) {
    if (tag_set[i] != 0) {  // 处理赋值
      lazy_set(2 * i, tag_set_val[i], left_count);
      lazy_set(2 * i + 1, tag_set_val[i], right_count);
      tag_set[i] = 0;  // 清空根节点赋值标记
    }
  }

  // 区间赋值: range_set(x, y, val, 1, 1, n) 将区间 [x,y] 的值修改为 val
  void range_set(int ql, int qr, int val, int i, int l, int r) {
    if (ql <= l && r <= qr) {  // 区间覆盖, 直接更新
      lazy_set(i, val, r - l + 1);
      return;
    }
    int mid = l + ((r - l) / 2);
    push_down(i, mid - l + 1, r - mid);
    if (ql <= mid) { range_set(ql, qr, val, 2 * i, l, mid); }
    if (qr > mid) { range_set(ql, qr, val, 2 * i + 1, mid + 1, r); }
    push_up(i);
  }

  // 区间求和: range_sum(x, y, 1, 1, n) 查询区间 [x,y] 的和
  int range_sum(int ql, int qr, int i, int l, int r) {
    if (ql <= l && r <= qr) { return sum[i]; }  // 区间覆盖,直接返回
    int mid = l + ((r - l) / 2);
    push_down(i, mid - l + 1, r - mid);
    // 汇总结果
    int res = 0;
    if (ql <= mid) { res += range_sum(ql, qr, 2 * i, l, mid); }
    if (qr > mid) { res += range_sum(ql, qr, 2 * i + 1, mid + 1, r); }
    return res;
  }
};

int main() {
  ios::sync_with_stdio(false);
  cin.tie(nullptr);
  cout.tie(nullptr);
  int n, m;
  cin >> n >> m;

  set<PII> edges;  // 存储当前图中的边, 每条边只存一次, (u,v): u<v
  for (int i = 0; i < m; ++i) {
    int u, v;
    cin >> u >> v;
    edges.insert({min(u, v), max(u, v)});
  }

  vector<TIII> operations;
  int op;
  while (cin >> op) {
    if (op == -1) { break; }
    int u, v;
    cin >> u >> v;
    if (op == 0) {  // 删除操作
      operations.emplace_back(0, min(u, v), max(u, v));
      edges.erase({min(u, v), max(u, v)});  // 不能在生成树中使用该边
    } else {
      operations.emplace_back(1, min(u, v), max(u, v));  // 查询
    }
  }

  // 并查集, 构造生成树
  vector<int> root(n + 1);
  iota(root.begin(), root.end(), 0);
  function<int(int)> find;
  find = [&](int x) { return root[x] == x ? x : root[x] = find(root[x]); };

  vector<vector<int>> tree(n + 1);
  for (const auto &[u, v] : edges) {
    int ru = find(u);
    int rv = find(v);
    if (ru != rv) {  // 树边
      root[rv] = ru;
      tree[u].push_back(v);
      tree[v].push_back(u);
    } else {                             // 剩下的非树边
      operations.emplace_back(0, u, v);  // 相当于删除操作, 最后需要优先删除操作的边进行操作
    }
  }
  // 重链剖分准备
  int r = 1;  // 以1号节点为根节点
  // 父节点, 深度, 子树大小, 重儿子
  vector<int> parent(n + 1), depth(n + 1), size(n + 1), heavy_son(n + 1, -1);
  auto dfs = [&](auto &self, int u, int from) -> void {
    parent[u]    = from;
    size[u]      = 1;
    int max_size = 0;
    for (int v : tree[u]) {
      if (v != from) {
        depth[v] = depth[u] + 1;
        self(self, v, u);
        size[u] += size[v];
        if (size[v] > max_size) {
          max_size     = size[v];
          heavy_son[u] = v;
        }
      }
    }
  };
  dfs(dfs, r, -1);  // 计算父节点, 深度, 子树大小, 重儿子

  // 链顶, dfs序, dfs反序(dfn为i的节点编号)
  vector<int> top(n + 1), dfn(n + 1), rank(n + 1);
  int timer      = 0;
  auto decompose = [&](auto &self, int u, int t) -> void {
    top[u]      = t;
    dfn[u]      = ++timer;
    rank[timer] = u;
    if (heavy_son[u] != -1) { self(self, heavy_son[u], t); }
    for (int v : tree[u]) {  // 处理轻儿子, 轻儿子各自成链,链顶为自己
      if (v != parent[u] && v != heavy_son[u]) { self(self, v, v); }
    }
  };
  decompose(decompose, r, r);  // 重链剖分, 根节点为r, 链顶为自己
  // 构建线段树, 一开始都是关键边, 所以点权都为1
  segment_tree seg(n);
  seg.build(1, 1, n);
  // 边权下放到子节点的点权上, 根节点清0
  seg.range_set(1, 1, 0, 1, 1, n);
  // 查询u-v路径上的关键边数
  auto path_sum = [&](int u, int v) {
    int res = 0;
    while (top[u] != top[v]) {
      if (depth[top[u]] < depth[top[v]]) { swap(u, v); }
      res = (res + seg.range_sum(dfn[top[u]], dfn[u], 1, 1, n));
      u   = parent[top[u]];
    }
    if (depth[u] > depth[v]) { swap(u, v); }
    res = (res + seg.range_sum(dfn[u] + 1, dfn[v], 1, 1, n));
    return res;
  };
  // 路径赋值, 此处用于将u-v之间的边权清0
  auto path_set = [&](int u, int v, int val) {
    while (top[u] != top[v]) {
      if (depth[top[u]] < depth[top[v]]) { swap(u, v); }
      seg.range_set(dfn[top[u]], dfn[u], val, 1, 1, n);
      u = parent[top[u]];
    }
    if (depth[u] > depth[v]) { swap(u, v); }
    seg.range_set(dfn[u] + 1, dfn[v], val, 1, 1, n);
  };

  vector<int> answer;
  for (int i = operations.size() - 1; i >= 0; --i) {
    auto [op, u, v] = operations[i];
    if (op == 0) {  // 删除操作, 逆着就是将边加回去, uv之间的树边都变成非关键边
      path_set(u, v, 0);
    } else {
      int ans = path_sum(u, v);
      answer.push_back(ans);
    }
  }
  reverse(answer.begin(), answer.end());
  for (int ans : answer) { cout << ans << "\n"; }

  return 0;
}

评论