「AHOI2005」航线规划

知识点:LCT,维护加边的边双连通分量

原题面:Luogu

LCT 相关内容可以阅读:「笔记」Link Cut Tree

简述

给定一 \(n\) 个点 \(m\) 条边的无向图,给定 \(q\) 次操作:

  1. 断开一条边,保证断开的边存在。
  2. 查询给定两点间所有路径的必经边的数量。

\(1< n< 3\times 10^4\)\(1< m< 10^5\)\(0\le q\le 4\times 10^4\)
1S,128MB。

分析

先离线反向操作,将删边转化为加边。
容易发现查询的答案即原图边双缩点后指定两点间的边数。无向图边双缩点后一定是一棵树,考虑使用 LCT 维护加边操作。

具体地,使用并查集维护每个点所在的边双(注意不是连通性)。加边时分类讨论。

  • 若两点已经在一个边双中,跳过操作。
  • 两点不连通,直接连边即可。
  • 两点已连通,考虑把两点路径上所有点取出,把它们合并成一个点。新点代表它们所在的边双。

查询时取出指定路径,答案即路径上的点数 \(-1\),维护 Splay 子树大小即可。

具体实现上:对于需要合并成一个点的路径,将该路径取出后遍历其中所有点,并在并查集中把它们合并。之后每次需要调用一个 LCT 节点时,都找到该节点在并查集中的祖先,然后调用它的祖先。由于事先 Access(u,v) 了,这样做显然不会破坏 LCT 中原有的各 Splay 的父子关系,可以保证正确性。
考虑复杂度。每个原图中的节点都只会被合并一次,合并一个节点均摊 \(O(1)\)。则总复杂度为常数飞天\(O((m+q)\log n)\) 级别。


上述做法通用性较强。对于此题来说,还有另外一种解法。
同样离线反向操作,先进行边双缩点,在新图上考虑边转点。将代表边的点的权值赋为 1,其它点权值为 0。边权为 1 的实际含义是该边不在一个环中。
若一次加边后会出现环,则将这条路径上所有点的点权都置为 0 即可。查询的答案即指定两点间路径的权值和。
此时树是静态的,甚至可以直接树剖维护,常数要小很多。

代码

LCT 代码

//知识点:LCT
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <map>
#define LL long long
#define pr std::pair
#define mp std::make_pair
const int kN = 5e4 + 10;
//=============================================================
int n, m, e_num, u[kN << 1], v[kN << 1];
int q_num, q[kN][3], ans[kN];
std::map <pr <int, int>, bool> del;
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir, int sec) {
  if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
  if (sec < fir) fir = sec;
}
namespace UF { //Union-Find data structure
  const int kMaxNode = kN;
  int fa[kMaxNode];
  int Find(int x_) {
    int y = fa[x_];
    return fa[x_] == x_ ? x_ : fa[x_] = Find(fa[x_]);
  }
  void Union(int x_, int y_) {
    x_ = Find(x_), y_ = Find(y_);
    if (x_ != y_) fa[x_] = y_;
  }
}
namespace LCT {
  #define f fa[now_]
  #define ls son[now_][0]
  #define rs son[now_][1]
  const int kMaxNode = kN;
  int fa[kMaxNode], son[kMaxNode][2], siz[kMaxNode];
  bool tagrev[kMaxNode];
  void Pushup(int now_) { //维护 size
    now_ = UF::Find(now_);
    siz[now_] = siz[ls] + siz[rs] + 1;
  }
  void PushReverse(int now_) {
    now_ = UF::Find(now_);
    if (!now_) return;
    std::swap(ls, rs);
    tagrev[now_] ^= 1;
  }
  void Pushdown(int now_) {
    now_ = UF::Find(now_);
    if (!now_) return ;
    if (tagrev[now_]) PushReverse(ls), PushReverse(rs);
    tagrev[now_] = 0;
  }
  bool IsRoot(int now_) {
    int x = f, y = UF::Find(f);
    return son[UF::Find(f)][0] != now_ && son[UF::Find(f)][1] != now_;
  }
  bool WhichSon(int now_) {
    return son[UF::Find(f)][1] == now_;
  }
  void Rotate(int now_) {
    now_ = UF::Find(now_);
    int fa_ = UF::Find(f), w = WhichSon(now_);
    if (!IsRoot(fa_)) son[UF::Find(fa[fa_])][WhichSon(fa_)] = now_;
    f = UF::Find(fa[fa_]);

    son[fa_][w] = son[now_][w ^ 1];
    fa[son[fa_][w]] = fa_;

    son[now_][w ^ 1] = fa_;
    fa[fa_] = now_;
    Pushup(fa_), Pushup(now_); 
  }
  void Update(int now_) {
    now_ = UF::Find(now_);
    if (!IsRoot(now_)) Update(f);
    Pushdown(now_);
  }
  void Splay(int now_) {
    now_ = UF::Find(now_);
    Update(now_);
    for (; !IsRoot(now_); Rotate(now_)) {
      if (!IsRoot(UF::Find(f))) Rotate(WhichSon(UF::Find(f)) == WhichSon(now_) ? UF::Find(f) : now_);
    }
  }
  void Access(int now_) { 
    now_ = UF::Find(now_);
    for (int last_ = 0; now_; last_ = now_, now_ = UF::Find(f)) {
      Splay(now_), rs = last_;
      Pushup(now_);
    }
  }
  void MakeRoot(int now_) { 
    now_ = UF::Find(now_);
    Access(now_); 
    Splay(now_); 
    PushReverse(now_); 
  }
  int Find(int now_) {
    now_ = UF::Find(now_);
    Access(now_); 
    Splay(now_);
    while (ls) Pushdown(now_), now_ = ls;
    
    Splay(now_); 
    return now_; 
  }
  void Split(int x_, int y_) { 
    x_ = UF::Find(x_), y_ = UF::Find(y_);
    MakeRoot(x_);
    Access(y_);
    Splay(y_);
  }
  void Delete(int now_, int fa_) { //递归地遍历 Splay,合并原节点
    now_ = UF::Find(now_);
    if (!now_) return;
    UF::Union(now_, fa_);
    Delete(ls, fa_), Delete(rs, fa_);
  }
  void Link(int x_, int y_) {
    x_ = UF::Find(x_), y_ = UF::Find(y_);
    if (x_ == y_) return;
    MakeRoot(x_);
    if (Find(y_) != x_) {
      fa[x_] = y_;
      return ;
    }
    Delete(x_, x_); //合并原节点
    son[x_][1] = 0; //x 即合并后的得到的代表边双的节点。将它的儿子置零。
    Pushup(x_);
  }
  int Query(int x_, int y_) {
    x_ = UF::Find(x_), y_ = UF::Find(y_);
    Split(x_, y_);
    return siz[y_] - 1;
  }
}
//=============================================================
signed main() { 
  n = read(), m = read();
  for (int i = 1; i <= n; ++ i) {
    UF::fa[i] = i;
    LCT::siz[i] = 1;
  }
  for (int i = 1; i <= m; ++ i) { 
    int u_ = read(), v_ = read();
    u[++ e_num] = u_, v[e_num] = v_;
    if (u_ > v_) std::swap(u[e_num], v[e_num]);
  }
  while(true) {
    int opt = read();
    if (opt == -1) break;
    int u_ = read(), v_ = read();
    if (u_ > v_) std::swap(u_, v_);
    q[++ q_num][0] = opt, q[q_num][1] = u_, q[q_num][2] = v_;
    if (!opt) del[mp(u_, v_)] = true;
  }

  for (int i = 1; i <= m; ++ i) {
    int u_ = u[i], v_ = v[i];
    if (del[mp(u_, v_)]) continue;
    LCT::Link(u_, v_);
  }
  for (int i = q_num; i; -- i) { //注意反向
    int opt = q[i][0], u_ = q[i][1], v_ = q[i][2];
    if (!opt) LCT::Link(u_, v_);
    if (opt) ans[i] = LCT::Query(u_, v_);
  }
  for (int i = 1; i <= q_num; ++ i) {
    if (q[i][0]) printf("%d\n", ans[i]);
  }
  return 0; 
}
posted @ 2021-01-25 17:15  Luckyblock  阅读(69)  评论(0编辑  收藏  举报